From e1e31b7ae907d553c98a5737eba9bfecffca911a Mon Sep 17 00:00:00 2001 From: Yibo Zhuang Date: Wed, 3 Jun 2026 09:07:20 -0700 Subject: [PATCH 01/11] add IPv6 support (#752) Extends the network plumbing to support per-interface IPv6 address configuration. The `Interface` protocol supports `ipv6Address` and `ipv6Gateway`. The agent's networking RPCs carry per-family fields via new `InterfaceAddress`, `LinkRoute`, and `DefaultRoute` types in `ContainerizationExtras`. `NetlinkSession` adds IPv6 methods for address and route operations. --------- Co-authored-by: michael_crosby Co-authored-by: Michael Crosby --- .../Containerization/ContainerManager.swift | 25 +- Sources/Containerization/Interface.swift | 11 +- Sources/Containerization/LinuxContainer.swift | 22 +- Sources/Containerization/LinuxPod.swift | 22 +- Sources/Containerization/NATInterface.swift | 13 +- .../SandboxContext/SandboxContext.pb.swift | 81 +++- .../SandboxContext/SandboxContext.proto | 4 + .../VirtualMachineAgent+Interface.swift | 84 ++++ .../VirtualMachineAgent.swift | 6 +- Sources/Containerization/Vminitd.swift | 37 +- Sources/Containerization/VmnetNetwork.swift | 181 +++++-- .../NetworkConfiguration.swift | 57 +++ .../NetlinkSession.swift | 201 ++++++++ Sources/ContainerizationNetlink/Types.swift | 5 + Sources/Integration/ContainerTests.swift | 441 ++++++++++++++++++ Sources/Integration/PodTests.swift | 57 +++ Sources/Integration/Suite.swift | 8 + Sources/cctl/RunCommand.swift | 3 +- .../NetlinkSessionTest.swift | 183 ++++++++ .../AllocatorTests.swift | 170 +++++++ .../InterfaceTests.swift | 59 +++ vminitd/Sources/VminitdCore/Server+GRPC.swift | 74 ++- 22 files changed, 1632 insertions(+), 112 deletions(-) create mode 100644 Sources/Containerization/VirtualMachineAgent+Interface.swift create mode 100644 Sources/ContainerizationExtras/NetworkConfiguration.swift create mode 100644 Tests/ContainerizationTests/AllocatorTests.swift create mode 100644 Tests/ContainerizationTests/InterfaceTests.swift diff --git a/Sources/Containerization/ContainerManager.swift b/Sources/Containerization/ContainerManager.swift index 48682b38..2e2edd7b 100644 --- a/Sources/Containerization/ContainerManager.swift +++ b/Sources/Containerization/ContainerManager.swift @@ -302,15 +302,17 @@ public struct ContainerManager: Sendable { if let imageConfig { config.process = .init(from: imageConfig) } - if networking, let interface = try self.network?.createInterface(id) { - config.interfaces = [interface] - guard let gateway = interface.ipv4Gateway else { - throw ContainerizationError( - .invalidState, - message: "missing ipv4 gateway for container \(id)" - ) + if networking { + if let interface = try self.network?.createInterface(id) { + config.interfaces = [interface] + guard let gateway = interface.ipv4Gateway else { + throw ContainerizationError( + .invalidState, + message: "missing ipv4 gateway for container \(id)" + ) + } + config.dns = .init(nameservers: [gateway.description]) } - config.dns = .init(nameservers: [gateway.description]) } config.bootLog = BootLog.file(path: self.containerRoot.appendingPathComponent(id).appendingPathComponent("bootlog.log")) try configuration(&config) @@ -378,4 +380,11 @@ extension CIDRv4 { } } +extension CIDRv6 { + /// The gateway address of the network. + public var gateway: IPv6Address { + IPv6Address(self.lower.value + 1) + } +} + #endif diff --git a/Sources/Containerization/Interface.swift b/Sources/Containerization/Interface.swift index 80eac49b..a95c58f9 100644 --- a/Sources/Containerization/Interface.swift +++ b/Sources/Containerization/Interface.swift @@ -22,9 +22,16 @@ public protocol Interface: Sendable { /// Example: `192.168.64.3/24` var ipv4Address: CIDRv4 { get } - /// The IP address for the default route, or nil for no default route. + /// The IPv4 gateway address for the default route, or nil for no IPv4 default route. var ipv4Gateway: IPv4Address? { get } + /// The interface IPv6 address and subnet prefix length, as a CIDRv6 address, or nil for no IPv6 address. + /// Example: `fd00::1/64` + var ipv6Address: CIDRv6? { get } + + /// The IPv6 gateway address for the default route, or nil for no IPv6 default route. + var ipv6Gateway: IPv6Address? { get } + /// The interface MAC address, or nil to auto-configure the address. var macAddress: MACAddress? { get } @@ -34,4 +41,6 @@ public protocol Interface: Sendable { extension Interface { public var mtu: UInt32 { 1500 } + public var ipv6Address: CIDRv6? { nil } + public var ipv6Gateway: IPv6Address? { nil } } diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index 7b1395b0..58ffa4a3 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -635,22 +635,12 @@ extension LinuxContainer { var defaultRouteSet = false for (index, i) in self.interfaces.enumerated() { let name = "eth\(index)" - self.logger?.debug("setting up interface \(name) with address \(i.ipv4Address)") - try await agent.addressAdd(name: name, ipv4Address: i.ipv4Address) - try await agent.up(name: name, mtu: i.mtu) - if defaultRouteSet { - continue - } - if let ipv4Gateway = i.ipv4Gateway { - if !i.ipv4Address.contains(ipv4Gateway) { - self.logger?.debug("gateway \(ipv4Gateway) is outside subnet \(i.ipv4Address), adding a route first") - try await agent.routeAddLink(name: name, dstIPv4Addr: ipv4Gateway, srcIPv4Addr: i.ipv4Address.address) - } - try await agent.routeAddDefault(name: name, ipv4Gateway: ipv4Gateway) - } else { - self.logger?.debug("no gateway for \(name)") - try await agent.routeAddDefault(name: name, ipv4Gateway: nil) - } + try await agent.setupInterface( + i, + name: name, + setDefaultRoute: !defaultRouteSet, + logger: self.logger + ) defaultRouteSet = true } diff --git a/Sources/Containerization/LinuxPod.swift b/Sources/Containerization/LinuxPod.swift index 1ace38b6..3e5d58f0 100644 --- a/Sources/Containerization/LinuxPod.swift +++ b/Sources/Containerization/LinuxPod.swift @@ -659,22 +659,12 @@ extension LinuxPod { var defaultRouteSet = false for (index, i) in self.interfaces.enumerated() { let name = "eth\(index)" - self.logger?.debug("setting up interface \(name) with address \(i.ipv4Address)") - try await agent.addressAdd(name: name, ipv4Address: i.ipv4Address) - try await agent.up(name: name, mtu: i.mtu) - if defaultRouteSet { - continue - } - if let ipv4Gateway = i.ipv4Gateway { - if !i.ipv4Address.contains(ipv4Gateway) { - self.logger?.debug("gateway \(ipv4Gateway) is outside subnet \(i.ipv4Address), adding a route first") - try await agent.routeAddLink(name: name, dstIPv4Addr: ipv4Gateway, srcIPv4Addr: nil) - } - try await agent.routeAddDefault(name: name, ipv4Gateway: ipv4Gateway) - } else { - self.logger?.debug("no gateway for \(name)") - try await agent.routeAddDefault(name: name, ipv4Gateway: nil) - } + try await agent.setupInterface( + i, + name: name, + setDefaultRoute: !defaultRouteSet, + logger: self.logger + ) defaultRouteSet = true } diff --git a/Sources/Containerization/NATInterface.swift b/Sources/Containerization/NATInterface.swift index 22383627..c37fbc06 100644 --- a/Sources/Containerization/NATInterface.swift +++ b/Sources/Containerization/NATInterface.swift @@ -19,12 +19,23 @@ import ContainerizationExtras public struct NATInterface: Interface { public var ipv4Address: CIDRv4 public var ipv4Gateway: IPv4Address? + public var ipv6Address: CIDRv6? + public var ipv6Gateway: IPv6Address? public var macAddress: MACAddress? public var mtu: UInt32 - public init(ipv4Address: CIDRv4, ipv4Gateway: IPv4Address?, macAddress: MACAddress? = nil, mtu: UInt32 = 1500) { + public init( + ipv4Address: CIDRv4, + ipv4Gateway: IPv4Address?, + ipv6Address: CIDRv6? = nil, + ipv6Gateway: IPv6Address? = nil, + macAddress: MACAddress? = nil, + mtu: UInt32 = 1500 + ) { self.ipv4Address = ipv4Address self.ipv4Gateway = ipv4Gateway + self.ipv6Address = ipv6Address + self.ipv6Gateway = ipv6Gateway self.macAddress = macAddress self.mtu = mtu } diff --git a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift index 8157b5ae..e15abcbb 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.pb.swift +++ b/Sources/Containerization/SandboxContext/SandboxContext.pb.swift @@ -1098,9 +1098,20 @@ public struct Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest: Sendable { public var ipv4Address: String = String() + public var ipv6Address: String { + get {_ipv6Address ?? String()} + set {_ipv6Address = newValue} + } + /// Returns true if `ipv6Address` has been explicitly set. + public var hasIpv6Address: Bool {self._ipv6Address != nil} + /// Clears the value of `ipv6Address`. Subsequent reads from it will return its default value. + public mutating func clearIpv6Address() {self._ipv6Address = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _ipv6Address: String? = nil } public struct Com_Apple_Containerization_Sandbox_V3_IpAddrAddResponse: Sendable { @@ -1124,9 +1135,30 @@ public struct Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: Senda public var srcIpv4Addr: String = String() + public var dstIpv6Addr: String { + get {_dstIpv6Addr ?? String()} + set {_dstIpv6Addr = newValue} + } + /// Returns true if `dstIpv6Addr` has been explicitly set. + public var hasDstIpv6Addr: Bool {self._dstIpv6Addr != nil} + /// Clears the value of `dstIpv6Addr`. Subsequent reads from it will return its default value. + public mutating func clearDstIpv6Addr() {self._dstIpv6Addr = nil} + + public var srcIpv6Addr: String { + get {_srcIpv6Addr ?? String()} + set {_srcIpv6Addr = newValue} + } + /// Returns true if `srcIpv6Addr` has been explicitly set. + public var hasSrcIpv6Addr: Bool {self._srcIpv6Addr != nil} + /// Clears the value of `srcIpv6Addr`. Subsequent reads from it will return its default value. + public mutating func clearSrcIpv6Addr() {self._srcIpv6Addr = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _dstIpv6Addr: String? = nil + fileprivate var _srcIpv6Addr: String? = nil } public struct Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkResponse: Sendable { @@ -1148,9 +1180,20 @@ public struct Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest: Se public var ipv4Gateway: String = String() + public var ipv6Gateway: String { + get {_ipv6Gateway ?? String()} + set {_ipv6Gateway = newValue} + } + /// Returns true if `ipv6Gateway` has been explicitly set. + public var hasIpv6Gateway: Bool {self._ipv6Gateway != nil} + /// Clears the value of `ipv6Gateway`. Subsequent reads from it will return its default value. + public mutating func clearIpv6Gateway() {self._ipv6Gateway = nil} + public var unknownFields = SwiftProtobuf.UnknownStorage() public init() {} + + fileprivate var _ipv6Gateway: String? = nil } public struct Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultResponse: Sendable { @@ -3203,7 +3246,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpLinkSetResponse: SwiftProtobuf extension Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".IpAddrAddRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}interface\0\u{1}ipv4Address\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}interface\0\u{1}ipv4Address\0\u{1}ipv6Address\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -3213,24 +3256,33 @@ extension Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest: SwiftProtobuf. switch fieldNumber { case 1: try { try decoder.decodeSingularStringField(value: &self.interface) }() case 2: try { try decoder.decodeSingularStringField(value: &self.ipv4Address) }() + case 3: try { try decoder.decodeSingularStringField(value: &self._ipv6Address) }() default: break } } } public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 if !self.interface.isEmpty { try visitor.visitSingularStringField(value: self.interface, fieldNumber: 1) } if !self.ipv4Address.isEmpty { try visitor.visitSingularStringField(value: self.ipv4Address, fieldNumber: 2) } + try { if let v = self._ipv6Address { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest, rhs: Com_Apple_Containerization_Sandbox_V3_IpAddrAddRequest) -> Bool { if lhs.interface != rhs.interface {return false} if lhs.ipv4Address != rhs.ipv4Address {return false} + if lhs._ipv6Address != rhs._ipv6Address {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3257,7 +3309,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpAddrAddResponse: SwiftProtobuf extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".IpRouteAddLinkRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}interface\0\u{1}dstIpv4Addr\0\u{1}srcIpv4Addr\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}interface\0\u{1}dstIpv4Addr\0\u{1}srcIpv4Addr\0\u{1}dstIpv6Addr\0\u{1}srcIpv6Addr\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -3268,12 +3320,18 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: SwiftProt case 1: try { try decoder.decodeSingularStringField(value: &self.interface) }() case 2: try { try decoder.decodeSingularStringField(value: &self.dstIpv4Addr) }() case 3: try { try decoder.decodeSingularStringField(value: &self.srcIpv4Addr) }() + case 4: try { try decoder.decodeSingularStringField(value: &self._dstIpv6Addr) }() + case 5: try { try decoder.decodeSingularStringField(value: &self._srcIpv6Addr) }() default: break } } } public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 if !self.interface.isEmpty { try visitor.visitSingularStringField(value: self.interface, fieldNumber: 1) } @@ -3283,6 +3341,12 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: SwiftProt if !self.srcIpv4Addr.isEmpty { try visitor.visitSingularStringField(value: self.srcIpv4Addr, fieldNumber: 3) } + try { if let v = self._dstIpv6Addr { + try visitor.visitSingularStringField(value: v, fieldNumber: 4) + } }() + try { if let v = self._srcIpv6Addr { + try visitor.visitSingularStringField(value: v, fieldNumber: 5) + } }() try unknownFields.traverse(visitor: &visitor) } @@ -3290,6 +3354,8 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkRequest: SwiftProt if lhs.interface != rhs.interface {return false} if lhs.dstIpv4Addr != rhs.dstIpv4Addr {return false} if lhs.srcIpv4Addr != rhs.srcIpv4Addr {return false} + if lhs._dstIpv6Addr != rhs._dstIpv6Addr {return false} + if lhs._srcIpv6Addr != rhs._srcIpv6Addr {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -3316,7 +3382,7 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddLinkResponse: SwiftPro extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".IpRouteAddDefaultRequest" - public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}interface\0\u{1}ipv4Gateway\0") + public static let _protobuf_nameMap = SwiftProtobuf._NameMap(bytecode: "\0\u{1}interface\0\u{1}ipv4Gateway\0\u{1}ipv6Gateway\0") public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { @@ -3326,24 +3392,33 @@ extension Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest: SwiftP switch fieldNumber { case 1: try { try decoder.decodeSingularStringField(value: &self.interface) }() case 2: try { try decoder.decodeSingularStringField(value: &self.ipv4Gateway) }() + case 3: try { try decoder.decodeSingularStringField(value: &self._ipv6Gateway) }() default: break } } } public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 if !self.interface.isEmpty { try visitor.visitSingularStringField(value: self.interface, fieldNumber: 1) } if !self.ipv4Gateway.isEmpty { try visitor.visitSingularStringField(value: self.ipv4Gateway, fieldNumber: 2) } + try { if let v = self._ipv6Gateway { + try visitor.visitSingularStringField(value: v, fieldNumber: 3) + } }() try unknownFields.traverse(visitor: &visitor) } public static func ==(lhs: Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest, rhs: Com_Apple_Containerization_Sandbox_V3_IpRouteAddDefaultRequest) -> Bool { if lhs.interface != rhs.interface {return false} if lhs.ipv4Gateway != rhs.ipv4Gateway {return false} + if lhs._ipv6Gateway != rhs._ipv6Gateway {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Sources/Containerization/SandboxContext/SandboxContext.proto b/Sources/Containerization/SandboxContext/SandboxContext.proto index dfe8cd66..efce1ee5 100644 --- a/Sources/Containerization/SandboxContext/SandboxContext.proto +++ b/Sources/Containerization/SandboxContext/SandboxContext.proto @@ -303,6 +303,7 @@ message IpLinkSetResponse {} message IpAddrAddRequest { string interface = 1; string ipv4Address = 2; + optional string ipv6Address = 3; } message IpAddrAddResponse {} @@ -311,6 +312,8 @@ message IpRouteAddLinkRequest { string interface = 1; string dstIpv4Addr = 2; string srcIpv4Addr = 3; + optional string dstIpv6Addr = 4; + optional string srcIpv6Addr = 5; } message IpRouteAddLinkResponse {} @@ -318,6 +321,7 @@ message IpRouteAddLinkResponse {} message IpRouteAddDefaultRequest { string interface = 1; string ipv4Gateway = 2; + optional string ipv6Gateway = 3; } message IpRouteAddDefaultResponse {} diff --git a/Sources/Containerization/VirtualMachineAgent+Interface.swift b/Sources/Containerization/VirtualMachineAgent+Interface.swift new file mode 100644 index 00000000..e2fe7227 --- /dev/null +++ b/Sources/Containerization/VirtualMachineAgent+Interface.swift @@ -0,0 +1,84 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ContainerizationExtras +import Logging + +extension VirtualMachineAgent { + /// Configure a single network interface inside the sandbox: assign addresses, + /// bring the link up, and (when requested) install the link/default routes. + func setupInterface( + _ interface: any Interface, + name: String, + setDefaultRoute: Bool, + logger: Logger? + ) async throws { + logger?.debug("setting up interface \(name) with v4 \(interface.ipv4Address) v6 \(interface.ipv6Address?.description ?? "")") + try await addressAdd( + name: name, + address: .init(ipv4Address: interface.ipv4Address, ipv6Address: interface.ipv6Address) + ) + try await up(name: name, mtu: interface.mtu) + + guard setDefaultRoute else { return } + + let ipv4Address = interface.ipv4Address + let ipv4Gateway = interface.ipv4Gateway + let ipv6Gateway = interface.ipv6Gateway + let ipv6Address = interface.ipv6Address + + let needsIPv4LinkRoute: Bool + if let ipv4Gateway { + needsIPv4LinkRoute = !ipv4Address.contains(ipv4Gateway) + } else { + needsIPv4LinkRoute = false + } + + let needsIPv6LinkRoute: Bool + if let ipv6Gateway, let ipv6Address { + needsIPv6LinkRoute = !ipv6Address.contains(ipv6Gateway) + } else { + needsIPv6LinkRoute = false + } + + if needsIPv4LinkRoute, let ipv4Gateway { + logger?.debug("v4 gateway \(ipv4Gateway) is outside subnet \(ipv4Address), adding a route first") + } + if needsIPv6LinkRoute, let ipv6Gateway, let ipv6Address { + logger?.debug("v6 gateway \(ipv6Gateway) is outside subnet \(ipv6Address), adding a route first") + } + + if needsIPv4LinkRoute || needsIPv6LinkRoute { + try await routeAddLink( + name: name, + route: .init( + ipv4Destination: needsIPv4LinkRoute ? ipv4Gateway : nil, + ipv4Source: needsIPv4LinkRoute ? ipv4Address.address : nil, + ipv6Destination: needsIPv6LinkRoute ? ipv6Gateway : nil, + ipv6Source: needsIPv6LinkRoute ? ipv6Address?.address : nil + ) + ) + } + + if ipv4Gateway == nil && ipv6Gateway == nil { + logger?.debug("no gateway for \(name)") + } + try await routeAddDefault( + name: name, + route: .init(ipv4Gateway: ipv4Gateway, ipv6Gateway: ipv6Gateway) + ) + } +} diff --git a/Sources/Containerization/VirtualMachineAgent.swift b/Sources/Containerization/VirtualMachineAgent.swift index 9ded5d7f..88c641ae 100644 --- a/Sources/Containerization/VirtualMachineAgent.swift +++ b/Sources/Containerization/VirtualMachineAgent.swift @@ -67,9 +67,9 @@ public protocol VirtualMachineAgent: Sendable { // Networking func up(name: String, mtu: UInt32?) async throws func down(name: String) async throws - func addressAdd(name: String, ipv4Address: CIDRv4) async throws - func routeAddLink(name: String, dstIPv4Addr: IPv4Address, srcIPv4Addr: IPv4Address?) async throws - func routeAddDefault(name: String, ipv4Gateway: IPv4Address?) async throws + func addressAdd(name: String, address: InterfaceAddress) async throws + func routeAddLink(name: String, route: LinkRoute) async throws + func routeAddDefault(name: String, route: DefaultRoute) async throws func configureDNS(config: DNS, location: String) async throws func configureHosts(config: Hosts, location: String) async throws diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index 92e6ee8d..fd49f095 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -397,33 +397,50 @@ extension Vminitd { } /// Add an IP address to the sandbox's network interfaces. - public func addressAdd(name: String, ipv4Address: CIDRv4) async throws { + public func addressAdd(name: String, address: InterfaceAddress) async throws { _ = try await client.ipAddrAdd( .with { $0.interface = name - $0.ipv4Address = ipv4Address.description + $0.ipv4Address = address.ipv4Address.description + if let ipv6Address = address.ipv6Address { + $0.ipv6Address = ipv6Address.description + } }) } - /// Add a route in the sandbox's environment. - public func routeAddLink(name: String, dstIPv4Addr: IPv4Address, srcIPv4Addr: IPv4Address? = nil) async throws { - let dstCIDR = "\(dstIPv4Addr.description)/32" + /// Add a link-scoped route in the sandbox's environment, used to install an + /// on-link host route (a /32 for v4, /128 for v6) to a gateway that lives + /// outside the interface's subnet so the kernel will accept the default route. + /// `route.ipv4Destination`/`route.ipv6Destination` carry the + /// gateway address; the wire format is a CIDR string with the per-family host prefix appended. + public func routeAddLink(name: String, route: LinkRoute) async throws { _ = try await client.ipRouteAddLink( .with { $0.interface = name - $0.dstIpv4Addr = dstCIDR - if let srcIPv4Addr { - $0.srcIpv4Addr = srcIPv4Addr.description + if let ipv4Destination = route.ipv4Destination { + $0.dstIpv4Addr = "\(ipv4Destination.description)/32" + } + if let ipv4Source = route.ipv4Source { + $0.srcIpv4Addr = ipv4Source.description + } + if let ipv6Destination = route.ipv6Destination { + $0.dstIpv6Addr = "\(ipv6Destination.description)/128" + } + if let ipv6Source = route.ipv6Source { + $0.srcIpv6Addr = ipv6Source.description } }) } /// Set the default route in the sandbox's environment. - public func routeAddDefault(name: String, ipv4Gateway: IPv4Address?) async throws { + public func routeAddDefault(name: String, route: DefaultRoute) async throws { _ = try await client.ipRouteAddDefault( .with { $0.interface = name - $0.ipv4Gateway = ipv4Gateway?.description ?? "" + $0.ipv4Gateway = route.ipv4Gateway?.description ?? "" + if let ipv6Gateway = route.ipv6Gateway { + $0.ipv6Gateway = ipv6Gateway.description + } }) } diff --git a/Sources/Containerization/VmnetNetwork.swift b/Sources/Containerization/VmnetNetwork.swift index 28ea32a8..86ec92cd 100644 --- a/Sources/Containerization/VmnetNetwork.swift +++ b/Sources/Containerization/VmnetNetwork.swift @@ -31,39 +31,82 @@ public struct VmnetNetwork: Network { /// The IPv4 subnet of this network. public let subnet: CIDRv4 + /// The IPv6 prefix of this network. + public let prefixV6: CIDRv6? + /// The IPv4 gateway address of this network. public var ipv4Gateway: IPv4Address { subnet.gateway } - struct Allocator: Sendable { - private let addressAllocator: any AddressAllocator - private let cidr: CIDRv4 - private var allocations: [String: UInt32] + /// The IPv6 gateway address of this network, if a prefix exists. + public var ipv6Gateway: IPv6Address? { + prefixV6?.gateway + } - init(cidr: CIDRv4) throws { - self.cidr = cidr + struct Allocator: Sendable { + private let indexAllocatorV4: any AddressAllocator + private let indexAllocatorV6: (any AddressAllocator)? + private let cidrV4: CIDRv4 + private let cidrV6: CIDRv6? + private var allocations: [String: (v4: UInt32, v6: UInt32?)] + + init(cidrV4: CIDRv4, cidrV6: CIDRv6?) throws { + self.cidrV4 = cidrV4 + self.cidrV6 = cidrV6 self.allocations = .init() - let size = Int(cidr.upper.value - cidr.lower.value - 3) - self.addressAllocator = try UInt32.rotatingAllocator( - lower: cidr.lower.value + 2, - size: UInt32(size) + let v4Size = Int(cidrV4.upper.value - cidrV4.lower.value - 3) + self.indexAllocatorV4 = try UInt32.rotatingAllocator( + lower: cidrV4.lower.value + 2, + size: UInt32(v4Size) ) + if cidrV6 != nil { + // Independent v6 allocator. The host portion is sourced from a + // UInt32 index regardless of prefix length, and we never need + // more v6 entries than v4 can serve. + self.indexAllocatorV6 = try UInt32.rotatingAllocator( + lower: 2, + size: UInt32(v4Size) + ) + } else { + self.indexAllocatorV6 = nil + } } - mutating func allocate(_ id: String) throws -> CIDRv4 { + mutating func allocate(_ id: String) throws -> (CIDRv4, CIDRv6?) { if allocations[id] != nil { throw ContainerizationError(.exists, message: "allocation with id \(id) already exists") } - let index = try addressAllocator.allocate() - allocations[id] = index - let ip = IPv4Address(index) - return try CIDRv4(ip, prefix: cidr.prefix) + let v4Index = try indexAllocatorV4.allocate() + let v4 = try CIDRv4(IPv4Address(v4Index), prefix: cidrV4.prefix) + + var v6Index: UInt32? = nil + let v6: CIDRv6? + if let indexAllocatorV6, let cidrV6 { + do { + let idx = try indexAllocatorV6.allocate() + v6Index = idx + let v6Value = (cidrV6.address.value & cidrV6.prefix.prefixMask128) | UInt128(idx) + v6 = try CIDRv6(IPv6Address(v6Value), prefix: cidrV6.prefix) + } catch { + // Roll back v4 so the pair stays atomic. + try? indexAllocatorV4.release(v4Index) + throw error + } + } else { + v6 = nil + } + + allocations[id] = (v4: v4Index, v6: v6Index) + return (v4, v6) } mutating func release(_ id: String) throws { - if let index = self.allocations[id] { - try addressAllocator.release(index) + if let entry = self.allocations[id] { + try indexAllocatorV4.release(entry.v4) + if let v6Index = entry.v6 { + try indexAllocatorV6?.release(v6Index) + } allocations.removeValue(forKey: id) } } @@ -73,6 +116,8 @@ public struct VmnetNetwork: Network { public struct Interface: Containerization.Interface, VZInterface, Sendable { public let ipv4Address: CIDRv4 public let ipv4Gateway: IPv4Address? + public let ipv6Address: CIDRv6? + public let ipv6Gateway: IPv6Address? public let macAddress: MACAddress? public let mtu: UInt32 @@ -83,11 +128,15 @@ public struct VmnetNetwork: Network { reference: vmnet_network_ref, ipv4Address: CIDRv4, ipv4Gateway: IPv4Address? = nil, + ipv6Address: CIDRv6? = nil, + ipv6Gateway: IPv6Address? = nil, macAddress: MACAddress? = nil, mtu: UInt32 = 1500 ) { self.ipv4Address = ipv4Address self.ipv4Gateway = ipv4Gateway + self.ipv6Address = ipv6Address + self.ipv6Gateway = ipv6Gateway self.macAddress = macAddress self.mtu = mtu self.reference = reference @@ -110,8 +159,13 @@ public struct VmnetNetwork: Network { /// Creates a new network. /// - Parameters: /// - mode: The vmnet operating mode. Defaults to `.VMNET_SHARED_MODE`. - /// - subnet: The subnet to use for this network. - public init(mode: vmnet.operating_modes_t = .VMNET_SHARED_MODE, subnet: CIDRv4? = nil) throws { + /// - subnetV4: The IPv4 subnet to use for this network. + /// - prefixV6: The IPv6 prefix to use for this network. + public init( + mode: vmnet.operating_modes_t = .VMNET_SHARED_MODE, + subnet: CIDRv4? = nil, + prefixV6: CIDRv6? = nil + ) throws { var status: vmnet_return_t = .VMNET_FAILURE guard let config = vmnet_network_configuration_create(mode, &status) else { throw ContainerizationError(.unsupported, message: "failed to create vmnet config with status \(status)") @@ -120,39 +174,38 @@ public struct VmnetNetwork: Network { vmnet_network_configuration_disable_dhcp(config) if let subnet { - try Self.configureSubnet(config, subnet: subnet) + try Self.configureSubnetV4(config, subnetV4: subnet) + } + if let prefixV6 { + try Self.configurePrefixV6(config, prefixV6: prefixV6) } guard let ref = vmnet_network_create(config, &status), status == .VMNET_SUCCESS else { throw ContainerizationError(.unsupported, message: "failed to create vmnet network with status \(status)") } - let cidr = try Self.getSubnet(ref) + let cidrV4 = try Self.getSubnetV4(ref) + let cidrV6 = Self.getPrefixV6(ref) - self.allocator = try .init(cidr: cidr) - self.subnet = cidr + self.allocator = try .init(cidrV4: cidrV4, cidrV6: cidrV6) + self.subnet = cidrV4 + self.prefixV6 = cidrV6 self.reference = ref } - /// Returns a new interface for use with a container. + /// Returns a new interface for use with a container. Allocates an IPv4 + /// address from the network's subnet, and — when the network has an IPv6 + /// prefix — an IPv6 address from that prefix. The two allocations are + /// independent. /// - Parameter id: The container ID. public mutating func createInterface(_ id: String) throws -> Containerization.Interface? { - let ipv4Address = try allocator.allocate(id) + let (v4, v6) = try allocator.allocate(id) return Self.Interface( reference: self.reference, - ipv4Address: ipv4Address, + ipv4Address: v4, ipv4Gateway: self.ipv4Gateway, - ) - } - - /// Returns a new interface without a default gateway route. - /// Use this for secondary interfaces where another interface already provides the default route. - /// - Parameter id: The container ID. - public mutating func createInterfaceWithoutGateway(_ id: String) throws -> Containerization.Interface? { - let ipv4Address = try allocator.allocate(id) - return Self.Interface( - reference: self.reference, - ipv4Address: ipv4Address, + ipv6Address: v6, + ipv6Gateway: self.ipv6Gateway ) } @@ -161,22 +214,37 @@ public struct VmnetNetwork: Network { /// - id: The container ID. /// - mtu: The MTU for the interface. public mutating func createInterface(_ id: String, mtu: UInt32) throws -> Containerization.Interface? { - let ipv4Address = try allocator.allocate(id) + let (v4, v6) = try allocator.allocate(id) return Self.Interface( reference: self.reference, - ipv4Address: ipv4Address, + ipv4Address: v4, ipv4Gateway: self.ipv4Gateway, + ipv6Address: v6, + ipv6Gateway: self.ipv6Gateway, mtu: mtu ) } + /// Returns a new interface without a default gateway route. Useful for + /// secondary interfaces where another interface already provides the + /// default route. + /// - Parameter id: The container ID. + public mutating func createInterfaceWithoutGateway(_ id: String) throws -> Containerization.Interface? { + let (v4, v6) = try allocator.allocate(id) + return Self.Interface( + reference: self.reference, + ipv4Address: v4, + ipv6Address: v6 + ) + } + /// Performs cleanup of an interface. /// - Parameter id: The container ID. public mutating func releaseInterface(_ id: String) throws { try allocator.release(id) } - private static func getSubnet(_ ref: vmnet_network_ref) throws -> CIDRv4 { + private static func getSubnetV4(_ ref: vmnet_network_ref) throws -> CIDRv4 { var subnet = in_addr() var mask = in_addr() vmnet_network_get_ipv4_subnet(ref, &subnet, &mask) @@ -190,18 +258,43 @@ public struct VmnetNetwork: Network { return try CIDRv4(lower: lower, upper: upper) } - private static func configureSubnet(_ config: vmnet_network_configuration_ref, subnet: CIDRv4) throws { - let gateway = subnet.gateway + private static func configureSubnetV4(_ config: vmnet_network_configuration_ref, subnetV4: CIDRv4) throws { + let gateway = subnetV4.gateway var ga = in_addr() inet_pton(AF_INET, gateway.description, &ga) - let mask = IPv4Address(subnet.prefix.prefixMask32) + let mask = IPv4Address(subnetV4.prefix.prefixMask32) var ma = in_addr() inet_pton(AF_INET, mask.description, &ma) guard vmnet_network_configuration_set_ipv4_subnet(config, &ga, &ma) == .VMNET_SUCCESS else { - throw ContainerizationError(.internalError, message: "failed to set subnet \(subnet) for network") + throw ContainerizationError(.internalError, message: "failed to set IPv4 subnet \(subnetV4) for network") + } + } + + private static func getPrefixV6(_ ref: vmnet_network_ref) -> CIDRv6? { + var p = in6_addr() + var len: UInt8 = 0 + vmnet_network_get_ipv6_prefix(ref, &p, &len) + + guard len > 0, let prefix = Prefix.ipv6(len) else { + return nil + } + + let bytes: [UInt8] = withUnsafeBytes(of: p) { Array($0) } + guard let address = try? IPv6Address(bytes) else { + return nil + } + return try? CIDRv6(address, prefix: prefix) + } + + private static func configurePrefixV6(_ config: vmnet_network_configuration_ref, prefixV6: CIDRv6) throws { + var p = in6_addr() + inet_pton(AF_INET6, prefixV6.lower.description, &p) + + guard vmnet_network_configuration_set_ipv6_prefix(config, &p, prefixV6.prefix.length) == .VMNET_SUCCESS else { + throw ContainerizationError(.internalError, message: "failed to set IPv6 prefix \(prefixV6) for network") } } } diff --git a/Sources/ContainerizationExtras/NetworkConfiguration.swift b/Sources/ContainerizationExtras/NetworkConfiguration.swift new file mode 100644 index 00000000..a72a5f45 --- /dev/null +++ b/Sources/ContainerizationExtras/NetworkConfiguration.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +/// A network interface's addresses. +public struct InterfaceAddress: Sendable, Hashable { + public var ipv4Address: CIDRv4 + public var ipv6Address: CIDRv6? + + public init(ipv4Address: CIDRv4, ipv6Address: CIDRv6? = nil) { + self.ipv4Address = ipv4Address + self.ipv6Address = ipv6Address + } +} + +/// A link-scoped route — a destination directly reachable on an interface. +public struct LinkRoute: Sendable, Hashable { + public var ipv4Destination: IPv4Address? + public var ipv4Source: IPv4Address? + public var ipv6Destination: IPv6Address? + public var ipv6Source: IPv6Address? + + public init( + ipv4Destination: IPv4Address? = nil, + ipv4Source: IPv4Address? = nil, + ipv6Destination: IPv6Address? = nil, + ipv6Source: IPv6Address? = nil + ) { + self.ipv4Destination = ipv4Destination + self.ipv4Source = ipv4Source + self.ipv6Destination = ipv6Destination + self.ipv6Source = ipv6Source + } +} + +/// The default-route gateway for a network interface. +public struct DefaultRoute: Sendable, Hashable { + public var ipv4Gateway: IPv4Address? + public var ipv6Gateway: IPv6Address? + + public init(ipv4Gateway: IPv4Address? = nil, ipv6Gateway: IPv6Address? = nil) { + self.ipv4Gateway = ipv4Gateway + self.ipv6Gateway = ipv6Gateway + } +} diff --git a/Sources/ContainerizationNetlink/NetlinkSession.swift b/Sources/ContainerizationNetlink/NetlinkSession.swift index 19478f0b..a586fc74 100644 --- a/Sources/ContainerizationNetlink/NetlinkSession.swift +++ b/Sources/ContainerizationNetlink/NetlinkSession.swift @@ -259,6 +259,53 @@ public struct NetlinkSession { } } + /// Adds an IPv6 address to an interface. + /// - Parameters: + /// - interface: The name of the interface. + /// - ipv6Address: The CIDRv6 address describing the interface IP and subnet prefix length. + public func addressAdd(interface: String, ipv6Address: CIDRv6) throws { + let interfaceIndex = try getInterfaceIndex(interface) + + let ipAddressBytes = ipv6Address.address.bytes + let addressAttrSize = RTAttribute.size + MemoryLayout.size * ipAddressBytes.count + let requestSize = NetlinkMessageHeader.size + AddressInfo.size + addressAttrSize + var requestBuffer = [UInt8](repeating: 0, count: requestSize) + var requestOffset = 0 + + let header = NetlinkMessageHeader( + len: UInt32(requestBuffer.count), + type: NetlinkType.RTM_NEWADDR, + flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK | NetlinkFlags.NLM_F_EXCL + | NetlinkFlags.NLM_F_CREATE, + seq: 0, + pid: socket.pid) + requestOffset = try header.appendBuffer(&requestBuffer, offset: requestOffset) + + let requestInfo = AddressInfo( + family: UInt8(AddressFamily.AF_INET6), + prefixLength: ipv6Address.prefix.length, + flags: AddressFlags.IFA_F_PERMANENT | AddressFlags.IFA_F_NODAD, + scope: NetlinkScope.RT_SCOPE_UNIVERSE, + index: UInt32(interfaceIndex)) + requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset) + + let ipAddressAttr = RTAttribute(len: UInt16(addressAttrSize), type: AddressAttributeType.IFA_ADDRESS) + requestOffset = try ipAddressAttr.appendBuffer(&requestBuffer, offset: requestOffset) + guard let requestOffset = requestBuffer.copyIn(buffer: ipAddressBytes, offset: requestOffset) else { + throw BindError.sendMarshalFailure(type: "RTAttribute", field: "IFA_ADDRESS") + } + + guard requestOffset == requestSize else { + throw Error.unexpectedOffset(offset: requestOffset, size: requestSize) + } + + try sendRequest(buffer: &requestBuffer) + let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_NEWADDR) { AddressInfo() } + guard infos.count == 0 else { + throw Error.unexpectedResultSet(count: infos.count, expected: 0) + } + } + /// Adds an IPv4 route to an interface. /// - Parameters: /// - interface: The name of the interface. @@ -421,6 +468,160 @@ public struct NetlinkSession { } } + /// Adds an IPv6 route to an interface. Used to install an on-link host + /// route (typically a /128) to a gateway that lives outside the interface's + /// subnet, so the kernel will accept the v6 default route. The chosen + /// `proto STATIC, scope LINK` matches what `iproute2` emits for explicit + /// `ip -6 route add /128 dev `. + /// - Parameters: + /// - interface: The name of the interface. + /// - dstIpv6Addr: The CIDRv6 address describing the destination network and prefix length. + /// - srcIpv6Addr: The source IPv6 address to route from. + public func routeAdd( + interface: String, + dstIpv6Addr: CIDRv6, + srcIpv6Addr: IPv6Address? + ) throws { + let interfaceIndex = try getInterfaceIndex(interface) + + let dstAddrBytes = dstIpv6Addr.address.bytes + let dstAddrAttrSize = RTAttribute.size + dstAddrBytes.count + let srcAddrAttrSize: Int + if let srcIpv6Addr { + let srcAddrBytes = srcIpv6Addr.bytes + srcAddrAttrSize = RTAttribute.size + srcAddrBytes.count + } else { + srcAddrAttrSize = 0 + } + let interfaceAttrSize = RTAttribute.size + MemoryLayout.size + let requestSize = + NetlinkMessageHeader.size + RouteInfo.size + dstAddrAttrSize + srcAddrAttrSize + interfaceAttrSize + var requestBuffer = [UInt8](repeating: 0, count: requestSize) + var requestOffset = 0 + + let header = NetlinkMessageHeader( + len: UInt32(requestBuffer.count), + type: NetlinkType.RTM_NEWROUTE, + flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK | NetlinkFlags.NLM_F_EXCL + | NetlinkFlags.NLM_F_CREATE, + pid: socket.pid) + requestOffset = try header.appendBuffer(&requestBuffer, offset: requestOffset) + + let requestInfo = RouteInfo( + family: UInt8(AddressFamily.AF_INET6), + dstLen: dstIpv6Addr.prefix.length, + srcLen: 0, + tos: 0, + table: RouteTable.MAIN, + proto: RouteProtocol.STATIC, + scope: RouteScope.LINK, + type: RouteType.UNICAST, + flags: 0) + requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset) + + let dstAddrAttr = RTAttribute(len: UInt16(dstAddrAttrSize), type: RouteAttributeType.DST) + requestOffset = try dstAddrAttr.appendBuffer(&requestBuffer, offset: requestOffset) + guard var requestOffset = requestBuffer.copyIn(buffer: dstAddrBytes, offset: requestOffset) else { + throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_DST") + } + + if let srcIpv6Addr { + let srcAddrBytes = srcIpv6Addr.bytes + let srcAddrAttr = RTAttribute(len: UInt16(srcAddrAttrSize), type: RouteAttributeType.PREFSRC) + requestOffset = try srcAddrAttr.appendBuffer(&requestBuffer, offset: requestOffset) + guard let newOffset = requestBuffer.copyIn(buffer: srcAddrBytes, offset: requestOffset) else { + throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_PREFSRC") + } + requestOffset = newOffset + } + + let interfaceAttr = RTAttribute(len: UInt16(interfaceAttrSize), type: RouteAttributeType.OIF) + requestOffset = try interfaceAttr.appendBuffer(&requestBuffer, offset: requestOffset) + guard + let requestOffset = requestBuffer.copyIn( + as: UInt32.self, + value: UInt32(interfaceIndex), + offset: requestOffset) + else { + throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_OIF") + } + + guard requestOffset == requestSize else { + throw Error.unexpectedOffset(offset: requestOffset, size: requestSize) + } + + try sendRequest(buffer: &requestBuffer) + let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_NEWROUTE) { AddressInfo() } + guard infos.count == 0 else { + throw Error.unexpectedResultSet(count: infos.count, expected: 0) + } + } + + /// Adds a default IPv6 route to an interface. + /// - Parameters: + /// - interface: The name of the interface. + /// - ipv6Gateway: The gateway address. + public func routeAddDefault( + interface: String, + ipv6Gateway: IPv6Address + ) throws { + let gatewayBytes = ipv6Gateway.bytes + let gatewaySize = RTAttribute.size + gatewayBytes.count + + let interfaceAttrSize = RTAttribute.size + MemoryLayout.size + let interfaceIndex = try getInterfaceIndex(interface) + let requestSize = NetlinkMessageHeader.size + RouteInfo.size + gatewaySize + interfaceAttrSize + + var requestBuffer = [UInt8](repeating: 0, count: requestSize) + var requestOffset = 0 + + let header = NetlinkMessageHeader( + len: UInt32(requestBuffer.count), + type: NetlinkType.RTM_NEWROUTE, + flags: NetlinkFlags.NLM_F_REQUEST | NetlinkFlags.NLM_F_ACK | NetlinkFlags.NLM_F_EXCL + | NetlinkFlags.NLM_F_CREATE, + pid: socket.pid) + requestOffset = try header.appendBuffer(&requestBuffer, offset: requestOffset) + + let requestInfo = RouteInfo( + family: UInt8(AddressFamily.AF_INET6), + dstLen: 0, + srcLen: 0, + tos: 0, + table: RouteTable.MAIN, + proto: RouteProtocol.BOOT, + scope: RouteScope.UNIVERSE, + type: RouteType.UNICAST, + flags: 0) + requestOffset = try requestInfo.appendBuffer(&requestBuffer, offset: requestOffset) + + let dstAddrAttr = RTAttribute(len: UInt16(gatewaySize), type: RouteAttributeType.GATEWAY) + requestOffset = try dstAddrAttr.appendBuffer(&requestBuffer, offset: requestOffset) + guard var requestOffset = requestBuffer.copyIn(buffer: gatewayBytes, offset: requestOffset) else { + throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_GATEWAY") + } + let interfaceAttr = RTAttribute(len: UInt16(interfaceAttrSize), type: RouteAttributeType.OIF) + requestOffset = try interfaceAttr.appendBuffer(&requestBuffer, offset: requestOffset) + guard + let requestOffset = requestBuffer.copyIn( + as: UInt32.self, + value: UInt32(interfaceIndex), + offset: requestOffset) + else { + throw BindError.sendMarshalFailure(type: "RTAttribute", field: "RTA_OIF") + } + + guard requestOffset == requestSize else { + throw Error.unexpectedOffset(offset: requestOffset, size: requestSize) + } + + try sendRequest(buffer: &requestBuffer) + let (infos, _) = try parseResponse(infoType: NetlinkType.RTM_NEWROUTE) { AddressInfo() } + guard infos.count == 0 else { + throw Error.unexpectedResultSet(count: infos.count, expected: 0) + } + } + private func getInterfaceName(_ interface: String) throws -> [UInt8] { guard let interfaceNameData = interface.data(using: .utf8) else { throw BindError.sendMarshalFailure(type: "String", field: "interface") diff --git a/Sources/ContainerizationNetlink/Types.swift b/Sources/ContainerizationNetlink/Types.swift index 2691fd1c..5c0a0e8c 100644 --- a/Sources/ContainerizationNetlink/Types.swift +++ b/Sources/ContainerizationNetlink/Types.swift @@ -101,6 +101,11 @@ struct AddressAttributeType { static let IFA_LOCAL: UInt16 = 2 } +struct AddressFlags { + static let IFA_F_NODAD: UInt8 = 0x02 + static let IFA_F_PERMANENT: UInt8 = 0x80 +} + struct RouteTable { static let MAIN: UInt8 = 254 } diff --git a/Sources/Integration/ContainerTests.swift b/Sources/Integration/ContainerTests.swift index eda29b75..62c37918 100644 --- a/Sources/Integration/ContainerTests.swift +++ b/Sources/Integration/ContainerTests.swift @@ -4549,6 +4549,447 @@ extension IntegrationSuite { } } + @available(macOS 26.0, *) + func testNetworkingEnabledIPv6() async throws { + let id = "test-networking-enabled-ipv6" + let bs = try await bootstrap(id) + + let network = try VmnetNetwork() + var manager = try ContainerManager(vmm: bs.vmm, network: network) + defer { + try? manager.delete(id) + } + + let buffer = BufferWriter() + let container = try await manager.create( + id, + image: bs.image, + rootfs: bs.rootfs + ) { config in + config.process.arguments = ["ip", "-6", "addr", "show", "eth0", "scope", "global"] + config.process.stdout = buffer + config.bootLog = bs.bootLog + } + + do { + try await container.create() + try await container.start() + + let status = try await container.wait() + try await container.stop() + + guard status.exitCode == 0 else { + throw IntegrationError.assert(msg: "ip -6 addr show failed with status \(status)") + } + + guard let output = String(data: buffer.data, encoding: .utf8) else { + throw IntegrationError.assert(msg: "failed to convert output to UTF8") + } + + guard output.contains("inet6 fd") else { + throw IntegrationError.assert( + msg: "expected a global-scope IPv6 address on eth0, got: \(output)") + } + } catch { + try? await container.stop() + throw error + } + } + + @available(macOS 26.0, *) + func testIPv6AddressAdd() async throws { + let id = "test-ipv6-address" + let bs = try await bootstrap(id) + + // Pin the v6 prefix so the allocator's first allocation yields fd00::2. + var network = try VmnetNetwork(prefixV6: try CIDRv6("fd00::/64")) + defer { + try? network.releaseInterface(id) + } + + guard let interface = try network.createInterface(id) else { + throw IntegrationError.assert(msg: "failed to create network interface") + } + + let buffer = BufferWriter() + let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in + config.process.arguments = ["sleep", "100"] + config.interfaces = [interface] + config.bootLog = bs.bootLog + } + + do { + try await container.create() + try await container.start() + + // Check that the IPv6 address was assigned to eth0. + let exec = try await container.exec("check-ipv6") { config in + config.arguments = ["ip", "-6", "addr", "show", "eth0"] + config.stdout = buffer + } + + try await exec.start() + let status = try await exec.wait() + try await exec.delete() + + guard status.exitCode == 0 else { + throw IntegrationError.assert(msg: "ip -6 addr show failed with status \(status)") + } + + guard let output = String(data: buffer.data, encoding: .utf8) else { + throw IntegrationError.assert(msg: "failed to convert output to UTF8") + } + + guard output.contains("fd00::2") else { + throw IntegrationError.assert( + msg: "expected fd00::2 in output, got: \(output)") + } + + try await container.kill(.kill) + try await container.wait() + try await container.stop() + } catch { + try? await container.stop() + throw error + } + } + + @available(macOS 26.0, *) + func testIPv6DefaultRoute() async throws { + let id = "test-ipv6-default-route" + let bs = try await bootstrap(id) + + // Pin the network's v6 prefix so the gateway is deterministically fd00::1 + // and the allocator's first allocation yields fd00::2. + var network = try VmnetNetwork(prefixV6: try CIDRv6("fd00::/64")) + defer { + try? network.releaseInterface(id) + } + + guard let interface = try network.createInterface(id) else { + throw IntegrationError.assert(msg: "failed to create network interface") + } + + let buffer = BufferWriter() + let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in + config.process.arguments = ["sleep", "100"] + config.interfaces = [interface] + config.bootLog = bs.bootLog + } + + do { + try await container.create() + try await container.start() + + // Inspect IPv6 routes inside the container. + let exec = try await container.exec("check-v6-route") { config in + config.arguments = ["ip", "-6", "route", "show"] + config.stdout = buffer + } + + try await exec.start() + let status = try await exec.wait() + try await exec.delete() + + guard status.exitCode == 0 else { + throw IntegrationError.assert(msg: "ip -6 route show failed with status \(status)") + } + + guard let output = String(data: buffer.data, encoding: .utf8) else { + throw IntegrationError.assert(msg: "failed to convert output to UTF8") + } + + // The default v6 route must point at the gateway we configured, on eth0. + guard output.contains("default via fd00::1 dev eth0") else { + throw IntegrationError.assert( + msg: "expected 'default via fd00::1 dev eth0' in v6 routes, got: \(output)") + } + + try await container.kill(.kill) + try await container.wait() + try await container.stop() + } catch { + try? await container.stop() + throw error + } + } + + @available(macOS 26.0, *) + func testIPv6GatewayOutsideSubnet() async throws { + let id = "test-ipv6-gateway-outside-subnet" + let bs = try await bootstrap(id) + + // Address in fd00::/120, gateway in fd01::/120 — subnets don't overlap, so the + // LinuxContainer wiring must add a /128 link route to the gateway before the + // default route. The two prefixes are independent so we drive this directly + // via NATInterface rather than the VmnetNetwork allocator (which always + // derives the gateway from the network's own prefix). + let interface = NATInterface( + ipv4Address: try CIDRv4("192.0.2.2/24"), + ipv4Gateway: try IPv4Address("192.0.2.1"), + ipv6Address: try CIDRv6("fd00::2/120"), + ipv6Gateway: try IPv6Address("fd01::1")) + + let buffer = BufferWriter() + let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in + config.process.arguments = ["sleep", "100"] + config.interfaces = [interface] + config.bootLog = bs.bootLog + } + + do { + try await container.create() + try await container.start() + + let exec = try await container.exec("check-v6-routes") { config in + config.arguments = ["ip", "-6", "route", "show"] + config.stdout = buffer + } + + try await exec.start() + let status = try await exec.wait() + try await exec.delete() + + guard status.exitCode == 0 else { + throw IntegrationError.assert(msg: "ip -6 route show failed with status \(status)") + } + + guard let output = String(data: buffer.data, encoding: .utf8) else { + throw IntegrationError.assert(msg: "failed to convert output to UTF8") + } + + // Both the link-scoped route to the gateway AND the default via that gateway + // must be present. Without the link route, the kernel would refuse the default. + // Match the link route on a line that starts with the gateway address (no "via") + // so it can't be satisfied by a substring of the default-via line. + let lines = output.split(separator: "\n").map(String.init) + let hasLinkRoute = lines.contains { $0.hasPrefix("fd01::1 ") && $0.contains("dev eth0") && !$0.contains("via") } + guard hasLinkRoute else { + throw IntegrationError.assert( + msg: "expected an on-link route 'fd01::1 ... dev eth0' (no 'via') in v6 routes, got: \(output)") + } + guard output.contains("default via fd01::1 dev eth0") else { + throw IntegrationError.assert( + msg: "expected 'default via fd01::1 dev eth0' in v6 routes, got: \(output)") + } + + try await container.kill(.kill) + try await container.wait() + try await container.stop() + } catch { + try? await container.stop() + throw error + } + } + + @available(macOS 26.0, *) + func testIPv6OnlyDefaultRoute() async throws { + let id = "test-ipv6-only-default-route" + let bs = try await bootstrap(id) + + // Construct a NATInterface with a nil IPv4 gateway and a v6 gateway, so + // LinuxContainer takes the no-v4-gateway branch in setupInterface. The v4 + // address comes from TEST-NET-1; nothing in the test traffics over v4. + let interface = NATInterface( + ipv4Address: try CIDRv4("192.0.2.2/24"), + ipv4Gateway: nil, + ipv6Address: try CIDRv6("fd00::2/64"), + ipv6Gateway: try IPv6Address("fd00::1")) + + let buffer = BufferWriter() + let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in + config.process.arguments = ["sleep", "100"] + config.interfaces = [interface] + config.bootLog = bs.bootLog + } + + do { + try await container.create() + try await container.start() + + let exec = try await container.exec("check-v6-route") { config in + config.arguments = ["ip", "-6", "route", "show"] + config.stdout = buffer + } + + try await exec.start() + let status = try await exec.wait() + try await exec.delete() + + guard status.exitCode == 0 else { + throw IntegrationError.assert(msg: "ip -6 route show failed with status \(status)") + } + + guard let output = String(data: buffer.data, encoding: .utf8) else { + throw IntegrationError.assert(msg: "failed to convert output to UTF8") + } + + guard output.contains("default via fd00::1 dev eth0") else { + throw IntegrationError.assert( + msg: "expected 'default via fd00::1 dev eth0' in v6 routes when ipv4Gateway is nil, got: \(output)") + } + + try await container.kill(.kill) + try await container.wait() + try await container.stop() + } catch { + try? await container.stop() + throw error + } + } + + @available(macOS 26.0, *) + func testIPv6OnlyGatewayOutsideSubnet() async throws { + let id = "test-ipv6-only-gateway-outside-subnet" + let bs = try await bootstrap(id) + + // No v4 gateway AND v6 gateway is outside the v6 subnet. Exercises + // setupInterface's "no v4 gateway, but v6 link route required before + // v6 default route" branch — the exact bug the helper extraction fixed. + let interface = NATInterface( + ipv4Address: try CIDRv4("192.0.2.2/24"), + ipv4Gateway: nil, + ipv6Address: try CIDRv6("fd00::2/120"), + ipv6Gateway: try IPv6Address("fd01::1")) + + let buffer = BufferWriter() + let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in + config.process.arguments = ["sleep", "100"] + config.interfaces = [interface] + config.bootLog = bs.bootLog + } + + do { + try await container.create() + try await container.start() + + let exec = try await container.exec("check-v6-routes") { config in + config.arguments = ["ip", "-6", "route", "show"] + config.stdout = buffer + } + + try await exec.start() + let status = try await exec.wait() + try await exec.delete() + + guard status.exitCode == 0 else { + throw IntegrationError.assert(msg: "ip -6 route show failed with status \(status)") + } + + guard let output = String(data: buffer.data, encoding: .utf8) else { + throw IntegrationError.assert(msg: "failed to convert output to UTF8") + } + + // Both the on-link route to the gateway AND the default via it must be present. + // Without the link route the kernel rejects the default — that was the bug. + let lines = output.split(separator: "\n").map(String.init) + let hasLinkRoute = lines.contains { $0.hasPrefix("fd01::1 ") && $0.contains("dev eth0") && !$0.contains("via") } + guard hasLinkRoute else { + throw IntegrationError.assert( + msg: "expected an on-link route 'fd01::1 ... dev eth0' (no 'via') in v6 routes, got: \(output)") + } + guard output.contains("default via fd01::1 dev eth0") else { + throw IntegrationError.assert( + msg: "expected 'default via fd01::1 dev eth0' in v6 routes, got: \(output)") + } + + try await container.kill(.kill) + try await container.wait() + try await container.stop() + } catch { + try? await container.stop() + throw error + } + } + + @available(macOS 26.0, *) + func testIPv6DualStack() async throws { + let id = "test-ipv6-dual-stack" + let bs = try await bootstrap(id) + + // Pin the network's v6 prefix so the gateway is deterministically fd00::1 + // and the allocator's first allocation yields fd00::2. + var network = try VmnetNetwork(prefixV6: try CIDRv6("fd00::/64")) + defer { + try? network.releaseInterface(id) + } + + guard let interface = try network.createInterface(id) else { + throw IntegrationError.assert(msg: "failed to create network interface") + } + + // Capture the v4 address vmnet allocated so we can assert it ends up on eth0. + let expectedV4 = interface.ipv4Address.address.description + + let addrBuffer = BufferWriter() + let routeBuffer = BufferWriter() + let container = try LinuxContainer(id, rootfs: bs.rootfs, vmm: bs.vmm) { config in + config.process.arguments = ["sleep", "100"] + config.interfaces = [interface] + config.bootLog = bs.bootLog + } + + do { + try await container.create() + try await container.start() + + // `ip addr show` (no family flag) lists both v4 and v6. + let addrExec = try await container.exec("check-dual-stack-addr") { config in + config.arguments = ["ip", "addr", "show", "eth0"] + config.stdout = addrBuffer + } + try await addrExec.start() + let addrStatus = try await addrExec.wait() + try await addrExec.delete() + + guard addrStatus.exitCode == 0 else { + throw IntegrationError.assert(msg: "ip addr show failed with status \(addrStatus)") + } + + guard let addrOutput = String(data: addrBuffer.data, encoding: .utf8) else { + throw IntegrationError.assert(msg: "failed to convert addr output to UTF8") + } + + guard addrOutput.contains(expectedV4) else { + throw IntegrationError.assert( + msg: "expected v4 address \(expectedV4) on eth0, got: \(addrOutput)") + } + guard addrOutput.contains("fd00::2") else { + throw IntegrationError.assert( + msg: "expected v6 address fd00::2 on eth0, got: \(addrOutput)") + } + + // The dual-stack default routes must both be installed. + let routeExec = try await container.exec("check-dual-stack-route") { config in + config.arguments = ["ip", "-6", "route", "show"] + config.stdout = routeBuffer + } + try await routeExec.start() + let routeStatus = try await routeExec.wait() + try await routeExec.delete() + + guard routeStatus.exitCode == 0 else { + throw IntegrationError.assert(msg: "ip -6 route show failed with status \(routeStatus)") + } + + guard let routeOutput = String(data: routeBuffer.data, encoding: .utf8) else { + throw IntegrationError.assert(msg: "failed to convert route output to UTF8") + } + + guard routeOutput.contains("default via fd00::1 dev eth0") else { + throw IntegrationError.assert( + msg: "expected 'default via fd00::1 dev eth0' in v6 routes, got: \(routeOutput)") + } + + try await container.kill(.kill) + try await container.wait() + try await container.stop() + } catch { + try? await container.stop() + throw error + } + } + func testSysctl() async throws { let id = "test-container-sysctl" diff --git a/Sources/Integration/PodTests.swift b/Sources/Integration/PodTests.swift index 42b43df6..74342013 100644 --- a/Sources/Integration/PodTests.swift +++ b/Sources/Integration/PodTests.swift @@ -17,6 +17,7 @@ import ArgumentParser import Containerization import ContainerizationError +import ContainerizationExtras import ContainerizationOCI import ContainerizationOS import Foundation @@ -2087,4 +2088,60 @@ extension IntegrationSuite { } } } + + @available(macOS 26.0, *) + func testPodIPv6AddressAdd() async throws { + let id = "test-pod-ipv6-address" + let bs = try await bootstrap(id) + + var network = try VmnetNetwork(prefixV6: try CIDRv6("fd00::/64")) + defer { + try? network.releaseInterface(id) + } + + guard let interface = try network.createInterface(id) else { + throw IntegrationError.assert(msg: "failed to create network interface") + } + + let pod = try LinuxPod(id, vmm: bs.vmm) { config in + config.cpus = 4 + config.memoryInBytes = 1024.mib() + config.bootLog = bs.bootLog + config.interfaces = [interface] + } + + try await pod.addContainer("container1", rootfs: bs.rootfs) { config in + config.process.arguments = ["/bin/sleep", "100"] + } + + try await pod.create() + try await pod.startContainer("container1") + + let buffer = BufferWriter() + let exec = try await pod.execInContainer("container1", processID: "check-v6") { config in + config.arguments = ["ip", "-6", "addr", "show", "eth0"] + config.stdout = buffer + } + + try await exec.start() + let status = try await exec.wait() + try await exec.delete() + + try await pod.killContainer("container1", signal: .kill) + try await pod.waitContainer("container1") + try await pod.stop() + + guard status.exitCode == 0 else { + throw IntegrationError.assert(msg: "ip -6 addr show failed with status \(status)") + } + + guard let output = String(data: buffer.data, encoding: .utf8) else { + throw IntegrationError.assert(msg: "failed to convert output to UTF8") + } + + guard output.contains("fd00::2") else { + throw IntegrationError.assert( + msg: "expected fd00::2 on eth0 inside pod container, got: \(output)") + } + } } diff --git a/Sources/Integration/Suite.swift b/Sources/Integration/Suite.swift index 276fce0a..786763c9 100644 --- a/Sources/Integration/Suite.swift +++ b/Sources/Integration/Suite.swift @@ -264,6 +264,14 @@ struct IntegrationSuite: AsyncParsableCommand { Test("container interface custom MTU", testInterfaceMTU), Test("container networking disabled", testNetworkingDisabled), Test("container networking enabled", testNetworkingEnabled), + Test("container networking enabled ipv6", testNetworkingEnabledIPv6), + Test("container IPv6 address", testIPv6AddressAdd), + Test("container IPv6 default route", testIPv6DefaultRoute), + Test("container IPv6 gateway outside subnet", testIPv6GatewayOutsideSubnet), + Test("container IPv6 only default route", testIPv6OnlyDefaultRoute), + Test("container IPv6 only gateway outside subnet", testIPv6OnlyGatewayOutsideSubnet), + Test("container IPv6 dual stack", testIPv6DualStack), + Test("pod IPv6 address", testPodIPv6AddressAdd), ] } return [] diff --git a/Sources/cctl/RunCommand.swift b/Sources/cctl/RunCommand.swift index 3a3d579e..66cf2081 100644 --- a/Sources/cctl/RunCommand.swift +++ b/Sources/cctl/RunCommand.swift @@ -106,7 +106,8 @@ extension Application { id, reference: imageReference, rootfsSizeInBytes: fsSizeInMB.mib(), - readOnly: readOnly + readOnly: readOnly, + networking: true ) { config in config.cpus = cpus config.memoryInBytes = memory.mib() diff --git a/Tests/ContainerizationNetlinkTests/NetlinkSessionTest.swift b/Tests/ContainerizationNetlinkTests/NetlinkSessionTest.swift index d0f3a956..6c31a0ca 100644 --- a/Tests/ContainerizationNetlinkTests/NetlinkSessionTest.swift +++ b/Tests/ContainerizationNetlinkTests/NetlinkSessionTest.swift @@ -290,6 +290,46 @@ struct NetlinkSessionTest { #expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString()) } + @Test func testNetworkAddressAddIPv6() throws { + let mockSocket = try MockNetlinkSocket() + mockSocket.pid = 0xc00c_c00c + + // Lookup interface by name, truncated response with no attributes (not needed at present). + let expectedLookupRequest = + "3400000012000100000000000cc00cc0" // Netlink header (16 B) + + "110000000000000001000000ffffffff" // struct ifinfomsg (16 B) + + "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME ("eth0") + mockSocket.responses.append( + [UInt8]( + hex: + "2000000010000000000000000cc00cc0" // Netlink header (16 B) + + "00000100020000004310010000000000" // struct ifinfomsg (16 B) – no attributes + ) + ) + + // Add IPv6 address to interface. + let expectedAddRequest = + "2c00000014000506000000000cc00cc0" // Netlink header (16 B): len=44 + + "0a40820002000000" // ifaddrmsg (8 B): AF_INET6, /64, flags=PERMANENT|NODAD, ifindex 2 + + "14000100fd000000000000000000000000000001" // RT attr: IFA_ADDRESS fd00::1 + mockSocket.responses.append( + [UInt8]( + hex: + "2400000002000001000000000cc00cc0" // Netlink header (16 B) + + "0000000040000000140005060000000000000000" // nlmsg_err payload (20 B) + ) + ) + + let session = NetlinkSession(socket: mockSocket) + try session.addressAdd(interface: "eth0", ipv6Address: try CIDRv6("fd00::1/64")) + + #expect(mockSocket.requests.count == 2) + #expect(mockSocket.responseIndex == 2) + mockSocket.requests[0][8..<12] = [0, 0, 0, 0] + #expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString()) + #expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString()) + } + @Test func testNetworkRouteAddIpLink() throws { let mockSocket = try MockNetlinkSocket() mockSocket.pid = 0xc00c_c00c @@ -389,6 +429,149 @@ struct NetlinkSessionTest { #expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString()) } + @Test func testNetworkRouteAddIpv6Link() throws { + let mockSocket = try MockNetlinkSocket() + mockSocket.pid = 0xc00c_c00c + + // Lookup interface by name. + let expectedLookupRequest = + "3400000012000100000000000cc00cc0" // Netlink header (16 B) + + "110000000000000001000000ffffffff" // struct ifinfomsg (16 B) + + "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME ("eth0") + mockSocket.responses.append( + [UInt8]( + hex: + "2000000010000000000000000cc00cc0" // Netlink header (16 B) + + "00000100020000004310010000000000" // struct ifinfomsg (16 B) – no attributes + ) + ) + + // Add IPv6 link route with source. + let expectedAddRequest = + "4c00000018000506000000000cc00cc0" // Netlink header (16 B): len=76 + + "0a400000fe04fd0100000000" // struct rtmsg (12 B): AF_INET6, dst/64, + // table=MAIN(0xfe), proto=STATIC(0x04), scope=LINK(0xfd), type=UNICAST(0x01) + + "14000100fd000000000000000000000000000000" // RTA_DST fd00:: + + "14000700fd000000000000000000000000000001" // RTA_PREFSRC fd00::1 + + "0800040002000000" // RTA_OIF ifindex 2 (eth0) + mockSocket.responses.append( + [UInt8]( + hex: + "2400000002000001000000000cc00cc0" // Netlink header (16 B) + + "00000000280000001400050600000000" // nlmsg_err payload (16 B) + + "1f000000" + ) + ) + + let session = NetlinkSession(socket: mockSocket) + try session.routeAdd( + interface: "eth0", + dstIpv6Addr: try CIDRv6("fd00::/64"), + srcIpv6Addr: try IPv6Address("fd00::1") + ) + + #expect(mockSocket.requests.count == 2) + #expect(mockSocket.responseIndex == 2) + mockSocket.requests[0][8..<12] = [0, 0, 0, 0] + #expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString()) + mockSocket.requests[1][8..<12] = [0, 0, 0, 0] + #expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString()) + } + + @Test func testNetworkRouteAddIpv6LinkWithoutSrc() throws { + let mockSocket = try MockNetlinkSocket() + mockSocket.pid = 0xc00c_c00c + + // Lookup interface by name. + let expectedLookupRequest = + "3400000012000100000000000cc00cc0" // Netlink header (16 B) + + "110000000000000001000000ffffffff" // struct ifinfomsg (16 B) + + "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME ("eth0") + mockSocket.responses.append( + [UInt8]( + hex: + "2000000010000000000000000cc00cc0" // Netlink header (16 B) + + "00000100020000004310010000000000" // struct ifinfomsg (16 B) – no attributes + ) + ) + + // Add IPv6 link route without source. + let expectedAddRequest = + "3800000018000506000000000cc00cc0" // Netlink header (16 B): len=56 + + "0a400000fe04fd0100000000" // struct rtmsg (12 B): AF_INET6, dst/64 + + "14000100fd000000000000000000000000000000" // RTA_DST fd00:: + + "0800040002000000" // RTA_OIF ifindex 2 (eth0) + mockSocket.responses.append( + [UInt8]( + hex: + "2400000002000001000000000cc00cc0" // Netlink header (16 B) + + "00000000280000001400050600000000" // nlmsg_err payload (16 B) + + "1f000000" + ) + ) + + let session = NetlinkSession(socket: mockSocket) + try session.routeAdd( + interface: "eth0", + dstIpv6Addr: try CIDRv6("fd00::/64"), + srcIpv6Addr: nil + ) + + #expect(mockSocket.requests.count == 2) + #expect(mockSocket.responseIndex == 2) + mockSocket.requests[0][8..<12] = [0, 0, 0, 0] + #expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString()) + mockSocket.requests[1][8..<12] = [0, 0, 0, 0] + #expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString()) + } + + @Test func testNetworkRouteAddDefaultIpv6() throws { + let mockSocket = try MockNetlinkSocket() + mockSocket.pid = 0xc00c_c00c + + // Lookup interface by name. + let expectedLookupRequest = + "3400000012000100000000000cc00cc0" // Netlink header (16 B) + + "110000000000000001000000ffffffff" // struct ifinfomsg (16 B) + + "08001d00090000000c0003006574683000000000" // RT attrs: IFLA_EXT_MASK + IFLA_IFNAME ("eth0") + mockSocket.responses.append( + [UInt8]( + hex: + "2000000010000000000000000cc00cc0" // Netlink header (16 B) + + "00000100020000004310010000000000" // struct ifinfomsg (16 B) – no attributes + ) + ) + + // Add default IPv6 route via gateway. + let expectedAddRequest = + "3800000018000506000000000cc00cc0" // Netlink header (16 B): len=56 + + "0a000000fe03000100000000" // struct rtmsg (12 B): AF_INET6, dst/0, + // table=MAIN(0xfe), proto=BOOT(0x03), scope=UNIVERSE(0x00), type=UNICAST(0x01) + + "14000500fd000000000000000000000000000001" // RTA_GATEWAY fd00::1 + + "0800040002000000" // RTA_OIF ifindex 2 (eth0) + mockSocket.responses.append( + [UInt8]( + hex: + "2400000002000001000000000cc00cc0" // Netlink header (16 B) + + "00000000280000001400050600000000" // nlmsg_err payload (16 B) + + "1f000000" + ) + ) + + let session = NetlinkSession(socket: mockSocket) + try session.routeAddDefault( + interface: "eth0", + ipv6Gateway: try IPv6Address("fd00::1") + ) + + #expect(mockSocket.requests.count == 2) + #expect(mockSocket.responseIndex == 2) + mockSocket.requests[0][8..<12] = [0, 0, 0, 0] + #expect(expectedLookupRequest == mockSocket.requests[0].hexEncodedString()) + mockSocket.requests[1][8..<12] = [0, 0, 0, 0] + #expect(expectedAddRequest == mockSocket.requests[1].hexEncodedString()) + } + @Test func testNetworkLinkGetMultipleMessagesInSingleBuffer() throws { let mockSocket = try MockNetlinkSocket() mockSocket.pid = 0x8765_4321 diff --git a/Tests/ContainerizationTests/AllocatorTests.swift b/Tests/ContainerizationTests/AllocatorTests.swift new file mode 100644 index 00000000..8bb80d2a --- /dev/null +++ b/Tests/ContainerizationTests/AllocatorTests.swift @@ -0,0 +1,170 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +#if os(macOS) + +import ContainerizationError +import ContainerizationExtras +import Testing + +@testable import Containerization + +struct AllocatorTests { + + @Test func allocateDualStackReturnsDistinctPairs() throws { + guard #available(macOS 26, *) else { return } + var alloc = try VmnetNetwork.Allocator( + cidrV4: try CIDRv4("192.168.64.0/24"), + cidrV6: try CIDRv6("fd00::/64")) + + let (a4, a6) = try alloc.allocate("a") + let (b4, b6) = try alloc.allocate("b") + + #expect(a4 != b4) + #expect(a6 != nil && b6 != nil) + #expect(a6 != b6) + + // The v4 allocator starts at lower + 2 (skipping network base + gateway), + // so the first two allocations are .2 and .3. + #expect(a4 == (try CIDRv4("192.168.64.2/24"))) + #expect(b4 == (try CIDRv4("192.168.64.3/24"))) + } + + @Test func allocateWithNoV6PrefixReturnsNilV6() throws { + guard #available(macOS 26, *) else { return } + var alloc = try VmnetNetwork.Allocator( + cidrV4: try CIDRv4("192.168.64.0/24"), + cidrV6: nil) + + let (_, a6) = try alloc.allocate("a") + #expect(a6 == nil) + } + + @Test func duplicateIdThrows() throws { + guard #available(macOS 26, *) else { return } + var alloc = try VmnetNetwork.Allocator( + cidrV4: try CIDRv4("192.168.64.0/24"), + cidrV6: try CIDRv6("fd00::/64")) + _ = try alloc.allocate("a") + #expect(throws: ContainerizationError.self) { + _ = try alloc.allocate("a") + } + } + + @Test func releaseAllowsIdReuse() throws { + guard #available(macOS 26, *) else { return } + var alloc = try VmnetNetwork.Allocator( + cidrV4: try CIDRv4("192.168.64.0/24"), + cidrV6: try CIDRv6("fd00::/64")) + + _ = try alloc.allocate("a") + // Re-allocating 'a' would throw .exists if release didn't clear it. + try alloc.release("a") + _ = try alloc.allocate("a") + } + + @Test func releaseUnknownIdIsNoOp() throws { + guard #available(macOS 26, *) else { return } + var alloc = try VmnetNetwork.Allocator( + cidrV4: try CIDRv4("192.168.64.0/24"), + cidrV6: try CIDRv6("fd00::/64")) + try alloc.release("never-allocated") + } + + @Test func v6HostPortionUsesOrdinalIndex() throws { + guard #available(macOS 26, *) else { + return + } + var alloc = try VmnetNetwork.Allocator( + cidrV4: try CIDRv4("192.168.64.0/24"), + cidrV6: try CIDRv6("fd00::/64")) + + let (_, a6) = try alloc.allocate("a") + let (_, b6) = try alloc.allocate("b") + + let aHost = a6!.address.value & a6!.prefix.suffixMask128 + let bHost = b6!.address.value & b6!.prefix.suffixMask128 + #expect(aHost == 2) + #expect(bHost == 3) + } + + @Test func cidrV6Gateway() throws { + // The network gateway is the lowest address + 1. + #expect((try CIDRv6("fd00::/64")).gateway == (try IPv6Address("fd00::1"))) + #expect((try CIDRv6("fd00:abcd:1234::/48")).gateway == (try IPv6Address("fd00:abcd:1234::1"))) + } + + @available(macOS 26, *) + private actor SerialAllocator { + private var inner: VmnetNetwork.Allocator + init(cidrV4: CIDRv4, cidrV6: CIDRv6?) throws { + self.inner = try VmnetNetwork.Allocator(cidrV4: cidrV4, cidrV6: cidrV6) + } + func allocate(_ id: String) throws -> (CIDRv4, CIDRv6?) { + try inner.allocate(id) + } + func release(_ id: String) throws { + try inner.release(id) + } + } + + @Test func returnsUniqueAddressesUnderConcurrentLoad() async throws { + guard #available(macOS 26, *) else { return } + // /16 host space is much larger than `count`, so we won't hit the + // pool ceiling — we're testing for collisions/state corruption. + let alloc = try SerialAllocator( + cidrV4: try CIDRv4("10.0.0.0/16"), + cidrV6: try CIDRv6("fd00::/64")) + + let count = 1000 + let pairs = try await withThrowingTaskGroup(of: (CIDRv4, CIDRv6?).self) { group in + for i in 0..")", ]) do { @@ -1199,6 +1200,30 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ let session = NetlinkSession(socket: socket, log: log) let ipv4Address = try CIDRv4(request.ipv4Address) try session.addressAdd(interface: request.interface, ipv4Address: ipv4Address) + if request.hasIpv6Address { + // Suppress SLAAC on this interface before adding the static + // address: the host would provide a static IPv6 config, this + // auto-derived IPv6 config would compete with the static one. + let confPath = URL(fileURLWithPath: "/proc/sys/net/ipv6/conf/\(request.interface)") + for key in ["accept_ra", "autoconf"] { + let setting = confPath.appendingPathComponent(key) + do { + let fh = try FileHandle(forWritingTo: setting) + defer { try? fh.close() } + try fh.write(contentsOf: Data("0".utf8)) + } catch { + log.warning( + "ipAddrAdd: failed to disable IPv6 auto-configuration", + metadata: [ + "path": "\(setting.path)", + "error": "\(error)", + ]) + } + } + + let ipv6Address = try CIDRv6(request.ipv6Address) + try session.addressAdd(interface: request.interface, ipv6Address: ipv6Address) + } } catch { log.error( "ipAddrAdd", @@ -1220,18 +1245,38 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ "interface": "\(request.interface)", "dstIpv4Addr": "\(request.dstIpv4Addr)", "srcIpv4Addr": "\(request.srcIpv4Addr)", + "dstIpv6Addr": "\(request.hasDstIpv6Addr ? request.dstIpv6Addr : "")", + "srcIpv6Addr": "\(request.hasSrcIpv6Addr ? request.srcIpv6Addr : "")", ]) + guard !request.dstIpv4Addr.isEmpty || request.hasDstIpv6Addr else { + throw RPCError( + code: .invalidArgument, + message: "ipRouteAddLink requires at least one of dstIpv4Addr or dstIpv6Addr" + ) + } + do { let socket = try DefaultNetlinkSocket() let session = NetlinkSession(socket: socket, log: log) - let dstIpv4Addr = try CIDRv4(request.dstIpv4Addr) - let srcIpv4Addr = request.srcIpv4Addr.isEmpty ? nil : try IPv4Address(request.srcIpv4Addr) - try session.routeAdd( - interface: request.interface, - dstIpv4Addr: dstIpv4Addr, - srcIpv4Addr: srcIpv4Addr - ) + if !request.dstIpv4Addr.isEmpty { + let dstIpv4Addr = try CIDRv4(request.dstIpv4Addr) + let srcIpv4Addr = request.srcIpv4Addr.isEmpty ? nil : try IPv4Address(request.srcIpv4Addr) + try session.routeAdd( + interface: request.interface, + dstIpv4Addr: dstIpv4Addr, + srcIpv4Addr: srcIpv4Addr + ) + } + if request.hasDstIpv6Addr { + let dstIpv6Addr = try CIDRv6(request.dstIpv6Addr) + let srcIpv6Addr = request.hasSrcIpv6Addr ? try IPv6Address(request.srcIpv6Addr) : nil + try session.routeAdd( + interface: request.interface, + dstIpv6Addr: dstIpv6Addr, + srcIpv6Addr: srcIpv6Addr + ) + } } catch { log.error( "ipRouteAddLink", @@ -1253,13 +1298,24 @@ extension Initd: Com_Apple_Containerization_Sandbox_V3_SandboxContext.SimpleServ metadata: [ "interface": "\(request.interface)", "ipv4Gateway": "\(request.ipv4Gateway)", + "ipv6Gateway": "\(request.hasIpv6Gateway ? request.ipv6Gateway : "")", ]) do { let socket = try DefaultNetlinkSocket() let session = NetlinkSession(socket: socket, log: log) - let ipv4Gateway = !request.ipv4Gateway.isEmpty ? try IPv4Address(request.ipv4Gateway) : nil - try session.routeAddDefault(interface: request.interface, ipv4Gateway: ipv4Gateway) + if !request.ipv4Gateway.isEmpty { + let ipv4Gateway = try IPv4Address(request.ipv4Gateway) + try session.routeAddDefault(interface: request.interface, ipv4Gateway: ipv4Gateway) + } else if !request.hasIpv6Gateway { + // No v4 gateway and no v6 either: install a v4 default route + // with no gateway (preserves pre-IPv6 behavior). + try session.routeAddDefault(interface: request.interface, ipv4Gateway: nil) + } + if request.hasIpv6Gateway { + let ipv6Gateway = try IPv6Address(request.ipv6Gateway) + try session.routeAddDefault(interface: request.interface, ipv6Gateway: ipv6Gateway) + } } catch { log.error( "ipRouteAddDefault", From 9275f365dd555c8f072e7d250d809f5eb7bdd746 Mon Sep 17 00:00:00 2001 From: Danny Canter Date: Wed, 3 Jun 2026 09:27:49 -0700 Subject: [PATCH 02/11] [Experimental] Add Sandboxy binary (#607) Sandboxy is an example tool to run isolated coding agents. --- .../containerization-build-template.yml | 4 +- Makefile | 12 +- examples/sandboxy/Makefile | 38 + examples/sandboxy/Package.resolved | 249 ++++ examples/sandboxy/Package.swift | 57 + examples/sandboxy/README.md | 374 ++++++ .../Sources/sandboxy/AgentDefinition.swift | 243 ++++ .../Sources/sandboxy/CacheCommand.swift | 263 +++++ .../Sources/sandboxy/ConfigCommand.swift | 170 +++ .../Sources/sandboxy/EditCommand.swift | 245 ++++ .../sandboxy/Sources/sandboxy/HostProxy.swift | 460 ++++++++ .../Sources/sandboxy/InstanceState.swift | 120 ++ .../Sources/sandboxy/KernelManager.swift | 153 +++ .../Sources/sandboxy/ListCommand.swift | 94 ++ .../Sources/sandboxy/OutputCapture.swift | 47 + .../Sources/sandboxy/ProgressUI.swift | 70 ++ .../Sources/sandboxy/RemoveCommand.swift | 69 ++ .../Sources/sandboxy/RunAgentCommand.swift | 1000 +++++++++++++++++ .../sandboxy/Sources/sandboxy/Sandboxy.swift | 101 ++ .../Sources/sandboxy/SandboxyConfig.swift | 81 ++ .../Sources/sandboxy/SandboxyError.swift | 52 + .../TerminalProgress/Int+Formatted.swift | 52 + .../TerminalProgress/Int64+Formatted.swift | 36 + .../TerminalProgress/ProgressBar+Add.swift | 236 ++++ .../TerminalProgress/ProgressBar+State.swift | 100 ++ .../ProgressBar+Terminal.swift | 95 ++ .../TerminalProgress/ProgressBar.swift | 362 ++++++ .../TerminalProgress/ProgressConfig.swift | 170 +++ .../ProgressTaskCoordinator.swift | 72 ++ .../TerminalProgress/ProgressTheme.swift | 37 + .../TerminalProgress/ProgressUpdate.swift | 41 + .../TerminalProgress/StandardError.swift | 25 + examples/sandboxy/sandboxy.entitlements | 8 + 33 files changed, 5132 insertions(+), 4 deletions(-) create mode 100644 examples/sandboxy/Makefile create mode 100644 examples/sandboxy/Package.resolved create mode 100644 examples/sandboxy/Package.swift create mode 100644 examples/sandboxy/README.md create mode 100644 examples/sandboxy/Sources/sandboxy/AgentDefinition.swift create mode 100644 examples/sandboxy/Sources/sandboxy/CacheCommand.swift create mode 100644 examples/sandboxy/Sources/sandboxy/ConfigCommand.swift create mode 100644 examples/sandboxy/Sources/sandboxy/EditCommand.swift create mode 100644 examples/sandboxy/Sources/sandboxy/HostProxy.swift create mode 100644 examples/sandboxy/Sources/sandboxy/InstanceState.swift create mode 100644 examples/sandboxy/Sources/sandboxy/KernelManager.swift create mode 100644 examples/sandboxy/Sources/sandboxy/ListCommand.swift create mode 100644 examples/sandboxy/Sources/sandboxy/OutputCapture.swift create mode 100644 examples/sandboxy/Sources/sandboxy/ProgressUI.swift create mode 100644 examples/sandboxy/Sources/sandboxy/RemoveCommand.swift create mode 100644 examples/sandboxy/Sources/sandboxy/RunAgentCommand.swift create mode 100644 examples/sandboxy/Sources/sandboxy/Sandboxy.swift create mode 100644 examples/sandboxy/Sources/sandboxy/SandboxyConfig.swift create mode 100644 examples/sandboxy/Sources/sandboxy/SandboxyError.swift create mode 100644 examples/sandboxy/Sources/sandboxy/TerminalProgress/Int+Formatted.swift create mode 100644 examples/sandboxy/Sources/sandboxy/TerminalProgress/Int64+Formatted.swift create mode 100644 examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar+Add.swift create mode 100644 examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar+State.swift create mode 100644 examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar+Terminal.swift create mode 100644 examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar.swift create mode 100644 examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressConfig.swift create mode 100644 examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressTaskCoordinator.swift create mode 100644 examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressTheme.swift create mode 100644 examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressUpdate.swift create mode 100644 examples/sandboxy/Sources/sandboxy/TerminalProgress/StandardError.swift create mode 100644 examples/sandboxy/sandboxy.entitlements diff --git a/.github/workflows/containerization-build-template.yml b/.github/workflows/containerization-build-template.yml index beb3dad8..d3847193 100644 --- a/.github/workflows/containerization-build-template.yml +++ b/.github/workflows/containerization-build-template.yml @@ -50,9 +50,9 @@ jobs: make protos if ! git diff --quiet ; then echo the following files require formatting or license headers: ; git diff --name-only ; false ; fi - - name: Make containerization and docs + - name: Make containerization, examples, and docs run: | - make clean containerization docs + make clean containerization examples docs tar cfz _site.tgz _site env: BUILD_CONFIGURATION: ${{ inputs.release && 'release' || 'debug' }} diff --git a/Makefile b/Makefile index b44d7aef..d83c7691 100644 --- a/Makefile +++ b/Makefile @@ -203,8 +203,8 @@ swift-fmt: @$(SWIFT) format --recursive --configuration .swift-format -i $(SWIFT_SRC) swift-fmt-check: - @echo Checking code formatting compliance... - @$(SWIFT) format lint --recursive --strict --configuration .swift-format-nolint $(SWIFT_SRC) + @echo Checking code formatting compliance... + @$(SWIFT) format lint --recursive --strict --configuration .swift-format-nolint $(SWIFT_SRC) .PHONY: update-licenses update-licenses: @@ -246,6 +246,14 @@ cleancontent: @echo Cleaning the content... @rm -rf ~/Library/Application\ Support/com.apple.containerization +.PHONY: examples +examples: + @echo Building examples... + @mkdir -p bin + @"$(MAKE)" -C examples/sandboxy build BUILD_CONFIGURATION=$(BUILD_CONFIGURATION) + @install examples/sandboxy/bin/sandboxy ./bin/ + @codesign --force --sign - --timestamp=none --entitlements=signing/vz.entitlements bin/sandboxy + .PHONY: clean clean: @echo Cleaning build files... diff --git a/examples/sandboxy/Makefile b/examples/sandboxy/Makefile new file mode 100644 index 00000000..2be0b9c0 --- /dev/null +++ b/examples/sandboxy/Makefile @@ -0,0 +1,38 @@ +# Copyright © 2026 Apple Inc. and the Containerization project authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +BUILD_CONFIGURATION ?= debug +SWIFT = /usr/bin/swift +SWIFT_STRIP := $(if $(filter release,$(BUILD_CONFIGURATION)),-Xlinker -s) +BUILD_BIN_DIR = $(shell $(SWIFT) build -c $(BUILD_CONFIGURATION) --show-bin-path) + +.PHONY: all build clean run fmt + +all: build + +build: + $(SWIFT) build -c $(BUILD_CONFIGURATION) $(SWIFT_STRIP) + @mkdir -p bin + @install "$(BUILD_BIN_DIR)/sandboxy" ./bin/ + codesign --force --sign - --entitlements sandboxy.entitlements ./bin/sandboxy + +clean: + $(SWIFT) package clean + rm -rf bin + +run: build + ./bin/sandboxy + +fmt: + $(SWIFT) format --in-place --recursive Sources/ diff --git a/examples/sandboxy/Package.resolved b/examples/sandboxy/Package.resolved new file mode 100644 index 00000000..dad21397 --- /dev/null +++ b/examples/sandboxy/Package.resolved @@ -0,0 +1,249 @@ +{ + "originHash" : "7b278fed488f6dec94de45fcf45179f7556a1b32ebb5b19499b932a307ddcad0", + "pins" : [ + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "2fc4652fb4689eb24af10e55cabaa61d8ba774fd", + "version" : "1.32.0" + } + }, + { + "identity" : "containerization", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/containerization.git", + "state" : { + "revision" : "72432f148c9cb35d6a7e7c0ad61d6f9d226cbef7", + "version" : "0.30.0" + } + }, + { + "identity" : "grpc-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift.git", + "state" : { + "revision" : "ac715c584bb1e2e5cdfb7684ccb46fab8dafc641", + "version" : "1.27.4" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615", + "version" : "1.7.0" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "810496cf121e525d660cd0ea89a758740476b85f", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "9d349bcc328ac3c31ce40e746b5882742a0d1272", + "version" : "1.1.3" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "24ccdeeeed4dfaae7955fcac9dbf5489ed4f1a25", + "version" : "1.18.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "8d9834a6189db730f6264db7556a7ffb751e99ee", + "version" : "1.4.0" + } + }, + { + "identity" : "swift-configuration", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-configuration.git", + "state" : { + "revision" : "be76c4ad929eb6c4bcaf3351799f2adf9e6848a9", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "95ba0316a9b733e92bb6b071255ff46263bbe7dc", + "version" : "3.15.1" + } + }, + { + "identity" : "swift-distributed-tracing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-distributed-tracing.git", + "state" : { + "revision" : "e109d8b5308d0e05201d9a1dd1c475446a946a11", + "version" : "1.4.0" + } + }, + { + "identity" : "swift-http-structured-headers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-structured-headers.git", + "state" : { + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "bbd81b6725ae874c69e9b8c8804d462356b55523", + "version" : "1.10.1" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "e932d3c4d8f77433c8f7093b5ebcbf91463948a0", + "version" : "2.95.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "3df009d563dc9f21a5c85b33d8c2e34d2e4f8c3b", + "version" : "1.32.1" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "b6571f3db40799df5a7fc0e92c399aa71c883edd", + "version" : "1.40.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "173cc69a058623525a58ae6710e2f5727c663793", + "version" : "2.36.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "60c3e187154421171721c1a38e800b390680fb5d", + "version" : "1.26.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "a008af1a102ff3dd6cc3764bb69bf63226d0f5f6", + "version" : "1.36.1" + } + }, + { + "identity" : "swift-service-context", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-service-context.git", + "state" : { + "revision" : "d0997351b0c7779017f88e7a93bc30a1878d7f29", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle.git", + "state" : { + "revision" : "89888196dd79c61c50bca9a103d8114f32e1e598", + "version" : "2.10.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "7c6ad0fc39d0763e0b699210e4124afd5041c5df", + "version" : "1.6.4" + } + }, + { + "identity" : "zstd", + "kind" : "remoteSourceControl", + "location" : "https://github.com/facebook/zstd.git", + "state" : { + "revision" : "f8745da6ff1ad1e7bab384bd1f9d742439278e99", + "version" : "1.5.7" + } + } + ], + "version" : 3 +} diff --git a/examples/sandboxy/Package.swift b/examples/sandboxy/Package.swift new file mode 100644 index 00000000..15b0c52e --- /dev/null +++ b/examples/sandboxy/Package.swift @@ -0,0 +1,57 @@ +// swift-tools-version: 6.2 +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import PackageDescription + +let containerizationVersion = "0.26.5" + +let package = Package( + name: "sandboxy", + platforms: [ + .macOS("26.0") + ], + products: [ + .executable( + name: "sandboxy", + targets: ["sandboxy"] + ) + ], + dependencies: [ + .package(url: "https://github.com/apple/containerization.git", from: "0.30.0"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), + .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.20.1"), + ], + targets: [ + .executableTarget( + name: "sandboxy", + dependencies: [ + .product(name: "Containerization", package: "containerization"), + .product(name: "ContainerizationExtras", package: "containerization"), + .product(name: "ContainerizationOS", package: "containerization"), + .product(name: "ContainerizationArchive", package: "containerization"), + .product(name: "AsyncHTTPClient", package: "async-http-client"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "NIOHTTP1", package: "swift-nio"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Logging", package: "swift-log"), + ], + ) + ] +) diff --git a/examples/sandboxy/README.md b/examples/sandboxy/README.md new file mode 100644 index 00000000..3246c2e8 --- /dev/null +++ b/examples/sandboxy/README.md @@ -0,0 +1,374 @@ +# sandboxy + +``` +$ sandboxy run claude + ┌──────────────┐ + │ ░░░░░░░░░░░░ │ Sandboxy + │ ░░░░░░░░░░░░ │ Agent: Claude Code + │ ░░░░░░░░░░░░ │ Instance: claude-20260328-150531 + │ ░░░░░░░░░░░░ │ Environment: 15 hours ago + │ ░░░░░░░░░░░░ │ Workspace: /Volumes/code/vessel/containerization + │ ░░░░░░░░░░░░ │ CPUs: 4 Memory: 4 GB + └──────────────┘ + Command: claude --dangerously-skip-permissions + Allowed hosts: *.anthropic.com, npm.org, *.npmjs.org, *.github.com, *.githubusercontent.com, *.pypi.org + Mounts: + /your/code -> /your/code + /Users/you/.claude -> /root/.claude + +Welcome to Claude Code v2.1.76 +………………………………………………………………………………………………………………………………………………………… + + * █████▓▓░ + * ███▓░ ░░ + ░░░░░░ ███▓░ + ░░░ ░░░░░░░░░░ ███▓░ + ░░░░░░░░░░░░░░░░░░░ * ██▓░░ ▓ + ░▓▓███▓▓░ + * ░░░░ + ░░░░░░░░ + ░░░░░░░░░░░░░░░░ + █████████ * + ██▄█████▄██ * + █████████ * +…………………█ █ █ █……………………………………………………………………………………………………………… + + Let's get started. +``` + +`sandboxy` runs AI coding agents in sandboxed Linux environments on macOS with Apple silicon. + +One command to get an isolated agent session. Your current working directory is mounted in, your config carries over, and the environment is cached for fast subsequent runs. + +> **Note:** This is an experimental tool. Behavior/flags/commands may change across releases. Its main goal was to be a good showcase of using the `Containerization` libraries API surface to build novel tools. + +> **Note:** The tool does HTTPS/HTTP filtering today for network traffic originating from the container, but can currently reach services listening on `0.0.0.0` on the host. This should be tightened up in a future release whenever `Containerization` gains nftables support. + +## Why? + +AI coding agents work best when they can install packages, run builds, and execute code freely, but giving them unrestricted access to your host machine is risky. `sandboxy` aims to alleviate some worry by running agents inside lightweight Linux VMs on macOS. Your project directory is mounted in so the agent can read and write files, but everything else like network access, host filesystem view, and installed packages is isolated. No daemon, just a single command that gets out of your way. + +## How? + +`sandboxy` boots a Micro VM for every agent session using the [`Containerization`](https://github.com/apple/containerization) Swift package. Each agent session runs in its own VM. + +### Caching + +The first time you run an agent, `sandboxy` does a fair amount of setup: downloading a Linux kernel (unless you provide one), pulling the base OCI image, unpacking it into a root filesystem, and running the agent's install commands. All of this is cached so that subsequent runs skip straight to booting the VM. In practice, a warm start takes under a second. + +The cache has a few layers: + +- **Kernel** -- downloaded once and reused across all agents. +- **Init image** -- the minimal init process (`vminit`) that bootstraps the VM, pulled from an OCI registry and cached locally. +- **Agent rootfs** -- the fully-installed root filesystem for a given agent (e.g., `claude`). This is an ext4 disk image that includes the base image layers plus everything the agent's install commands produce. +- **Instance rootfs** -- every session saves its rootfs so it can be resumed later with `--name`. See [Instance Persistence](#instance-persistence). + +On each run, the cached rootfs is cloned (via copy-on-write when the filesystem supports it) so the original cache stays clean. You can blow away any layer independently: `sandboxy cache rm ` to rebuild a single agent, or `sandboxy cache clean --all` to start completely from scratch. + +### Agent definitions + +An agent is just a JSON file that describes how to set up and launch a particular tool. The built-in Claude Code definition specifies a base container image, a list of shell commands to install the toolchain, a launch command, and the environment variables needed at runtime. You can list available agents with `sandboxy config list --agents` and view any agent's definition with `sandboxy config list --agent `. + +You can override any built-in agent or define entirely new agents by dropping a JSON file in `~/.config/sandboxy/agents/`. Use `sandboxy config create --agent ` to scaffold a definition file (pre-filled with built-in defaults for known agents). Use `sandboxy config list --paths` to see all configuration file paths. + +If the built-in install steps don't cover what you need, `sandboxy edit ` drops you into an interactive shell inside the cached rootfs. Install extra packages, configure MCP servers, add language runtimes etc. Whatever you do is saved back to the cache and included in every future run. + +### Workspace and mounts + +Your host workspace directory is shared into the VM using virtio-fs, so reads and writes are reflected immediately on both sides. Additional host directories can be mounted with `--mount`, including read-only mounts for things like config or reference data that the agent shouldn't modify. + +Agent definitions can include default mounts (e.g., `~/.claude` for Claude Code). To skip these on a specific run, pass `--no-agent-mounts`. CLI `--mount` flags are always applied regardless. + +### Network isolation + +By default, `sandboxy` enforces network isolation by placing the workload container on a host-only network with no internet route. A HTTP CONNECT proxy runs on the host and listens on the host-only network's gateway address. The workload's `HTTP_PROXY`/`HTTPS_PROXY` environment variables point at this proxy, which checks each request's target hostname against an allowlist and either tunnels it to the internet or returns a 403. + +Additional hosts can be added at runtime with `--allow-hosts`. To disable filtering entirely, pass `--no-network-filter`. + +## Quick Start + +```bash +# Build +BUILD_CONFIGURATION=release make build + +# Run Claude Code on the current directory +.build/release/sandboxy run claude +``` + +On first run, `sandboxy` downloads a kernel, pulls a base image, and installs the agent toolchain. This is cached automatically so subsequent runs generally start in less than a second. + +## Supported Agents + +- **Claude Code** - built-in + +Additional agents can be added via JSON config files. See [Adding a New Agent](#adding-a-new-agent). + +## Commands + +### `sandboxy run ` + +Run an agent in a sandboxed container. + +```bash +# Run on the current directory +sandboxy run claude + +# Specify a workspace +sandboxy run --workspace ~/projects/myapp claude + +# Allocate more resources +sandboxy run --cpus 8 --memory 8g claude + +# Restrict network to specific hosts (in addition to agent defaults) +sandboxy run --allow-hosts api.example.com --allow-hosts internal.corp.com claude + +# Disable network filtering entirely +sandboxy run --no-network-filter claude + +# Mount additional directories (read-only or read-write) +sandboxy run --mount /tmp:/tmp:ro --mount ~/data:/data claude + +# Skip mounts defined in the agent configuration +sandboxy run --no-agent-mounts claude + +# Forward environment variables into the container +sandboxy run -e MY_TOKEN -e DEBUG=1 claude + +# Forward the host SSH agent for git-over-SSH +sandboxy run --ssh-agent claude + +# Give the instance a friendly name +sandboxy run --name my-feature claude + +# Resume a named session +sandboxy run --name my-feature claude + +# Ephemeral run (remove instance after session ends) +sandboxy run --rm claude + +# Pass flags through to the agent +sandboxy run claude -- --model foobar +``` + +**Options:** + +| Flag | Description | Default | +|------|-------------|---------| +| `-w`, `--workspace` | Host directory to mount | Current directory | +| `-m`, `--mount` | Additional mount (hostpath:containerpath[:ro\|rw], repeatable) | None | +| `-e`, `--env` | Set environment variable (KEY=VALUE or KEY to forward from host, repeatable) | None | +| `-k`, `--kernel` | Path to a Linux kernel | Auto-download | +| `--cpus` | Number of CPUs | 4 | +| `--memory` | Memory to allocate (e.g. `4g`, `512m`, `4096` for MB) | `4g` | +| `--allow-hosts` | Additional hostnames to allow (merged with agent defaults) | Agent defaults | +| `--no-network-filter` | Disable network filtering (allow unrestricted access) | Off | +| `--no-agent-mounts` | Skip mounts defined in the agent configuration | Off | +| `--name` | Persistent session name | Auto-generated | +| `--rm` | Remove instance after session ends | Off | +| `--reinstall` | Rebuild the cached environment from scratch | Off | +| `--ssh-agent` | Forward the host SSH agent socket into the container | Off | + +### `sandboxy edit ` + +Open an interactive shell in the agent's cached environment. Install packages, configure MCP tools, add language runtimes etc. +Changes are saved back to the cache when you exit. + +If no cache exists yet, the agent's install commands are run first. If any install step fails, you're dropped into the shell anyway so you can diagnose or finish the setup manually. + +```bash +sandboxy edit claude + +# Inside the container: +apt-get install -y python3-pip +pip3 install some-mcp-tool +exit # changes are saved +``` + +Every future `sandboxy run claude` will include your changes. + +### `sandboxy list` (alias: `ls`) + +Show sandbox instances and their status. + +```bash +sandboxy ls +``` + +### `sandboxy rm [...]` + +Remove one or more instances and their preserved state. + +```bash +# Remove a single instance +sandboxy rm my-feature + +# Remove multiple instances +sandboxy rm instance-1 instance-2 + +# Remove all instances +sandboxy rm --all +sandboxy rm -a +``` + +### `sandboxy cache list` + +Show cached environments and their disk usage. + +### `sandboxy cache rm ` + +Remove a specific agent's cached environment. The next run will rebuild it. + +### `sandboxy cache clean [--all] [--yes]` + +Remove all cached environments and named instance state. If named instances exist, you'll be prompted for confirmation. With `--yes`, skip the prompt. With `--all`, also removes the kernel, init image, and content store, forcing a full re-download on the next run. + +### `sandboxy config list` + +Print current configuration or agent definitions. + +```bash +# Print global defaults +sandboxy config list + +# Print a specific agent's definition +sandboxy config list --agent claude + +# List all available agents (built-in and custom) +sandboxy config list --agents + +# Print configuration file paths +sandboxy config list --paths +``` + +### `sandboxy config create` + +Create a default configuration or agent definition file. If the file already exists, you'll be prompted to confirm overwriting (use `--force` to skip). + +For built-in agents (e.g. `claude`), the file is pre-filled with the built-in definition so you have a working starting point to customize. + +```bash +# Create a global config.json with defaults +sandboxy config create + +# Create an override for the built-in claude agent +sandboxy config create --agent claude + +# Scaffold a new agent definition +sandboxy config create --agent myagent + +# Overwrite an existing definition without prompting +sandboxy config create --agent claude --force +``` + +## Instance Persistence + +Every session automatically saves its rootfs when it exits. The instance appears in `sandboxy ls` and can be resumed by passing its name to `--name`: + +```bash +# First run -- auto-named instance +sandboxy run claude +# => Instance claude-20260328-091522 saved. Resume with: sandboxy run claude --name claude-20260328-091522 + +# Resume it +sandboxy run --name claude-20260328-091522 claude + +# Or give it a memorable name upfront +sandboxy run --name my-feature claude + +# List all instances +sandboxy ls + +# Clean up +sandboxy rm my-feature + +# Ephemeral run (nothing saved) +sandboxy run --rm claude +``` + +Use `--rm` for throwaway sessions that shouldn't persist. + +## API Keys + +Agent definitions can include environment variable names without values (e.g. `"ANTHROPIC_API_KEY"`), which are automatically forwarded from the host if set. The built-in Claude Code agent forwards `ANTHROPIC_API_KEY` this way. For custom agents, add the relevant key name to the `environmentVariables` array, or pass it at runtime with `-e`. + +## Network Filtering + +Network filtering is enabled by default. Each agent definition includes an `allowedHosts` list. Additional hosts can be added at runtime with `--allow-hosts`. An empty `allowedHosts` list means all traffic is denied. To disable filtering entirely, pass `--no-network-filter`. + +The workload container runs on a **host-only network** with no internet route. A lightweight HTTP CONNECT proxy runs on the macOS host, bound to the host-only network's gateway address. The workload's proxy environment variables point at this address, so tools that respect `HTTP_PROXY`/`HTTPS_PROXY` route their traffic through the proxy automatically. + +Note that the proxy relies on applications honoring `HTTP_PROXY`/`HTTPS_PROXY` environment variables. Tools that ignore these variables won't be able to reach the internet since the container has no direct internet route. + +Agent toolchain installation (apt-get, npm install, etc.) runs with full network access on a shared network before the proxy is set up, so package repos don't need to be allowlisted. + +## Adding a New Agent + +Create a JSON file in the `agents/` directory, or use `sandboxy config create --agent ` to scaffold one: + +`~/.config/sandboxy/agents/.json` + +Example (`foo.json`): + +```json +{ + "displayName": "Foo", + "baseImage": "docker.io/library/python:3.12-slim", + "installCommands": [ + "pip install foo" + ], + "launchCommand": ["foo"], + "environmentVariables": [], + "mounts": [], + "allowedHosts": ["api.example.com", "*.cdn.example.com"] +} +``` + +Then run it with `sandboxy run foo`. + +To override a built-in agent, create a file with the same name (e.g., `claude.json`). Only the fields you include are overridden. Omitted fields keep their defaults. Use `sandboxy config list --agent claude` to see the full default definition. + +## Configuration + +Global defaults can be overridden with a config file: + +`~/.config/sandboxy/config.json` + +```json +{ + "dataDir": "/Volumes/fast/sandboxy", + "kernel": "/path/to/vmlinux", + "initfsReference": "ghcr.io/apple/containerization/vminit:0.26.5", + "defaultCPUs": 8, + "defaultMemory": "8g" +} +``` + +All fields are optional. Use `sandboxy config list` to see the defaults. + +## Kernel + +By default, `sandboxy` downloads a Linux kernel from the [Kata Containers](https://github.com/kata-containers/kata-containers) project (arm64 static release). The kernel is cached at `~/Library/Application Support/com.apple.containerization.sandboxy/kernel/vmlinux` and reused across all agent sessions. + +To use your own kernel, pass it directly: + +```bash +sandboxy run -k /path/to/vmlinux claude +``` + +Or set it permanently in `config.json`: + +```json +{ + "kernel": "/path/to/vmlinux" +} +``` + +The kernel must be an uncompressed Linux kernel binary (`vmlinux`, not `bzImage` or `zImage`) built for arm64 with virtio drivers enabled (virtio-net, virtio-blk, virtio-fs, virtio-console at minimum). + +## Building + +```bash +make build +``` + +The built binary is at `.build/release/sandboxy`. It requires macOS 26 on Apple silicon. diff --git a/examples/sandboxy/Sources/sandboxy/AgentDefinition.swift b/examples/sandboxy/Sources/sandboxy/AgentDefinition.swift new file mode 100644 index 00000000..38b8f4c8 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/AgentDefinition.swift @@ -0,0 +1,243 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +/// Defines an AI coding agent that can be run inside a sandbox container. +/// +/// Agent definitions can be built-in or loaded from JSON files in the +/// `agents/` subdirectory of the sandboxy config directory. +/// +/// Location: `~/.config/sandboxy/agents/.json` +/// +/// Example (`foo.json`): +/// ```json +/// { +/// "displayName": "Foo", +/// "baseImage": "docker.io/library/python:3.12-slim", +/// "installCommands": [ +/// "pip install foo" +/// ], +/// "launchCommand": ["foo"], +/// "environmentVariables": [], +/// "mounts": [ +/// {"hostPath": "~/.foo", "containerPath": "/root/.foo", "readOnly": true} +/// ], +/// "allowedHosts": ["api.example.com", "*.cdn.example.com"] +/// } +/// ``` +struct AgentDefinition: Codable, Sendable { + /// Human-readable name used in output messages. + let displayName: String + + /// The base container image reference (e.g., "docker.io/library/node:22"). + let baseImage: String + + /// Shell commands run sequentially inside the container to install the agent + /// and its dependencies. Each string is passed as an argument to `sh -c`. + let installCommands: [String] + + /// The command and arguments to launch the agent interactively. + let launchCommand: [String] + + /// Environment variables required by the agent (key=value format). + let environmentVariables: [String] + + /// Host paths to mount into the container. Each entry specifies a host path + /// and a container path. Paths starting with `~` are expanded to the user's + /// home directory. Only mounted if the host path exists. + let mounts: [AgentMount] + + /// Default hostnames to allow through the network filtering proxy. + /// Supports exact matches and `*.suffix` wildcard patterns. + /// Merged with CLI `--allow-hosts` values. + /// An empty list means all traffic is denied. Use `--no-network-filter` to disable filtering. + let allowedHosts: [String] +} + +/// A host-to-container mount for an agent definition. +struct AgentMount: Codable, Sendable { + /// Path on the host. Supports `~` for the user's home directory. + let hostPath: String + + /// Path inside the container where the host path is mounted. + let containerPath: String + + /// Whether the mount is read-only. Defaults to `false`. + let readOnly: Bool + + init(hostPath: String, containerPath: String, readOnly: Bool = false) { + self.hostPath = hostPath + self.containerPath = containerPath + self.readOnly = readOnly + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + hostPath = try container.decode(String.self, forKey: .hostPath) + containerPath = try container.decode(String.self, forKey: .containerPath) + readOnly = try container.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false + } + + /// Returns the resolved absolute host path, expanding `~`. + var resolvedHostPath: String { + if hostPath.hasPrefix("~/") { + let home = FileManager.default.homeDirectoryForCurrentUser.path(percentEncoded: false) + return home + String(hostPath.dropFirst(1)) + } + if hostPath == "~" { + return FileManager.default.homeDirectoryForCurrentUser.path(percentEncoded: false) + } + return hostPath + } +} + +extension AgentDefinition { + /// Built-in agent definitions, keyed by their CLI name. + static let builtIn: [String: AgentDefinition] = [ + "claude": .claude + ] + + /// Returns all available agents: built-in definitions merged with any + /// user-defined agents from `/agents/`. If a user file matches + /// a built-in agent name, non-nil fields from the user file override the + /// built-in values, allowing partial overrides. + static func allAgents(configRoot: URL) -> [String: AgentDefinition] { + var agents = builtIn + + let agentsDir = configRoot.appendingPathComponent("agents") + let agentsDirPath = agentsDir.path(percentEncoded: false) + guard FileManager.default.fileExists(atPath: agentsDirPath) else { + return agents + } + + do { + let files = try FileManager.default.contentsOfDirectory( + at: agentsDir, + includingPropertiesForKeys: nil + ).filter { $0.pathExtension == "json" } + + let decoder = JSONDecoder() + for file in files { + let name = file.deletingPathExtension().lastPathComponent + do { + let data = try Data(contentsOf: file) + let override = try decoder.decode(AgentOverride.self, from: data) + if let base = agents[name] { + agents[name] = override.merged(onto: base) + } else { + agents[name] = try override.asFullDefinition() + } + } catch { + ProgressUI.printWarning( + "Failed to load agent definition from \(file.lastPathComponent): \(error)") + } + } + } catch { + ProgressUI.printWarning("Failed to read agents directory: \(error)") + } + + return agents + } + + /// Returns the sorted list of available agent names for display in help text. + static func knownAgentNames(configRoot: URL) -> [String] { + allAgents(configRoot: configRoot).keys.sorted() + } + + static let claude = AgentDefinition( + displayName: "Claude Code", + baseImage: "docker.io/library/node:22", + installCommands: [ + "apt-get update && apt-get install -y --no-install-recommends less git procps sudo fzf zsh man-db unzip gnupg2 gh ipset iproute2 dnsutils aggregate jq nano vim ripgrep ca-certificates && apt-get clean && rm -rf /var/lib/apt/lists/*", + "npm install -g @anthropic-ai/claude-code", + "npm install -g global-agent", + ], + launchCommand: ["claude", "--dangerously-skip-permissions"], + environmentVariables: [ + "ANTHROPIC_API_KEY", + "NODE_OPTIONS=--max-old-space-size=4096", + "IS_SANDBOX=1", + ], + mounts: [ + AgentMount(hostPath: "~/.claude", containerPath: "/root/.claude") + ], + allowedHosts: [ + "*.anthropic.com", + "*.claude.com", + "npm.org", + "*.npmjs.org", + "*.github.com", + "*.githubusercontent.com", + "*.pypi.org", + "*.pythonhosted.org", + ] + ) +} + +/// All-optional mirror of `AgentDefinition` used when loading user override files. +/// For agents that match a built-in name, only the non-nil fields override the defaults. +/// For entirely new agents, all required fields must be provided. +struct AgentOverride: Codable, Sendable { + var displayName: String? + var baseImage: String? + var installCommands: [String]? + var launchCommand: [String]? + var environmentVariables: [String]? + var mounts: [AgentMount]? + var allowedHosts: [String]? + + /// Merges this override onto a base definition, replacing only the fields + /// that are non-nil in the override. + func merged(onto base: AgentDefinition) -> AgentDefinition { + AgentDefinition( + displayName: displayName ?? base.displayName, + baseImage: baseImage ?? base.baseImage, + installCommands: installCommands ?? base.installCommands, + launchCommand: launchCommand ?? base.launchCommand, + environmentVariables: environmentVariables ?? base.environmentVariables, + mounts: mounts ?? base.mounts, + allowedHosts: allowedHosts ?? base.allowedHosts + ) + } + + /// Converts this override into a full definition, throwing if any required + /// fields are missing. Used for entirely new (non-built-in) agents. + func asFullDefinition() throws -> AgentDefinition { + guard let displayName, let baseImage, let installCommands, + let launchCommand + else { + throw SandboxyError.incompleteAgentDefinition( + missing: [ + displayName == nil ? "displayName" : nil, + baseImage == nil ? "baseImage" : nil, + installCommands == nil ? "installCommands" : nil, + launchCommand == nil ? "launchCommand" : nil, + ].compactMap { $0 } + ) + } + + return AgentDefinition( + displayName: displayName, + baseImage: baseImage, + installCommands: installCommands, + launchCommand: launchCommand, + environmentVariables: environmentVariables ?? [], + mounts: mounts ?? [], + allowedHosts: allowedHosts ?? [] + ) + } +} diff --git a/examples/sandboxy/Sources/sandboxy/CacheCommand.swift b/examples/sandboxy/Sources/sandboxy/CacheCommand.swift new file mode 100644 index 00000000..a9b94b2b --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/CacheCommand.swift @@ -0,0 +1,263 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Foundation + +extension Sandboxy { + struct Cache: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "cache", + abstract: "Manage cached rootfs images", + subcommands: [ + CacheList.self, + CacheRemove.self, + CacheClean.self, + ] + ) + } + + struct CacheList: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "list", + abstract: "List cached rootfs images and named instance state" + ) + + func run() async throws { + _ = try Sandboxy.loadConfig() + + let cacheDir = Sandboxy.appRoot.appendingPathComponent("cache") + let namedDir = InstanceState.namedRootfsDir(appRoot: Sandboxy.appRoot) + + let agentCaches = listRootfsFiles(in: cacheDir, suffix: "-rootfs.ext4") + let namedCaches = listRootfsFiles(in: namedDir, suffix: "-rootfs.ext4") + + if agentCaches.isEmpty && namedCaches.isEmpty { + print("No cached images found.") + return + } + + if !agentCaches.isEmpty { + print("Agent caches:") + print(pad(" NAME", to: 20) + pad("SIZE", to: 12) + "MODIFIED") + for entry in agentCaches { + print( + pad(" \(entry.name)", to: 20) + + pad(formatBytes(entry.diskSize), to: 12) + + entry.modified + ) + } + } + + if !namedCaches.isEmpty { + if !agentCaches.isEmpty { print() } + print("Named instances:") + print(pad(" NAME", to: 20) + pad("SIZE", to: 12) + "MODIFIED") + for entry in namedCaches { + print( + pad(" \(entry.name)", to: 20) + + pad(formatBytes(entry.diskSize), to: 12) + + entry.modified + ) + } + } + } + + private struct CacheEntry { + let name: String + let diskSize: UInt64 + let modified: String + } + + private func listRootfsFiles(in dir: URL, suffix: String) -> [CacheEntry] { + let dirPath = dir.path(percentEncoded: false) + guard FileManager.default.fileExists(atPath: dirPath) else { + return [] + } + + do { + let files = try FileManager.default.contentsOfDirectory( + at: dir, + includingPropertiesForKeys: [ + .totalFileAllocatedSizeKey, + .contentModificationDateKey, + ] + ).filter { $0.lastPathComponent.hasSuffix(suffix) } + + return files.compactMap { file -> CacheEntry? in + let cleanName = file.lastPathComponent + .replacingOccurrences(of: suffix, with: "") + + do { + let values = try file.resourceValues(forKeys: [ + .totalFileAllocatedSizeKey, + .contentModificationDateKey, + ]) + let diskSize = UInt64(values.totalFileAllocatedSize ?? 0) + let date = values.contentModificationDate ?? Date() + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .short + return CacheEntry( + name: cleanName, + diskSize: diskSize, + modified: formatter.string(from: date) + ) + } catch { + return nil + } + }.sorted { $0.name < $1.name } + } catch { + return [] + } + } + + private func pad(_ s: String, to width: Int) -> String { + if s.count >= width { return s } + return s + String(repeating: " ", count: width - s.count) + } + + private func formatBytes(_ bytes: UInt64) -> String { + if bytes < 1024 { return "\(bytes) B" } + let kb = Double(bytes) / 1024 + if kb < 1024 { return String(format: "%.1f KB", kb) } + let mb = kb / 1024 + if mb < 1024 { return String(format: "%.1f MB", mb) } + let gb = mb / 1024 + return String(format: "%.1f GB", gb) + } + } + + struct CacheRemove: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "rm", + abstract: "Remove a specific agent cache" + ) + + @Argument(help: "Name of the agent cache to remove") + var name: String + + func run() async throws { + _ = try Sandboxy.loadConfig() + + let cacheDir = Sandboxy.appRoot.appendingPathComponent("cache") + let cachePath = cacheDir.appendingPathComponent("\(name)-rootfs.ext4") + + guard FileManager.default.fileExists(atPath: cachePath.path(percentEncoded: false)) else { + print("No cache found for agent '\(name)'.") + throw ExitCode.failure + } + + try FileManager.default.removeItem(at: cachePath) + print("Removed cache for '\(name)'.") + } + } + + struct CacheClean: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "clean", + abstract: "Remove all cached rootfs images (use --all to also remove kernel, init image, and content store)" + ) + + @Flag(name: .long, help: "Also remove kernel, init image, and content store, forcing a full re-download on next run") + var all = false + + @Flag(name: .long, help: "Skip confirmation prompts") + var yes = false + + func run() async throws { + _ = try Sandboxy.loadConfig() + + let fm = FileManager.default + + // Check for named instances and warn the user before deleting them. + let namedDir = InstanceState.namedRootfsDir(appRoot: Sandboxy.appRoot) + let namedInstances = listNamedInstances(in: namedDir) + if !namedInstances.isEmpty && !yes { + print("This will also delete the following named instances:") + for name in namedInstances { + print(" - \(name)") + } + print() + print("Are you sure? [y/N] ", terminator: "") + guard let response = readLine()?.lowercased(), response == "y" || response == "yes" else { + print("Aborted.") + return + } + } + + let cacheDir = Sandboxy.appRoot.appendingPathComponent("cache") + if fm.fileExists(atPath: cacheDir.path(percentEncoded: false)) { + try fm.removeItem(at: cacheDir) + try fm.createDirectory(at: cacheDir, withIntermediateDirectories: true) + } + + if fm.fileExists(atPath: namedDir.path(percentEncoded: false)) { + try fm.removeItem(at: namedDir) + try fm.createDirectory(at: namedDir, withIntermediateDirectories: true) + } + + if all { + let kernelDir = Sandboxy.appRoot.appendingPathComponent("kernel") + if fm.fileExists(atPath: kernelDir.path(percentEncoded: false)) { + try fm.removeItem(at: kernelDir) + print("Removed kernel.") + } + + let initfs = Sandboxy.appRoot.appendingPathComponent("initfs.ext4") + if fm.fileExists(atPath: initfs.path(percentEncoded: false)) { + try fm.removeItem(at: initfs) + print("Removed init image.") + } + + let contentDir = Sandboxy.appRoot.appendingPathComponent("content") + if fm.fileExists(atPath: contentDir.path(percentEncoded: false)) { + try fm.removeItem(at: contentDir) + print("Removed content store.") + } + + // Remove the image store reference database so stale references + // don't point to missing content. + let stateFile = Sandboxy.appRoot.appendingPathComponent("state.json") + if fm.fileExists(atPath: stateFile.path(percentEncoded: false)) { + try fm.removeItem(at: stateFile) + } + + print("All caches and downloaded artifacts removed.") + } else { + print("All caches removed. Use --all to also remove kernel, init image, and content store.") + } + } + + private func listNamedInstances(in dir: URL) -> [String] { + let path = dir.path(percentEncoded: false) + guard FileManager.default.fileExists(atPath: path) else { + return [] + } + do { + return try FileManager.default.contentsOfDirectory( + at: dir, + includingPropertiesForKeys: nil + ) + .filter { $0.lastPathComponent.hasSuffix("-rootfs.ext4") } + .map { $0.lastPathComponent.replacingOccurrences(of: "-rootfs.ext4", with: "") } + .sorted() + } catch { + return [] + } + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/ConfigCommand.swift b/examples/sandboxy/Sources/sandboxy/ConfigCommand.swift new file mode 100644 index 00000000..68e81c5c --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/ConfigCommand.swift @@ -0,0 +1,170 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Foundation + +extension Sandboxy { + struct Config: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "config", + abstract: "View and create configuration files", + subcommands: [ + ConfigList.self, + ConfigCreate.self, + ] + ) + } + + struct ConfigList: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "list", + abstract: "Print current configuration and agent definitions" + ) + + @Option(name: .long, help: "Print the definition for a specific agent") + var agent: String? + + @Flag(name: .long, help: "Print built-in defaults instead of the resolved configuration") + var defaults = false + + @Flag(name: .long, help: "Print configuration file paths") + var paths = false + + @Flag(name: .long, help: "List available agents") + var agents = false + + func run() async throws { + if paths { + let configPath = Sandboxy.configRoot.appendingPathComponent("config.json") + let agentsDir = Sandboxy.configRoot.appendingPathComponent("agents") + print("Config: \(configPath.path(percentEncoded: false))") + print("Agents: \(agentsDir.path(percentEncoded: false))") + print("Data: \(Sandboxy.appRoot.path(percentEncoded: false))") + return + } + + if agents { + let allAgents = AgentDefinition.allAgents(configRoot: Sandboxy.configRoot) + let builtInNames = Set(AgentDefinition.builtIn.keys) + for name in allAgents.keys.sorted() { + let definition = allAgents[name]! + let source = builtInNames.contains(name) ? "built-in" : "custom" + print(" \(name) - \(definition.displayName) (\(source))") + } + return + } + + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + + if let agentName = agent { + let allAgents = AgentDefinition.allAgents(configRoot: Sandboxy.configRoot) + guard let definition = allAgents[agentName] else { + let available = allAgents.keys.sorted().joined(separator: ", ") + throw ValidationError( + "Unknown agent '\(agentName)'. Available agents: \(available)" + ) + } + let data = try encoder.encode(definition) + print(String(data: data, encoding: .utf8)!) + } else { + let config = defaults ? SandboxyConfig.defaults : try Sandboxy.loadConfig() + let data = try encoder.encode(config) + print(String(data: data, encoding: .utf8)!) + } + } + } + + struct ConfigCreate: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "create", + abstract: "Create a default configuration or agent definition file" + ) + + @Option(name: .long, help: "Create a definition file for a new agent with this name") + var agent: String? + + @Flag(name: .long, help: "Overwrite existing files without prompting") + var force = false + + func run() async throws { + let fm = FileManager.default + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + + if let agentName = agent { + let agentsDir = Sandboxy.configRoot.appendingPathComponent("agents") + try fm.createDirectory(at: agentsDir, withIntermediateDirectories: true) + + let filePath = agentsDir.appendingPathComponent("\(agentName).json") + if fm.fileExists(atPath: filePath.path(percentEncoded: false)) && !force { + guard isatty(STDIN_FILENO) != 0 else { + print("Agent definition already exists at \(filePath.path(percentEncoded: false)). Use --force to overwrite.") + throw ExitCode.failure + } + print("Agent definition already exists at \(filePath.path(percentEncoded: false))") + print("Overwrite? [y/N] ", terminator: "") + guard let response = readLine()?.lowercased(), response == "y" || response == "yes" else { + print("Aborted.") + return + } + } + + let definition: AgentDefinition + if let builtIn = AgentDefinition.builtIn[agentName] { + definition = builtIn + } else { + definition = AgentDefinition( + displayName: agentName.capitalized, + baseImage: "docker.io/library/node:22", + installCommands: [], + launchCommand: [agentName], + environmentVariables: [], + mounts: [], + allowedHosts: [] + ) + } + + let data = try encoder.encode(definition) + try data.write(to: filePath, options: .atomic) + print("Created agent definition at \(filePath.path(percentEncoded: false))") + print() + print(String(data: data, encoding: .utf8)!) + } else { + let configPath = Sandboxy.configRoot.appendingPathComponent("config.json") + if fm.fileExists(atPath: configPath.path(percentEncoded: false)) && !force { + guard isatty(STDIN_FILENO) != 0 else { + print("Configuration file already exists at \(configPath.path(percentEncoded: false)). Use --force to overwrite.") + throw ExitCode.failure + } + print("Configuration file already exists at \(configPath.path(percentEncoded: false))") + print("Overwrite? [y/N] ", terminator: "") + guard let response = readLine()?.lowercased(), response == "y" || response == "yes" else { + print("Aborted.") + return + } + } + + try fm.createDirectory(at: Sandboxy.configRoot, withIntermediateDirectories: true) + + let data = try encoder.encode(SandboxyConfig.defaults) + try data.write(to: configPath, options: .atomic) + print("Created configuration file at \(configPath.path(percentEncoded: false))") + } + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/EditCommand.swift b/examples/sandboxy/Sources/sandboxy/EditCommand.swift new file mode 100644 index 00000000..d55a81df --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/EditCommand.swift @@ -0,0 +1,245 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Containerization +import ContainerizationExtras +import ContainerizationOS +import Foundation + +extension Sandboxy { + struct Edit: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "edit", + abstract: "Open an interactive shell in an agent's cached environment", + discussion: """ + Boots the cached rootfs for the given agent and drops you into a shell. + Any changes you make (installing packages, editing configs, etc.) are + saved back to the cache when you exit. If no cache exists, the agent + is installed from scratch first. + """ + ) + + @Argument(help: "Agent whose environment to edit (e.g. claude)") + var agent: String + + @Option( + name: [.customLong("kernel"), .customShort("k")], + help: "Path to Linux kernel binary (auto-downloads if omitted)", + completion: .file(), + transform: { str in + URL(fileURLWithPath: str, relativeTo: .currentDirectory()) + .absoluteURL.path(percentEncoded: false) + }) + var kernel: String? + + func run() async throws { + let config = try Sandboxy.loadConfig() + + let agents = AgentDefinition.allAgents(configRoot: Sandboxy.configRoot) + guard let definition = agents[agent] else { + let available = agents.keys.sorted().joined(separator: ", ") + throw ValidationError( + "Unknown agent '\(agent)'. Available agents: \(available)" + ) + } + + ProgressUI.printStatus("Opening \(definition.displayName) environment for editing...") + + let kernelPath = try await KernelManager.ensureKernel( + explicitPath: kernel, + appRoot: Sandboxy.appRoot, + config: config + ) + let vmKernel = Kernel(path: kernelPath, platform: .linuxArm) + + let enableNetworking: Bool + var sharedNetwork: VmnetNetwork? + if #available(macOS 26, *) { + sharedNetwork = try VmnetNetwork() + enableNetworking = true + } else { + sharedNetwork = nil + enableNetworking = false + } + + let vmnetMTU: UInt32 = 1400 + + let initfsReference = config.initfsReference ?? SandboxyConfig.defaults.initfsReference! + var manager = try await ContainerManager( + kernel: vmKernel, + initfsReference: initfsReference, + root: Sandboxy.appRoot + ) + + let containerId = "\(agent)-edit-\(ProcessInfo.processInfo.processIdentifier)" + + let cacheDir = Sandboxy.appRoot.appendingPathComponent("cache") + try FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true) + let agentCachePath = cacheDir.appendingPathComponent("\(agent)-rootfs.ext4") + let containerRootfsPath = Sandboxy.appRoot + .appendingPathComponent("containers") + .appendingPathComponent(containerId) + .appendingPathComponent("rootfs.ext4") + + let hasCachedRootfs = FileManager.default.fileExists( + atPath: agentCachePath.path(percentEncoded: false)) + + let container: LinuxContainer + + if hasCachedRootfs { + ProgressUI.printDetail("Using cached environment...") + let containerDir = Sandboxy.appRoot + .appendingPathComponent("containers") + .appendingPathComponent(containerId) + try FileManager.default.createDirectory( + at: containerDir, withIntermediateDirectories: true) + + let result = Darwin.clonefile( + agentCachePath.path(percentEncoded: false), + containerRootfsPath.path(percentEncoded: false), + 0 + ) + if result != 0 { + try FileManager.default.copyItem(at: agentCachePath, to: containerRootfsPath) + } + + let rootfsMount = Mount.block( + format: "ext4", + source: containerRootfsPath.path(percentEncoded: false), + destination: "/" + ) + + let image = try await Sandboxy.imageStore.get( + reference: definition.baseImage, pull: true) + + container = try await manager.create( + containerId, + image: image, + rootfs: rootfsMount, + networking: false + ) { config in + if enableNetworking, let iface = try sharedNetwork?.createInterface(containerId, mtu: vmnetMTU) { + config.interfaces = [iface] + config.dns = .init(nameservers: [sharedNetwork!.ipv4Gateway.description]) + } + config.cpus = 4 + config.memoryInBytes = 4096 * 1024 * 1024 + config.process.arguments = ["/bin/sleep", "infinity"] + config.process.workingDirectory = "/" + config.process.capabilities = .allCapabilities + config.useInit = true + } + } else { + ProgressUI.printDetail("No cached environment, setting up from scratch...") + container = try await manager.create( + containerId, + reference: definition.baseImage, + rootfsSizeInBytes: 512.gib(), + networking: false + ) { config in + if enableNetworking, let iface = try sharedNetwork?.createInterface(containerId, mtu: vmnetMTU) { + config.interfaces = [iface] + config.dns = .init(nameservers: [sharedNetwork!.ipv4Gateway.description]) + } + config.cpus = 4 + config.memoryInBytes = 4096 * 1024 * 1024 + config.process.arguments = ["/bin/sleep", "infinity"] + config.process.workingDirectory = "/" + config.process.capabilities = .allCapabilities + config.useInit = true + } + } + + do { + try await container.create() + try await container.start() + + // If no cache existed, run the agent's install commands first. + if !hasCachedRootfs { + ProgressUI.printStatus("Installing \(definition.displayName) toolchain...") + do { + try await installAgent(in: container, definition: definition) + ProgressUI.printStatus("Installation complete.") + } catch { + ProgressUI.printError("Installation failed: \(error)") + ProgressUI.printStatus("Dropping into shell...") + } + } + + // Drop into an interactive shell. + ProgressUI.printStatus("Launching shell (exit to save changes)...\n") + + let sigwinchStream = AsyncSignalHandler.create(notify: [SIGWINCH]) + let current = try Terminal.current + try current.setraw() + defer { current.tryReset() } + + let shellProcess = try await container.exec("edit-shell") { config in + config.arguments = ["/bin/bash"] + config.environmentVariables = [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "TERM=xterm", + "HOME=/root", + ] + config.workingDirectory = "/root" + config.setTerminalIO(terminal: current) + config.capabilities = .allCapabilities + } + + try await shellProcess.start() + try? await shellProcess.resize(to: try current.size) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + for await _ in sigwinchStream.signals { + try await shellProcess.resize(to: try current.size) + } + } + + _ = try await shellProcess.wait() + group.cancelAll() + try await shellProcess.delete() + } + + // Stop container so rootfs is cleanly unmounted before caching. + try await container.stop() + + // Save the modified rootfs back to the cache. + ProgressUI.printStatus("Saving changes to cache...") + removeIfExists(at: agentCachePath) + try FileManager.default.copyItem(at: containerRootfsPath, to: agentCachePath) + ProgressUI.printStatus("Done.") + + try manager.delete(containerId) + try? sharedNetwork?.releaseInterface(containerId) + } catch { + do { + try await container.stop() + } catch { + log.warning("Failed to stop container \(containerId): \(error)") + } + do { + try manager.delete(containerId) + } catch { + log.warning("Failed to delete container \(containerId): \(error)") + } + try? sharedNetwork?.releaseInterface(containerId) + throw error + } + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/HostProxy.swift b/examples/sandboxy/Sources/sandboxy/HostProxy.swift new file mode 100644 index 00000000..af08eda1 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/HostProxy.swift @@ -0,0 +1,460 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +// Adapted from https://github.com/apple/swift-nio-examples/tree/main/connect-proxy + +import Foundation +import NIOCore +import NIOHTTP1 +import NIOPosix + +/// A lightweight HTTP proxy that runs on the host and filters by hostname. +/// +/// Binds to the host's gateway IP on a vmnet host-only network. Workload containers +/// that have no direct internet route use this proxy (via HTTP_PROXY/HTTPS_PROXY env vars) +/// to reach the outside world. Only hostnames matching the allowlist are permitted. +/// +/// Handles both HTTPS (via CONNECT tunneling) and plain HTTP (via request forwarding). +/// For HTTPS, the client sends a CONNECT request with the target hostname in plaintext +/// before TLS begins, so we can filter without any certificate interception. +final class HostProxy: @unchecked Sendable { + private let group: MultiThreadedEventLoopGroup + private let channel: any Channel + + /// The port the proxy is listening on. + let port: Int + + /// The host address the proxy is bound to. + let host: String + + /// Start a proxy bound to the given address. + /// - Parameters: + /// - host: IP address to bind to (e.g. the vmnet gateway IP). + /// - port: Port to bind to. Use 0 for an OS-assigned port. + /// - allowedHosts: Hostname patterns to allow. Supports `*.example.com` wildcards. + init(host: String, port: Int = 0, allowedHosts: [String]) async throws { + self.host = host + let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + self.group = group + + let bootstrap = ServerBootstrap(group: group) + .serverChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1) + .childChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1) + .childChannelInitializer { channel in + channel.eventLoop.makeCompletedFuture { + try channel.pipeline.syncOperations.addHandler( + ByteToMessageHandler(HTTPRequestDecoder(leftOverBytesStrategy: .forwardBytes)) + ) + try channel.pipeline.syncOperations.addHandler(HTTPResponseEncoder()) + try channel.pipeline.syncOperations.addHandler( + ConnectHandler(allowedHosts: allowedHosts) + ) + } + } + + let channel = try await bootstrap.bind( + to: SocketAddress(ipAddress: host, port: port) + ).get() + + guard let localAddress = channel.localAddress, let assignedPort = localAddress.port else { + throw SandboxyError.proxyFailed(reason: "could not determine proxy listen port") + } + + self.port = assignedPort + self.channel = channel + } + + /// Stop the proxy and release resources. + func stop() async throws { + try await channel.close() + try await group.shutdownGracefully() + } + + /// Check if a hostname matches any pattern in the allowlist. + static func isAllowed(host: String, allowedHosts: [String]) -> Bool { + let host = host.lowercased() + for pattern in allowedHosts { + let pattern = pattern.lowercased() + if pattern.hasPrefix("*.") { + let suffix = String(pattern.dropFirst(1)) // e.g. ".example.com" + if host == String(pattern.dropFirst(2)) || host.hasSuffix(suffix) { + return true + } + } else if host == pattern { + return true + } + } + return false + } +} + +/// Channel handler that processes HTTP CONNECT and plain HTTP proxy requests. +/// Checks the target hostname against an allowlist before connecting. +private final class ConnectHandler { + private var upgradeState: State + private let allowedHosts: [String] + + /// Buffered request for plain HTTP forwarding (nil for CONNECT). + private var pendingHTTPHead: HTTPRequestHead? + private var pendingHTTPBody: [ByteBuffer] = [] + + init(allowedHosts: [String]) { + self.upgradeState = .idle + self.allowedHosts = allowedHosts + } +} + +extension ConnectHandler { + fileprivate enum State { + case idle + case beganConnecting + case awaitingEnd(connectResult: Channel) + case awaitingConnection(pendingBytes: [NIOAny]) + case upgradeComplete(pendingBytes: [NIOAny]) + case upgradeFailed + } +} + +extension ConnectHandler: ChannelInboundHandler { + typealias InboundIn = HTTPServerRequestPart + typealias OutboundOut = HTTPServerResponsePart + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + switch self.upgradeState { + case .idle: + self.handleInitialMessage(context: context, data: self.unwrapInboundIn(data)) + + case .beganConnecting: + switch self.unwrapInboundIn(data) { + case .body(let body): + self.pendingHTTPBody.append(body) + case .end: + self.upgradeState = .awaitingConnection(pendingBytes: []) + self.removeDecoder(context: context) + default: + break + } + + case .awaitingEnd(let peerChannel): + switch self.unwrapInboundIn(data) { + case .body(let body): + self.pendingHTTPBody.append(body) + case .end: + self.upgradeState = .upgradeComplete(pendingBytes: []) + self.removeDecoder(context: context) + self.glue(peerChannel, context: context) + default: + break + } + + case .awaitingConnection(var pendingBytes): + self.upgradeState = .awaitingConnection(pendingBytes: []) + pendingBytes.append(data) + self.upgradeState = .awaitingConnection(pendingBytes: pendingBytes) + + case .upgradeComplete(var pendingBytes): + self.upgradeState = .upgradeComplete(pendingBytes: []) + pendingBytes.append(data) + self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes) + + case .upgradeFailed: + break + } + } +} + +extension ConnectHandler: RemovableChannelHandler { + func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) { + var didRead = false + + while case .upgradeComplete(var pendingBytes) = self.upgradeState, pendingBytes.count > 0 { + self.upgradeState = .upgradeComplete(pendingBytes: []) + let nextRead = pendingBytes.removeFirst() + self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes) + + context.fireChannelRead(nextRead) + didRead = true + } + + if didRead { + context.fireChannelReadComplete() + } + + context.leavePipeline(removalToken: removalToken) + } +} + +extension ConnectHandler { + private func handleInitialMessage(context: ChannelHandlerContext, data: InboundIn) { + guard case .head(let head) = data else { + self.httpErrorAndClose(context: context, status: .badRequest) + return + } + + if head.method == .CONNECT { + // HTTPS: CONNECT host:port + let components = head.uri.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) + let host = String(components.first!) + let port = components.last.flatMap { Int($0, radix: 10) } ?? 443 + + guard HostProxy.isAllowed(host: host, allowedHosts: self.allowedHosts) else { + self.httpErrorAndClose(context: context, status: .forbidden) + return + } + + self.upgradeState = .beganConnecting + self.connectTo(host: host, port: port, context: context) + } else { + // Plain HTTP: GET http://host/path, POST http://host/path, etc. + guard let url = URLComponents(string: head.uri), + let hostname = url.host, !hostname.isEmpty + else { + self.httpErrorAndClose(context: context, status: .badRequest) + return + } + + guard HostProxy.isAllowed(host: hostname, allowedHosts: self.allowedHosts) else { + self.httpErrorAndClose(context: context, status: .forbidden) + return + } + + let port = url.port ?? 80 + + // Rewrite URI from absolute (http://host/path) to relative (/path). + var relativePath = url.path + if relativePath.isEmpty { relativePath = "/" } + if let query = url.query { + relativePath += "?\(query)" + } + + var rewritten = head + rewritten.uri = relativePath + self.pendingHTTPHead = rewritten + + self.upgradeState = .beganConnecting + self.connectTo(host: hostname, port: port, context: context) + } + } + + private func connectTo(host: String, port: Int, context: ChannelHandlerContext) { + ClientBootstrap(group: context.eventLoop) + .connect(host: host, port: port).assumeIsolatedUnsafeUnchecked().whenComplete { result in + switch result { + case .success(let channel): + self.connectSucceeded(channel: channel, context: context) + case .failure(let error): + self.connectFailed(error: error, context: context) + } + } + } + + private func connectSucceeded(channel: Channel, context: ChannelHandlerContext) { + switch self.upgradeState { + case .beganConnecting: + self.upgradeState = .awaitingEnd(connectResult: channel) + + case .awaitingConnection(let pendingBytes): + self.upgradeState = .upgradeComplete(pendingBytes: pendingBytes) + self.glue(channel, context: context) + + case .awaitingEnd(let peerChannel): + peerChannel.close(mode: .all, promise: nil) + context.close(promise: nil) + + case .idle, .upgradeFailed, .upgradeComplete: + context.close(promise: nil) + } + } + + private func connectFailed(error: Error, context: ChannelHandlerContext) { + switch self.upgradeState { + case .beganConnecting, .awaitingConnection: + self.httpErrorAndClose(context: context, status: .badGateway) + + case .awaitingEnd(let peerChannel): + peerChannel.close(mode: .all, promise: nil) + context.close(promise: nil) + + case .idle, .upgradeFailed, .upgradeComplete: + context.close(promise: nil) + } + + context.fireErrorCaught(error) + } + + private func glue(_ peerChannel: Channel, context: ChannelHandlerContext) { + if let httpHead = self.pendingHTTPHead { + // Plain HTTP: forward the buffered request to the peer, then glue. + var buffer = context.channel.allocator.buffer(capacity: 256) + buffer.writeString("\(httpHead.method) \(httpHead.uri) HTTP/\(httpHead.version.major).\(httpHead.version.minor)\r\n") + for (name, value) in httpHead.headers { + buffer.writeString("\(name): \(value)\r\n") + } + buffer.writeString("\r\n") + for var body in self.pendingHTTPBody { + buffer.writeBuffer(&body) + } + peerChannel.writeAndFlush(buffer, promise: nil) + self.pendingHTTPHead = nil + self.pendingHTTPBody = [] + } else { + // CONNECT: send 200 OK to the client. + // Content-Length: 0 prevents the encoder from adding chunked transfer encoding, + // which would inject a chunked terminator into the raw tunnel and break TLS. + let headers = HTTPHeaders([("Content-Length", "0")]) + let head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: .ok, headers: headers) + context.write(self.wrapOutboundOut(.head(head)), promise: nil) + context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil) + } + + self.removeEncoder(context: context) + + let (localGlue, peerGlue) = GlueHandler.matchedPair() + do { + try context.channel.pipeline.syncOperations.addHandler(localGlue) + try peerChannel.pipeline.syncOperations.addHandler(peerGlue) + context.pipeline.syncOperations.removeHandler(self, promise: nil) + } catch { + peerChannel.close(mode: .all, promise: nil) + context.close(promise: nil) + } + } + + private func httpErrorAndClose(context: ChannelHandlerContext, status: HTTPResponseStatus) { + self.upgradeState = .upgradeFailed + + let headers = HTTPHeaders([("Content-Length", "0"), ("Connection", "close")]) + let head = HTTPResponseHead(version: .init(major: 1, minor: 1), status: status, headers: headers) + context.write(self.wrapOutboundOut(.head(head)), promise: nil) + context.writeAndFlush(self.wrapOutboundOut(.end(nil))).assumeIsolatedUnsafeUnchecked().whenComplete { + (_: Result) in + context.close(mode: .output, promise: nil) + } + } + + private func removeDecoder(context: ChannelHandlerContext) { + if let ctx = try? context.pipeline.syncOperations.context( + handlerType: ByteToMessageHandler.self + ) { + context.pipeline.syncOperations.removeHandler(context: ctx, promise: nil) + } + } + + private func removeEncoder(context: ChannelHandlerContext) { + if let ctx = try? context.pipeline.syncOperations.context( + handlerType: HTTPResponseEncoder.self + ) { + context.pipeline.syncOperations.removeHandler(context: ctx, promise: nil) + } + } +} + +/// Bidirectional relay handler that glues two channels together. +private final class GlueHandler { + private var partner: GlueHandler? + private var context: ChannelHandlerContext? + private var pendingRead: Bool = false + + private init() {} + + static func matchedPair() -> (GlueHandler, GlueHandler) { + let first = GlueHandler() + let second = GlueHandler() + first.partner = second + second.partner = first + return (first, second) + } +} + +extension GlueHandler { + fileprivate func partnerWrite(_ data: NIOAny) { + self.context?.write(data, promise: nil) + } + + fileprivate func partnerFlush() { + self.context?.flush() + } + + fileprivate func partnerWriteEOF() { + self.context?.close(mode: .output, promise: nil) + } + + fileprivate func partnerCloseFull() { + self.context?.close(promise: nil) + } + + fileprivate func partnerBecameWritable() { + if self.pendingRead { + self.pendingRead = false + self.context?.read() + } + } + + fileprivate var partnerWritable: Bool { + self.context?.channel.isWritable ?? false + } +} + +extension GlueHandler: ChannelDuplexHandler { + typealias InboundIn = NIOAny + typealias OutboundIn = NIOAny + typealias OutboundOut = NIOAny + + func handlerAdded(context: ChannelHandlerContext) { + self.context = context + } + + func handlerRemoved(context: ChannelHandlerContext) { + self.context = nil + self.partner = nil + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + self.partner?.partnerWrite(data) + } + + func channelReadComplete(context: ChannelHandlerContext) { + self.partner?.partnerFlush() + } + + func channelInactive(context: ChannelHandlerContext) { + self.partner?.partnerCloseFull() + } + + func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) { + if let event = event as? ChannelEvent, case .inputClosed = event { + self.partner?.partnerWriteEOF() + } + } + + func errorCaught(context: ChannelHandlerContext, error: Error) { + self.partner?.partnerCloseFull() + } + + func channelWritabilityChanged(context: ChannelHandlerContext) { + if context.channel.isWritable { + self.partner?.partnerBecameWritable() + } + } + + func read(context: ChannelHandlerContext) { + if let partner = self.partner, partner.partnerWritable { + context.read() + } else { + self.pendingRead = true + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/InstanceState.swift b/examples/sandboxy/Sources/sandboxy/InstanceState.swift new file mode 100644 index 00000000..333be1e0 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/InstanceState.swift @@ -0,0 +1,120 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +/// Persistent metadata about a sandbox instance. +struct InstanceState: Codable, Sendable { + let id: String + /// User-provided name for persistent instances. Nil for ephemeral runs. + let name: String? + let agent: String + let workspace: String + let status: Status + let createdAt: Date + var stoppedAt: Date? + let cpus: Int + let memoryMB: UInt64 + + enum Status: String, Codable, Sendable { + case running + case stopped + } + + /// Whether this is a named (persistent) instance. + var isNamed: Bool { name != nil } + + /// Directory where instance state files are stored. + static func instancesDir(appRoot: URL) -> URL { + appRoot.appendingPathComponent("instances") + } + + /// Directory where named instance rootfs files are preserved. + static func namedRootfsDir(appRoot: URL) -> URL { + appRoot.appendingPathComponent("named") + } + + /// Path to the preserved rootfs for a named instance. + static func namedRootfsPath(appRoot: URL, name: String) -> URL { + namedRootfsDir(appRoot: appRoot).appendingPathComponent("\(name)-rootfs.ext4") + } + + /// Path to this instance's state file. + func statePath(appRoot: URL) -> URL { + Self.instancesDir(appRoot: appRoot).appendingPathComponent("\(id).json") + } + + /// Saves this instance state to disk. + func save(appRoot: URL) throws { + let dir = Self.instancesDir(appRoot: appRoot) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + encoder.outputFormatting = .prettyPrinted + let data = try encoder.encode(self) + try data.write(to: statePath(appRoot: appRoot)) + } + + /// Loads all instance states from disk. + static func loadAll(appRoot: URL) throws -> [InstanceState] { + let dir = instancesDir(appRoot: appRoot) + let path = dir.path(percentEncoded: false) + guard FileManager.default.fileExists(atPath: path) else { + return [] + } + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + + let files = try FileManager.default.contentsOfDirectory( + at: dir, + includingPropertiesForKeys: nil + ).filter { $0.pathExtension == "json" } + + return files.compactMap { file -> InstanceState? in + do { + let data = try Data(contentsOf: file) + return try decoder.decode(InstanceState.self, from: data) + } catch { + log.warning("Failed to load instance state from \(file.lastPathComponent): \(error)") + return nil + } + } + } + + /// Finds a named instance by name. + static func find(name: String, appRoot: URL) throws -> InstanceState? { + try loadAll(appRoot: appRoot).first { $0.name == name } + } + + /// Removes this instance's state file from disk. + func remove(appRoot: URL) throws { + try FileManager.default.removeItem(at: statePath(appRoot: appRoot)) + } + + /// Removes this instance's state file and preserved rootfs (for named instances). + func removeAll(appRoot: URL) throws { + try remove(appRoot: appRoot) + if let name { + let rootfs = Self.namedRootfsPath(appRoot: appRoot, name: name) + let path = rootfs.path(percentEncoded: false) + if FileManager.default.fileExists(atPath: path) { + try FileManager.default.removeItem(at: rootfs) + } + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/KernelManager.swift b/examples/sandboxy/Sources/sandboxy/KernelManager.swift new file mode 100644 index 00000000..fd4bcafe --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/KernelManager.swift @@ -0,0 +1,153 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import AsyncHTTPClient +import ContainerizationArchive +import ContainerizationExtras +import Foundation + +enum KernelManager { + // Hardcoded default kernel source (Kata Containers arm64 static release). + private static let defaultKernelURL = + "https://github.com/kata-containers/kata-containers/releases/download/3.26.0/kata-static-3.26.0-arm64.tar.zst" + private static let defaultKernelPathInTarball = "opt/kata/share/kata-containers/vmlinux.container" + + /// Ensures a kernel binary is available, returning its path. + /// + /// Resolution order: + /// 1. CLI flag (`-k` / `--kernel`) + /// 2. Config file (`"kernel"` field) + /// 3. Cached kernel at `appRoot/kernel/vmlinux` + /// 4. Auto-download from Kata Containers + static func ensureKernel(explicitPath: String?, appRoot: URL, config: SandboxyConfig) async throws -> URL { + // 1. CLI flag takes priority. + if let explicitPath { + let url = URL(fileURLWithPath: explicitPath) + guard FileManager.default.fileExists(atPath: url.path(percentEncoded: false)) else { + throw SandboxyError.kernelNotFound(path: explicitPath) + } + return url + } + + // 2. Config file path. + if let configKernel = config.kernel { + let url = URL(fileURLWithPath: configKernel) + guard FileManager.default.fileExists(atPath: url.path(percentEncoded: false)) else { + throw SandboxyError.kernelNotFound(path: configKernel) + } + return url + } + + // 3. Cached kernel. + let kernelDir = appRoot.appendingPathComponent("kernel") + let kernelPath = kernelDir.appendingPathComponent("vmlinux") + + if FileManager.default.fileExists(atPath: kernelPath.path(percentEncoded: false)) { + return kernelPath + } + + // 4. Auto-download. + try FileManager.default.createDirectory(at: kernelDir, withIntermediateDirectories: true) + + let progressConfig = try ProgressConfig( + description: "Downloading kernel", + showTasks: true, + totalTasks: 2 + ) + let progress = ProgressBar(config: progressConfig) + defer { progress.finish() } + progress.start() + + let tarballPath = kernelDir.appendingPathComponent("kata.tar.zst") + try await downloadFile(from: defaultKernelURL, to: tarballPath, progress: progress) + + progress.set(description: "Extracting kernel") + try extractKernel(from: tarballPath, kernelPathInTarball: defaultKernelPathInTarball, to: kernelPath) + + try FileManager.default.removeItem(at: tarballPath) + + return kernelPath + } + + private static func downloadFile(from urlString: String, to destination: URL, progress: ProgressBar) async throws { + guard let url = URL(string: urlString) else { + throw SandboxyError.kernelDownloadFailed(reason: "invalid URL: \(urlString)") + } + + let delegate = try FileDownloadDelegate( + path: destination.path(percentEncoded: false), + reportHead: { head in + if let contentLength = head.headers["Content-Length"].first, let totalBytes = Int64(contentLength) { + progress.add(totalSize: totalBytes) + } + }, + reportProgress: { progressUpdate in + progress.set(size: Int64(progressUpdate.receivedBytes)) + } + ) + + let request = try HTTPClient.Request(url: url) + let client = createClient(url: url) + do { + _ = try await client.execute(request: request, delegate: delegate).get() + } catch { + try? await client.shutdown() + throw error + } + try await client.shutdown() + } + + private static func createClient(url: URL) -> HTTPClient { + var httpConfiguration = HTTPClient.Configuration() + httpConfiguration.timeout = HTTPClient.Configuration.Timeout( + connect: .seconds(30), + read: .none + ) + if let host = url.host { + let proxyURL = ProxyUtils.proxyFromEnvironment(scheme: url.scheme, host: host) + if let proxyURL, let proxyHost = proxyURL.host { + httpConfiguration.proxy = HTTPClient.Configuration.Proxy.server(host: proxyHost, port: proxyURL.port ?? 8080) + } + } + + return HTTPClient(eventLoopGroupProvider: .singleton, configuration: httpConfiguration) + } + + private static func extractKernel(from tarball: URL, kernelPathInTarball: String, to destination: URL) throws { + var target = kernelPathInTarball + var reader = try ArchiveReader(file: tarball) + var (entry, data) = try reader.extractFile(path: target) + + // If the target file is a symlink, get the data for the actual file. + if entry.fileType == .symbolicLink, let symlinkRelative = entry.symlinkTarget { + reader = try ArchiveReader(file: tarball) + let symlinkTarget = URL(filePath: target).deletingLastPathComponent().appending(path: symlinkRelative) + + // Standardize so that we remove any and all ../ and ./ in the path since symlink targets + // are relative paths to the target file from the symlink's parent dir itself. + target = symlinkTarget.standardized.relativePath + let (_, targetData) = try reader.extractFile(path: target) + data = targetData + } + + try data.write(to: destination, options: .atomic) + + try FileManager.default.setAttributes( + [.posixPermissions: 0o755], + ofItemAtPath: destination.path(percentEncoded: false) + ) + } +} diff --git a/examples/sandboxy/Sources/sandboxy/ListCommand.swift b/examples/sandboxy/Sources/sandboxy/ListCommand.swift new file mode 100644 index 00000000..1a15ff49 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/ListCommand.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Foundation + +extension Sandboxy { + struct List: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "list", + abstract: "List sandbox instances", + aliases: ["ls"] + ) + + func run() async throws { + _ = try Sandboxy.loadConfig() + + let instances = try InstanceState.loadAll(appRoot: Sandboxy.appRoot) + + if instances.isEmpty { + print("No sandbox instances found.") + return + } + + // Deduplicate named instances, keeping only the most recent entry per name. + var seen = Set() + var deduplicated: [InstanceState] = [] + let sorted = instances.sorted { $0.createdAt > $1.createdAt } + for instance in sorted { + if let name = instance.name { + if seen.contains(name) { continue } + seen.insert(name) + } + deduplicated.append(instance) + } + + print( + pad("NAME", to: 30) + + pad("AGENT", to: 12) + + pad("STATUS", to: 12) + + pad("CREATED", to: 12) + + "WORKSPACE" + ) + + for instance in deduplicated { + let age = relativeTime(from: instance.createdAt) + let displayName = instance.name ?? "-" + print( + pad(truncate(displayName, to: 28), to: 30) + + pad(instance.agent, to: 12) + + pad(instance.status.rawValue, to: 12) + + pad(age, to: 12) + + instance.workspace + ) + } + } + + private func pad(_ s: String, to width: Int) -> String { + if s.count >= width { + return s + } + return s + String(repeating: " ", count: width - s.count) + } + + private func truncate(_ s: String, to maxLen: Int) -> String { + if s.count <= maxLen { return s } + return "..." + String(s.suffix(maxLen - 3)) + } + + private func relativeTime(from date: Date) -> String { + let seconds = Int(Date().timeIntervalSince(date)) + if seconds < 60 { return "\(seconds)s ago" } + let minutes = seconds / 60 + if minutes < 60 { return "\(minutes)m ago" } + let hours = minutes / 60 + if hours < 24 { return "\(hours)h ago" } + let days = hours / 24 + return "\(days)d ago" + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/OutputCapture.swift b/examples/sandboxy/Sources/sandboxy/OutputCapture.swift new file mode 100644 index 00000000..4f999173 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/OutputCapture.swift @@ -0,0 +1,47 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Containerization +import Foundation +import Synchronization + +/// A Writer that captures output into a Data buffer and optionally streams it. +final class OutputCapture: Writer, Sendable { + private let storage = Mutex(Data()) + private let streamTo: FileHandle? + + var data: Data { + storage.withLock { $0 } + } + + init(streamToStdout: Bool = false, streamToStderr: Bool = false) { + if streamToStdout { + self.streamTo = .standardOutput + } else if streamToStderr { + self.streamTo = .standardError + } else { + self.streamTo = nil + } + } + + func write(_ data: Data) throws { + guard data.count > 0 else { return } + storage.withLock { $0.append(data) } + streamTo?.write(data) + } + + func close() throws {} +} diff --git a/examples/sandboxy/Sources/sandboxy/ProgressUI.swift b/examples/sandboxy/Sources/sandboxy/ProgressUI.swift new file mode 100644 index 00000000..10d5f404 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/ProgressUI.swift @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +/// Status output helpers that write to stderr to keep stdout clean for the agent's terminal IO. +enum ProgressUI { + private static let boxLines = [ + "┌──────────────┐", + "│ ░░░░░░░░░░░░ │", + "│ ░░░░░░░░░░░░ │", + "│ ░░░░░░░░░░░░ │", + "│ ░░░░░░░░░░░░ │", + "│ ░░░░░░░░░░░░ │", + "│ ░░░░░░░░░░░░ │", + "└──────────────┘", + ] + + /// Prints the logo with info lines displayed to the right of the box. + static func printLogo(info: [String] = []) { + let yellow = "\u{1b}[33m" + let reset = "\u{1b}[0m" + let gap = " " + + for (i, boxLine) in boxLines.enumerated() { + let coloredBox = "\(yellow)\(boxLine)\(reset)" + if i < info.count { + FileHandle.standardError.write(Data(" \(coloredBox)\(gap)\(info[i])\n".utf8)) + } else { + FileHandle.standardError.write(Data(" \(coloredBox)\n".utf8)) + } + } + // Print any remaining info lines that don't fit beside the box. + if info.count > boxLines.count { + for j in boxLines.count.. \(message)\u{1b}[0m\n".utf8)) + } + + static func printDetail(_ message: String) { + FileHandle.standardError.write(Data("\u{1b}[32m \(message)\u{1b}[0m\n".utf8)) + } + + static func printWarning(_ message: String) { + FileHandle.standardError.write(Data("warning: \(message)\n".utf8)) + } + + static func printError(_ message: String) { + FileHandle.standardError.write(Data("\u{1b}[31merror: \(message)\u{1b}[0m\n".utf8)) + } +} diff --git a/examples/sandboxy/Sources/sandboxy/RemoveCommand.swift b/examples/sandboxy/Sources/sandboxy/RemoveCommand.swift new file mode 100644 index 00000000..3768f996 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/RemoveCommand.swift @@ -0,0 +1,69 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Foundation + +extension Sandboxy { + struct Remove: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "rm", + abstract: "Remove sandbox instances and their preserved state" + ) + + @Argument(help: "Name of the instance to remove") + var names: [String] = [] + + @Flag(name: [.customShort("a"), .long], help: "Remove all instances") + var all: Bool = false + + func run() async throws { + _ = try Sandboxy.loadConfig() + + if all { + let instances = try InstanceState.loadAll(appRoot: Sandboxy.appRoot) + if instances.isEmpty { + print("No instances to remove.") + return + } + for instance in instances { + let displayName = instance.name ?? instance.id + do { + try instance.removeAll(appRoot: Sandboxy.appRoot) + print("Removed instance '\(displayName)'.") + } catch { + print("Failed to remove instance '\(displayName)': \(error)") + } + } + return + } + + guard !names.isEmpty else { + print("Specify instance name(s) to remove, or use --all (-a).") + throw ExitCode.failure + } + + for name in names { + guard let instance = try InstanceState.find(name: name, appRoot: Sandboxy.appRoot) else { + print("No instance named '\(name)' found.") + continue + } + try instance.removeAll(appRoot: Sandboxy.appRoot) + print("Removed instance '\(name)'.") + } + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/RunAgentCommand.swift b/examples/sandboxy/Sources/sandboxy/RunAgentCommand.swift new file mode 100644 index 00000000..cdad0d31 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/RunAgentCommand.swift @@ -0,0 +1,1000 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Containerization +import ContainerizationError +import ContainerizationExtras +import ContainerizationOCI +import ContainerizationOS +import Foundation +import vmnet + +extension Sandboxy { + struct Run: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "run", + abstract: "Run an AI coding agent in a sandboxed Linux container", + discussion: """ + Available agents are determined by built-in definitions and any custom + agent JSON files in the agents/ subdirectory of the sandboxy application + support directory. + """ + ) + + @OptionGroup var options: AgentOptions + + @Argument(help: "Agent to run (e.g. claude)") + var agent: String + + @Argument(parsing: .captureForPassthrough) + var passthroughArgs: [String] = [] + + func run() async throws { + let config = try Sandboxy.loadConfig() + + let agents = AgentDefinition.allAgents(configRoot: Sandboxy.configRoot) + guard let definition = agents[agent] else { + let available = agents.keys.sorted().joined(separator: ", ") + throw ValidationError( + "Unknown agent '\(agent)'. Available agents: \(available)" + ) + } + + try await runAgent( + config: config, + agentName: agent, + definition: definition, + options: options, + passthroughArgs: passthroughArgs + ) + } + } +} + +struct AgentOptions: ParsableArguments { + @Option( + name: [.customLong("workspace"), .customShort("w")], + help: "Workspace directory on the host (defaults to current directory)", + completion: .directory, + transform: { str in + URL(fileURLWithPath: str, relativeTo: .currentDirectory()) + .absoluteURL.path(percentEncoded: false) + }) + var workspace: String? + + @Option(name: .long, help: "Number of CPUs to allocate") + var cpus: Int = 4 + + @Option(name: .long, help: "Memory to allocate (e.g. 4g, 512m, 4096 for MB)") + var memory: String = "4g" + + /// Parses the memory string into bytes. Supports suffixes: b, k/kb, m/mb, g/gb, t/tb. + /// A bare number is treated as megabytes for backward compatibility. + var memoryBytes: UInt64 { + get throws { + let str = memory.lowercased().trimmingCharacters(in: .whitespaces) + guard !str.isEmpty else { + throw ValidationError("Memory value cannot be empty") + } + + let suffixes: [(String, UInt64)] = [ + ("tb", 1024 * 1024 * 1024 * 1024), + ("gb", 1024 * 1024 * 1024), + ("mb", 1024 * 1024), + ("kb", 1024), + ("t", 1024 * 1024 * 1024 * 1024), + ("g", 1024 * 1024 * 1024), + ("m", 1024 * 1024), + ("k", 1024), + ("b", 1), + ] + + for (suffix, multiplier) in suffixes { + if str.hasSuffix(suffix) { + let numStr = String(str.dropLast(suffix.count)) + guard let value = Double(numStr), value > 0 else { + throw ValidationError("Invalid memory value: \(memory)") + } + return UInt64(value * Double(multiplier)) + } + } + + // Bare number: treat as megabytes. + guard let value = Double(str), value > 0 else { + throw ValidationError("Invalid memory value: \(memory)") + } + return UInt64(value * 1024 * 1024) + } + } + + /// Returns a human-readable memory string (e.g. "4 GB", "512 MB"). + var memoryDisplay: String { + get throws { + let bytes = try memoryBytes + if bytes >= 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024) == 0 { + return "\(bytes / (1024 * 1024 * 1024)) GB" + } else if bytes >= 1024 * 1024 { + return "\(bytes / (1024 * 1024)) MB" + } else if bytes >= 1024 { + return "\(bytes / 1024) KB" + } + return "\(bytes) B" + } + } + + @Option( + name: .long, parsing: .upToNextOption, + help: "Hostnames to allow through the HTTP proxy") + var allowHosts: [String] = [] + + @Flag(name: .long, help: "Disable network filtering (allow unrestricted network access)") + var noNetworkFilter: Bool = false + + @Flag(name: .long, help: "Force reinstall of agent (ignore cached rootfs)") + var reinstall: Bool = false + + @Flag(name: .long, help: "Forward the host SSH agent socket into the container") + var sshAgent: Bool = false + + @Flag(name: .long, help: "Skip mounts defined in the agent configuration") + var noAgentMounts: Bool = false + + @Flag(name: .customLong("rm"), help: "Automatically remove the instance after the session ends") + var removeAfterRun: Bool = false + + @Option( + name: [.customLong("mount"), .customShort("m")], + parsing: .singleValue, + help: "Additional mount in hostpath:containerpath[:ro|rw] format (repeatable)") + var mount: [String] = [] + + @Option( + name: [.customLong("env"), .customShort("e")], + parsing: .singleValue, + help: "Set environment variable (KEY=VALUE or KEY to forward from host, repeatable)") + var env: [String] = [] + + @Option( + name: .long, + help: "Name for a persistent instance (preserves rootfs and resumes conversation)") + var name: String? + + @Option( + name: [.customLong("kernel"), .customShort("k")], + help: "Path to Linux kernel binary (auto-downloads if omitted)", + completion: .file(), + transform: { str in + URL(fileURLWithPath: str, relativeTo: .currentDirectory()) + .absoluteURL.path(percentEncoded: false) + }) + var kernel: String? +} + +func runAgent( + config: SandboxyConfig, + agentName: String, + definition: AgentDefinition, + options: AgentOptions, + passthroughArgs: [String] +) async throws { + signal(SIGINT) { _ in + var termios = termios() + tcgetattr(STDIN_FILENO, &termios) + termios.c_lflag |= UInt(ECHO | ICANON) + tcsetattr(STDIN_FILENO, TCSANOW, &termios) + write(STDERR_FILENO, "\u{001B}[?25h", 6) + _exit(130) + } + + let hostWorkspacePath = options.workspace ?? FileManager.default.currentDirectoryPath + let guestWorkspacePath = hostWorkspacePath + let extraMounts = try options.mount.map { try MountSpec.parse($0) } + let extraEnvVars = try options.env.map { try EnvSpec.resolve($0) } + + // Determine instance name: use --name if provided, otherwise auto-generate. + let instanceName: String + if let name = options.name { + instanceName = name + } else { + let formatter = DateFormatter() + formatter.dateFormat = "yyyyMMdd-HHmmss" + instanceName = "\(agentName)-\(formatter.string(from: Date()))" + } + + if let old = try InstanceState.find(name: instanceName, appRoot: Sandboxy.appRoot) { + try? old.remove(appRoot: Sandboxy.appRoot) + } + + // Check cache age for display. + let cacheDir = Sandboxy.appRoot.appendingPathComponent("cache") + let agentCachePath = cacheDir.appendingPathComponent("\(agentName)-rootfs.ext4") + let cacheAgeLine: String + if let attrs = try? FileManager.default.attributesOfItem(atPath: agentCachePath.path(percentEncoded: false)), + let created = attrs[.creationDate] as? Date + { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .full + let age = formatter.localizedString(for: created, relativeTo: Date()) + let capitalizedAge = age.prefix(1).uppercased() + age.dropFirst() + cacheAgeLine = "\u{1b}[1mEnvironment:\u{1b}[0m \(capitalizedAge)" + } else { + cacheAgeLine = "\u{1b}[1mEnvironment:\u{1b}[0m Not yet installed" + } + + ProgressUI.printLogo(info: [ + "", + "\u{1b}[1mSandboxy\u{1b}[0m", + "\u{1b}[1mAgent:\u{1b}[0m \(definition.displayName)", + "\u{1b}[1mInstance:\u{1b}[0m \(instanceName)", + cacheAgeLine, + "\u{1b}[1mWorkspace:\u{1b}[0m \(hostWorkspacePath)", + "\u{1b}[1mCPUs:\u{1b}[0m \(options.cpus) \u{1b}[1mMemory:\u{1b}[0m \(try options.memoryDisplay)", + ]) + + let kernelPath = try await KernelManager.ensureKernel( + explicitPath: options.kernel, + appRoot: Sandboxy.appRoot, + config: config + ) + guard FileManager.default.fileExists(atPath: kernelPath.path(percentEncoded: false)) else { + throw SandboxyError.kernelNotFound(path: kernelPath.path(percentEncoded: false)) + } + let kernel = Kernel(path: kernelPath, platform: .linuxArm) + + // Merge allowed hosts from agent definition and CLI flags. + var allowedHosts = definition.allowedHosts + allowedHosts.append(contentsOf: options.allowHosts) + let filteringEnabled = !options.noNetworkFilter + + let filteredPassthroughArgs = passthroughArgs.filter { $0 != "--" } + + var fullCommand = definition.launchCommand + fullCommand.append(contentsOf: filteredPassthroughArgs) + ProgressUI.printDetail("\u{1b}[1mCommand:\u{1b}[0m \(fullCommand.joined(separator: " "))") + + if filteringEnabled { + if allowedHosts.isEmpty { + ProgressUI.printDetail("\u{1b}[1mAllowed hosts:\u{1b}[0m\u{1b}[33m none (all traffic denied)") + } else { + let hostList = allowedHosts.joined(separator: ", ") + ProgressUI.printDetail("\u{1b}[1mAllowed hosts:\u{1b}[0m\u{1b}[32m \(hostList)") + } + } else { + ProgressUI.printDetail("\u{1b}[1mAllowed hosts:\u{1b}[0m\u{1b}[32m unrestricted") + } + + // Log mounts. + ProgressUI.printDetail("\u{1b}[1mMounts:\u{1b}[0m") + ProgressUI.printDetail(" \(hostWorkspacePath) -> \(guestWorkspacePath)") + if options.noAgentMounts { + for agentMount in definition.mounts { + let ro = agentMount.readOnly ? " (ro)" : "" + ProgressUI.printDetail(" \(agentMount.resolvedHostPath) -> \(agentMount.containerPath)\(ro) \u{1b}[33m(skipped, --no-agent-mounts)\u{1b}[0m") + } + } else { + for agentMount in definition.mounts { + let hostPath = agentMount.resolvedHostPath + let ro = agentMount.readOnly ? " (ro)" : "" + if FileManager.default.fileExists(atPath: hostPath) { + ProgressUI.printDetail(" \(hostPath) -> \(agentMount.containerPath)\(ro)") + } else { + ProgressUI.printDetail(" \(hostPath) -> \(agentMount.containerPath)\(ro) \u{1b}[33m(skipped, host path not found)\u{1b}[0m") + } + } + } + for mountSpec in extraMounts { + let ro = mountSpec.readOnly ? " (ro)" : "" + ProgressUI.printDetail(" \(mountSpec.hostPath) -> \(mountSpec.containerPath)\(ro)") + } + + // Setup networking. + let enableNetworking: Bool + var sharedNetwork: VmnetNetwork? + if #available(macOS 26, *) { + sharedNetwork = try VmnetNetwork() + enableNetworking = true + } else { + sharedNetwork = nil + enableNetworking = false + } + + // Pull the init image with progress if it hasn't been cached yet. + let initfsReference = config.initfsReference ?? SandboxyConfig.defaults.initfsReference! + _ = try await pullImageWithProgress(reference: initfsReference) + + var manager = try await ContainerManager( + kernel: kernel, + initfsReference: initfsReference, + root: Sandboxy.appRoot + ) + + /// MTU for vmnet interfaces. Lowered from the default 1500 to avoid + /// PMTU black-hole issues on networks that block ICMP fragmentation-needed. + let vmnetMTU: UInt32 = 1400 + + let containerId = "\(agentName)-\(ProcessInfo.processInfo.processIdentifier)" + + // + // Workload container setup + // + + // Determine rootfs source: named instance > agent cache > fresh install. + try FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true) + let containerRootfsPath = Sandboxy.appRoot + .appendingPathComponent("containers") + .appendingPathComponent(containerId) + .appendingPathComponent("rootfs.ext4") + + let sourceRootfs: URL? + var needsInstall = false + + if options.reinstall { + removeIfExists(at: agentCachePath) + removeIfExists( + at: InstanceState.namedRootfsPath(appRoot: Sandboxy.appRoot, name: instanceName)) + sourceRootfs = nil + needsInstall = true + } else { + let namedPath = InstanceState.namedRootfsPath(appRoot: Sandboxy.appRoot, name: instanceName) + if FileManager.default.fileExists(atPath: namedPath.path(percentEncoded: false)) { + sourceRootfs = namedPath + } else if FileManager.default.fileExists(atPath: agentCachePath.path(percentEncoded: false)) { + sourceRootfs = agentCachePath + } else { + sourceRootfs = nil + needsInstall = true + } + } + + // Create workload container with full network for installation. + // After install, we recreate with the filtered network if needed. + var container: LinuxContainer + + if let sourceRootfs { + let containerDir = Sandboxy.appRoot + .appendingPathComponent("containers") + .appendingPathComponent(containerId) + try FileManager.default.createDirectory(at: containerDir, withIntermediateDirectories: true) + + let result = Darwin.clonefile( + sourceRootfs.path(percentEncoded: false), + containerRootfsPath.path(percentEncoded: false), + 0 + ) + if result != 0 { + try FileManager.default.copyItem(at: sourceRootfs, to: containerRootfsPath) + } + + let rootfsMount = Mount.block( + format: "ext4", + source: containerRootfsPath.path(percentEncoded: false), + destination: "/", + runtimeOptions: ["vzDiskImageSynchronizationMode=fsync"] + ) + + let image = try await pullImageWithProgress(reference: definition.baseImage) + + container = try await manager.create( + containerId, + image: image, + rootfs: rootfsMount, + networking: false + ) { config in + if enableNetworking, let iface = try sharedNetwork?.createInterface(containerId, mtu: vmnetMTU) { + config.interfaces = [iface] + config.dns = .init(nameservers: [sharedNetwork!.ipv4Gateway.description]) + } + try configureContainer( + config: &config, + definition: definition, + options: options, + containerId: containerId, + hostWorkspacePath: hostWorkspacePath, + guestWorkspacePath: guestWorkspacePath, + extraMounts: extraMounts + ) + } + } else { + let progressConfig = try ProgressConfig( + showTasks: true, + showItems: true, + ignoreSmallSize: true, + totalTasks: 2 + ) + let progress = ProgressBar(config: progressConfig) + defer { progress.finish() } + progress.start() + + progress.set(description: "Pulling image (\(definition.baseImage))") + progress.set(itemsName: "blobs") + let image = try await Sandboxy.imageStore.pull( + reference: definition.baseImage, + progress: progressEventAdapter(for: progress) + ) + + progress.set(description: "Unpacking image") + container = try await manager.create( + containerId, + image: image, + rootfsSizeInBytes: 512.gib(), + networking: false + ) { config in + if enableNetworking, let iface = try sharedNetwork?.createInterface(containerId, mtu: vmnetMTU) { + config.interfaces = [iface] + config.dns = .init(nameservers: [sharedNetwork!.ipv4Gateway.description]) + } + try configureContainer( + config: &config, + definition: definition, + options: options, + containerId: containerId, + hostWorkspacePath: hostWorkspacePath, + guestWorkspacePath: guestWorkspacePath, + extraMounts: extraMounts + ) + } + } + + // Boot and install toolchain if needed (with full network). + if needsInstall { + try await container.create() + try await container.start() + + ProgressUI.printStatus("Installing \(definition.displayName) toolchain...") + try await installAgent(in: container, definition: definition) + ProgressUI.printStatus("Installation complete.") + + try await container.stop() + ProgressUI.printDetail("Caching environment for future runs...") + try FileManager.default.copyItem(at: containerRootfsPath, to: agentCachePath) + + // Delete so we can recreate (possibly on a different network). + try manager.delete(containerId) + try? sharedNetwork?.releaseInterface(containerId) + + // Recreate from the freshly-cached rootfs (unless filtering will recreate again). + if !filteringEnabled { + let cachedRootfsMount = Mount.block( + format: "ext4", + source: containerRootfsPath.path(percentEncoded: false), + destination: "/", + runtimeOptions: ["vzDiskImageSynchronizationMode=fsync"] + ) + let cachedImage = try await pullImageWithProgress(reference: definition.baseImage) + container = try await manager.create( + containerId, + image: cachedImage, + rootfs: cachedRootfsMount, + networking: false + ) { config in + if enableNetworking, let iface = try sharedNetwork?.createInterface(containerId, mtu: vmnetMTU) { + config.interfaces = [iface] + config.dns = .init(nameservers: [sharedNetwork!.ipv4Gateway.description]) + } + try configureContainer( + config: &config, + definition: definition, + options: options, + containerId: containerId, + hostWorkspacePath: hostWorkspacePath, + guestWorkspacePath: guestWorkspacePath, + extraMounts: extraMounts + ) + } + } + } + + // Host-only network setup (only when filtering is active) + var proxyIP: String? + var hostOnlyNetwork: VmnetNetwork? + + if filteringEnabled, enableNetworking, #available(macOS 26, *) { + hostOnlyNetwork = try VmnetNetwork(mode: .VMNET_HOST_MODE) + + let gatewayIP = hostOnlyNetwork!.ipv4Gateway.description + let workloadHostOnlyInterface = try hostOnlyNetwork!.createInterface(containerId, mtu: vmnetMTU) + proxyIP = gatewayIP + + // Recreate the workload container on the host-only network. + if !needsInstall { + try manager.delete(containerId) + try? sharedNetwork?.releaseInterface(containerId) + } + + let filteredContainerDir = Sandboxy.appRoot + .appendingPathComponent("containers") + .appendingPathComponent(containerId) + try FileManager.default.createDirectory(at: filteredContainerDir, withIntermediateDirectories: true) + + let filteredRootfsSource = needsInstall ? agentCachePath : (sourceRootfs ?? agentCachePath) + let cloneResult2 = Darwin.clonefile( + filteredRootfsSource.path(percentEncoded: false), + containerRootfsPath.path(percentEncoded: false), + 0 + ) + if cloneResult2 != 0 { + try FileManager.default.copyItem(at: filteredRootfsSource, to: containerRootfsPath) + } + + let filteredRootfsMount = Mount.block( + format: "ext4", + source: containerRootfsPath.path(percentEncoded: false), + destination: "/", + runtimeOptions: ["vzDiskImageSynchronizationMode=fsync"] + ) + + let image = try await pullImageWithProgress(reference: definition.baseImage) + + container = try await manager.create( + containerId, + image: image, + rootfs: filteredRootfsMount, + networking: false + ) { config in + if let iface = workloadHostOnlyInterface { + config.interfaces = [iface] + config.dns = .init(nameservers: [gatewayIP]) + } + try configureContainer( + config: &config, + definition: definition, + options: options, + containerId: containerId, + hostWorkspacePath: hostWorkspacePath, + guestWorkspacePath: guestWorkspacePath, + extraMounts: extraMounts + ) + } + } + + // Run the container session, cleaning up on both success and failure. + do { + try await runContainerSession( + container: container, + containerId: containerId, + instanceName: instanceName, + agentName: agentName, + definition: definition, + options: options, + containerRootfsPath: containerRootfsPath, + agentCachePath: agentCachePath, + hostWorkspacePath: hostWorkspacePath, + guestWorkspacePath: guestWorkspacePath, + extraEnvVars: extraEnvVars, + proxyIP: proxyIP, + allowedHosts: allowedHosts, + passthroughArgs: filteredPassthroughArgs + ) + + // Cleanup + try manager.delete(containerId) + try? sharedNetwork?.releaseInterface(containerId) + if hostOnlyNetwork != nil { + try? hostOnlyNetwork?.releaseInterface(containerId) + } + } catch { + do { + try await container.stop() + } catch { + log.warning("Failed to stop container \(containerId): \(error)") + } + do { + try manager.delete(containerId) + } catch { + log.warning("Failed to delete container \(containerId): \(error)") + } + try? sharedNetwork?.releaseInterface(containerId) + if hostOnlyNetwork != nil { + try? hostOnlyNetwork?.releaseInterface(containerId) + } + throw error + } +} + +private func runContainerSession( + container: LinuxContainer, + containerId: String, + instanceName: String, + agentName: String, + definition: AgentDefinition, + options: AgentOptions, + containerRootfsPath: URL, + agentCachePath: URL, + hostWorkspacePath: String, + guestWorkspacePath: String, + extraEnvVars: [String], + proxyIP: String?, + allowedHosts: [String], + passthroughArgs: [String] +) async throws { + // create() boots the VM and brings up the vmnet bridge on the host. + try await container.create() + + // Start the proxy now that the bridge interface is up. + var hostProxy: HostProxy? + if let proxyIP { + let proxy = try await HostProxy( + host: proxyIP, + port: 0, + allowedHosts: allowedHosts + ) + hostProxy = proxy + } + + // start() launches the container process. + try await container.start() + + // Write instance state. + let instanceState = InstanceState( + id: containerId, + name: instanceName, + agent: agentName, + workspace: hostWorkspacePath, + status: .running, + createdAt: Date(), + cpus: options.cpus, + memoryMB: try options.memoryBytes / (1024 * 1024) + ) + try instanceState.save(appRoot: Sandboxy.appRoot) + + let sigwinchStream = AsyncSignalHandler.create(notify: [SIGWINCH]) + let current = try Terminal.current + try current.setraw() + defer { current.tryReset() } + + // Build environment for the agent process. + var envVarsBuilder = [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "TERM=xterm-256color", + "HOME=/root", + ] + for envVar in definition.environmentVariables { + if envVar.contains("=") { + envVarsBuilder.append(envVar) + } else if let value = ProcessInfo.processInfo.environment[envVar] { + envVarsBuilder.append("\(envVar)=\(value)") + } + } + + envVarsBuilder.append(contentsOf: extraEnvVars) + + if options.sshAgent, ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] != nil { + envVarsBuilder.append("SSH_AUTH_SOCK=/tmp/ssh-agent.sock") + } + + if let proxyIP, let proxyPort = hostProxy?.port { + let proxyURL = "http://\(proxyIP):\(proxyPort)" + envVarsBuilder.append("HTTP_PROXY=\(proxyURL)") + envVarsBuilder.append("HTTPS_PROXY=\(proxyURL)") + envVarsBuilder.append("http_proxy=\(proxyURL)") + envVarsBuilder.append("https_proxy=\(proxyURL)") + envVarsBuilder.append("NO_PROXY=localhost,127.0.0.1") + envVarsBuilder.append("no_proxy=localhost,127.0.0.1") + envVarsBuilder.append("GLOBAL_AGENT_HTTP_PROXY=\(proxyURL)") + envVarsBuilder.append("GLOBAL_AGENT_HTTPS_PROXY=\(proxyURL)") + envVarsBuilder.append("GLOBAL_AGENT_NO_PROXY=localhost,127.0.0.1") + + // Prepend global-agent bootstrap to NODE_OPTIONS so Node.js http/https + // modules respect the proxy environment variables. + if let idx = envVarsBuilder.firstIndex(where: { $0.hasPrefix("NODE_OPTIONS=") }) { + let existing = String(envVarsBuilder[idx].dropFirst("NODE_OPTIONS=".count)) + envVarsBuilder[idx] = "NODE_OPTIONS=-r /usr/local/lib/node_modules/global-agent/dist/routines/bootstrap.js \(existing)" + } else { + envVarsBuilder.append("NODE_OPTIONS=-r /usr/local/lib/node_modules/global-agent/dist/routines/bootstrap.js") + } + } + + var launchArgsBuilder = definition.launchCommand + launchArgsBuilder.append(contentsOf: passthroughArgs) + + let envVars = envVarsBuilder + let launchArgs = launchArgsBuilder + + let agentProcess = try await container.exec("agent-session") { config in + config.arguments = launchArgs + config.environmentVariables = envVars + config.workingDirectory = guestWorkspacePath + config.terminal = true + config.stdin = current + config.stdout = current + } + + try await agentProcess.start() + try? await agentProcess.resize(to: try current.size) + + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + for await _ in sigwinchStream.signals { + try await agentProcess.resize(to: try current.size) + } + } + + let status = try await agentProcess.wait() + group.cancelAll() + + try await agentProcess.delete() + + // Stop container so rootfs is cleanly unmounted before caching. + try await container.stop() + + if options.removeAfterRun { + ProgressUI.printStatus("Instance \u{1b}[1m\(instanceName)\u{1b}[0m removed (--rm).") + } else { + // Preserve the rootfs for all instances. + let namedDir = InstanceState.namedRootfsDir(appRoot: Sandboxy.appRoot) + try FileManager.default.createDirectory(at: namedDir, withIntermediateDirectories: true) + let namedPath = InstanceState.namedRootfsPath(appRoot: Sandboxy.appRoot, name: instanceName) + removeIfExists(at: namedPath) + try FileManager.default.copyItem(at: containerRootfsPath, to: namedPath) + + let stopped = InstanceState( + id: instanceState.id, + name: instanceState.name, + agent: instanceState.agent, + workspace: instanceState.workspace, + status: .stopped, + createdAt: instanceState.createdAt, + stoppedAt: Date(), + cpus: instanceState.cpus, + memoryMB: instanceState.memoryMB + ) + try stopped.save(appRoot: Sandboxy.appRoot) + + ProgressUI.printStatus("Instance \u{1b}[1m\(instanceName)\u{1b}[0m saved. Resume with: sandboxy run \(agentName) --name \(instanceName)") + } + + if status.exitCode != 0 { + throw ExitCode(status.exitCode) + } + } + + if let proxy = hostProxy { + try await proxy.stop() + } +} + +private func configureContainer( + config: inout LinuxContainer.Configuration, + definition: AgentDefinition, + options: AgentOptions, + containerId: String, + hostWorkspacePath: String, + guestWorkspacePath: String, + extraMounts: [MountSpec] +) throws { + config.cpus = options.cpus + config.memoryInBytes = try options.memoryBytes + + config.process.arguments = ["/bin/sleep", "infinity"] + config.process.workingDirectory = "/" + config.process.capabilities = .allCapabilities + config.useInit = true + + // SSH agent forwarding. + if options.sshAgent, + let authSock = ProcessInfo.processInfo.environment["SSH_AUTH_SOCK"] + { + let guestSocketPath = "/tmp/ssh-agent.sock" + config.sockets.append( + UnixSocketConfiguration( + source: URL(fileURLWithPath: authSock), + destination: URL(fileURLWithPath: guestSocketPath), + direction: .into + ) + ) + } + + config.mounts.append( + Mount.share( + source: hostWorkspacePath, + destination: guestWorkspacePath + ) + ) + + if !options.noAgentMounts { + for agentMount in definition.mounts { + let hostPath = agentMount.resolvedHostPath + if FileManager.default.fileExists(atPath: hostPath) { + config.mounts.append( + Mount.share( + source: hostPath, + destination: agentMount.containerPath, + options: agentMount.readOnly ? ["ro"] : [] + ) + ) + } + } + } + + for mountSpec in extraMounts { + config.mounts.append(mountSpec.toMount()) + } + + var hosts = Hosts.default + if #available(macOS 26, *), !config.interfaces.isEmpty { + let interface = config.interfaces[0] + hosts.entries.append( + Hosts.Entry( + ipAddress: interface.ipv4Address.address.description, + hostnames: [containerId] + ) + ) + } + config.hosts = hosts +} + +func installAgent( + in container: LinuxContainer, + definition: AgentDefinition +) async throws { + for (index, command) in definition.installCommands.enumerated() { + let truncated = command.count > 80 ? String(command.prefix(77)) + "..." : command + ProgressUI.printDetail("[\(index + 1)/\(definition.installCommands.count)] \(truncated)") + + let buffer = OutputCapture(streamToStdout: true) + let execId = "install-\(index)" + let process = try await container.exec(execId) { config in + config.arguments = ["/bin/sh", "-c", command] + config.workingDirectory = "/" + config.stdout = buffer + config.stderr = buffer + config.capabilities = .allCapabilities + config.environmentVariables = [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "DEBIAN_FRONTEND=noninteractive", + "HOME=/root", + ] + } + + try await process.start() + let status = try await process.wait() + try await process.delete() + + guard status.exitCode == 0 else { + throw SandboxyError.installFailed( + step: index + 1, + command: command, + exitCode: status.exitCode + ) + } + } +} + +func removeIfExists(at url: URL) { + let path = url.path(percentEncoded: false) + if FileManager.default.fileExists(atPath: path) { + do { + try FileManager.default.removeItem(at: url) + } catch { + log.warning("Failed to remove \(path): \(error)") + } + } +} + +/// A parsed `hostpath:containerpath[:ro|rw]` mount specification from the CLI. +struct MountSpec { + let hostPath: String + let containerPath: String + let readOnly: Bool + + static func parse(_ spec: String) throws -> MountSpec { + let parts = spec.split(separator: ":", maxSplits: 2).map(String.init) + guard parts.count >= 2 else { + throw SandboxyError.invalidMountSpec(spec: spec) + } + + let readOnly: Bool + if parts.count == 3 { + switch parts[2] { + case "ro": + readOnly = true + case "rw": + readOnly = false + default: + throw SandboxyError.invalidMountSpec(spec: spec) + } + } else { + readOnly = false + } + + // Resolve host path to absolute. + let hostPath = URL(fileURLWithPath: parts[0], relativeTo: .currentDirectory()) + .absoluteURL.path(percentEncoded: false) + + return MountSpec(hostPath: hostPath, containerPath: parts[1], readOnly: readOnly) + } + + func toMount() -> Containerization.Mount { + Containerization.Mount.share( + source: hostPath, + destination: containerPath, + options: readOnly ? ["ro"] : [] + ) + } +} + +func progressEventAdapter(for progress: ProgressBar) -> ProgressHandler { + { events in + for event in events { + switch event.event { + case "add-size": + if let value = event.value as? Int64 { + progress.add(size: value) + } + case "add-total-size": + if let value = event.value as? Int64 { + progress.add(totalSize: value) + } + case "add-items": + if let value = event.value as? Int { + progress.add(items: value) + } + case "add-total-items": + if let value = event.value as? Int { + progress.add(totalItems: value) + } + default: + break + } + } + } +} + +/// Pulls an image, showing a progress bar only if the image isn't already cached locally. +func pullImageWithProgress(reference: String) async throws -> Containerization.Image { + do { + return try await Sandboxy.imageStore.get(reference: reference) + } catch { + let progressConfig = try ProgressConfig( + description: "Pulling image (\(reference))", + showItems: true, + ignoreSmallSize: true + ) + let progress = ProgressBar(config: progressConfig) + defer { progress.finish() } + progress.start() + progress.set(itemsName: "blobs") + return try await Sandboxy.imageStore.pull( + reference: reference, + progress: progressEventAdapter(for: progress) + ) + } +} + +/// Resolves a `KEY=VALUE` or `KEY` environment variable specification. +/// +/// - `KEY=VALUE`: passed through as-is. +/// - `KEY`: looks up the variable in the host environment and produces `KEY=`. +/// Throws if the variable is not set. +enum EnvSpec { + static func resolve(_ spec: String) throws -> String { + if let eqIndex = spec.firstIndex(of: "=") { + // KEY=VALUE: Use as-is, but validate key is non-empty. + let key = spec[spec.startIndex.. SandboxyConfig { + let fm = FileManager.default + let agentsDir = configRoot.appendingPathComponent("agents") + try fm.createDirectory(at: agentsDir, withIntermediateDirectories: true) + + let config = try SandboxyConfig.load(configRoot: configRoot) + if let dataDir = config.dataDir { + appRoot = URL(fileURLWithPath: dataDir) + } + + try fm.createDirectory(at: appRoot, withIntermediateDirectories: true) + return config + } + + /// User configuration directory (`~/.config/sandboxy/`). + /// Holds `config.json` and `agents/` definition files. + static let configRoot: URL = { + FileManager.default.homeDirectoryForCurrentUser + .appendingPathComponent(".config") + .appendingPathComponent("sandboxy") + }() + + private static let _contentStore: ContentStore = { + try! LocalContentStore(path: appRoot.appendingPathComponent("content")) + }() + + private static let _imageStore: ImageStore = { + try! ImageStore( + path: appRoot, + contentStore: contentStore + ) + }() + + static var imageStore: ImageStore { + _imageStore + } + + static var contentStore: ContentStore { + _contentStore + } +} diff --git a/examples/sandboxy/Sources/sandboxy/SandboxyConfig.swift b/examples/sandboxy/Sources/sandboxy/SandboxyConfig.swift new file mode 100644 index 00000000..d946970d --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/SandboxyConfig.swift @@ -0,0 +1,81 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +/// Optional configuration file for overriding sandboxy defaults. +/// +/// If `config.json` exists in the sandboxy config directory, it is +/// loaded at startup and its values override the built-in defaults. Any field +/// can be omitted to keep the default. +/// +/// Location: `~/.config/sandboxy/config.json` +/// +/// Example: +/// ```json +/// { +/// "dataDir": "/Volumes/fast/sandboxy", +/// "kernel": "/path/to/vmlinux", +/// "initfsReference": "ghcr.io/apple/containerization/vminit:latest", +/// "defaultCPUs": 4, +/// "defaultMemory": "4g" +/// } +/// ``` +struct SandboxyConfig: Codable, Sendable { + /// Directory for runtime data (caches, content store, containers). + /// Defaults to `~/Library/Application Support/com.apple.containerization.sandboxy`. + var dataDir: String? + /// Path to a Linux kernel binary on disk. When set, the auto-download is skipped. + var kernel: String? + /// OCI reference for the VM init image. + var initfsReference: String? + /// Default number of CPUs for new containers. + var defaultCPUs: Int? + /// Default memory for new containers (e.g. "4g", "512m", "4096" for MB). + var defaultMemory: String? + + /// Built-in defaults used when no config file is present. + static let defaults = SandboxyConfig( + initfsReference: "ghcr.io/apple/containerization/vminit:0.30.0", + defaultCPUs: 4, + defaultMemory: "4g" + ) + + /// Loads the config from `/config.json`, falling back to defaults + /// for any missing fields. + static func load(configRoot: URL) throws -> SandboxyConfig { + let configPath = configRoot.appendingPathComponent("config.json") + + guard FileManager.default.fileExists(atPath: configPath.path(percentEncoded: false)) else { + return .defaults + } + + do { + let data = try Data(contentsOf: configPath) + let userConfig = try JSONDecoder().decode(SandboxyConfig.self, from: data) + + return SandboxyConfig( + dataDir: userConfig.dataDir, + kernel: userConfig.kernel, + initfsReference: userConfig.initfsReference ?? defaults.initfsReference, + defaultCPUs: userConfig.defaultCPUs ?? defaults.defaultCPUs, + defaultMemory: userConfig.defaultMemory ?? defaults.defaultMemory + ) + } catch { + throw SandboxyError.configFailedToLoad(error: error) + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/SandboxyError.swift b/examples/sandboxy/Sources/sandboxy/SandboxyError.swift new file mode 100644 index 00000000..d112c975 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/SandboxyError.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +enum SandboxyError: Error, CustomStringConvertible { + case configFailedToLoad(error: Swift.Error) + case installFailed(step: Int, command: String, exitCode: Int32) + case kernelDownloadFailed(reason: String) + case proxyFailed(reason: String) + case kernelNotFound(path: String) + case incompleteAgentDefinition(missing: [String]) + case invalidMountSpec(spec: String) + case envVarNotSet(name: String) + + var description: String { + switch self { + case .configFailedToLoad(let error): + return "Failed to load sandbox config: \(error)" + case .installFailed(let step, let command, let exitCode): + return """ + Installation step \(step) failed (exit code \(exitCode)). + Command: \(command) + """ + case .kernelDownloadFailed(let reason): + return "Failed to download kernel: \(reason)" + case .proxyFailed(let reason): + return "Proxy failed: \(reason)" + case .kernelNotFound(let path): + return "Kernel not found at \(path). Provide a valid path with -k or omit to auto-download." + case .incompleteAgentDefinition(let missing): + return "Agent definition is missing required fields: \(missing.joined(separator: ", ")). Use 'sandboxy config --agent claude' to see a complete example." + case .invalidMountSpec(let spec): + return "Invalid mount specification: '\(spec)'. Expected format: hostpath:containerpath[:ro|rw]" + case .envVarNotSet(let name): + return "Environment variable '\(name)' is not set on the host." + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/TerminalProgress/Int+Formatted.swift b/examples/sandboxy/Sources/sandboxy/TerminalProgress/Int+Formatted.swift new file mode 100644 index 00000000..28f7ee21 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/TerminalProgress/Int+Formatted.swift @@ -0,0 +1,52 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +extension Int { + func formattedTime() -> String { + let secondsInMinute = 60 + let secondsInHour = secondsInMinute * 60 + let secondsInDay = secondsInHour * 24 + + let days = self / secondsInDay + let hours = (self % secondsInDay) / secondsInHour + let minutes = (self % secondsInHour) / secondsInMinute + let seconds = self % secondsInMinute + + var components = [String]() + if days > 0 { + components.append("\(days)d") + } + if hours > 0 || days > 0 { + components.append("\(hours)h") + } + if minutes > 0 || hours > 0 || days > 0 { + components.append("\(minutes)m") + } + components.append("\(seconds)s") + return components.joined(separator: " ") + } + + func formattedNumber() -> String { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + guard let formattedNumber = formatter.string(from: NSNumber(value: self)) else { + return "" + } + return formattedNumber + } +} diff --git a/examples/sandboxy/Sources/sandboxy/TerminalProgress/Int64+Formatted.swift b/examples/sandboxy/Sources/sandboxy/TerminalProgress/Int64+Formatted.swift new file mode 100644 index 00000000..34b73ff0 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/TerminalProgress/Int64+Formatted.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +extension Int64 { + func formattedSize() -> String { + let formattedSize = ByteCountFormatter.string(fromByteCount: self, countStyle: .binary) + return formattedSize + } + + func formattedSizeSpeed(from startTime: DispatchTime) -> String { + let elapsedTimeNanoseconds = DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds + let elapsedTimeSeconds = Double(elapsedTimeNanoseconds) / 1_000_000_000 + guard elapsedTimeSeconds > 0 else { + return "0 B/s" + } + + let speed = Double(self) / elapsedTimeSeconds + let formattedSpeed = ByteCountFormatter.string(fromByteCount: Int64(speed), countStyle: .binary) + return "\(formattedSpeed)/s" + } +} diff --git a/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar+Add.swift b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar+Add.swift new file mode 100644 index 00000000..3710895d --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar+Add.swift @@ -0,0 +1,236 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +extension ProgressBar { + /// A handler function to update the progress bar. + /// - Parameter events: The events to handle. + public func handler(_ events: [ProgressUpdateEvent]) { + for event in events { + switch event { + case .setDescription(let description): + set(description: description) + case .setSubDescription(let subDescription): + set(subDescription: subDescription) + case .setItemsName(let itemsName): + set(itemsName: itemsName) + case .addTasks(let tasks): + add(tasks: tasks) + case .setTasks(let tasks): + set(tasks: tasks) + case .addTotalTasks(let totalTasks): + add(totalTasks: totalTasks) + case .setTotalTasks(let totalTasks): + set(totalTasks: totalTasks) + case .addSize(let size): + add(size: size) + case .setSize(let size): + set(size: size) + case .addTotalSize(let totalSize): + add(totalSize: totalSize) + case .setTotalSize(let totalSize): + set(totalSize: totalSize) + case .addItems(let items): + add(items: items) + case .setItems(let items): + set(items: items) + case .addTotalItems(let totalItems): + add(totalItems: totalItems) + case .setTotalItems(let totalItems): + set(totalItems: totalItems) + case .custom: + // Custom events are handled by the client. + break + } + } + } + + /// Performs a check to see if the progress bar should be finished. + public func checkIfFinished() { + let state = self.state.withLock { $0 } + + var finished = true + var defined = false + if let totalTasks = state.totalTasks, totalTasks > 0 { + // For tasks, we're showing the current task rather than the number of completed tasks. + finished = finished && state.tasks == totalTasks + defined = true + } + if let totalItems = state.totalItems, totalItems > 0 { + finished = finished && state.items == totalItems + defined = true + } + if let totalSize = state.totalSize, totalSize > 0 { + finished = finished && state.size == totalSize + defined = true + } + if defined && finished { + finish() + } + } + + /// Sets the current tasks. + /// - Parameter newTasks: The current tasks to set. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. + public func set(tasks newTasks: Int, render: Bool = true) { + state.withLock { $0.tasks = newTasks } + if render { + self.render() + } + checkIfFinished() + } + + /// Performs an addition to the current tasks. + /// - Parameter delta: The tasks to add to the current tasks. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. + public func add(tasks delta: Int, render: Bool = true) { + state.withLock { + let newTasks = $0.tasks + delta + $0.tasks = newTasks + } + if render { + self.render() + } + } + + /// Sets the total tasks. + /// - Parameter newTotalTasks: The total tasks to set. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. + public func set(totalTasks newTotalTasks: Int, render: Bool = true) { + state.withLock { $0.totalTasks = newTotalTasks } + if render { + self.render() + } + } + + /// Performs an addition to the total tasks. + /// - Parameter delta: The tasks to add to the total tasks. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. + public func add(totalTasks delta: Int, render: Bool = true) { + state.withLock { + let totalTasks = $0.totalTasks ?? 0 + let newTotalTasks = totalTasks + delta + $0.totalTasks = newTotalTasks + } + if render { + self.render() + } + } + + /// Sets the items name. + /// - Parameter newItemsName: The current items to set. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. + public func set(itemsName newItemsName: String, render: Bool = true) { + state.withLock { $0.itemsName = newItemsName } + if render { + self.render() + } + } + + /// Sets the current items. + /// - Parameter newItems: The current items to set. + public func set(items newItems: Int, render: Bool = true) { + state.withLock { $0.items = newItems } + if render { + self.render() + } + } + + /// Performs an addition to the current items. + /// - Parameter delta: The items to add to the current items. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. + public func add(items delta: Int, render: Bool = true) { + state.withLock { + let newItems = $0.items + delta + $0.items = newItems + } + if render { + self.render() + } + } + + /// Sets the total items. + /// - Parameter newTotalItems: The total items to set. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. + public func set(totalItems newTotalItems: Int, render: Bool = true) { + state.withLock { $0.totalItems = newTotalItems } + if render { + self.render() + } + } + + /// Performs an addition to the total items. + /// - Parameter delta: The items to add to the total items. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. + public func add(totalItems delta: Int, render: Bool = true) { + state.withLock { + let totalItems = $0.totalItems ?? 0 + let newTotalItems = totalItems + delta + $0.totalItems = newTotalItems + } + if render { + self.render() + } + } + + /// Sets the current size. + /// - Parameter newSize: The current size to set. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. + public func set(size newSize: Int64, render: Bool = true) { + state.withLock { $0.size = newSize } + if render { + self.render() + } + } + + /// Performs an addition to the current size. + /// - Parameter delta: The size to add to the current size. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. + public func add(size delta: Int64, render: Bool = true) { + state.withLock { + let newSize = $0.size + delta + $0.size = newSize + } + if render { + self.render() + } + } + + /// Sets the total size. + /// - Parameter newTotalSize: The total size to set. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. + public func set(totalSize newTotalSize: Int64, render: Bool = true) { + state.withLock { $0.totalSize = newTotalSize } + if render { + self.render() + } + } + + /// Performs an addition to the total size. + /// - Parameter delta: The size to add to the total size. + /// - Parameter render: The flag indicating whether the progress bar has to render after the update. + public func add(totalSize delta: Int64, render: Bool = true) { + state.withLock { + let totalSize = $0.totalSize ?? 0 + let newTotalSize = totalSize + delta + $0.totalSize = newTotalSize + } + if render { + self.render() + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar+State.swift b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar+State.swift new file mode 100644 index 00000000..0771e793 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar+State.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +extension ProgressBar { + /// State for the progress bar. + struct State { + /// A flag indicating whether the progress bar is finished. + var finished = false + var iteration = 0 + private let speedInterval: DispatchTimeInterval = .seconds(1) + + var description: String + var subDescription: String + var itemsName: String + + var tasks: Int + var totalTasks: Int? + + var items: Int + var totalItems: Int? + + private var sizeUpdateTime: DispatchTime? + private var sizeUpdateValue: Int64 = 0 + var size: Int64 { + didSet { + calculateSizeSpeed() + } + } + + var totalSize: Int64? + private var sizeUpdateSpeed: String? + var sizeSpeed: String? { + guard sizeUpdateTime == nil || sizeUpdateTime! > .now() - speedInterval - speedInterval else { + return Int64(0).formattedSizeSpeed(from: startTime) + } + return sizeUpdateSpeed + } + var averageSizeSpeed: String { + size.formattedSizeSpeed(from: startTime) + } + + var percent: String { + var value = 0 + if let totalSize, totalSize > 0 { + value = Int(size * 100 / totalSize) + } else if let totalItems, totalItems > 0 { + value = Int(items * 100 / totalItems) + } + value = min(value, 100) + return "\(value)%" + } + + var startTime: DispatchTime + var output = "" + var renderTask: Task? + + init( + description: String = "", subDescription: String = "", itemsName: String = "", tasks: Int = 0, totalTasks: Int? = nil, items: Int = 0, totalItems: Int? = nil, + size: Int64 = 0, totalSize: Int64? = nil, startTime: DispatchTime = .now() + ) { + self.description = description + self.subDescription = subDescription + self.itemsName = itemsName + self.tasks = tasks + self.totalTasks = totalTasks + self.items = items + self.totalItems = totalItems + self.size = size + self.totalSize = totalSize + self.startTime = startTime + } + + private mutating func calculateSizeSpeed() { + if sizeUpdateTime == nil || sizeUpdateTime! < .now() - speedInterval { + let partSize = size - sizeUpdateValue + let partStartTime = sizeUpdateTime ?? startTime + let partSizeSpeed = partSize.formattedSizeSpeed(from: partStartTime) + self.sizeUpdateSpeed = partSizeSpeed + + sizeUpdateTime = .now() + sizeUpdateValue = size + } + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar+Terminal.swift b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar+Terminal.swift new file mode 100644 index 00000000..ce84cc5c --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressBar+Terminal.swift @@ -0,0 +1,95 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ContainerizationOS +import Foundation + +enum EscapeSequence { + static let hideCursor = "\u{001B}[?25l" + static let showCursor = "\u{001B}[?25h" + static let moveUp = "\u{001B}[1A" + static let clearToEndOfLine = "\u{001B}[K" +} + +extension ProgressBar { + var termWidth: Int { + guard + let terminalHandle = term, + let terminal = try? Terminal(descriptor: terminalHandle.fileDescriptor) + else { + return 0 + } + + return (try? Int(terminal.size.width)) ?? 0 + } + + /// Clears the progress bar and resets the cursor. + public func clearAndResetCursor() { + state.withLock { s in + clear(state: &s) + resetCursor() + } + } + + /// Clears the progress bar. + public func clear() { + state.withLock { s in + clear(state: &s) + } + } + + /// Clears the progress bar (caller must hold state lock). + func clear(state: inout State) { + displayText("", state: &state) + } + + /// Resets the cursor. + public func resetCursor() { + display(EscapeSequence.showCursor) + } + + func display(_ text: String) { + guard let term else { + return + } + termQueue.sync { + try? term.write(contentsOf: Data(text.utf8)) + try? term.synchronize() + } + } + + func displayText(_ text: String, terminating: String = "\r") { + state.withLock { s in + displayText(text, state: &s, terminating: terminating) + } + } + + func displayText(_ text: String, state: inout State, terminating: String = "\r") { + state.output = text + + // Clears previously printed lines. + var lines = "" + if terminating.hasSuffix("\r") && termWidth > 0 { + let lineCount = (text.count - 1) / termWidth + for _ in 0.. + let term: FileHandle? + let termQueue = DispatchQueue(label: "com.apple.container.ProgressBar") + + /// Returns `true` if the progress bar has finished. + public var isFinished: Bool { + state.withLock { $0.finished } + } + + /// Creates a new progress bar. + /// - Parameter config: The configuration for the progress bar. + public init(config: ProgressConfig) { + self.config = config + term = isatty(config.terminal.fileDescriptor) == 1 ? config.terminal : nil + let state = State( + description: config.initialDescription, itemsName: config.initialItemsName, totalTasks: config.initialTotalTasks, + totalItems: config.initialTotalItems, + totalSize: config.initialTotalSize) + self.state = Mutex(state) + display(EscapeSequence.hideCursor) + } + + /// Allows resetting the progress state. + public func reset() { + state.withLock { + $0 = State(description: config.initialDescription) + } + } + + /// Allows resetting the progress state of the current task. + public func resetCurrentTask() { + state.withLock { + $0 = State(description: $0.description, itemsName: $0.itemsName, tasks: $0.tasks, totalTasks: $0.totalTasks, startTime: $0.startTime) + } + } + + /// Updates the description of the progress bar and increments the tasks by one. + /// - Parameter description: The description of the action being performed. + public func set(description: String) { + resetCurrentTask() + + state.withLock { + $0.description = description + $0.subDescription = "" + $0.tasks += 1 + } + } + + /// Updates the additional description of the progress bar. + /// - Parameter subDescription: The additional description of the action being performed. + public func set(subDescription: String) { + resetCurrentTask() + + state.withLock { $0.subDescription = subDescription } + } + + private func start(intervalSeconds: TimeInterval) async { + while true { + let done = state.withLock { s -> Bool in + guard !s.finished else { + return true + } + render(state: &s) + s.iteration += 1 + return false + } + + if done { + return + } + + let intervalNanoseconds = UInt64(intervalSeconds * 1_000_000_000) + guard (try? await Task.sleep(nanoseconds: intervalNanoseconds)) != nil else { + return + } + } + } + + /// Starts an animation of the progress bar. + /// - Parameter intervalSeconds: The time interval between updates in seconds. + public func start(intervalSeconds: TimeInterval = 0.04) { + state.withLock { + if $0.renderTask != nil { + return + } + $0.renderTask = Task(priority: .utility) { + await start(intervalSeconds: intervalSeconds) + } + } + } + + /// Finishes the progress bar. + /// - Parameter clearScreen: If true, clears the progress bar from the screen. + public func finish(clearScreen: Bool = false) { + state.withLock { s in + guard !s.finished else { return } + + s.finished = true + s.renderTask?.cancel() + + let shouldClear = clearScreen || config.clearOnFinish + if !config.disableProgressUpdates && !shouldClear { + let output = draw(state: s) + displayText(output, state: &s, terminating: "\n") + } + + if shouldClear { + clear(state: &s) + } + resetCursor() + } + } +} + +extension ProgressBar { + private func secondsSinceStart(from startTime: DispatchTime) -> Int { + let timeDifferenceNanoseconds = DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds + let timeDifferenceSeconds = Int(floor(Double(timeDifferenceNanoseconds) / 1_000_000_000)) + return timeDifferenceSeconds + } + + func render(force: Bool = false) { + guard term != nil && !config.disableProgressUpdates else { + return + } + state.withLock { s in + render(state: &s, force: force) + } + } + + func render(state: inout State, force: Bool = false) { + guard term != nil && !config.disableProgressUpdates else { + return + } + guard force || !state.finished else { + return + } + let output = draw(state: state) + displayText(output, state: &state) + } + + /// Detail levels for progressive truncation. + enum DetailLevel: Int, CaseIterable { + case full = 0 // Everything shown + case noSpeed // Drop speed from parens + case noSize // Drop size from parens + case noParens // Drop parens entirely (items, size, speed) + case noTime // Drop time + case noDescription // Drop description/subdescription + case minimal // Just spinner, tasks, percent + } + + func draw(state: State) -> String { + let width = termWidth + // If no terminal or width unknown, use full detail + guard width > 0 else { + return draw(state: state, detail: .full) + } + + // Add a small buffer to prevent wrapping issues during resize + let bufferChars = 4 + let targetWidth = max(1, width - bufferChars) + + for detail in DetailLevel.allCases { + let output = draw(state: state, detail: detail) + if output.count <= targetWidth { + return output + } + } + + return draw(state: state, detail: .minimal) + } + + func draw(state: State, detail: DetailLevel) -> String { + var components = [String]() + + // Spinner - always shown if configured (unless using progress bar) + if config.showSpinner && !config.showProgressBar { + if !state.finished { + let spinnerIcon = config.theme.getSpinnerIcon(state.iteration) + components.append("\(spinnerIcon)") + } else { + components.append("\(config.theme.done)") + } + } + + // Tasks [x/y] - always shown if configured + if config.showTasks, let totalTasks = state.totalTasks { + let tasks = min(state.tasks, totalTasks) + components.append("[\(tasks)/\(totalTasks)]") + } + + // Description - dropped at noDescription level + if detail.rawValue < DetailLevel.noDescription.rawValue { + if config.showDescription && !state.description.isEmpty { + components.append("\(state.description)") + if !state.subDescription.isEmpty { + components.append("\(state.subDescription)") + } + } + } + + let allowProgress = !config.ignoreSmallSize || state.totalSize == nil || state.totalSize! > Int64(1024 * 1024) + let value = state.totalSize != nil ? state.size : Int64(state.items) + let total = state.totalSize ?? Int64(state.totalItems ?? 0) + + // Percent - always shown if configured + if config.showPercent && total > 0 && allowProgress { + components.append("\(state.finished ? "100%" : state.percent)") + } + + // Progress bar - always shown if configured + if config.showProgressBar, total > 0, allowProgress { + let usedWidth = components.joined(separator: " ").count + 45 + let remainingWidth = max(config.width - usedWidth, 1) + let barLength = state.finished ? remainingWidth : Int(Int64(remainingWidth) * value / total) + let barPaddingLength = remainingWidth - barLength + let bar = "\(String(repeating: config.theme.bar, count: barLength))\(String(repeating: " ", count: barPaddingLength))" + components.append("|\(bar)|") + } + + // Additional components in parens - progressively dropped + if detail.rawValue < DetailLevel.noParens.rawValue { + var additionalComponents = [String]() + + // Items - dropped at noParens level + if config.showItems, state.items > 0 { + var itemsName = "" + if !state.itemsName.isEmpty { + itemsName = " \(state.itemsName)" + } + if state.finished { + if let totalItems = state.totalItems { + additionalComponents.append("\(totalItems.formattedNumber())\(itemsName)") + } + } else { + if let totalItems = state.totalItems { + additionalComponents.append("\(state.items.formattedNumber()) of \(totalItems.formattedNumber())\(itemsName)") + } else { + additionalComponents.append("\(state.items.formattedNumber())\(itemsName)") + } + } + } + + // Size and speed - progressively dropped + if state.size > 0 && allowProgress { + if state.finished { + // Size - dropped at noSize level + if detail.rawValue < DetailLevel.noSize.rawValue { + if config.showSize { + if let totalSize = state.totalSize { + var formattedTotalSize = totalSize.formattedSize() + formattedTotalSize = adjustFormattedSize(formattedTotalSize) + additionalComponents.append(formattedTotalSize) + } + } + } + } else { + // Size - dropped at noSize level + var formattedCombinedSize = "" + if detail.rawValue < DetailLevel.noSize.rawValue && config.showSize { + var formattedSize = state.size.formattedSize() + formattedSize = adjustFormattedSize(formattedSize) + if let totalSize = state.totalSize { + var formattedTotalSize = totalSize.formattedSize() + formattedTotalSize = adjustFormattedSize(formattedTotalSize) + formattedCombinedSize = combineSize(size: formattedSize, totalSize: formattedTotalSize) + } else { + formattedCombinedSize = formattedSize + } + } + + // Speed - dropped at noSpeed level + var formattedSpeed = "" + if detail.rawValue < DetailLevel.noSpeed.rawValue && config.showSpeed { + formattedSpeed = "\(state.sizeSpeed ?? state.averageSizeSpeed)" + formattedSpeed = adjustFormattedSize(formattedSpeed) + } + + if !formattedCombinedSize.isEmpty && !formattedSpeed.isEmpty { + additionalComponents.append(formattedCombinedSize) + additionalComponents.append(formattedSpeed) + } else if !formattedCombinedSize.isEmpty { + additionalComponents.append(formattedCombinedSize) + } else if !formattedSpeed.isEmpty { + additionalComponents.append(formattedSpeed) + } + } + } + + if additionalComponents.count > 0 { + let joinedAdditionalComponents = additionalComponents.joined(separator: ", ") + components.append("(\(joinedAdditionalComponents))") + } + } + + // Time - dropped at noTime level + if detail.rawValue < DetailLevel.noTime.rawValue && config.showTime { + let timeDifferenceSeconds = secondsSinceStart(from: state.startTime) + let formattedTime = timeDifferenceSeconds.formattedTime() + components.append("[\(formattedTime)]") + } + + return components.joined(separator: " ") + } + + private func adjustFormattedSize(_ size: String) -> String { + // Ensure we always have one digit after the decimal point to prevent flickering. + let zero = Int64(0).formattedSize() + let decimalSep = Locale.current.decimalSeparator ?? "." + guard !size.contains(decimalSep), let first = size.first, first.isNumber || !size.contains(zero) else { + return size + } + var size = size + for unit in ["MB", "GB", "TB"] { + size = size.replacingOccurrences(of: " \(unit)", with: "\(decimalSep)0 \(unit)") + } + return size + } + + private func combineSize(size: String, totalSize: String) -> String { + let sizeComponents = size.split(separator: " ", maxSplits: 1) + let totalSizeComponents = totalSize.split(separator: " ", maxSplits: 1) + guard sizeComponents.count == 2, totalSizeComponents.count == 2 else { + return "\(size)/\(totalSize)" + } + let sizeNumber = sizeComponents[0] + let sizeUnit = sizeComponents[1] + let totalSizeNumber = totalSizeComponents[0] + let totalSizeUnit = totalSizeComponents[1] + guard sizeUnit == totalSizeUnit else { + return "\(size)/\(totalSize)" + } + return "\(sizeNumber)/\(totalSizeNumber) \(totalSizeUnit)" + } + + func draw() -> String { + state.withLock { draw(state: $0) } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressConfig.swift b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressConfig.swift new file mode 100644 index 00000000..5579ef10 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressConfig.swift @@ -0,0 +1,170 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +/// A configuration for displaying a progress bar. +public struct ProgressConfig: Sendable { + /// The file handle for progress updates. + let terminal: FileHandle + /// The initial description of the progress bar. + let initialDescription: String + /// The initial additional description of the progress bar. + let initialSubDescription: String + /// The initial items name (e.g., "files"). + let initialItemsName: String + /// A flag indicating whether to show a spinner (e.g., "⠋"). + /// The spinner is hidden when a progress bar is shown. + public let showSpinner: Bool + /// A flag indicating whether to show tasks and total tasks (e.g., "[1]" or "[1/3]"). + public let showTasks: Bool + /// A flag indicating whether to show the description (e.g., "Downloading..."). + public let showDescription: Bool + /// A flag indicating whether to show a percentage (e.g., "100%"). + /// The percentage is hidden when no total size and total items are set. + public let showPercent: Bool + /// A flag indicating whether to show a progress bar (e.g., "|███ |"). + /// The progress bar is hidden when no total size and total items are set. + public let showProgressBar: Bool + /// A flag indicating whether to show items and total items (e.g., "(22 it)" or "(22/22 it)"). + public let showItems: Bool + /// A flag indicating whether to show a size and a total size (e.g., "(22 MB)" or "(22/22 MB)"). + public let showSize: Bool + /// A flag indicating whether to show a speed (e.g., "(4.834 MB/s)"). + /// The speed is combined with the size and total size (e.g., "(22/22 MB, 4.834 MB/s)"). + /// The speed is hidden when no total size is set. + public let showSpeed: Bool + /// A flag indicating whether to show the elapsed time (e.g., "[4s]"). + public let showTime: Bool + /// The flag indicating whether to ignore small size values (less than 1 MB). For example, this may help to avoid reaching 100% after downloading metadata before downloading content. + public let ignoreSmallSize: Bool + /// The initial total tasks of the progress bar. + let initialTotalTasks: Int? + /// The initial total size of the progress bar. + let initialTotalSize: Int64? + /// The initial total items of the progress bar. + let initialTotalItems: Int? + /// The width of the progress bar in characters. + public let width: Int + /// The theme of the progress bar. + public let theme: ProgressTheme + /// The flag indicating whether to clear the progress bar before resetting the cursor. + public let clearOnFinish: Bool + /// The flag indicating whether to update the progress bar. + public let disableProgressUpdates: Bool + /// Creates a new instance of `ProgressConfig`. + /// - Parameters: + /// - terminal: The file handle for progress updates. The default value is `FileHandle.standardError`. + /// - description: The initial description of the progress bar. The default value is `""`. + /// - subDescription: The initial additional description of the progress bar. The default value is `""`. + /// - itemsName: The initial items name. The default value is `"it"`. + /// - showSpinner: A flag indicating whether to show a spinner. The default value is `true`. + /// - showTasks: A flag indicating whether to show tasks and total tasks. The default value is `false`. + /// - showDescription: A flag indicating whether to show the description. The default value is `true`. + /// - showPercent: A flag indicating whether to show a percentage. The default value is `true`. + /// - showProgressBar: A flag indicating whether to show a progress bar. The default value is `false`. + /// - showItems: A flag indicating whether to show items and a total items. The default value is `false`. + /// - showSize: A flag indicating whether to show a size and a total size. The default value is `true`. + /// - showSpeed: A flag indicating whether to show a speed. The default value is `true`. + /// - showTime: A flag indicating whether to show the elapsed time. The default value is `true`. + /// - ignoreSmallSize: A flag indicating whether to ignore small size values. The default value is `false`. + /// - totalTasks: The initial total tasks of the progress bar. The default value is `nil`. + /// - totalItems: The initial total items of the progress bar. The default value is `nil`. + /// - totalSize: The initial total size of the progress bar. The default value is `nil`. + /// - width: The width of the progress bar in characters. The default value is `120`. + /// - theme: The theme of the progress bar. The default value is `nil`. + /// - clearOnFinish: The flag indicating whether to clear the progress bar before resetting the cursor. The default is `true`. + /// - disableProgressUpdates: The flag indicating whether to update the progress bar. The default is `false`. + public init( + terminal: FileHandle = .standardError, + description: String = "", + subDescription: String = "", + itemsName: String = "it", + showSpinner: Bool = true, + showTasks: Bool = false, + showDescription: Bool = true, + showPercent: Bool = true, + showProgressBar: Bool = false, + showItems: Bool = false, + showSize: Bool = true, + showSpeed: Bool = true, + showTime: Bool = true, + ignoreSmallSize: Bool = false, + totalTasks: Int? = nil, + totalItems: Int? = nil, + totalSize: Int64? = nil, + width: Int = 120, + theme: ProgressTheme? = nil, + clearOnFinish: Bool = true, + disableProgressUpdates: Bool = false + ) throws { + if let totalTasks { + guard totalTasks > 0 else { + throw Error.invalid("totalTasks must be greater than zero") + } + } + if let totalItems { + guard totalItems > 0 else { + throw Error.invalid("totalItems must be greater than zero") + } + } + if let totalSize { + guard totalSize > 0 else { + throw Error.invalid("totalSize must be greater than zero") + } + } + + self.terminal = terminal + self.initialDescription = description + self.initialSubDescription = subDescription + self.initialItemsName = itemsName + + self.showSpinner = showSpinner + self.showTasks = showTasks + self.showDescription = showDescription + self.showPercent = showPercent + self.showProgressBar = showProgressBar + self.showItems = showItems + self.showSize = showSize + self.showSpeed = showSpeed + self.showTime = showTime + + self.ignoreSmallSize = ignoreSmallSize + self.initialTotalTasks = totalTasks + self.initialTotalItems = totalItems + self.initialTotalSize = totalSize + + self.width = width + self.theme = theme ?? DefaultProgressTheme() + self.clearOnFinish = clearOnFinish + self.disableProgressUpdates = disableProgressUpdates + } +} + +extension ProgressConfig { + /// An enumeration of errors that can occur when creating a `ProgressConfig`. + public enum Error: Swift.Error, CustomStringConvertible { + case invalid(String) + + /// The description of the error. + public var description: String { + switch self { + case .invalid(let reason): + return "failed to validate config (\(reason))" + } + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressTaskCoordinator.swift b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressTaskCoordinator.swift new file mode 100644 index 00000000..ae09ca77 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressTaskCoordinator.swift @@ -0,0 +1,72 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +/// A type that represents a task whose progress is being monitored. +public struct ProgressTask: Sendable, Equatable { + private var id = UUID() + private var coordinator: ProgressTaskCoordinator + + init(manager: ProgressTaskCoordinator) { + self.coordinator = manager + } + + static public func == (lhs: ProgressTask, rhs: ProgressTask) -> Bool { + lhs.id == rhs.id + } + + /// Returns `true` if this task is the currently active task, `false` otherwise. + public func isCurrent() async -> Bool { + guard let currentTask = await coordinator.currentTask else { + return false + } + return currentTask == self + } +} + +/// A type that coordinates progress tasks to ignore updates from completed tasks. +public actor ProgressTaskCoordinator { + var currentTask: ProgressTask? + + /// Creates an instance of `ProgressTaskCoordinator`. + public init() {} + + /// Returns a new task that should be monitored for progress updates. + public func startTask() -> ProgressTask { + let newTask = ProgressTask(manager: self) + currentTask = newTask + return newTask + } + + /// Performs cleanup when the monitored tasks complete. + public func finish() { + currentTask = nil + } + + /// Returns a handler that updates the progress of a given task. + /// - Parameters: + /// - task: The task whose progress is being updated. + /// - progressUpdate: The handler to invoke when progress updates are received. + public static func handler(for task: ProgressTask, from progressUpdate: @escaping ProgressUpdateHandler) -> ProgressUpdateHandler { + { events in + // Ignore updates from completed tasks. + if await task.isCurrent() { + await progressUpdate(events) + } + } + } +} diff --git a/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressTheme.swift b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressTheme.swift new file mode 100644 index 00000000..44b5bc37 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressTheme.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +/// A theme for progress bar. +public protocol ProgressTheme: Sendable { + /// The icons used to represent a spinner. + var spinner: [String] { get } + /// The icon used to represent a progress bar. + var bar: String { get } + /// The icon used to indicate that a progress bar finished. + var done: String { get } +} + +public struct DefaultProgressTheme: ProgressTheme { + public let spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] + public let bar = "█" + public let done = "✔" +} + +extension ProgressTheme { + func getSpinnerIcon(_ iteration: Int) -> String { + spinner[iteration % spinner.count] + } +} diff --git a/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressUpdate.swift b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressUpdate.swift new file mode 100644 index 00000000..cf92db4e --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/TerminalProgress/ProgressUpdate.swift @@ -0,0 +1,41 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +public enum ProgressUpdateEvent: Sendable { + case setDescription(String) + case setSubDescription(String) + case setItemsName(String) + case addTasks(Int) + case setTasks(Int) + case addTotalTasks(Int) + case setTotalTasks(Int) + case addItems(Int) + case setItems(Int) + case addTotalItems(Int) + case setTotalItems(Int) + case addSize(Int64) + case setSize(Int64) + case addTotalSize(Int64) + case setTotalSize(Int64) + case custom(String) +} + +public typealias ProgressUpdateHandler = @Sendable (_ events: [ProgressUpdateEvent]) async -> Void + +public protocol ProgressAdapter { + associatedtype T + static func handler(from progressUpdate: ProgressUpdateHandler?) -> (@Sendable ([T]) async -> Void)? +} diff --git a/examples/sandboxy/Sources/sandboxy/TerminalProgress/StandardError.swift b/examples/sandboxy/Sources/sandboxy/TerminalProgress/StandardError.swift new file mode 100644 index 00000000..9295f862 --- /dev/null +++ b/examples/sandboxy/Sources/sandboxy/TerminalProgress/StandardError.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import Foundation + +struct StandardError { + func write(_ string: String) { + if let data = string.data(using: .utf8) { + FileHandle.standardError.write(data) + } + } +} diff --git a/examples/sandboxy/sandboxy.entitlements b/examples/sandboxy/sandboxy.entitlements new file mode 100644 index 00000000..d7d0d6e8 --- /dev/null +++ b/examples/sandboxy/sandboxy.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.virtualization + + + From 1437c67f5a07cb39e8f5e79d0b5aeac0327932bd Mon Sep 17 00:00:00 2001 From: Kathryn Baldauf Date: Thu, 4 Jun 2026 15:08:22 -0700 Subject: [PATCH 03/11] Update github actions to use SHA versions (#763) Related to https://github.com/apple/container/pull/1649. This PR updates the GitHub workflows to ensure all imported actions are referenced by commit SHA. Signed-off-by: Kathryn Baldauf --- .github/workflows/build-test-images.yml | 6 +++--- .github/workflows/containerization-build-template.yml | 2 +- .github/workflows/docs-release.yaml | 2 +- .github/workflows/release.yml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-test-images.yml b/.github/workflows/build-test-images.yml index 7ef25bab..4ad317d0 100644 --- a/.github/workflows/build-test-images.yml +++ b/.github/workflows/build-test-images.yml @@ -61,16 +61,16 @@ jobs: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx if: ${{ inputs.useBuildx }} - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Build dockerfile and push image - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: push: ${{ inputs.publish }} context: Tests/TestImages/${{ inputs.image }} diff --git a/.github/workflows/containerization-build-template.yml b/.github/workflows/containerization-build-template.yml index d3847193..057ec9aa 100644 --- a/.github/workflows/containerization-build-template.yml +++ b/.github/workflows/containerization-build-template.yml @@ -113,7 +113,7 @@ jobs: steps: - name: Setup Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 - name: Download a single artifact uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 diff --git a/.github/workflows/docs-release.yaml b/.github/workflows/docs-release.yaml index d59b6763..e1295d6c 100644 --- a/.github/workflows/docs-release.yaml +++ b/.github/workflows/docs-release.yaml @@ -43,4 +43,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e3cc727..7f211c71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 release: if: startsWith(github.ref, 'refs/tags/') @@ -47,7 +47,7 @@ jobs: packages: read steps: - name: Create release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: token: ${{ github.token }} name: ${{ github.ref_name }}-prerelease From 4f6df44c2a8529dadfa82586dec0840e61d10b34 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 16 Jun 2026 11:06:06 -0300 Subject: [PATCH 04/11] fix(Platform): make arm64 nil-variant and v8 hash identically (#764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `Platform.==` treats `arm64` with `nil` variant as equal to `arm64/v8`, but `hash(into:)` used `description` which serializes them differently (`linux/arm64` vs `linux/arm64/v8`). This violates the `Hashable` contract — equal values must produce the same hash. ### Root cause ```swift // == returns true for these two let a = Platform(arch: "arm64", os: "linux", variant: nil) let b = Platform(arch: "arm64", os: "linux", variant: "v8") a == b // true ✓ // but hash was different — broken a.hashValue == b.hashValue // false ✗ (before this fix) ``` This mismatch caused `Set` and `Dictionary` lookups to silently miss entries when one platform was decoded from JSON (no `variant` field in the manifest) and another was created via `Platform(from:)` or `Platform.current` (which both set `variant = "v8"`). ### Practical consequence In `apple/container`, this manifests as inconsistent platform-string normalization across stages of a single `container build` — some stages log `linux/arm64`, others `linux/arm64/v8` — which can cause `COPY --from=` to fail to resolve the source stage under concurrent builds. See apple/container#1542. ### Fix `hash(into:)` now normalizes `arm64` with `nil` variant to `"v8"` before hashing, matching the existing `==` behavior. --- Sources/ContainerizationOCI/Platform.swift | 9 ++++++++- .../OCIPlatformTests.swift | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Sources/ContainerizationOCI/Platform.swift b/Sources/ContainerizationOCI/Platform.swift index 3a54fbc9..7455e011 100644 --- a/Sources/ContainerizationOCI/Platform.swift +++ b/Sources/ContainerizationOCI/Platform.swift @@ -272,7 +272,14 @@ extension Platform: Hashable { } public func hash(into hasher: inout Swift.Hasher) { - hasher.combine(description) + hasher.combine(os) + hasher.combine(architecture) + // arm64 with no variant is equivalent to arm64/v8 per the == implementation + if architecture == "arm64" { + hasher.combine(variant ?? "v8") + } else { + hasher.combine(variant) + } } } diff --git a/Tests/ContainerizationOCITests/OCIPlatformTests.swift b/Tests/ContainerizationOCITests/OCIPlatformTests.swift index 8977a526..7ffebedc 100644 --- a/Tests/ContainerizationOCITests/OCIPlatformTests.swift +++ b/Tests/ContainerizationOCITests/OCIPlatformTests.swift @@ -66,4 +66,19 @@ struct OCIPlatformTests { let rhs = Platform(arch: "arm64", os: "linux", variant: nil) #expect(lhs == rhs, "Both nil variants => variantEqual is true => overall equal") } + + @Test func arm64_nilAndV8_sameHashValue() { + let withoutVariant = Platform(arch: "arm64", os: "linux", variant: nil) + let withV8 = Platform(arch: "arm64", os: "linux", variant: "v8") + // Equal platforms must produce the same hash — violating this breaks Set/Dictionary lookups + #expect(withoutVariant.hashValue == withV8.hashValue, "arm64 nil variant and v8 must hash identically") + } + + @Test func arm64_nilAndV8_setLookup() { + let withoutVariant = Platform(arch: "arm64", os: "linux", variant: nil) + let withV8 = Platform(arch: "arm64", os: "linux", variant: "v8") + var set = Set() + set.insert(withoutVariant) + #expect(set.contains(withV8), "arm64/v8 must be found in a Set that contains arm64 with nil variant") + } } From e29657b86e109c182f74e3ba699be12f1642f1c4 Mon Sep 17 00:00:00 2001 From: J Logan Date: Tue, 16 Jun 2026 08:44:27 -0700 Subject: [PATCH 05/11] Pin macOS CI builds to Swift 6.3 for now. (#771) - CI runners moved to the Xcode developer beta as the default, but macOS builds are failing with a conflicting options error for `-warnings-as-errors` and `-suppress-warnings`. --- .github/workflows/containerization-build-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/containerization-build-template.yml b/.github/workflows/containerization-build-template.yml index 057ec9aa..c67b5467 100644 --- a/.github/workflows/containerization-build-template.yml +++ b/.github/workflows/containerization-build-template.yml @@ -25,7 +25,7 @@ jobs: contents: read packages: write env: - DEVELOPER_DIR: "/Applications/Xcode-latest.app/Contents/Developer" + DEVELOPER_DIR: "/Applications/Xcode_swift_6.3.app/Contents/Developer" steps: - name: Checkout repository From ddd19a61a863a849244fdd2df0bd57845d5d5155 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 16 Jun 2026 12:47:20 -0400 Subject: [PATCH 06/11] add x86 kernel config (#765) Add x86 kernel config and build scripts. Signed-off-by: michael_crosby --- .gitignore | 3 +- Makefile | 29 +- Sources/Integration/Suite.swift | 18 +- examples/ctr-example/Makefile | 5 +- .../Sources/ctr-example/main.swift | 9 +- kernel/Makefile | 43 +- kernel/README.md | 13 +- kernel/build.sh | 31 +- kernel/config-x86_64 | 3650 +++++++++++++++++ kernel/image/Dockerfile | 5 +- 10 files changed, 3779 insertions(+), 27 deletions(-) create mode 100644 kernel/config-x86_64 diff --git a/.gitignore b/.gitignore index 63669d43..dfecc070 100644 --- a/.gitignore +++ b/.gitignore @@ -20,8 +20,9 @@ test_results/ *.swp *.tar.gz *.tar.xz -vmlinux +vmlinux* # API docs for local preview only. _site/ _serve/ + diff --git a/Makefile b/Makefile index d83c7691..e45835eb 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,14 @@ SWIFT_CONFIGURATION := $(if $(filter-out false,$(WARNINGS_AS_ERRORS)),-Xswiftc - # Commonly used locations UNAME_S := $(shell uname -s) +UNAME_M := $(shell uname -m) +KERNEL_ARCH := $(if $(filter $(UNAME_M),aarch64 arm64),arm64,$(UNAME_M)) +# Candidate kernel filenames in bin/ (compiled vmlinuz first, kata-fetched vmlinux fallback). +ifeq ($(KERNEL_ARCH),x86_64) +KERNEL_CANDIDATES := bin/vmlinuz-x86_64 bin/vmlinux-x86_64 +else +KERNEL_CANDIDATES := bin/vmlinux-$(KERNEL_ARCH) +endif ifeq ($(UNAME_S),Darwin) SWIFT ?= /usr/bin/swift else @@ -169,12 +177,13 @@ coverage: test .PHONY: integration integration: -ifeq (,$(wildcard bin/vmlinux)) - @echo No bin/vmlinux kernel found. See fetch-default-kernel target. - @exit 1 -endif - @echo Running the integration tests... - @./bin/containerization-integration + @kernel="$$(for f in $(KERNEL_CANDIDATES); do [ -f $$f ] && echo $$f && break; done)"; \ + if [ -z "$$kernel" ]; then \ + echo "No kernel found. Looked for: $(KERNEL_CANDIDATES). See fetch-default-kernel target or build via kernel/Makefile."; \ + exit 1; \ + fi; \ + echo "Running the integration tests with kernel $$kernel..."; \ + ./bin/containerization-integration --kernel "$$kernel" .PHONY: fetch-default-kernel fetch-default-kernel: @@ -182,12 +191,12 @@ fetch-default-kernel: ifeq (,$(wildcard .local/kata.tar.gz)) @curl -SsL -o .local/kata.tar.gz ${KATA_BINARY_PACKAGE} endif -ifeq (,$(wildcard .local/vmlinux)) +ifeq (,$(wildcard .local/vmlinux-$(KERNEL_ARCH))) @tar -zxf .local/kata.tar.gz -C .local/ --strip-components=1 - @cp -L .local/opt/kata/share/kata-containers/vmlinux.container .local/vmlinux + @cp -L .local/opt/kata/share/kata-containers/vmlinux.container .local/vmlinux-$(KERNEL_ARCH) endif -ifeq (,$(wildcard bin/vmlinux)) - @cp .local/vmlinux bin/vmlinux +ifeq (,$(wildcard bin/vmlinux-$(KERNEL_ARCH))) + @cp .local/vmlinux-$(KERNEL_ARCH) bin/vmlinux-$(KERNEL_ARCH) endif .PHONY: check diff --git a/Sources/Integration/Suite.swift b/Sources/Integration/Suite.swift index 786763c9..42da366e 100644 --- a/Sources/Integration/Suite.swift +++ b/Sources/Integration/Suite.swift @@ -149,7 +149,23 @@ struct IntegrationSuite: AsyncParsableCommand { var bootlogDir: String = "./bin/integration-bootlogs" @Option(name: .shortAndLong, help: "Path to a kernel binary") - var kernel: String = "./bin/vmlinux" + var kernel: String = Self.defaultKernelPath + + #if arch(arm64) + private static let kernelCandidates = ["./bin/vmlinux-arm64"] + #elseif arch(x86_64) + private static let kernelCandidates = ["./bin/vmlinuz-x86_64", "./bin/vmlinux-x86_64"] + #else + private static let kernelCandidates = ["./bin/vmlinux"] + #endif + + private static let defaultKernelPath: String = { + let fm = FileManager.default + for candidate in kernelCandidates where fm.fileExists(atPath: candidate) { + return candidate + } + return kernelCandidates[0] + }() @Option(name: .shortAndLong, help: "Maximum number of concurrent tests") var maxConcurrency: Int = 4 diff --git a/examples/ctr-example/Makefile b/examples/ctr-example/Makefile index 68cca066..06bda057 100644 --- a/examples/ctr-example/Makefile +++ b/examples/ctr-example/Makefile @@ -16,6 +16,9 @@ SWIFT = /usr/bin/swift +UNAME_M := $(shell uname -m) +KERNEL_ARCH := $(if $(filter $(UNAME_M),aarch64 arm64),arm64,$(UNAME_M)) + .PHONY: all build clean run all: run @@ -41,4 +44,4 @@ fmt: $(SWIFT) format --in-place --recursive Sources/ fetch-default-kernel: $(MAKE) -C ../.. fetch-default-kernel - cp -L ../../.local/vmlinux ./vmlinux + cp -L ../../.local/vmlinux-$(KERNEL_ARCH) ./vmlinux-$(KERNEL_ARCH) diff --git a/examples/ctr-example/Sources/ctr-example/main.swift b/examples/ctr-example/Sources/ctr-example/main.swift index 0ae8f651..101b40cb 100644 --- a/examples/ctr-example/Sources/ctr-example/main.swift +++ b/examples/ctr-example/Sources/ctr-example/main.swift @@ -29,7 +29,14 @@ struct CtrExample { defer { current.tryReset() } let initfsReference = "ghcr.io/apple/containerization/vminit:0.26.5" - let kernelPath = "./vmlinux" + #if arch(arm64) + let kernelCandidates = ["./vmlinux-arm64"] + #elseif arch(x86_64) + let kernelCandidates = ["./vmlinuz-x86_64", "./vmlinux-x86_64"] + #else + let kernelCandidates = ["./vmlinux"] + #endif + let kernelPath = kernelCandidates.first(where: { FileManager.default.fileExists(atPath: $0) }) ?? kernelCandidates[0] print("Fetching base container filesystem...") // Create container manager with file-based initfs var manager = try await ContainerManager( diff --git a/kernel/Makefile b/kernel/Makefile index 914b50d7..bc955b11 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -14,30 +14,61 @@ KSOURCE ?= https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.18.5.tar.xz KIMAGE ?= kernel-build:0.1 -CURDIR := $(shell pwd) -GIT_VERSION := $(shell git -C $(CURDIR) rev-parse --short=12 HEAD 2>/dev/null || echo unknown) +MAKEFILE_DIR := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) +GIT_VERSION := $(shell git -C $(MAKEFILE_DIR) rev-parse --short=12 HEAD 2>/dev/null || echo unknown) +TARGET_ARCH ?= + +# Resolve the effective target arch (TARGET_ARCH override, else host). +EFFECTIVE_ARCH := $(if $(TARGET_ARCH),$(TARGET_ARCH),$(shell uname -m)) +ifneq (,$(filter $(EFFECTIVE_ARCH),aarch64)) +EFFECTIVE_ARCH := arm64 +endif +ifneq (,$(filter $(EFFECTIVE_ARCH),amd64)) +EFFECTIVE_ARCH := x86_64 +endif + +# Compressed (vmlinuz) for x86_64 bzImage; uncompressed (vmlinux) for arm64 Image. +ifeq ($(EFFECTIVE_ARCH),x86_64) +KERNEL_OUTPUT := vmlinuz-x86_64 +else +KERNEL_OUTPUT := vmlinux-arm64 +endif + +BIN_DIR := $(abspath $(MAKEFILE_DIR)/../bin) .DEFAULT_GOAL := all .PHONY: all all: kernel-build-image all: kernel-build +all: kernel-install .PHONY: kernel-build-image kernel-build-image: - container build image/ -f image/Dockerfile -t ${KIMAGE} + container build $(MAKEFILE_DIR)/image -f $(MAKEFILE_DIR)/image/Dockerfile -t ${KIMAGE} .PHONY: kernel-build kernel-build: -ifeq (,$(wildcard source.tar.xz)) - curl -SsL -o source.tar.xz ${KSOURCE} +ifeq (,$(wildcard $(MAKEFILE_DIR)/source.tar.xz)) + curl -SsL -o $(MAKEFILE_DIR)/source.tar.xz ${KSOURCE} endif container run \ --cpus 8 \ --rm \ --memory 16g \ - -v ${CURDIR}:/kernel \ + -v $(MAKEFILE_DIR):/kernel \ --env LOCALVERSION=-cz-${GIT_VERSION} \ + $(if $(TARGET_ARCH),--env TARGET_ARCH=$(TARGET_ARCH),) \ --cwd /kernel \ ${KIMAGE} \ /bin/bash -c "./build.sh" + +.PHONY: kernel-install +kernel-install: + @mkdir -p $(BIN_DIR) + @cp -L $(MAKEFILE_DIR)/$(KERNEL_OUTPUT) $(BIN_DIR)/$(KERNEL_OUTPUT) + @echo "Installed $(KERNEL_OUTPUT) -> $(BIN_DIR)/$(KERNEL_OUTPUT)" + +.PHONY: x86_64 +x86_64: + $(MAKE) all TARGET_ARCH=x86_64 diff --git a/kernel/README.md b/kernel/README.md index bba12c08..797aa4fb 100644 --- a/kernel/README.md +++ b/kernel/README.md @@ -2,7 +2,7 @@ This directory includes an optimized kernel configuration to produce a fast and lightweight kernel for container use. -- `config-arm64` includes the kernel `CONFIG_` options. +- `config-arm64` and `config-x86_64` include the per-arch kernel `CONFIG_` options. - `Makefile` includes the kernel version and source package URL. - `build.sh` scripts the kernel build process. - `image/` includes the configuration for an image with build tooling. @@ -12,4 +12,13 @@ This directory includes an optimized kernel configuration to produce a fast and 1. The build process relies on having the `container` tool installed (https://github.com/apple/container/releases). 2. Run `make`. This should create the image used for building the resulting Linux kernel, and then run a container with that image to perform the kernel build. -A `kernel/vmlinux` file will be the result of the build. +### Target architecture + +The build target is selected by the `TARGET_ARCH` make variable, which accepts either `arm64` or `x86_64`. When unset, it falls back to the build host's architecture (as reported by `uname -m`, with `aarch64`/`amd64` normalized to `arm64`/`x86_64`). + +- `make` (default) → builds for the host arch +- `make TARGET_ARCH=arm64` → `vmlinux-arm64` (uncompressed `Image`) +- `make TARGET_ARCH=x86_64` → `vmlinuz-x86_64` (compressed `bzImage`, cross-compiled inside the arm64 container) +- `make x86_64` → convenience alias for `make TARGET_ARCH=x86_64` + +The `z` suffix on the x86 name follows Linux convention for a compressed kernel image. The resulting kernel is copied into the repo's `bin/` directory. diff --git a/kernel/build.sh b/kernel/build.sh index 2f3b5c45..a338de35 100755 --- a/kernel/build.sh +++ b/kernel/build.sh @@ -15,13 +15,36 @@ set -e +TARGET_ARCH="${TARGET_ARCH:-$(uname -m)}" + +case "${TARGET_ARCH}" in + aarch64|arm64) + CONFIG=config-arm64 + KARCH=arm64 + CROSS_COMPILE=aarch64-linux-gnu- + IMAGE_PATH=arch/arm64/boot/Image + OUTPUT_NAME=vmlinux-arm64 + ;; + x86_64|amd64) + CONFIG=config-x86_64 + KARCH=x86_64 + CROSS_COMPILE=x86_64-linux-gnu- + IMAGE_PATH=arch/x86/boot/bzImage + OUTPUT_NAME=vmlinuz-x86_64 + ;; + *) + echo "Unsupported target architecture: ${TARGET_ARCH}" >&2 + exit 1 + ;; +esac + mkdir -p /kbuild tar -xf /kernel/source.tar.xz -C /kbuild --strip-components=1 -cp /kernel/config-arm64 /kbuild/.config +cp "/kernel/${CONFIG}" /kbuild/.config ( cd /kbuild - make olddefconfig && \ - make -j$((`nproc`-1)) LOCALVERSION="${LOCALVERSION}" && \ - cp arch/arm64/boot/Image /kernel/vmlinux + make ARCH="${KARCH}" CROSS_COMPILE="${CROSS_COMPILE}" olddefconfig && \ + make ARCH="${KARCH}" CROSS_COMPILE="${CROSS_COMPILE}" -j$((`nproc`-1)) LOCALVERSION="${LOCALVERSION}" && \ + cp "${IMAGE_PATH}" "/kernel/${OUTPUT_NAME}" ) diff --git a/kernel/config-x86_64 b/kernel/config-x86_64 new file mode 100644 index 00000000..3f2746c9 --- /dev/null +++ b/kernel/config-x86_64 @@ -0,0 +1,3650 @@ +# +# Automatically generated file; DO NOT EDIT. +# Linux/x86_64 6.6.9 Kernel Configuration +# +CONFIG_CC_VERSION_TEXT="x86_64-linux-gnu-gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0" +CONFIG_CC_IS_GCC=y +CONFIG_GCC_VERSION=90400 +CONFIG_CLANG_VERSION=0 +CONFIG_AS_IS_GNU=y +CONFIG_AS_VERSION=23400 +CONFIG_LD_IS_BFD=y +CONFIG_LD_VERSION=23400 +CONFIG_LLD_VERSION=0 +CONFIG_CC_CAN_LINK=y +CONFIG_CC_CAN_LINK_STATIC=y +CONFIG_CC_HAS_ASM_INLINE=y +CONFIG_CC_HAS_NO_PROFILE_FN_ATTR=y +CONFIG_PAHOLE_VERSION=0 +CONFIG_IRQ_WORK=y +CONFIG_BUILDTIME_TABLE_SORT=y +CONFIG_THREAD_INFO_IN_TASK=y + +# +# General setup +# +CONFIG_INIT_ENV_ARG_LIMIT=32 +# CONFIG_COMPILE_TEST is not set +# CONFIG_WERROR is not set +CONFIG_LOCALVERSION="" +# CONFIG_LOCALVERSION_AUTO is not set +CONFIG_BUILD_SALT="" +CONFIG_HAVE_KERNEL_GZIP=y +CONFIG_HAVE_KERNEL_BZIP2=y +CONFIG_HAVE_KERNEL_LZMA=y +CONFIG_HAVE_KERNEL_XZ=y +CONFIG_HAVE_KERNEL_LZO=y +CONFIG_HAVE_KERNEL_LZ4=y +CONFIG_HAVE_KERNEL_ZSTD=y +CONFIG_KERNEL_GZIP=y +# CONFIG_KERNEL_BZIP2 is not set +# CONFIG_KERNEL_LZMA is not set +# CONFIG_KERNEL_XZ is not set +# CONFIG_KERNEL_LZO is not set +# CONFIG_KERNEL_LZ4 is not set +# CONFIG_KERNEL_ZSTD is not set +CONFIG_DEFAULT_INIT="" +CONFIG_DEFAULT_HOSTNAME="sandbox-vm" +CONFIG_SYSVIPC=y +CONFIG_SYSVIPC_SYSCTL=y +CONFIG_POSIX_MQUEUE=y +CONFIG_POSIX_MQUEUE_SYSCTL=y +# CONFIG_WATCH_QUEUE is not set +CONFIG_CROSS_MEMORY_ATTACH=y +# CONFIG_USELIB is not set +CONFIG_AUDIT=y +CONFIG_HAVE_ARCH_AUDITSYSCALL=y +CONFIG_AUDITSYSCALL=y + +# +# IRQ subsystem +# +CONFIG_GENERIC_IRQ_PROBE=y +CONFIG_GENERIC_IRQ_SHOW=y +CONFIG_GENERIC_IRQ_EFFECTIVE_AFF_MASK=y +CONFIG_GENERIC_PENDING_IRQ=y +CONFIG_GENERIC_IRQ_MIGRATION=y +CONFIG_HARDIRQS_SW_RESEND=y +CONFIG_IRQ_DOMAIN=y +CONFIG_IRQ_DOMAIN_HIERARCHY=y +CONFIG_GENERIC_MSI_IRQ=y +CONFIG_GENERIC_MSI_IRQ_DOMAIN=y +CONFIG_IRQ_MSI_IOMMU=y +CONFIG_GENERIC_IRQ_MATRIX_ALLOCATOR=y +CONFIG_GENERIC_IRQ_RESERVATION_MODE=y +CONFIG_IRQ_FORCED_THREADING=y +CONFIG_SPARSE_IRQ=y +# CONFIG_GENERIC_IRQ_DEBUGFS is not set +# end of IRQ subsystem + +CONFIG_CLOCKSOURCE_WATCHDOG=y +CONFIG_ARCH_CLOCKSOURCE_INIT=y +CONFIG_CLOCKSOURCE_VALIDATE_LAST_CYCLE=y +CONFIG_GENERIC_TIME_VSYSCALL=y +CONFIG_GENERIC_CLOCKEVENTS=y +CONFIG_GENERIC_CLOCKEVENTS_BROADCAST=y +CONFIG_GENERIC_CLOCKEVENTS_MIN_ADJUST=y +CONFIG_GENERIC_CMOS_UPDATE=y +CONFIG_HAVE_POSIX_CPU_TIMERS_TASK_WORK=y +CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y +CONFIG_CONTEXT_TRACKING=y +CONFIG_CONTEXT_TRACKING_IDLE=y + +# +# Timers subsystem +# +CONFIG_TICK_ONESHOT=y +CONFIG_NO_HZ_COMMON=y +# CONFIG_HZ_PERIODIC is not set +CONFIG_NO_HZ_IDLE=y +# CONFIG_NO_HZ_FULL is not set +CONFIG_NO_HZ=y +CONFIG_HIGH_RES_TIMERS=y +CONFIG_CLOCKSOURCE_WATCHDOG_MAX_SKEW_US=100 +# end of Timers subsystem + +CONFIG_BPF=y +CONFIG_HAVE_EBPF_JIT=y +CONFIG_ARCH_WANT_DEFAULT_BPF_JIT=y + +# +# BPF subsystem +# +CONFIG_BPF_SYSCALL=y +# CONFIG_BPF_UNPRIV_DEFAULT_OFF is not set +CONFIG_USERMODE_DRIVER=y +CONFIG_BPF_PRELOAD=y +CONFIG_BPF_PRELOAD_UMD=y +# end of BPF subsystem + +CONFIG_PREEMPT_BUILD=y +CONFIG_PREEMPT_NONE=y +# CONFIG_PREEMPT_VOLUNTARY is not set +# CONFIG_PREEMPT is not set +CONFIG_PREEMPT_COUNT=y +CONFIG_PREEMPTION=y +CONFIG_PREEMPT_DYNAMIC=y +# CONFIG_SCHED_CORE is not set + +# +# CPU/Task time and stats accounting +# +CONFIG_TICK_CPU_ACCOUNTING=y +# CONFIG_VIRT_CPU_ACCOUNTING_GEN is not set +# CONFIG_IRQ_TIME_ACCOUNTING is not set +CONFIG_BSD_PROCESS_ACCT=y +CONFIG_BSD_PROCESS_ACCT_V3=y +CONFIG_TASKSTATS=y +CONFIG_TASK_DELAY_ACCT=y +CONFIG_TASK_XACCT=y +CONFIG_TASK_IO_ACCOUNTING=y +# CONFIG_PSI is not set +# end of CPU/Task time and stats accounting + +CONFIG_CPU_ISOLATION=y + +# +# RCU Subsystem +# +CONFIG_TREE_RCU=y +CONFIG_PREEMPT_RCU=y +# CONFIG_RCU_EXPERT is not set +CONFIG_SRCU=y +CONFIG_TREE_SRCU=y +CONFIG_TASKS_RCU_GENERIC=y +CONFIG_TASKS_RCU=y +CONFIG_TASKS_TRACE_RCU=y +CONFIG_RCU_STALL_COMMON=y +CONFIG_RCU_NEED_SEGCBLIST=y +# end of RCU Subsystem + +CONFIG_IKCONFIG=y +CONFIG_IKCONFIG_PROC=y +# CONFIG_IKHEADERS is not set +CONFIG_LOG_BUF_SHIFT=21 +CONFIG_LOG_CPU_MAX_BUF_SHIFT=12 +CONFIG_PRINTK_SAFE_LOG_BUF_SHIFT=13 +# CONFIG_PRINTK_INDEX is not set +CONFIG_HAVE_UNSTABLE_SCHED_CLOCK=y + +# +# Scheduler features +# +# CONFIG_UCLAMP_TASK is not set +# end of Scheduler features + +CONFIG_ARCH_SUPPORTS_NUMA_BALANCING=y +CONFIG_ARCH_WANT_BATCHED_UNMAP_TLB_FLUSH=y +CONFIG_CC_HAS_INT128=y +CONFIG_CC_IMPLICIT_FALLTHROUGH="-Wimplicit-fallthrough=5" +CONFIG_GCC11_NO_ARRAY_BOUNDS=y +CONFIG_ARCH_SUPPORTS_INT128=y +CONFIG_NUMA_BALANCING=y +# CONFIG_NUMA_BALANCING_DEFAULT_ENABLED is not set +CONFIG_CGROUPS=y +CONFIG_PAGE_COUNTER=y +# CONFIG_CGROUP_FAVOR_DYNMODS is not set +CONFIG_MEMCG=y +CONFIG_MEMCG_V1=y +CONFIG_MEMCG_KMEM=y +CONFIG_BLK_CGROUP=y +CONFIG_CGROUP_WRITEBACK=y +CONFIG_CGROUP_SCHED=y +CONFIG_FAIR_GROUP_SCHED=y +CONFIG_CFS_BANDWIDTH=y +CONFIG_RT_GROUP_SCHED=y +CONFIG_CGROUP_PIDS=y +# CONFIG_CGROUP_RDMA is not set +CONFIG_CGROUP_FREEZER=y +CONFIG_CGROUP_HUGETLB=y +CONFIG_CPUSETS=y +CONFIG_CPUSETS_V1=y +CONFIG_PROC_PID_CPUSET=y +CONFIG_CGROUP_DEVICE=y +CONFIG_CGROUP_CPUACCT=y +CONFIG_CGROUP_PERF=y +CONFIG_CGROUP_BPF=y +# CONFIG_CGROUP_MISC is not set +# CONFIG_CGROUP_DEBUG is not set +CONFIG_SOCK_CGROUP_DATA=y +CONFIG_NAMESPACES=y +CONFIG_UTS_NS=y +CONFIG_TIME_NS=y +CONFIG_IPC_NS=y +CONFIG_USER_NS=y +CONFIG_PID_NS=y +CONFIG_NET_NS=y +# CONFIG_CHECKPOINT_RESTORE is not set +CONFIG_SCHED_AUTOGROUP=y +# CONFIG_SYSFS_DEPRECATED is not set +CONFIG_RELAY=y +CONFIG_BLK_DEV_INITRD=y +CONFIG_INITRAMFS_SOURCE="" +CONFIG_RD_GZIP=y +CONFIG_RD_BZIP2=y +CONFIG_RD_LZMA=y +CONFIG_RD_XZ=y +CONFIG_RD_LZO=y +CONFIG_RD_LZ4=y +CONFIG_RD_ZSTD=y +# CONFIG_BOOT_CONFIG is not set +CONFIG_INITRAMFS_PRESERVE_MTIME=y +CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE=y +# CONFIG_CC_OPTIMIZE_FOR_SIZE is not set +CONFIG_LD_ORPHAN_WARN=y +CONFIG_SYSCTL=y +CONFIG_SYSCTL_EXCEPTION_TRACE=y +CONFIG_HAVE_PCSPKR_PLATFORM=y +CONFIG_EXPERT=y +CONFIG_MULTIUSER=y +CONFIG_SGETMASK_SYSCALL=y +CONFIG_SYSFS_SYSCALL=y +CONFIG_FHANDLE=y +CONFIG_POSIX_TIMERS=y +CONFIG_PRINTK=y +CONFIG_BUG=y +CONFIG_ELF_CORE=y +CONFIG_PCSPKR_PLATFORM=y +CONFIG_BASE_FULL=y +CONFIG_FUTEX=y +CONFIG_FUTEX_PI=y +CONFIG_EPOLL=y +CONFIG_SIGNALFD=y +CONFIG_TIMERFD=y +CONFIG_EVENTFD=y +CONFIG_SHMEM=y +CONFIG_AIO=y +CONFIG_IO_URING=y +CONFIG_ADVISE_SYSCALLS=y +CONFIG_MEMBARRIER=y +CONFIG_KALLSYMS=y +# CONFIG_KALLSYMS_ALL is not set +CONFIG_KALLSYMS_ABSOLUTE_PERCPU=y +CONFIG_KALLSYMS_BASE_RELATIVE=y +CONFIG_ARCH_HAS_MEMBARRIER_SYNC_CORE=y +CONFIG_KCMP=y +CONFIG_RSEQ=y +# CONFIG_DEBUG_RSEQ is not set +# CONFIG_EMBEDDED is not set +CONFIG_HAVE_PERF_EVENTS=y +CONFIG_GUEST_PERF_EVENTS=y +# CONFIG_PC104 is not set + +# +# Kernel Performance Events And Counters +# +CONFIG_PERF_EVENTS=y +# CONFIG_DEBUG_PERF_USE_VMALLOC is not set +# end of Kernel Performance Events And Counters + +# CONFIG_PROFILING is not set +# end of General setup + +CONFIG_64BIT=y +CONFIG_X86_64=y +CONFIG_X86=y +CONFIG_INSTRUCTION_DECODER=y +CONFIG_OUTPUT_FORMAT="elf64-x86-64" +CONFIG_LOCKDEP_SUPPORT=y +CONFIG_STACKTRACE_SUPPORT=y +CONFIG_MMU=y +CONFIG_ARCH_MMAP_RND_BITS_MIN=28 +CONFIG_ARCH_MMAP_RND_BITS_MAX=32 +CONFIG_ARCH_MMAP_RND_COMPAT_BITS_MIN=8 +CONFIG_ARCH_MMAP_RND_COMPAT_BITS_MAX=16 +CONFIG_GENERIC_BUG=y +CONFIG_GENERIC_BUG_RELATIVE_POINTERS=y +CONFIG_GENERIC_CALIBRATE_DELAY=y +CONFIG_ARCH_HAS_CPU_RELAX=y +CONFIG_ARCH_HIBERNATION_POSSIBLE=y +CONFIG_ARCH_NR_GPIO=1024 +CONFIG_ARCH_SUSPEND_POSSIBLE=y +CONFIG_AUDIT_ARCH=y +CONFIG_X86_64_SMP=y +CONFIG_ARCH_SUPPORTS_UPROBES=y +CONFIG_FIX_EARLYCON_MEM=y +CONFIG_PGTABLE_LEVELS=5 +CONFIG_CC_HAS_SANE_STACKPROTECTOR=y + +# +# Processor type and features +# +CONFIG_SMP=y +CONFIG_X86_FEATURE_NAMES=y +CONFIG_X86_MPPARSE=y +# CONFIG_GOLDFISH is not set +# CONFIG_X86_CPU_RESCTRL is not set +# CONFIG_X86_EXTENDED_PLATFORM is not set +CONFIG_X86_INTEL_LPSS=y +# CONFIG_X86_AMD_PLATFORM_DEVICE is not set +CONFIG_IOSF_MBI=y +# CONFIG_IOSF_MBI_DEBUG is not set +CONFIG_SCHED_OMIT_FRAME_POINTER=y +# CONFIG_HYPERVISOR_GUEST is not set +# CONFIG_MK8 is not set +# CONFIG_MPSC is not set +# CONFIG_MCORE2 is not set +# CONFIG_MATOM is not set +CONFIG_GENERIC_CPU=y +CONFIG_X86_INTERNODE_CACHE_SHIFT=6 +CONFIG_X86_L1_CACHE_SHIFT=6 +CONFIG_X86_TSC=y +CONFIG_X86_CMPXCHG64=y +CONFIG_X86_CMOV=y +CONFIG_X86_MINIMUM_CPU_FAMILY=64 +CONFIG_X86_DEBUGCTLMSR=y +CONFIG_IA32_FEAT_CTL=y +CONFIG_X86_VMX_FEATURE_NAMES=y +CONFIG_PROCESSOR_SELECT=y +CONFIG_CPU_SUP_INTEL=y +# CONFIG_CPU_SUP_AMD is not set +# CONFIG_CPU_SUP_HYGON is not set +# CONFIG_CPU_SUP_CENTAUR is not set +# CONFIG_CPU_SUP_ZHAOXIN is not set +CONFIG_HPET_TIMER=y +CONFIG_DMI=y +# CONFIG_MAXSMP is not set +CONFIG_NR_CPUS_RANGE_BEGIN=2 +CONFIG_NR_CPUS_RANGE_END=512 +CONFIG_NR_CPUS_DEFAULT=64 +CONFIG_NR_CPUS=128 +CONFIG_SCHED_CLUSTER=y +CONFIG_SCHED_SMT=y +CONFIG_SCHED_MC=y +CONFIG_SCHED_MC_PRIO=y +CONFIG_X86_LOCAL_APIC=y +CONFIG_X86_IO_APIC=y +CONFIG_X86_REROUTE_FOR_BROKEN_BOOT_IRQS=y +# CONFIG_X86_MCE is not set + +# +# Performance monitoring +# +CONFIG_PERF_EVENTS_INTEL_UNCORE=y +CONFIG_PERF_EVENTS_INTEL_RAPL=y +CONFIG_PERF_EVENTS_INTEL_CSTATE=y +# end of Performance monitoring + +CONFIG_X86_16BIT=y +CONFIG_X86_ESPFIX64=y +CONFIG_X86_VSYSCALL_EMULATION=y +# CONFIG_X86_IOPL_IOPERM is not set +# CONFIG_MICROCODE is not set +CONFIG_X86_MSR=y +CONFIG_X86_CPUID=y +CONFIG_X86_5LEVEL=y +CONFIG_X86_DIRECT_GBPAGES=y +# CONFIG_X86_CPA_STATISTICS is not set +CONFIG_NUMA=y +# CONFIG_AMD_NUMA is not set +CONFIG_X86_64_ACPI_NUMA=y +# CONFIG_NUMA_EMU is not set +CONFIG_NODES_SHIFT=10 +CONFIG_ARCH_SPARSEMEM_ENABLE=y +CONFIG_ARCH_SPARSEMEM_DEFAULT=y +CONFIG_ARCH_MEMORY_PROBE=y +CONFIG_ARCH_PROC_KCORE_TEXT=y +CONFIG_ILLEGAL_POINTER_VALUE=0xdead000000000000 +# CONFIG_X86_PMEM_LEGACY is not set +CONFIG_X86_CHECK_BIOS_CORRUPTION=y +CONFIG_X86_BOOTPARAM_MEMORY_CORRUPTION_CHECK=y +CONFIG_MTRR=y +CONFIG_MTRR_SANITIZER=y +CONFIG_MTRR_SANITIZER_ENABLE_DEFAULT=0 +CONFIG_MTRR_SANITIZER_SPARE_REG_NR_DEFAULT=1 +CONFIG_X86_PAT=y +CONFIG_ARCH_USES_PG_UNCACHED=y +# CONFIG_X86_UMIP is not set +CONFIG_CC_HAS_IBT=y +# CONFIG_X86_KERNEL_IBT is not set +# CONFIG_X86_INTEL_MEMORY_PROTECTION_KEYS is not set +CONFIG_X86_INTEL_TSX_MODE_OFF=y +# CONFIG_X86_INTEL_TSX_MODE_ON is not set +# CONFIG_X86_INTEL_TSX_MODE_AUTO is not set +CONFIG_EFI=y +CONFIG_EFI_STUB=y +# CONFIG_EFI_MIXED is not set +# CONFIG_HZ_100 is not set +CONFIG_HZ_250=y +# CONFIG_HZ_300 is not set +# CONFIG_HZ_1000 is not set +CONFIG_HZ=250 +CONFIG_SCHED_HRTICK=y +# CONFIG_KEXEC is not set +CONFIG_KEXEC_FILE=y +CONFIG_ARCH_HAS_KEXEC_PURGATORY=y +# CONFIG_KEXEC_SIG is not set +# CONFIG_CRASH_DUMP is not set +CONFIG_PHYSICAL_START=0x1000000 +CONFIG_RELOCATABLE=y +CONFIG_RANDOMIZE_BASE=y +CONFIG_X86_NEED_RELOCS=y +CONFIG_PHYSICAL_ALIGN=0x1000000 +CONFIG_DYNAMIC_MEMORY_LAYOUT=y +CONFIG_RANDOMIZE_MEMORY=y +CONFIG_RANDOMIZE_MEMORY_PHYSICAL_PADDING=0xa +CONFIG_HOTPLUG_CPU=y +# CONFIG_BOOTPARAM_HOTPLUG_CPU0 is not set +# CONFIG_DEBUG_HOTPLUG_CPU0 is not set +CONFIG_LEGACY_VSYSCALL_XONLY=y +# CONFIG_LEGACY_VSYSCALL_NONE is not set +# CONFIG_CMDLINE_BOOL is not set +CONFIG_MODIFY_LDT_SYSCALL=y +# CONFIG_STRICT_SIGALTSTACK_SIZE is not set +CONFIG_HAVE_LIVEPATCH=y +# end of Processor type and features + +CONFIG_CC_HAS_RETURN_THUNK=y +CONFIG_SPECULATION_MITIGATIONS=y +CONFIG_PAGE_TABLE_ISOLATION=y +CONFIG_RETPOLINE=y +CONFIG_RETHUNK=y +CONFIG_CPU_IBRS_ENTRY=y +# CONFIG_GDS_FORCE_MITIGATION is not set +CONFIG_ARCH_HAS_ADD_PAGES=y +CONFIG_ARCH_MHP_MEMMAP_ON_MEMORY_ENABLE=y + +# +# Power management and ACPI options +# +# CONFIG_SUSPEND is not set +# CONFIG_HIBERNATION is not set +CONFIG_PM=y +# CONFIG_PM_DEBUG is not set +CONFIG_PM_CLK=y +# CONFIG_WQ_POWER_EFFICIENT_DEFAULT is not set +# CONFIG_ENERGY_MODEL is not set +CONFIG_ARCH_SUPPORTS_ACPI=y +CONFIG_ACPI=y +CONFIG_ACPI_LEGACY_TABLES_LOOKUP=y +CONFIG_ARCH_MIGHT_HAVE_ACPI_PDC=y +CONFIG_ACPI_SYSTEM_POWER_STATES_SUPPORT=y +# CONFIG_ACPI_DEBUGGER is not set +CONFIG_ACPI_SPCR_TABLE=y +# CONFIG_ACPI_FPDT is not set +CONFIG_ACPI_LPIT=y +CONFIG_ACPI_REV_OVERRIDE_POSSIBLE=y +# CONFIG_ACPI_EC_DEBUGFS is not set +# CONFIG_ACPI_AC is not set +# CONFIG_ACPI_BATTERY is not set +CONFIG_ACPI_BUTTON=y +# CONFIG_ACPI_FAN is not set +# CONFIG_ACPI_DOCK is not set +CONFIG_ACPI_CPU_FREQ_PSS=y +CONFIG_ACPI_PROCESSOR_CSTATE=y +CONFIG_ACPI_PROCESSOR_IDLE=y +CONFIG_ACPI_CPPC_LIB=y +CONFIG_ACPI_PROCESSOR=y +CONFIG_ACPI_HOTPLUG_CPU=y +# CONFIG_ACPI_PROCESSOR_AGGREGATOR is not set +CONFIG_ACPI_THERMAL=y +CONFIG_ARCH_HAS_ACPI_TABLE_UPGRADE=y +CONFIG_ACPI_TABLE_UPGRADE=y +# CONFIG_ACPI_DEBUG is not set +# CONFIG_ACPI_PCI_SLOT is not set +CONFIG_ACPI_CONTAINER=y +CONFIG_ACPI_HOTPLUG_MEMORY=y +CONFIG_ACPI_HOTPLUG_IOAPIC=y +# CONFIG_ACPI_SBS is not set +# CONFIG_ACPI_HED is not set +# CONFIG_ACPI_CUSTOM_METHOD is not set +# CONFIG_ACPI_BGRT is not set +# CONFIG_ACPI_REDUCED_HARDWARE_ONLY is not set +# CONFIG_ACPI_NFIT is not set +CONFIG_ACPI_NUMA=y +# CONFIG_ACPI_HMAT is not set +CONFIG_HAVE_ACPI_APEI=y +CONFIG_HAVE_ACPI_APEI_NMI=y +# CONFIG_ACPI_APEI is not set +# CONFIG_ACPI_DPTF is not set +# CONFIG_ACPI_CONFIGFS is not set +# CONFIG_ACPI_PFRUT is not set +CONFIG_ACPI_PCC=y +CONFIG_PMIC_OPREGION=y +CONFIG_ACPI_VIOT=y +CONFIG_ACPI_PRMT=y +CONFIG_X86_PM_TIMER=y + +# +# CPU Frequency scaling +# +CONFIG_CPU_FREQ=y +CONFIG_CPU_FREQ_GOV_ATTR_SET=y +# CONFIG_CPU_FREQ_STAT is not set +CONFIG_CPU_FREQ_DEFAULT_GOV_PERFORMANCE=y +# CONFIG_CPU_FREQ_DEFAULT_GOV_POWERSAVE is not set +# CONFIG_CPU_FREQ_DEFAULT_GOV_USERSPACE is not set +# CONFIG_CPU_FREQ_DEFAULT_GOV_SCHEDUTIL is not set +CONFIG_CPU_FREQ_GOV_PERFORMANCE=y +# CONFIG_CPU_FREQ_GOV_POWERSAVE is not set +# CONFIG_CPU_FREQ_GOV_USERSPACE is not set +# CONFIG_CPU_FREQ_GOV_ONDEMAND is not set +# CONFIG_CPU_FREQ_GOV_CONSERVATIVE is not set +CONFIG_CPU_FREQ_GOV_SCHEDUTIL=y + +# +# CPU frequency scaling drivers +# +CONFIG_X86_INTEL_PSTATE=y +# CONFIG_X86_PCC_CPUFREQ is not set +# CONFIG_X86_AMD_PSTATE is not set +# CONFIG_X86_AMD_PSTATE_UT is not set +# CONFIG_X86_ACPI_CPUFREQ is not set +# CONFIG_X86_SPEEDSTEP_CENTRINO is not set +# CONFIG_X86_P4_CLOCKMOD is not set + +# +# shared options +# +# end of CPU Frequency scaling + +# +# CPU Idle +# +CONFIG_CPU_IDLE=y +CONFIG_CPU_IDLE_GOV_LADDER=y +CONFIG_CPU_IDLE_GOV_MENU=y +# CONFIG_CPU_IDLE_GOV_TEO is not set +# end of CPU Idle + +CONFIG_INTEL_IDLE=y +# end of Power management and ACPI options + +# +# Bus options (PCI etc.) +# +CONFIG_PCI_DIRECT=y +CONFIG_PCI_MMCONFIG=y +CONFIG_MMCONF_FAM10H=y +# CONFIG_PCI_CNB20LE_QUIRK is not set +# CONFIG_ISA_BUS is not set +# CONFIG_ISA_DMA_API is not set +# end of Bus options (PCI etc.) + +# +# Binary Emulations +# +# CONFIG_IA32_EMULATION is not set +# CONFIG_X86_X32_ABI is not set +# end of Binary Emulations + +CONFIG_HAVE_KVM=y +CONFIG_HAVE_KVM_PFNCACHE=y +CONFIG_HAVE_KVM_IRQCHIP=y +CONFIG_HAVE_KVM_IRQFD=y +CONFIG_HAVE_KVM_IRQ_ROUTING=y +CONFIG_HAVE_KVM_DIRTY_RING=y +CONFIG_HAVE_KVM_DIRTY_RING_TSO=y +CONFIG_HAVE_KVM_DIRTY_RING_ACQ_REL=y +CONFIG_HAVE_KVM_EVENTFD=y +CONFIG_KVM_MMIO=y +CONFIG_KVM_ASYNC_PF=y +CONFIG_HAVE_KVM_MSI=y +CONFIG_HAVE_KVM_CPU_RELAX_INTERCEPT=y +CONFIG_KVM_VFIO=y +CONFIG_KVM_GENERIC_DIRTYLOG_READ_PROTECT=y +CONFIG_HAVE_KVM_IRQ_BYPASS=y +CONFIG_HAVE_KVM_NO_POLL=y +CONFIG_KVM_XFER_TO_GUEST_WORK=y +CONFIG_HAVE_KVM_PM_NOTIFIER=y +CONFIG_VIRTUALIZATION=y +CONFIG_KVM=y +# CONFIG_KVM_WERROR is not set +CONFIG_KVM_INTEL=y +# CONFIG_KVM_AMD is not set +# CONFIG_KVM_XEN is not set +CONFIG_AS_AVX512=y +CONFIG_AS_SHA1_NI=y +CONFIG_AS_SHA256_NI=y +CONFIG_AS_TPAUSE=y + +# +# General architecture-dependent options +# +CONFIG_CRASH_CORE=y +CONFIG_KEXEC_CORE=y +CONFIG_HOTPLUG_SMT=y +CONFIG_GENERIC_ENTRY=y +CONFIG_JUMP_LABEL=y +# CONFIG_STATIC_KEYS_SELFTEST is not set +# CONFIG_STATIC_CALL_SELFTEST is not set +CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS=y +CONFIG_ARCH_USE_BUILTIN_BSWAP=y +CONFIG_USER_RETURN_NOTIFIER=y +CONFIG_HAVE_IOREMAP_PROT=y +CONFIG_HAVE_KPROBES=y +CONFIG_HAVE_KRETPROBES=y +CONFIG_HAVE_OPTPROBES=y +CONFIG_HAVE_KPROBES_ON_FTRACE=y +CONFIG_ARCH_CORRECT_STACKTRACE_ON_KRETPROBE=y +CONFIG_HAVE_FUNCTION_ERROR_INJECTION=y +CONFIG_HAVE_NMI=y +CONFIG_TRACE_IRQFLAGS_SUPPORT=y +CONFIG_TRACE_IRQFLAGS_NMI_SUPPORT=y +CONFIG_HAVE_ARCH_TRACEHOOK=y +CONFIG_HAVE_DMA_CONTIGUOUS=y +CONFIG_GENERIC_SMP_IDLE_THREAD=y +CONFIG_ARCH_HAS_FORTIFY_SOURCE=y +CONFIG_ARCH_HAS_SET_MEMORY=y +CONFIG_ARCH_HAS_SET_DIRECT_MAP=y +CONFIG_ARCH_HAS_CPU_FINALIZE_INIT=y +CONFIG_HAVE_ARCH_THREAD_STRUCT_WHITELIST=y +CONFIG_ARCH_WANTS_DYNAMIC_TASK_STRUCT=y +CONFIG_ARCH_WANTS_NO_INSTR=y +CONFIG_HAVE_ASM_MODVERSIONS=y +CONFIG_HAVE_REGS_AND_STACK_ACCESS_API=y +CONFIG_HAVE_RSEQ=y +CONFIG_HAVE_RUST=y +CONFIG_HAVE_FUNCTION_ARG_ACCESS_API=y +CONFIG_HAVE_HW_BREAKPOINT=y +CONFIG_HAVE_MIXED_BREAKPOINTS_REGS=y +CONFIG_HAVE_USER_RETURN_NOTIFIER=y +CONFIG_HAVE_PERF_EVENTS_NMI=y +CONFIG_HAVE_HARDLOCKUP_DETECTOR_PERF=y +CONFIG_HAVE_PERF_REGS=y +CONFIG_HAVE_PERF_USER_STACK_DUMP=y +CONFIG_HAVE_ARCH_JUMP_LABEL=y +CONFIG_HAVE_ARCH_JUMP_LABEL_RELATIVE=y +CONFIG_MMU_GATHER_MERGE_VMAS=y +CONFIG_ARCH_HAVE_NMI_SAFE_CMPXCHG=y +CONFIG_HAVE_ALIGNED_STRUCT_PAGE=y +CONFIG_HAVE_CMPXCHG_LOCAL=y +CONFIG_HAVE_CMPXCHG_DOUBLE=y +CONFIG_HAVE_ARCH_SECCOMP=y +CONFIG_HAVE_ARCH_SECCOMP_FILTER=y +CONFIG_SECCOMP=y +CONFIG_SECCOMP_FILTER=y +# CONFIG_SECCOMP_CACHE_DEBUG is not set +CONFIG_HAVE_ARCH_STACKLEAK=y +CONFIG_HAVE_STACKPROTECTOR=y +CONFIG_STACKPROTECTOR=y +CONFIG_STACKPROTECTOR_STRONG=y +CONFIG_ARCH_SUPPORTS_LTO_CLANG=y +CONFIG_ARCH_SUPPORTS_LTO_CLANG_THIN=y +CONFIG_LTO_NONE=y +CONFIG_ARCH_SUPPORTS_CFI_CLANG=y +CONFIG_HAVE_ARCH_WITHIN_STACK_FRAMES=y +CONFIG_HAVE_CONTEXT_TRACKING_USER=y +CONFIG_HAVE_CONTEXT_TRACKING_USER_OFFSTACK=y +CONFIG_HAVE_VIRT_CPU_ACCOUNTING_GEN=y +CONFIG_HAVE_IRQ_TIME_ACCOUNTING=y +CONFIG_HAVE_MOVE_PUD=y +CONFIG_HAVE_MOVE_PMD=y +CONFIG_HAVE_ARCH_TRANSPARENT_HUGEPAGE=y +CONFIG_HAVE_ARCH_TRANSPARENT_HUGEPAGE_PUD=y +CONFIG_HAVE_ARCH_HUGE_VMAP=y +CONFIG_HAVE_ARCH_HUGE_VMALLOC=y +CONFIG_ARCH_WANT_HUGE_PMD_SHARE=y +CONFIG_HAVE_ARCH_SOFT_DIRTY=y +CONFIG_HAVE_MOD_ARCH_SPECIFIC=y +CONFIG_MODULES_USE_ELF_RELA=y +CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK=y +CONFIG_HAVE_SOFTIRQ_ON_OWN_STACK=y +CONFIG_SOFTIRQ_ON_OWN_STACK=y +CONFIG_ARCH_HAS_ELF_RANDOMIZE=y +CONFIG_HAVE_ARCH_MMAP_RND_BITS=y +CONFIG_HAVE_EXIT_THREAD=y +CONFIG_ARCH_MMAP_RND_BITS=28 +CONFIG_PAGE_SIZE_LESS_THAN_64KB=y +CONFIG_PAGE_SIZE_LESS_THAN_256KB=y +CONFIG_HAVE_OBJTOOL=y +CONFIG_HAVE_JUMP_LABEL_HACK=y +CONFIG_HAVE_NOINSTR_HACK=y +CONFIG_HAVE_NOINSTR_VALIDATION=y +CONFIG_HAVE_UACCESS_VALIDATION=y +CONFIG_HAVE_STACK_VALIDATION=y +CONFIG_HAVE_RELIABLE_STACKTRACE=y +# CONFIG_COMPAT_32BIT_TIME is not set +CONFIG_HAVE_ARCH_VMAP_STACK=y +CONFIG_VMAP_STACK=y +CONFIG_HAVE_ARCH_RANDOMIZE_KSTACK_OFFSET=y +CONFIG_RANDOMIZE_KSTACK_OFFSET=y +# CONFIG_RANDOMIZE_KSTACK_OFFSET_DEFAULT is not set +CONFIG_ARCH_HAS_STRICT_KERNEL_RWX=y +CONFIG_STRICT_KERNEL_RWX=y +CONFIG_ARCH_HAS_STRICT_MODULE_RWX=y +CONFIG_HAVE_ARCH_PREL32_RELOCATIONS=y +CONFIG_ARCH_USE_MEMREMAP_PROT=y +# CONFIG_LOCK_EVENT_COUNTS is not set +CONFIG_ARCH_HAS_MEM_ENCRYPT=y +CONFIG_HAVE_STATIC_CALL=y +CONFIG_HAVE_STATIC_CALL_INLINE=y +CONFIG_HAVE_PREEMPT_DYNAMIC=y +CONFIG_HAVE_PREEMPT_DYNAMIC_CALL=y +CONFIG_ARCH_WANT_LD_ORPHAN_WARN=y +CONFIG_ARCH_SUPPORTS_DEBUG_PAGEALLOC=y +CONFIG_ARCH_SUPPORTS_PAGE_TABLE_CHECK=y +CONFIG_ARCH_HAS_ELFCORE_COMPAT=y +CONFIG_ARCH_HAS_PARANOID_L1D_FLUSH=y +CONFIG_DYNAMIC_SIGFRAME=y +CONFIG_ARCH_HAS_NONLEAF_PMD_YOUNG=y + +# +# GCOV-based kernel profiling +# +# CONFIG_GCOV_KERNEL is not set +CONFIG_ARCH_HAS_GCOV_PROFILE_ALL=y +# end of GCOV-based kernel profiling + +CONFIG_HAVE_GCC_PLUGINS=y +# end of General architecture-dependent options + +CONFIG_RT_MUTEXES=y +CONFIG_BASE_SMALL=0 +# CONFIG_MODULES is not set +CONFIG_BLOCK=y +CONFIG_BLOCK_LEGACY_AUTOLOAD=y +CONFIG_BLK_CGROUP_RWSTAT=y +CONFIG_BLK_DEV_BSG_COMMON=y +CONFIG_BLK_DEV_BSGLIB=y +CONFIG_BLK_DEV_INTEGRITY=y +# CONFIG_BLK_DEV_ZONED is not set +CONFIG_BLK_DEV_THROTTLING=y +# CONFIG_BLK_DEV_THROTTLING_LOW is not set +CONFIG_BLK_WBT=y +CONFIG_BLK_WBT_MQ=y +# CONFIG_BLK_CGROUP_IOLATENCY is not set +# CONFIG_BLK_CGROUP_IOCOST is not set +# CONFIG_BLK_CGROUP_IOPRIO is not set +CONFIG_BLK_DEBUG_FS=y +# CONFIG_BLK_SED_OPAL is not set +# CONFIG_BLK_INLINE_ENCRYPTION is not set + +# +# Partition Types +# +CONFIG_PARTITION_ADVANCED=y +# CONFIG_ACORN_PARTITION is not set +# CONFIG_AIX_PARTITION is not set +# CONFIG_OSF_PARTITION is not set +# CONFIG_AMIGA_PARTITION is not set +# CONFIG_ATARI_PARTITION is not set +# CONFIG_MAC_PARTITION is not set +# CONFIG_MSDOS_PARTITION is not set +# CONFIG_LDM_PARTITION is not set +# CONFIG_SGI_PARTITION is not set +# CONFIG_ULTRIX_PARTITION is not set +# CONFIG_SUN_PARTITION is not set +# CONFIG_KARMA_PARTITION is not set +CONFIG_EFI_PARTITION=y +# CONFIG_SYSV68_PARTITION is not set +# CONFIG_CMDLINE_PARTITION is not set +# end of Partition Types + +CONFIG_BLK_MQ_PCI=y +CONFIG_BLK_MQ_VIRTIO=y +CONFIG_BLK_PM=y + +# +# IO Schedulers +# +# CONFIG_MQ_IOSCHED_DEADLINE is not set +# CONFIG_MQ_IOSCHED_KYBER is not set +# CONFIG_IOSCHED_BFQ is not set +# end of IO Schedulers + +CONFIG_PREEMPT_NOTIFIERS=y +CONFIG_UNINLINE_SPIN_UNLOCK=y +CONFIG_ARCH_SUPPORTS_ATOMIC_RMW=y +CONFIG_MUTEX_SPIN_ON_OWNER=y +CONFIG_RWSEM_SPIN_ON_OWNER=y +CONFIG_LOCK_SPIN_ON_OWNER=y +CONFIG_ARCH_USE_QUEUED_SPINLOCKS=y +CONFIG_QUEUED_SPINLOCKS=y +CONFIG_ARCH_USE_QUEUED_RWLOCKS=y +CONFIG_QUEUED_RWLOCKS=y +CONFIG_ARCH_HAS_NON_OVERLAPPING_ADDRESS_SPACE=y +CONFIG_ARCH_HAS_SYNC_CORE_BEFORE_USERMODE=y +CONFIG_ARCH_HAS_SYSCALL_WRAPPER=y +CONFIG_FREEZER=y + +# +# Executable file formats +# +CONFIG_BINFMT_ELF=y +CONFIG_ELFCORE=y +CONFIG_CORE_DUMP_DEFAULT_ELF_HEADERS=y +CONFIG_BINFMT_SCRIPT=y +CONFIG_BINFMT_MISC=y +CONFIG_COREDUMP=y +# end of Executable file formats + +# +# Memory Management options +# +CONFIG_ZPOOL=y +CONFIG_SWAP=y +CONFIG_ZSWAP=y +# CONFIG_ZSWAP_DEFAULT_ON is not set +# CONFIG_ZSWAP_COMPRESSOR_DEFAULT_DEFLATE is not set +CONFIG_ZSWAP_COMPRESSOR_DEFAULT_LZO=y +# CONFIG_ZSWAP_COMPRESSOR_DEFAULT_842 is not set +# CONFIG_ZSWAP_COMPRESSOR_DEFAULT_LZ4 is not set +# CONFIG_ZSWAP_COMPRESSOR_DEFAULT_LZ4HC is not set +# CONFIG_ZSWAP_COMPRESSOR_DEFAULT_ZSTD is not set +CONFIG_ZSWAP_COMPRESSOR_DEFAULT="lzo" +CONFIG_ZSWAP_ZPOOL_DEFAULT_ZBUD=y +# CONFIG_ZSWAP_ZPOOL_DEFAULT_Z3FOLD is not set +# CONFIG_ZSWAP_ZPOOL_DEFAULT_ZSMALLOC is not set +CONFIG_ZSWAP_ZPOOL_DEFAULT="zbud" +CONFIG_ZBUD=y +# CONFIG_Z3FOLD is not set +CONFIG_ZSMALLOC=y +CONFIG_ZSMALLOC_STAT=y + +# +# SLAB allocator options +# +# CONFIG_SLAB is not set +CONFIG_SLUB=y +# CONFIG_SLOB is not set +CONFIG_SLAB_MERGE_DEFAULT=y +# CONFIG_SLAB_FREELIST_RANDOM is not set +CONFIG_SLAB_FREELIST_HARDENED=y +# CONFIG_SLUB_STATS is not set +CONFIG_SLUB_CPU_PARTIAL=y +# end of SLAB allocator options + +# CONFIG_SHUFFLE_PAGE_ALLOCATOR is not set +# CONFIG_COMPAT_BRK is not set +CONFIG_SPARSEMEM=y +CONFIG_SPARSEMEM_EXTREME=y +CONFIG_SPARSEMEM_VMEMMAP_ENABLE=y +CONFIG_SPARSEMEM_VMEMMAP=y +CONFIG_HAVE_FAST_GUP=y +CONFIG_NUMA_KEEP_MEMINFO=y +CONFIG_MEMORY_ISOLATION=y +CONFIG_EXCLUSIVE_SYSTEM_RAM=y +CONFIG_HAVE_BOOTMEM_INFO_NODE=y +CONFIG_ARCH_ENABLE_MEMORY_HOTPLUG=y +CONFIG_ARCH_ENABLE_MEMORY_HOTREMOVE=y +CONFIG_MEMORY_HOTPLUG=y +CONFIG_MEMORY_HOTPLUG_DEFAULT_ONLINE=y +CONFIG_MEMORY_HOTREMOVE=y +CONFIG_MHP_MEMMAP_ON_MEMORY=y +CONFIG_SPLIT_PTLOCK_CPUS=4 +CONFIG_ARCH_ENABLE_SPLIT_PMD_PTLOCK=y +CONFIG_MEMORY_BALLOON=y +CONFIG_BALLOON_COMPACTION=y +CONFIG_COMPACTION=y +CONFIG_COMPACT_UNEVICTABLE_DEFAULT=1 +CONFIG_PAGE_REPORTING=y +CONFIG_MIGRATION=y +CONFIG_DEVICE_MIGRATION=y +CONFIG_ARCH_ENABLE_HUGEPAGE_MIGRATION=y +CONFIG_ARCH_ENABLE_THP_MIGRATION=y +CONFIG_CONTIG_ALLOC=y +CONFIG_PHYS_ADDR_T_64BIT=y +CONFIG_MMU_NOTIFIER=y +CONFIG_KSM=y +CONFIG_DEFAULT_MMAP_MIN_ADDR=4096 +CONFIG_ARCH_WANT_GENERAL_HUGETLB=y +CONFIG_ARCH_WANTS_THP_SWAP=y +CONFIG_TRANSPARENT_HUGEPAGE=y +# CONFIG_TRANSPARENT_HUGEPAGE_ALWAYS is not set +CONFIG_TRANSPARENT_HUGEPAGE_MADVISE=y +CONFIG_THP_SWAP=y +# CONFIG_READ_ONLY_THP_FOR_FS is not set +CONFIG_NEED_PER_CPU_EMBED_FIRST_CHUNK=y +CONFIG_NEED_PER_CPU_PAGE_FIRST_CHUNK=y +CONFIG_USE_PERCPU_NUMA_NODE_ID=y +CONFIG_HAVE_SETUP_PER_CPU_AREA=y +CONFIG_FRONTSWAP=y +# CONFIG_CMA is not set +CONFIG_GENERIC_EARLY_IOREMAP=y +# CONFIG_DEFERRED_STRUCT_PAGE_INIT is not set +# CONFIG_IDLE_PAGE_TRACKING is not set +CONFIG_ARCH_HAS_CACHE_LINE_SIZE=y +CONFIG_ARCH_HAS_CURRENT_STACK_POINTER=y +CONFIG_ARCH_HAS_PTE_DEVMAP=y +CONFIG_ARCH_HAS_ZONE_DMA_SET=y +CONFIG_ZONE_DMA=y +CONFIG_ZONE_DMA32=y +CONFIG_ZONE_DEVICE=y +# CONFIG_DEVICE_PRIVATE is not set +CONFIG_VM_EVENT_COUNTERS=y +CONFIG_PERCPU_STATS=y +# CONFIG_GUP_TEST is not set +CONFIG_ARCH_HAS_PTE_SPECIAL=y +CONFIG_SECRETMEM=y +# CONFIG_ANON_VMA_NAME is not set +CONFIG_USERFAULTFD=y +CONFIG_HAVE_ARCH_USERFAULTFD_WP=y +CONFIG_HAVE_ARCH_USERFAULTFD_MINOR=y +CONFIG_PTE_MARKER=y +CONFIG_PTE_MARKER_UFFD_WP=y +# CONFIG_LRU_GEN is not set +CONFIG_LOCK_MM_AND_FIND_VMA=y + +# +# Data Access Monitoring +# +# CONFIG_DAMON is not set +# end of Data Access Monitoring +# end of Memory Management options + +CONFIG_NET=y +CONFIG_NET_INGRESS=y +CONFIG_NET_EGRESS=y +CONFIG_SKB_EXTENSIONS=y + +# +# Networking options +# +CONFIG_PACKET=y +CONFIG_PACKET_DIAG=y +CONFIG_UNIX=y +CONFIG_UNIX_SCM=y +CONFIG_AF_UNIX_OOB=y +CONFIG_UNIX_DIAG=y +CONFIG_TLS=y +# CONFIG_TLS_DEVICE is not set +# CONFIG_TLS_TOE is not set +CONFIG_XFRM=y +CONFIG_XFRM_OFFLOAD=y +CONFIG_XFRM_ALGO=y +CONFIG_XFRM_USER=y +# CONFIG_XFRM_INTERFACE is not set +CONFIG_XFRM_SUB_POLICY=y +CONFIG_XFRM_MIGRATE=y +CONFIG_XFRM_STATISTICS=y +CONFIG_XFRM_AH=y +CONFIG_XFRM_ESP=y +CONFIG_XFRM_IPCOMP=y +CONFIG_NET_KEY=y +CONFIG_NET_KEY_MIGRATE=y +CONFIG_XFRM_ESPINTCP=y +CONFIG_XDP_SOCKETS=y +# CONFIG_XDP_SOCKETS_DIAG is not set +CONFIG_INET=y +# CONFIG_IP_MULTICAST is not set +CONFIG_IP_ADVANCED_ROUTER=y +# CONFIG_IP_FIB_TRIE_STATS is not set +CONFIG_IP_MULTIPLE_TABLES=y +# CONFIG_IP_ROUTE_MULTIPATH is not set +# CONFIG_IP_ROUTE_VERBOSE is not set +CONFIG_IP_ROUTE_CLASSID=y +CONFIG_IP_PNP=y +CONFIG_IP_PNP_DHCP=y +CONFIG_IP_PNP_BOOTP=y +CONFIG_IP_PNP_RARP=y +CONFIG_NET_IPIP=y +# CONFIG_NET_IPGRE_DEMUX is not set +CONFIG_NET_IP_TUNNEL=y +CONFIG_IP_MROUTE_COMMON=y +CONFIG_SYN_COOKIES=y +# CONFIG_NET_IPVTI is not set +CONFIG_NET_UDP_TUNNEL=y +CONFIG_NET_FOU=y +CONFIG_NET_FOU_IP_TUNNELS=y +# CONFIG_INET_AH is not set +# CONFIG_INET_ESP is not set +# CONFIG_INET_IPCOMP is not set +CONFIG_INET_TABLE_PERTURB_ORDER=16 +CONFIG_INET_TUNNEL=y +CONFIG_INET_DIAG=y +CONFIG_INET_TCP_DIAG=y +CONFIG_INET_UDP_DIAG=y +CONFIG_INET_RAW_DIAG=y +# CONFIG_INET_DIAG_DESTROY is not set +# CONFIG_TCP_CONG_ADVANCED is not set +CONFIG_TCP_CONG_CUBIC=y +CONFIG_DEFAULT_TCP_CONG="cubic" +# CONFIG_TCP_MD5SIG is not set +CONFIG_IPV6=y +CONFIG_IPV6_ROUTER_PREF=y +CONFIG_IPV6_ROUTE_INFO=y +CONFIG_IPV6_OPTIMISTIC_DAD=y +CONFIG_INET6_AH=y +CONFIG_INET6_ESP=y +CONFIG_INET6_ESP_OFFLOAD=y +CONFIG_INET6_ESPINTCP=y +CONFIG_INET6_IPCOMP=y +# CONFIG_IPV6_MIP6 is not set +CONFIG_IPV6_ILA=y +CONFIG_INET6_XFRM_TUNNEL=y +CONFIG_INET6_TUNNEL=y +CONFIG_IPV6_VTI=y +CONFIG_IPV6_SIT=y +# CONFIG_IPV6_SIT_6RD is not set +CONFIG_IPV6_NDISC_NODETYPE=y +CONFIG_IPV6_TUNNEL=y +CONFIG_IPV6_FOU=y +CONFIG_IPV6_FOU_TUNNEL=y +CONFIG_IPV6_MULTIPLE_TABLES=y +CONFIG_IPV6_SUBTREES=y +CONFIG_IPV6_MROUTE=y +CONFIG_IPV6_MROUTE_MULTIPLE_TABLES=y +CONFIG_IPV6_PIMSM_V2=y +CONFIG_IPV6_SEG6_LWTUNNEL=y +CONFIG_IPV6_SEG6_HMAC=y +CONFIG_IPV6_SEG6_BPF=y +CONFIG_IPV6_RPL_LWTUNNEL=y +# CONFIG_IPV6_IOAM6_LWTUNNEL is not set +# CONFIG_MPTCP is not set +CONFIG_NETWORK_SECMARK=y +CONFIG_NET_PTP_CLASSIFY=y +# CONFIG_NETWORK_PHY_TIMESTAMPING is not set +CONFIG_NETFILTER=y +CONFIG_NETFILTER_ADVANCED=y +CONFIG_BRIDGE_NETFILTER=y + +# +# Core Netfilter Configuration +# +CONFIG_NETFILTER_INGRESS=y +CONFIG_NETFILTER_EGRESS=y +CONFIG_NETFILTER_SKIP_EGRESS=y +CONFIG_NETFILTER_NETLINK=y +CONFIG_NETFILTER_FAMILY_BRIDGE=y +CONFIG_NETFILTER_FAMILY_ARP=y +# CONFIG_NETFILTER_NETLINK_HOOK is not set +CONFIG_NETFILTER_NETLINK_ACCT=y +CONFIG_NETFILTER_NETLINK_QUEUE=y +CONFIG_NETFILTER_NETLINK_LOG=y +CONFIG_NETFILTER_NETLINK_OSF=y +CONFIG_NF_CONNTRACK=y +CONFIG_NF_LOG_SYSLOG=y +CONFIG_NETFILTER_CONNCOUNT=y +CONFIG_NF_CONNTRACK_MARK=y +# CONFIG_NF_CONNTRACK_SECMARK is not set +CONFIG_NF_CONNTRACK_ZONES=y +CONFIG_NF_CONNTRACK_PROCFS=y +CONFIG_NF_CONNTRACK_EVENTS=y +CONFIG_NF_CONNTRACK_TIMEOUT=y +CONFIG_NF_CONNTRACK_TIMESTAMP=y +CONFIG_NF_CONNTRACK_LABELS=y +CONFIG_NF_CT_PROTO_DCCP=y +CONFIG_NF_CT_PROTO_SCTP=y +CONFIG_NF_CT_PROTO_UDPLITE=y +# CONFIG_NF_CONNTRACK_AMANDA is not set +# CONFIG_NF_CONNTRACK_FTP is not set +# CONFIG_NF_CONNTRACK_H323 is not set +# CONFIG_NF_CONNTRACK_IRC is not set +# CONFIG_NF_CONNTRACK_NETBIOS_NS is not set +# CONFIG_NF_CONNTRACK_SNMP is not set +# CONFIG_NF_CONNTRACK_PPTP is not set +# CONFIG_NF_CONNTRACK_SANE is not set +# CONFIG_NF_CONNTRACK_SIP is not set +# CONFIG_NF_CONNTRACK_TFTP is not set +CONFIG_NF_CT_NETLINK=y +# CONFIG_NF_CT_NETLINK_TIMEOUT is not set +# CONFIG_NETFILTER_NETLINK_GLUE_CT is not set +CONFIG_NF_NAT=y +CONFIG_NF_NAT_REDIRECT=y +CONFIG_NF_NAT_MASQUERADE=y +CONFIG_NETFILTER_SYNPROXY=y +CONFIG_NF_TABLES=y +CONFIG_NF_TABLES_INET=y +CONFIG_NF_TABLES_NETDEV=y +CONFIG_NFT_NUMGEN=y +CONFIG_NFT_CT=y +CONFIG_NFT_CONNLIMIT=y +CONFIG_NFT_LOG=y +CONFIG_NFT_LIMIT=y +CONFIG_NFT_MASQ=y +CONFIG_NFT_REDIR=y +CONFIG_NFT_NAT=y +CONFIG_NFT_TUNNEL=y +CONFIG_NFT_OBJREF=y +CONFIG_NFT_QUEUE=y +CONFIG_NFT_QUOTA=y +CONFIG_NFT_REJECT=y +CONFIG_NFT_REJECT_INET=y +CONFIG_NFT_COMPAT=y +CONFIG_NFT_HASH=y +CONFIG_NFT_FIB=y +CONFIG_NFT_FIB_INET=y +CONFIG_NFT_XFRM=y +CONFIG_NFT_SOCKET=y +CONFIG_NFT_OSF=y +CONFIG_NFT_TPROXY=y +CONFIG_NFT_SYNPROXY=y +CONFIG_NF_DUP_NETDEV=y +CONFIG_NFT_DUP_NETDEV=y +CONFIG_NFT_FWD_NETDEV=y +CONFIG_NFT_FIB_NETDEV=y +CONFIG_NFT_REJECT_NETDEV=y +CONFIG_NETFILTER_XTABLES=y + +# +# Xtables combined modules +# +CONFIG_NETFILTER_XT_MARK=y +CONFIG_NETFILTER_XT_CONNMARK=y +CONFIG_NETFILTER_XT_SET=y + +# +# Xtables targets +# +CONFIG_NETFILTER_XT_TARGET_AUDIT=y +CONFIG_NETFILTER_XT_TARGET_CHECKSUM=y +CONFIG_NETFILTER_XT_TARGET_CLASSIFY=y +CONFIG_NETFILTER_XT_TARGET_CONNMARK=y +CONFIG_NETFILTER_XT_TARGET_CT=y +CONFIG_NETFILTER_XT_TARGET_DSCP=y +CONFIG_NETFILTER_XT_TARGET_HL=y +CONFIG_NETFILTER_XT_TARGET_HMARK=y +CONFIG_NETFILTER_XT_TARGET_IDLETIMER=y +CONFIG_NETFILTER_XT_TARGET_LOG=y +CONFIG_NETFILTER_XT_TARGET_MARK=y +CONFIG_NETFILTER_XT_NAT=y +CONFIG_NETFILTER_XT_TARGET_NETMAP=y +CONFIG_NETFILTER_XT_TARGET_NFLOG=y +CONFIG_NETFILTER_XT_TARGET_NFQUEUE=y +CONFIG_NETFILTER_XT_TARGET_NOTRACK=y +CONFIG_NETFILTER_XT_TARGET_RATEEST=y +CONFIG_NETFILTER_XT_TARGET_REDIRECT=y +CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y +CONFIG_NETFILTER_XT_TARGET_TEE=y +CONFIG_NETFILTER_XT_TARGET_TPROXY=y +CONFIG_NETFILTER_XT_TARGET_TRACE=y +# CONFIG_NETFILTER_XT_TARGET_SECMARK is not set +CONFIG_NETFILTER_XT_TARGET_TCPMSS=y +CONFIG_NETFILTER_XT_TARGET_TCPOPTSTRIP=y + +# +# Xtables matches +# +CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y +CONFIG_NETFILTER_XT_MATCH_BPF=y +CONFIG_NETFILTER_XT_MATCH_CGROUP=y +CONFIG_NETFILTER_XT_MATCH_CLUSTER=y +CONFIG_NETFILTER_XT_MATCH_COMMENT=y +CONFIG_NETFILTER_XT_MATCH_CONNBYTES=y +CONFIG_NETFILTER_XT_MATCH_CONNLABEL=y +CONFIG_NETFILTER_XT_MATCH_CONNLIMIT=y +CONFIG_NETFILTER_XT_MATCH_CONNMARK=y +CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y +CONFIG_NETFILTER_XT_MATCH_CPU=y +CONFIG_NETFILTER_XT_MATCH_DCCP=y +CONFIG_NETFILTER_XT_MATCH_DEVGROUP=y +CONFIG_NETFILTER_XT_MATCH_DSCP=y +CONFIG_NETFILTER_XT_MATCH_ECN=y +CONFIG_NETFILTER_XT_MATCH_ESP=y +CONFIG_NETFILTER_XT_MATCH_HASHLIMIT=y +CONFIG_NETFILTER_XT_MATCH_HELPER=y +CONFIG_NETFILTER_XT_MATCH_HL=y +CONFIG_NETFILTER_XT_MATCH_IPCOMP=y +CONFIG_NETFILTER_XT_MATCH_IPRANGE=y +# CONFIG_NETFILTER_XT_MATCH_IPVS is not set +CONFIG_NETFILTER_XT_MATCH_L2TP=y +CONFIG_NETFILTER_XT_MATCH_LENGTH=y +CONFIG_NETFILTER_XT_MATCH_LIMIT=y +CONFIG_NETFILTER_XT_MATCH_MAC=y +CONFIG_NETFILTER_XT_MATCH_MARK=y +CONFIG_NETFILTER_XT_MATCH_MULTIPORT=y +CONFIG_NETFILTER_XT_MATCH_NFACCT=y +CONFIG_NETFILTER_XT_MATCH_OSF=y +CONFIG_NETFILTER_XT_MATCH_OWNER=y +CONFIG_NETFILTER_XT_MATCH_POLICY=y +CONFIG_NETFILTER_XT_MATCH_PHYSDEV=y +CONFIG_NETFILTER_XT_MATCH_PKTTYPE=y +CONFIG_NETFILTER_XT_MATCH_QUOTA=y +CONFIG_NETFILTER_XT_MATCH_RATEEST=y +CONFIG_NETFILTER_XT_MATCH_REALM=y +CONFIG_NETFILTER_XT_MATCH_RECENT=y +CONFIG_NETFILTER_XT_MATCH_SCTP=y +CONFIG_NETFILTER_XT_MATCH_SOCKET=y +CONFIG_NETFILTER_XT_MATCH_STATE=y +CONFIG_NETFILTER_XT_MATCH_STATISTIC=y +CONFIG_NETFILTER_XT_MATCH_STRING=y +CONFIG_NETFILTER_XT_MATCH_TCPMSS=y +CONFIG_NETFILTER_XT_MATCH_TIME=y +CONFIG_NETFILTER_XT_MATCH_U32=y +# end of Core Netfilter Configuration + +CONFIG_IP_SET=y +CONFIG_IP_SET_MAX=256 +# CONFIG_IP_SET_BITMAP_IP is not set +# CONFIG_IP_SET_BITMAP_IPMAC is not set +# CONFIG_IP_SET_BITMAP_PORT is not set +CONFIG_IP_SET_HASH_IP=y +# CONFIG_IP_SET_HASH_IPMARK is not set +# CONFIG_IP_SET_HASH_IPPORT is not set +# CONFIG_IP_SET_HASH_IPPORTIP is not set +# CONFIG_IP_SET_HASH_IPPORTNET is not set +# CONFIG_IP_SET_HASH_IPMAC is not set +# CONFIG_IP_SET_HASH_MAC is not set +# CONFIG_IP_SET_HASH_NETPORTNET is not set +# CONFIG_IP_SET_HASH_NET is not set +# CONFIG_IP_SET_HASH_NETNET is not set +# CONFIG_IP_SET_HASH_NETPORT is not set +# CONFIG_IP_SET_HASH_NETIFACE is not set +# CONFIG_IP_SET_LIST_SET is not set +CONFIG_IP_VS=y +# CONFIG_IP_VS_IPV6 is not set +# CONFIG_IP_VS_DEBUG is not set +CONFIG_IP_VS_TAB_BITS=12 + +# +# IPVS transport protocol load balancing support +# +CONFIG_IP_VS_PROTO_TCP=y +CONFIG_IP_VS_PROTO_UDP=y +CONFIG_IP_VS_PROTO_AH_ESP=y +CONFIG_IP_VS_PROTO_ESP=y +CONFIG_IP_VS_PROTO_AH=y +CONFIG_IP_VS_PROTO_SCTP=y + +# +# IPVS scheduler +# +# CONFIG_IP_VS_RR is not set +# CONFIG_IP_VS_WRR is not set +# CONFIG_IP_VS_LC is not set +# CONFIG_IP_VS_WLC is not set +# CONFIG_IP_VS_FO is not set +# CONFIG_IP_VS_OVF is not set +# CONFIG_IP_VS_LBLC is not set +# CONFIG_IP_VS_LBLCR is not set +CONFIG_IP_VS_DH=y +# CONFIG_IP_VS_SH is not set +# CONFIG_IP_VS_MH is not set +# CONFIG_IP_VS_SED is not set +# CONFIG_IP_VS_NQ is not set +# CONFIG_IP_VS_TWOS is not set + +# +# IPVS SH scheduler +# +CONFIG_IP_VS_SH_TAB_BITS=8 + +# +# IPVS MH scheduler +# +CONFIG_IP_VS_MH_TAB_INDEX=12 + +# +# IPVS application helper +# +# CONFIG_IP_VS_NFCT is not set + +# +# IP: Netfilter Configuration +# +CONFIG_NF_DEFRAG_IPV4=y +CONFIG_NF_SOCKET_IPV4=y +CONFIG_NF_TPROXY_IPV4=y +CONFIG_NF_TABLES_IPV4=y +CONFIG_NFT_REJECT_IPV4=y +CONFIG_NFT_DUP_IPV4=y +CONFIG_NFT_FIB_IPV4=y +CONFIG_NF_TABLES_ARP=y +CONFIG_NF_DUP_IPV4=y +CONFIG_NF_LOG_ARP=y +CONFIG_NF_LOG_IPV4=y +CONFIG_NF_REJECT_IPV4=y +CONFIG_IP_NF_IPTABLES=y +CONFIG_IP_NF_MATCH_AH=y +CONFIG_IP_NF_MATCH_ECN=y +CONFIG_IP_NF_MATCH_RPFILTER=y +CONFIG_IP_NF_MATCH_TTL=y +CONFIG_IP_NF_FILTER=y +CONFIG_IP_NF_TARGET_REJECT=y +CONFIG_IP_NF_TARGET_SYNPROXY=y +CONFIG_IP_NF_NAT=y +CONFIG_IP_NF_TARGET_MASQUERADE=y +CONFIG_IP_NF_TARGET_NETMAP=y +CONFIG_IP_NF_TARGET_REDIRECT=y +CONFIG_IP_NF_MANGLE=y +CONFIG_IP_NF_TARGET_CLUSTERIP=y +CONFIG_IP_NF_TARGET_ECN=y +CONFIG_IP_NF_TARGET_TTL=y +CONFIG_IP_NF_RAW=y +CONFIG_IP_NF_ARPTABLES=y +CONFIG_IP_NF_ARPFILTER=y +CONFIG_IP_NF_ARP_MANGLE=y +# end of IP: Netfilter Configuration + +# +# IPv6: Netfilter Configuration +# +CONFIG_NF_SOCKET_IPV6=y +CONFIG_NF_TPROXY_IPV6=y +CONFIG_NF_TABLES_IPV6=y +CONFIG_NFT_REJECT_IPV6=y +CONFIG_NFT_DUP_IPV6=y +CONFIG_NFT_FIB_IPV6=y +CONFIG_NF_DUP_IPV6=y +CONFIG_NF_REJECT_IPV6=y +CONFIG_NF_LOG_IPV6=y +CONFIG_IP6_NF_IPTABLES=y +CONFIG_IP6_NF_MATCH_AH=y +CONFIG_IP6_NF_MATCH_EUI64=y +CONFIG_IP6_NF_MATCH_FRAG=y +CONFIG_IP6_NF_MATCH_OPTS=y +CONFIG_IP6_NF_MATCH_HL=y +CONFIG_IP6_NF_MATCH_IPV6HEADER=y +CONFIG_IP6_NF_MATCH_MH=y +CONFIG_IP6_NF_MATCH_RPFILTER=y +CONFIG_IP6_NF_MATCH_RT=y +CONFIG_IP6_NF_MATCH_SRH=y +CONFIG_IP6_NF_TARGET_HL=y +CONFIG_IP6_NF_FILTER=y +CONFIG_IP6_NF_TARGET_REJECT=y +CONFIG_IP6_NF_TARGET_SYNPROXY=y +CONFIG_IP6_NF_MANGLE=y +CONFIG_IP6_NF_RAW=y +CONFIG_IP6_NF_NAT=y +CONFIG_IP6_NF_TARGET_MASQUERADE=y +CONFIG_IP6_NF_TARGET_NPT=y +# end of IPv6: Netfilter Configuration + +CONFIG_NF_DEFRAG_IPV6=y +CONFIG_NF_TABLES_BRIDGE=y +# CONFIG_NFT_BRIDGE_META is not set +# CONFIG_NFT_BRIDGE_REJECT is not set +CONFIG_NF_CONNTRACK_BRIDGE=y +CONFIG_BRIDGE_NF_EBTABLES=y +CONFIG_BRIDGE_EBT_BROUTE=y +CONFIG_BRIDGE_EBT_T_FILTER=y +CONFIG_BRIDGE_EBT_T_NAT=y +CONFIG_BRIDGE_EBT_802_3=y +CONFIG_BRIDGE_EBT_AMONG=y +CONFIG_BRIDGE_EBT_ARP=y +CONFIG_BRIDGE_EBT_IP=y +CONFIG_BRIDGE_EBT_IP6=y +CONFIG_BRIDGE_EBT_LIMIT=y +CONFIG_BRIDGE_EBT_MARK=y +CONFIG_BRIDGE_EBT_PKTTYPE=y +CONFIG_BRIDGE_EBT_STP=y +CONFIG_BRIDGE_EBT_VLAN=y +CONFIG_BRIDGE_EBT_ARPREPLY=y +CONFIG_BRIDGE_EBT_DNAT=y +CONFIG_BRIDGE_EBT_MARK_T=y +CONFIG_BRIDGE_EBT_REDIRECT=y +CONFIG_BRIDGE_EBT_SNAT=y +CONFIG_BRIDGE_EBT_LOG=y +CONFIG_BRIDGE_EBT_NFLOG=y +CONFIG_BPFILTER=y +CONFIG_BPFILTER_UMH=y +# CONFIG_IP_DCCP is not set +CONFIG_IP_SCTP=y +# CONFIG_SCTP_DBG_OBJCNT is not set +CONFIG_SCTP_DEFAULT_COOKIE_HMAC_MD5=y +# CONFIG_SCTP_DEFAULT_COOKIE_HMAC_SHA1 is not set +# CONFIG_SCTP_DEFAULT_COOKIE_HMAC_NONE is not set +CONFIG_SCTP_COOKIE_HMAC_MD5=y +# CONFIG_SCTP_COOKIE_HMAC_SHA1 is not set +CONFIG_INET_SCTP_DIAG=y +# CONFIG_RDS is not set +# CONFIG_TIPC is not set +# CONFIG_ATM is not set +# CONFIG_L2TP is not set +CONFIG_STP=y +CONFIG_BRIDGE=y +CONFIG_BRIDGE_IGMP_SNOOPING=y +# CONFIG_BRIDGE_VLAN_FILTERING is not set +# CONFIG_BRIDGE_MRP is not set +# CONFIG_BRIDGE_CFM is not set +# CONFIG_NET_DSA is not set +CONFIG_VLAN_8021Q=y +# CONFIG_VLAN_8021Q_GVRP is not set +# CONFIG_VLAN_8021Q_MVRP is not set +CONFIG_LLC=y +# CONFIG_LLC2 is not set +# CONFIG_ATALK is not set +# CONFIG_X25 is not set +# CONFIG_LAPB is not set +# CONFIG_PHONET is not set +# CONFIG_6LOWPAN is not set +# CONFIG_IEEE802154 is not set +CONFIG_NET_SCHED=y + +# +# Queueing/Scheduling +# +CONFIG_NET_SCH_CBQ=y +CONFIG_NET_SCH_HTB=y +CONFIG_NET_SCH_HFSC=y +CONFIG_NET_SCH_PRIO=y +CONFIG_NET_SCH_MULTIQ=y +CONFIG_NET_SCH_RED=y +CONFIG_NET_SCH_SFB=y +CONFIG_NET_SCH_SFQ=y +CONFIG_NET_SCH_TEQL=y +CONFIG_NET_SCH_TBF=y +CONFIG_NET_SCH_CBS=y +CONFIG_NET_SCH_ETF=y +CONFIG_NET_SCH_TAPRIO=y +CONFIG_NET_SCH_GRED=y +CONFIG_NET_SCH_DSMARK=y +CONFIG_NET_SCH_NETEM=y +CONFIG_NET_SCH_DRR=y +CONFIG_NET_SCH_MQPRIO=y +CONFIG_NET_SCH_SKBPRIO=y +CONFIG_NET_SCH_CHOKE=y +CONFIG_NET_SCH_QFQ=y +CONFIG_NET_SCH_CODEL=y +CONFIG_NET_SCH_FQ_CODEL=y +CONFIG_NET_SCH_CAKE=y +CONFIG_NET_SCH_FQ=y +CONFIG_NET_SCH_HHF=y +CONFIG_NET_SCH_PIE=y +CONFIG_NET_SCH_FQ_PIE=y +CONFIG_NET_SCH_INGRESS=y +CONFIG_NET_SCH_PLUG=y +CONFIG_NET_SCH_ETS=y +# CONFIG_NET_SCH_DEFAULT is not set + +# +# Classification +# +CONFIG_NET_CLS=y +CONFIG_NET_CLS_BASIC=y +CONFIG_NET_CLS_ROUTE4=y +CONFIG_NET_CLS_FW=y +CONFIG_NET_CLS_U32=y +CONFIG_CLS_U32_PERF=y +CONFIG_CLS_U32_MARK=y +CONFIG_NET_CLS_FLOW=y +CONFIG_NET_CLS_CGROUP=y +CONFIG_NET_CLS_BPF=y +CONFIG_NET_CLS_FLOWER=y +CONFIG_NET_CLS_MATCHALL=y +CONFIG_NET_EMATCH=y +CONFIG_NET_EMATCH_STACK=32 +CONFIG_NET_EMATCH_CMP=y +CONFIG_NET_EMATCH_NBYTE=y +CONFIG_NET_EMATCH_U32=y +CONFIG_NET_EMATCH_META=y +CONFIG_NET_EMATCH_TEXT=y +CONFIG_NET_EMATCH_IPT=y +CONFIG_NET_CLS_ACT=y +CONFIG_NET_ACT_POLICE=y +CONFIG_NET_ACT_GACT=y +CONFIG_GACT_PROB=y +CONFIG_NET_ACT_MIRRED=y +CONFIG_NET_ACT_SAMPLE=y +CONFIG_NET_ACT_IPT=y +CONFIG_NET_ACT_NAT=y +CONFIG_NET_ACT_PEDIT=y +CONFIG_NET_ACT_SIMP=y +CONFIG_NET_ACT_SKBEDIT=y +CONFIG_NET_ACT_CSUM=y +CONFIG_NET_ACT_MPLS=y +CONFIG_NET_ACT_VLAN=y +CONFIG_NET_ACT_BPF=y +CONFIG_NET_ACT_CONNMARK=y +CONFIG_NET_ACT_CTINFO=y +CONFIG_NET_ACT_SKBMOD=y +CONFIG_NET_ACT_IFE=y +CONFIG_NET_ACT_TUNNEL_KEY=y +CONFIG_NET_ACT_GATE=y +CONFIG_NET_IFE_SKBMARK=y +CONFIG_NET_IFE_SKBPRIO=y +CONFIG_NET_IFE_SKBTCINDEX=y +CONFIG_NET_TC_SKB_EXT=y +CONFIG_NET_SCH_FIFO=y +# CONFIG_DCB is not set +CONFIG_DNS_RESOLVER=y +# CONFIG_BATMAN_ADV is not set +# CONFIG_OPENVSWITCH is not set +CONFIG_VSOCKETS=y +CONFIG_VSOCKETS_DIAG=y +CONFIG_VSOCKETS_LOOPBACK=y +CONFIG_VIRTIO_VSOCKETS=y +CONFIG_VIRTIO_VSOCKETS_COMMON=y +CONFIG_NETLINK_DIAG=y +# CONFIG_MPLS is not set +# CONFIG_NET_NSH is not set +# CONFIG_HSR is not set +# CONFIG_NET_SWITCHDEV is not set +CONFIG_NET_L3_MASTER_DEV=y +# CONFIG_QRTR is not set +# CONFIG_NET_NCSI is not set +CONFIG_PCPU_DEV_REFCNT=y +CONFIG_RPS=y +CONFIG_RFS_ACCEL=y +CONFIG_SOCK_RX_QUEUE_MAPPING=y +CONFIG_XPS=y +CONFIG_CGROUP_NET_PRIO=y +CONFIG_CGROUP_NET_CLASSID=y +CONFIG_NET_RX_BUSY_POLL=y +CONFIG_BQL=y +# CONFIG_BPF_STREAM_PARSER is not set +CONFIG_NET_FLOW_LIMIT=y + +# +# Network testing +# +# CONFIG_NET_PKTGEN is not set +# end of Network testing +# end of Networking options + +# CONFIG_HAMRADIO is not set +# CONFIG_CAN is not set +# CONFIG_BT is not set +# CONFIG_AF_RXRPC is not set +# CONFIG_AF_KCM is not set +CONFIG_STREAM_PARSER=y +# CONFIG_MCTP is not set +CONFIG_FIB_RULES=y +# CONFIG_WIRELESS is not set +# CONFIG_RFKILL is not set +# CONFIG_NET_9P is not set +# CONFIG_CAIF is not set +# CONFIG_CEPH_LIB is not set +# CONFIG_NFC is not set +CONFIG_PSAMPLE=y +CONFIG_NET_IFE=y +CONFIG_LWTUNNEL=y +CONFIG_LWTUNNEL_BPF=y +CONFIG_DST_CACHE=y +CONFIG_GRO_CELLS=y +CONFIG_NET_SOCK_MSG=y +CONFIG_PAGE_POOL=y +# CONFIG_PAGE_POOL_STATS is not set +CONFIG_FAILOVER=y +# CONFIG_ETHTOOL_NETLINK is not set + +# +# Device Drivers +# +CONFIG_HAVE_EISA=y +# CONFIG_EISA is not set +CONFIG_HAVE_PCI=y +CONFIG_PCI=y +CONFIG_PCI_DOMAINS=y +CONFIG_PCIEPORTBUS=y +# CONFIG_HOTPLUG_PCI_PCIE is not set +CONFIG_PCIEAER=y +# CONFIG_PCIEAER_INJECT is not set +# CONFIG_PCIE_ECRC is not set +CONFIG_PCIEASPM=y +CONFIG_PCIEASPM_DEFAULT=y +# CONFIG_PCIEASPM_POWERSAVE is not set +# CONFIG_PCIEASPM_POWER_SUPERSAVE is not set +# CONFIG_PCIEASPM_PERFORMANCE is not set +CONFIG_PCIE_PME=y +# CONFIG_PCIE_DPC is not set +# CONFIG_PCIE_PTM is not set +CONFIG_PCI_MSI=y +CONFIG_PCI_MSI_IRQ_DOMAIN=y +CONFIG_PCI_QUIRKS=y +CONFIG_PCI_DEBUG=y +CONFIG_PCI_STUB=y +CONFIG_PCI_LOCKLESS_CONFIG=y +# CONFIG_PCI_IOV is not set +# CONFIG_PCI_PRI is not set +# CONFIG_PCI_PASID is not set +# CONFIG_PCI_P2PDMA is not set +CONFIG_PCI_LABEL=y +# CONFIG_PCIE_BUS_TUNE_OFF is not set +CONFIG_PCIE_BUS_DEFAULT=y +# CONFIG_PCIE_BUS_SAFE is not set +# CONFIG_PCIE_BUS_PERFORMANCE is not set +# CONFIG_PCIE_BUS_PEER2PEER is not set +CONFIG_VGA_ARB=y +CONFIG_VGA_ARB_MAX_GPUS=16 +CONFIG_HOTPLUG_PCI=y +CONFIG_HOTPLUG_PCI_ACPI=y +# CONFIG_HOTPLUG_PCI_ACPI_IBM is not set +# CONFIG_HOTPLUG_PCI_CPCI is not set +# CONFIG_HOTPLUG_PCI_SHPC is not set + +# +# PCI controller drivers +# +# CONFIG_VMD is not set + +# +# DesignWare PCI Core Support +# +# CONFIG_PCIE_DW_PLAT_HOST is not set +# CONFIG_PCI_MESON is not set +# end of DesignWare PCI Core Support + +# +# Mobiveil PCIe Core Support +# +# end of Mobiveil PCIe Core Support + +# +# Cadence PCIe controllers support +# +# end of Cadence PCIe controllers support +# end of PCI controller drivers + +# +# PCI Endpoint +# +# CONFIG_PCI_ENDPOINT is not set +# end of PCI Endpoint + +# +# PCI switch controller drivers +# +# CONFIG_PCI_SW_SWITCHTEC is not set +# end of PCI switch controller drivers + +# CONFIG_CXL_BUS is not set +# CONFIG_PCCARD is not set +# CONFIG_RAPIDIO is not set + +# +# Generic Driver Options +# +CONFIG_UEVENT_HELPER=y +CONFIG_UEVENT_HELPER_PATH="/sbin/hotplug" +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +# CONFIG_DEVTMPFS_SAFE is not set +CONFIG_STANDALONE=y +CONFIG_PREVENT_FIRMWARE_BUILD=y + +# +# Firmware loader +# +CONFIG_FW_LOADER=y +CONFIG_FW_LOADER_PAGED_BUF=y +CONFIG_FW_LOADER_SYSFS=y +CONFIG_EXTRA_FIRMWARE="" +CONFIG_FW_LOADER_USER_HELPER=y +# CONFIG_FW_LOADER_USER_HELPER_FALLBACK is not set +# CONFIG_FW_LOADER_COMPRESS is not set +# CONFIG_FW_UPLOAD is not set +# end of Firmware loader + +CONFIG_ALLOW_DEV_COREDUMP=y +# CONFIG_DEBUG_DRIVER is not set +# CONFIG_DEBUG_DEVRES is not set +# CONFIG_DEBUG_TEST_DRIVER_REMOVE is not set +CONFIG_GENERIC_CPU_AUTOPROBE=y +CONFIG_GENERIC_CPU_VULNERABILITIES=y +# end of Generic Driver Options + +# +# Bus devices +# +# CONFIG_MHI_BUS is not set +# CONFIG_MHI_BUS_EP is not set +# end of Bus devices + +CONFIG_CONNECTOR=y +CONFIG_PROC_EVENTS=y + +# +# Firmware Drivers +# + +# +# ARM System Control and Management Interface Protocol +# +# end of ARM System Control and Management Interface Protocol + +# CONFIG_EDD is not set +CONFIG_FIRMWARE_MEMMAP=y +CONFIG_DMIID=y +CONFIG_DMI_SYSFS=y +CONFIG_DMI_SCAN_MACHINE_NON_EFI_FALLBACK=y +# CONFIG_FW_CFG_SYSFS is not set +# CONFIG_SYSFB_SIMPLEFB is not set +# CONFIG_GOOGLE_FIRMWARE is not set + +# +# EFI (Extensible Firmware Interface) Support +# +CONFIG_EFI_ESRT=y +CONFIG_EFI_RUNTIME_MAP=y +# CONFIG_EFI_FAKE_MEMMAP is not set +CONFIG_EFI_DXE_MEM_ATTRIBUTES=y +CONFIG_EFI_RUNTIME_WRAPPERS=y +CONFIG_EFI_GENERIC_STUB_INITRD_CMDLINE_LOADER=y +# CONFIG_EFI_BOOTLOADER_CONTROL is not set +# CONFIG_EFI_CAPSULE_LOADER is not set +# CONFIG_EFI_TEST is not set +# CONFIG_APPLE_PROPERTIES is not set +# CONFIG_RESET_ATTACK_MITIGATION is not set +# CONFIG_EFI_RCI2_TABLE is not set +# CONFIG_EFI_DISABLE_PCI_DMA is not set +CONFIG_EFI_EARLYCON=y +# CONFIG_EFI_CUSTOM_SSDT_OVERLAYS is not set +# CONFIG_EFI_DISABLE_RUNTIME is not set +# CONFIG_EFI_COCO_SECRET is not set +# end of EFI (Extensible Firmware Interface) Support + +# +# Tegra firmware driver +# +# end of Tegra firmware driver +# end of Firmware Drivers + +# CONFIG_GNSS is not set +# CONFIG_MTD is not set +# CONFIG_OF is not set +CONFIG_ARCH_MIGHT_HAVE_PC_PARPORT=y +# CONFIG_PARPORT is not set +CONFIG_PNP=y +# CONFIG_PNP_DEBUG_MESSAGES is not set + +# +# Protocols +# +CONFIG_PNPACPI=y +CONFIG_BLK_DEV=y +CONFIG_BLK_DEV_NULL_BLK=y +# CONFIG_BLK_DEV_PCIESSD_MTIP32XX is not set +CONFIG_ZRAM=y +CONFIG_ZRAM_DEF_COMP_LZORLE=y +# CONFIG_ZRAM_DEF_COMP_LZO is not set +CONFIG_ZRAM_DEF_COMP="lzo-rle" +# CONFIG_ZRAM_WRITEBACK is not set +# CONFIG_ZRAM_MEMORY_TRACKING is not set +CONFIG_BLK_DEV_LOOP=y +CONFIG_BLK_DEV_LOOP_MIN_COUNT=8 +# CONFIG_BLK_DEV_DRBD is not set +# CONFIG_BLK_DEV_NBD is not set +CONFIG_BLK_DEV_RAM=y +CONFIG_BLK_DEV_RAM_COUNT=16 +CONFIG_BLK_DEV_RAM_SIZE=16384 +# CONFIG_ATA_OVER_ETH is not set +CONFIG_VIRTIO_BLK=y +# CONFIG_BLK_DEV_RBD is not set +# CONFIG_BLK_DEV_UBLK is not set + +# +# NVME Support +# +# CONFIG_BLK_DEV_NVME is not set +# CONFIG_NVME_FC is not set +# CONFIG_NVME_TCP is not set +# CONFIG_NVME_TARGET is not set +# end of NVME Support + +# +# Misc devices +# +# CONFIG_DUMMY_IRQ is not set +# CONFIG_IBM_ASM is not set +# CONFIG_PHANTOM is not set +# CONFIG_TIFM_CORE is not set +# CONFIG_ENCLOSURE_SERVICES is not set +# CONFIG_HP_ILO is not set +# CONFIG_SRAM is not set +# CONFIG_DW_XDATA_PCIE is not set +# CONFIG_PCI_ENDPOINT_TEST is not set +# CONFIG_XILINX_SDFEC is not set +# CONFIG_C2PORT is not set + +# +# EEPROM support +# +# CONFIG_EEPROM_93CX6 is not set +# end of EEPROM support + +# CONFIG_CB710_CORE is not set + +# +# Texas Instruments shared transport line discipline +# +# end of Texas Instruments shared transport line discipline + +# +# Altera FPGA firmware download module (requires I2C) +# +# CONFIG_INTEL_MEI is not set +# CONFIG_INTEL_MEI_ME is not set +# CONFIG_INTEL_MEI_TXE is not set +# CONFIG_VMWARE_VMCI is not set +# CONFIG_GENWQE is not set +# CONFIG_ECHO is not set +# CONFIG_BCM_VK is not set +# CONFIG_MISC_ALCOR_PCI is not set +# CONFIG_MISC_RTSX_PCI is not set +# CONFIG_HABANA_AI is not set +# CONFIG_UACCE is not set +# CONFIG_PVPANIC is not set +# end of Misc devices + +# +# SCSI device support +# +CONFIG_SCSI_MOD=y +# CONFIG_RAID_ATTRS is not set +# CONFIG_SCSI is not set +# end of SCSI device support + +# CONFIG_ATA is not set +# CONFIG_MD is not set +# CONFIG_TARGET_CORE is not set +# CONFIG_FUSION is not set + +# +# IEEE 1394 (FireWire) support +# +# CONFIG_FIREWIRE is not set +# CONFIG_FIREWIRE_NOSY is not set +# end of IEEE 1394 (FireWire) support + +# CONFIG_MACINTOSH_DRIVERS is not set +CONFIG_NETDEVICES=y +CONFIG_NET_CORE=y +# CONFIG_BONDING is not set +# CONFIG_DUMMY is not set +CONFIG_WIREGUARD=y +# CONFIG_WIREGUARD_DEBUG is not set +# CONFIG_EQUALIZER is not set +# CONFIG_IFB is not set +# CONFIG_NET_TEAM is not set +CONFIG_MACVLAN=y +# CONFIG_MACVTAP is not set +CONFIG_IPVLAN_L3S=y +CONFIG_IPVLAN=y +# CONFIG_IPVTAP is not set +CONFIG_VXLAN=y +CONFIG_GENEVE=y +# CONFIG_BAREUDP is not set +# CONFIG_GTP is not set +# CONFIG_MACSEC is not set +# CONFIG_NETCONSOLE is not set +CONFIG_TUN=y +# CONFIG_TUN_VNET_CROSS_LE is not set +CONFIG_VETH=y +CONFIG_VIRTIO_NET=y +# CONFIG_NLMON is not set +# CONFIG_NET_VRF is not set +# CONFIG_ARCNET is not set +# CONFIG_ETHERNET is not set +# CONFIG_FDDI is not set +# CONFIG_HIPPI is not set +# CONFIG_NET_SB1000 is not set +# CONFIG_PHYLIB is not set +# CONFIG_PSE_CONTROLLER is not set +# CONFIG_MDIO_DEVICE is not set + +# +# PCS device drivers +# +# end of PCS device drivers + +# CONFIG_PPP is not set +# CONFIG_SLIP is not set + +# +# Host-side USB support is needed for USB Network Adapter support +# +# CONFIG_WLAN is not set +# CONFIG_WAN is not set + +# +# Wireless WAN +# +# CONFIG_WWAN is not set +# end of Wireless WAN + +# CONFIG_VMXNET3 is not set +# CONFIG_FUJITSU_ES is not set +# CONFIG_NETDEVSIM is not set +CONFIG_NET_FAILOVER=y +# CONFIG_ISDN is not set + +# +# Input device support +# +CONFIG_INPUT=y +CONFIG_INPUT_FF_MEMLESS=y +CONFIG_INPUT_SPARSEKMAP=y +# CONFIG_INPUT_MATRIXKMAP is not set + +# +# Userland interfaces +# +CONFIG_INPUT_MOUSEDEV=y +CONFIG_INPUT_MOUSEDEV_PSAUX=y +CONFIG_INPUT_MOUSEDEV_SCREEN_X=1024 +CONFIG_INPUT_MOUSEDEV_SCREEN_Y=768 +# CONFIG_INPUT_JOYDEV is not set +CONFIG_INPUT_EVDEV=y +# CONFIG_INPUT_EVBUG is not set + +# +# Input Device Drivers +# +CONFIG_INPUT_KEYBOARD=y +# CONFIG_KEYBOARD_ATKBD is not set +# CONFIG_KEYBOARD_LKKBD is not set +# CONFIG_KEYBOARD_NEWTON is not set +# CONFIG_KEYBOARD_OPENCORES is not set +# CONFIG_KEYBOARD_SAMSUNG is not set +# CONFIG_KEYBOARD_STOWAWAY is not set +# CONFIG_KEYBOARD_SUNKBD is not set +# CONFIG_KEYBOARD_XTKBD is not set +# CONFIG_INPUT_MOUSE is not set +# CONFIG_INPUT_JOYSTICK is not set +# CONFIG_INPUT_TABLET is not set +# CONFIG_INPUT_TOUCHSCREEN is not set +CONFIG_INPUT_MISC=y +# CONFIG_INPUT_AD714X is not set +# CONFIG_INPUT_E3X0_BUTTON is not set +# CONFIG_INPUT_PCSPKR is not set +# CONFIG_INPUT_ATLAS_BTNS is not set +CONFIG_INPUT_UINPUT=y +# CONFIG_INPUT_ADXL34X is not set +# CONFIG_INPUT_CMA3000 is not set +# CONFIG_RMI4_CORE is not set + +# +# Hardware I/O ports +# +# CONFIG_SERIO is not set +CONFIG_ARCH_MIGHT_HAVE_PC_SERIO=y +# CONFIG_GAMEPORT is not set +# end of Hardware I/O ports +# end of Input device support + +# +# Character devices +# +CONFIG_TTY=y +CONFIG_VT=y +CONFIG_CONSOLE_TRANSLATIONS=y +CONFIG_VT_CONSOLE=y +CONFIG_HW_CONSOLE=y +CONFIG_VT_HW_CONSOLE_BINDING=y +CONFIG_UNIX98_PTYS=y +# CONFIG_LEGACY_PTYS is not set +# CONFIG_LDISC_AUTOLOAD is not set + +# +# Serial drivers +# +CONFIG_SERIAL_EARLYCON=y +CONFIG_SERIAL_8250=y +# CONFIG_SERIAL_8250_DEPRECATED_OPTIONS is not set +CONFIG_SERIAL_8250_PNP=y +# CONFIG_SERIAL_8250_16550A_VARIANTS is not set +# CONFIG_SERIAL_8250_FINTEK is not set +CONFIG_SERIAL_8250_CONSOLE=y +CONFIG_SERIAL_8250_DMA=y +CONFIG_SERIAL_8250_PCI=y +CONFIG_SERIAL_8250_EXAR=y +CONFIG_SERIAL_8250_NR_UARTS=1 +CONFIG_SERIAL_8250_RUNTIME_UARTS=1 +# CONFIG_SERIAL_8250_EXTENDED is not set +CONFIG_SERIAL_8250_DWLIB=y +# CONFIG_SERIAL_8250_DW is not set +# CONFIG_SERIAL_8250_RT288X is not set +CONFIG_SERIAL_8250_LPSS=y +CONFIG_SERIAL_8250_MID=y +CONFIG_SERIAL_8250_PERICOM=y + +# +# Non-8250 serial port support +# +# CONFIG_SERIAL_UARTLITE is not set +CONFIG_SERIAL_CORE=y +CONFIG_SERIAL_CORE_CONSOLE=y +# CONFIG_SERIAL_JSM is not set +# CONFIG_SERIAL_LANTIQ is not set +# CONFIG_SERIAL_SCCNXP is not set +# CONFIG_SERIAL_ALTERA_JTAGUART is not set +# CONFIG_SERIAL_ALTERA_UART is not set +CONFIG_SERIAL_ARC=y +# CONFIG_SERIAL_ARC_CONSOLE is not set +CONFIG_SERIAL_ARC_NR_PORTS=1 +# CONFIG_SERIAL_RP2 is not set +# CONFIG_SERIAL_FSL_LPUART is not set +# CONFIG_SERIAL_FSL_LINFLEXUART is not set +# CONFIG_SERIAL_SPRD is not set +# end of Serial drivers + +# CONFIG_SERIAL_NONSTANDARD is not set +# CONFIG_N_GSM is not set +# CONFIG_NOZOMI is not set +# CONFIG_NULL_TTY is not set +CONFIG_HVC_DRIVER=y +CONFIG_SERIAL_DEV_BUS=y +CONFIG_SERIAL_DEV_CTRL_TTYPORT=y +# CONFIG_TTY_PRINTK is not set +CONFIG_VIRTIO_CONSOLE=y +# CONFIG_IPMI_HANDLER is not set +CONFIG_HW_RANDOM=y +# CONFIG_HW_RANDOM_TIMERIOMEM is not set +CONFIG_HW_RANDOM_INTEL=y +CONFIG_HW_RANDOM_AMD=y +# CONFIG_HW_RANDOM_BA431 is not set +CONFIG_HW_RANDOM_VIA=y +CONFIG_HW_RANDOM_VIRTIO=y +# CONFIG_HW_RANDOM_XIPHERA is not set +# CONFIG_APPLICOM is not set +# CONFIG_MWAVE is not set +CONFIG_DEVMEM=y +CONFIG_NVRAM=y +CONFIG_DEVPORT=y +# CONFIG_HPET is not set +CONFIG_HANGCHECK_TIMER=y +# CONFIG_TCG_TPM is not set +# CONFIG_TELCLOCK is not set +# CONFIG_XILLYBUS is not set +# CONFIG_RANDOM_TRUST_CPU is not set +# CONFIG_RANDOM_TRUST_BOOTLOADER is not set +# end of Character devices + +# +# I2C support +# +# CONFIG_I2C is not set +# end of I2C support + +# CONFIG_I3C is not set +# CONFIG_SPI is not set +# CONFIG_SPMI is not set +# CONFIG_HSI is not set +CONFIG_PPS=y +CONFIG_PPS_DEBUG=y + +# +# PPS clients support +# +CONFIG_PPS_CLIENT_KTIMER=y +CONFIG_PPS_CLIENT_LDISC=y +# CONFIG_PPS_CLIENT_GPIO is not set + +# +# PPS generators support +# + +# +# PTP clock support +# +CONFIG_PTP_1588_CLOCK=y +CONFIG_PTP_1588_CLOCK_OPTIONAL=y + +# +# Enable PHYLIB and NETWORK_PHY_TIMESTAMPING to see the additional clocks. +# +CONFIG_PTP_1588_CLOCK_KVM=y +# end of PTP clock support + +CONFIG_PINCTRL=y +# CONFIG_DEBUG_PINCTRL is not set +# CONFIG_PINCTRL_AMD is not set + +# +# Intel pinctrl drivers +# +# CONFIG_PINCTRL_BAYTRAIL is not set +# CONFIG_PINCTRL_CHERRYVIEW is not set +# CONFIG_PINCTRL_LYNXPOINT is not set +# CONFIG_PINCTRL_ALDERLAKE is not set +# CONFIG_PINCTRL_BROXTON is not set +# CONFIG_PINCTRL_CANNONLAKE is not set +# CONFIG_PINCTRL_CEDARFORK is not set +# CONFIG_PINCTRL_DENVERTON is not set +# CONFIG_PINCTRL_ELKHARTLAKE is not set +# CONFIG_PINCTRL_EMMITSBURG is not set +# CONFIG_PINCTRL_GEMINILAKE is not set +# CONFIG_PINCTRL_ICELAKE is not set +# CONFIG_PINCTRL_JASPERLAKE is not set +# CONFIG_PINCTRL_LAKEFIELD is not set +# CONFIG_PINCTRL_LEWISBURG is not set +# CONFIG_PINCTRL_METEORLAKE is not set +# CONFIG_PINCTRL_SUNRISEPOINT is not set +# CONFIG_PINCTRL_TIGERLAKE is not set +# end of Intel pinctrl drivers + +# +# Renesas pinctrl drivers +# +# end of Renesas pinctrl drivers + +# CONFIG_GPIOLIB is not set +# CONFIG_W1 is not set +CONFIG_POWER_RESET=y +CONFIG_POWER_SUPPLY=y +# CONFIG_POWER_SUPPLY_DEBUG is not set +# CONFIG_HWMON is not set +CONFIG_THERMAL=y +# CONFIG_THERMAL_NETLINK is not set +# CONFIG_THERMAL_STATISTICS is not set +CONFIG_THERMAL_EMERGENCY_POWEROFF_DELAY_MS=0 +CONFIG_THERMAL_WRITABLE_TRIPS=y +CONFIG_THERMAL_DEFAULT_GOV_STEP_WISE=y +# CONFIG_THERMAL_DEFAULT_GOV_FAIR_SHARE is not set +# CONFIG_THERMAL_DEFAULT_GOV_USER_SPACE is not set +CONFIG_THERMAL_GOV_FAIR_SHARE=y +CONFIG_THERMAL_GOV_STEP_WISE=y +# CONFIG_THERMAL_GOV_BANG_BANG is not set +CONFIG_THERMAL_GOV_USER_SPACE=y +# CONFIG_THERMAL_EMULATION is not set + +# +# Intel thermal drivers +# +# CONFIG_INTEL_POWERCLAMP is not set +CONFIG_X86_THERMAL_VECTOR=y +# CONFIG_X86_PKG_TEMP_THERMAL is not set +# CONFIG_INTEL_SOC_DTS_THERMAL is not set + +# +# ACPI INT340X thermal drivers +# +# CONFIG_INT340X_THERMAL is not set +# end of ACPI INT340X thermal drivers + +# CONFIG_INTEL_PCH_THERMAL is not set +# CONFIG_INTEL_TCC_COOLING is not set +# CONFIG_INTEL_MENLOW is not set +# CONFIG_INTEL_HFI_THERMAL is not set +# end of Intel thermal drivers + +CONFIG_WATCHDOG=y +CONFIG_WATCHDOG_CORE=y +# CONFIG_WATCHDOG_NOWAYOUT is not set +CONFIG_WATCHDOG_HANDLE_BOOT_ENABLED=y +CONFIG_WATCHDOG_OPEN_TIMEOUT=0 +# CONFIG_WATCHDOG_SYSFS is not set +# CONFIG_WATCHDOG_HRTIMER_PRETIMEOUT is not set + +# +# Watchdog Pretimeout Governors +# +# CONFIG_WATCHDOG_PRETIMEOUT_GOV is not set + +# +# Watchdog Device Drivers +# +# CONFIG_SOFT_WATCHDOG is not set +# CONFIG_WDAT_WDT is not set +# CONFIG_XILINX_WATCHDOG is not set +# CONFIG_CADENCE_WATCHDOG is not set +# CONFIG_DW_WATCHDOG is not set +# CONFIG_MAX63XX_WATCHDOG is not set +# CONFIG_ACQUIRE_WDT is not set +# CONFIG_ADVANTECH_WDT is not set +# CONFIG_ALIM1535_WDT is not set +# CONFIG_ALIM7101_WDT is not set +# CONFIG_EBC_C384_WDT is not set +# CONFIG_EXAR_WDT is not set +# CONFIG_F71808E_WDT is not set +# CONFIG_SP5100_TCO is not set +# CONFIG_SBC_FITPC2_WATCHDOG is not set +# CONFIG_EUROTECH_WDT is not set +# CONFIG_IB700_WDT is not set +# CONFIG_IBMASR is not set +# CONFIG_WAFER_WDT is not set +# CONFIG_I6300ESB_WDT is not set +# CONFIG_IE6XX_WDT is not set +# CONFIG_ITCO_WDT is not set +# CONFIG_IT8712F_WDT is not set +# CONFIG_IT87_WDT is not set +# CONFIG_HP_WATCHDOG is not set +# CONFIG_SC1200_WDT is not set +# CONFIG_PC87413_WDT is not set +# CONFIG_NV_TCO is not set +# CONFIG_60XX_WDT is not set +# CONFIG_CPU5_WDT is not set +# CONFIG_SMSC_SCH311X_WDT is not set +# CONFIG_SMSC37B787_WDT is not set +# CONFIG_TQMX86_WDT is not set +# CONFIG_VIA_WDT is not set +# CONFIG_W83627HF_WDT is not set +# CONFIG_W83877F_WDT is not set +# CONFIG_W83977F_WDT is not set +# CONFIG_MACHZ_WDT is not set +# CONFIG_SBC_EPX_C3_WATCHDOG is not set +# CONFIG_NI903X_WDT is not set +# CONFIG_NIC7018_WDT is not set + +# +# PCI-based Watchdog Cards +# +# CONFIG_PCIPCWATCHDOG is not set +# CONFIG_WDTPCI is not set +CONFIG_SSB_POSSIBLE=y +# CONFIG_SSB is not set +CONFIG_BCMA_POSSIBLE=y +# CONFIG_BCMA is not set + +# +# Multifunction device drivers +# +# CONFIG_MFD_MADERA is not set +# CONFIG_HTC_PASIC3 is not set +# CONFIG_MFD_INTEL_QUARK_I2C_GPIO is not set +# CONFIG_LPC_ICH is not set +# CONFIG_LPC_SCH is not set +# CONFIG_MFD_INTEL_LPSS_ACPI is not set +# CONFIG_MFD_INTEL_LPSS_PCI is not set +# CONFIG_MFD_JANZ_CMODIO is not set +# CONFIG_MFD_KEMPLD is not set +# CONFIG_MFD_MT6397 is not set +# CONFIG_MFD_RDC321X is not set +# CONFIG_MFD_SM501 is not set +# CONFIG_MFD_SYSCON is not set +# CONFIG_MFD_TI_AM335X_TSCADC is not set +# CONFIG_MFD_TQMX86 is not set +# CONFIG_MFD_VX855 is not set +# CONFIG_RAVE_SP_CORE is not set +# end of Multifunction device drivers + +# CONFIG_REGULATOR is not set +# CONFIG_RC_CORE is not set + +# +# CEC support +# +# CONFIG_MEDIA_CEC_SUPPORT is not set +# end of CEC support + +# CONFIG_MEDIA_SUPPORT is not set + +# +# Graphics support +# +CONFIG_APERTURE_HELPERS=y +# CONFIG_AGP is not set +# CONFIG_VGA_SWITCHEROO is not set +# CONFIG_DRM is not set +# CONFIG_DRM_DEBUG_MODESET_LOCK is not set + +# +# ARM devices +# +# end of ARM devices + +# +# Frame buffer Devices +# +CONFIG_FB_CMDLINE=y +CONFIG_FB_NOTIFY=y +CONFIG_FB=y +# CONFIG_FIRMWARE_EDID is not set +# CONFIG_FB_FOREIGN_ENDIAN is not set +# CONFIG_FB_MODE_HELPERS is not set +# CONFIG_FB_TILEBLITTING is not set + +# +# Frame buffer hardware drivers +# +# CONFIG_FB_CIRRUS is not set +# CONFIG_FB_PM2 is not set +# CONFIG_FB_CYBER2000 is not set +# CONFIG_FB_ARC is not set +# CONFIG_FB_ASILIANT is not set +# CONFIG_FB_IMSTT is not set +# CONFIG_FB_VGA16 is not set +# CONFIG_FB_UVESA is not set +# CONFIG_FB_VESA is not set +# CONFIG_FB_EFI is not set +# CONFIG_FB_N411 is not set +# CONFIG_FB_HGA is not set +# CONFIG_FB_OPENCORES is not set +# CONFIG_FB_S1D13XXX is not set +# CONFIG_FB_NVIDIA is not set +# CONFIG_FB_RIVA is not set +# CONFIG_FB_I740 is not set +# CONFIG_FB_LE80578 is not set +# CONFIG_FB_MATROX is not set +# CONFIG_FB_RADEON is not set +# CONFIG_FB_ATY128 is not set +# CONFIG_FB_ATY is not set +# CONFIG_FB_S3 is not set +# CONFIG_FB_SAVAGE is not set +# CONFIG_FB_SIS is not set +# CONFIG_FB_NEOMAGIC is not set +# CONFIG_FB_KYRO is not set +# CONFIG_FB_3DFX is not set +# CONFIG_FB_VOODOO1 is not set +# CONFIG_FB_VT8623 is not set +# CONFIG_FB_TRIDENT is not set +# CONFIG_FB_ARK is not set +# CONFIG_FB_PM3 is not set +# CONFIG_FB_CARMINE is not set +# CONFIG_FB_IBM_GXT4500 is not set +# CONFIG_FB_VIRTUAL is not set +# CONFIG_FB_METRONOME is not set +# CONFIG_FB_MB862XX is not set +# CONFIG_FB_SIMPLE is not set +# CONFIG_FB_SM712 is not set +# end of Frame buffer Devices + +# +# Backlight & LCD device support +# +CONFIG_LCD_CLASS_DEVICE=y +# CONFIG_LCD_PLATFORM is not set +CONFIG_BACKLIGHT_CLASS_DEVICE=y +# CONFIG_BACKLIGHT_APPLE is not set +# CONFIG_BACKLIGHT_QCOM_WLED is not set +# CONFIG_BACKLIGHT_SAHARA is not set +# end of Backlight & LCD device support + +# +# Console display driver support +# +CONFIG_VGA_CONSOLE=y +CONFIG_DUMMY_CONSOLE=y +CONFIG_DUMMY_CONSOLE_COLUMNS=80 +CONFIG_DUMMY_CONSOLE_ROWS=25 +CONFIG_FRAMEBUFFER_CONSOLE=y +# CONFIG_FRAMEBUFFER_CONSOLE_LEGACY_ACCELERATION is not set +CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY=y +# CONFIG_FRAMEBUFFER_CONSOLE_ROTATION is not set +# CONFIG_FRAMEBUFFER_CONSOLE_DEFERRED_TAKEOVER is not set +# end of Console display driver support + +# CONFIG_LOGO is not set +# end of Graphics support + +# CONFIG_SOUND is not set + +# +# HID support +# +CONFIG_HID=y +# CONFIG_HID_BATTERY_STRENGTH is not set +CONFIG_HIDRAW=y +CONFIG_UHID=y +CONFIG_HID_GENERIC=y + +# +# Special HID drivers +# +# CONFIG_HID_A4TECH is not set +# CONFIG_HID_ACRUX is not set +# CONFIG_HID_AUREAL is not set +# CONFIG_HID_BELKIN is not set +# CONFIG_HID_CHERRY is not set +# CONFIG_HID_COUGAR is not set +# CONFIG_HID_MACALLY is not set +# CONFIG_HID_CMEDIA is not set +# CONFIG_HID_CYPRESS is not set +# CONFIG_HID_DRAGONRISE is not set +# CONFIG_HID_EMS_FF is not set +# CONFIG_HID_ELECOM is not set +# CONFIG_HID_EZKEY is not set +# CONFIG_HID_GEMBIRD is not set +# CONFIG_HID_GFRM is not set +# CONFIG_HID_GLORIOUS is not set +# CONFIG_HID_VIVALDI is not set +# CONFIG_HID_KEYTOUCH is not set +# CONFIG_HID_KYE is not set +# CONFIG_HID_WALTOP is not set +# CONFIG_HID_VIEWSONIC is not set +# CONFIG_HID_VRC2 is not set +# CONFIG_HID_XIAOMI is not set +# CONFIG_HID_GYRATION is not set +# CONFIG_HID_ICADE is not set +# CONFIG_HID_ITE is not set +# CONFIG_HID_JABRA is not set +# CONFIG_HID_TWINHAN is not set +# CONFIG_HID_KENSINGTON is not set +# CONFIG_HID_LCPOWER is not set +# CONFIG_HID_LENOVO is not set +# CONFIG_HID_MAGICMOUSE is not set +# CONFIG_HID_MALTRON is not set +# CONFIG_HID_MAYFLASH is not set +CONFIG_HID_REDRAGON=y +# CONFIG_HID_MICROSOFT is not set +# CONFIG_HID_MONTEREY is not set +# CONFIG_HID_MULTITOUCH is not set +# CONFIG_HID_NTI is not set +# CONFIG_HID_ORTEK is not set +# CONFIG_HID_PANTHERLORD is not set +# CONFIG_HID_PETALYNX is not set +# CONFIG_HID_PICOLCD is not set +# CONFIG_HID_PLANTRONICS is not set +# CONFIG_HID_PXRC is not set +# CONFIG_HID_RAZER is not set +# CONFIG_HID_PRIMAX is not set +# CONFIG_HID_SAITEK is not set +# CONFIG_HID_SEMITEK is not set +# CONFIG_HID_SPEEDLINK is not set +# CONFIG_HID_STEAM is not set +# CONFIG_HID_STEELSERIES is not set +# CONFIG_HID_SUNPLUS is not set +# CONFIG_HID_RMI is not set +# CONFIG_HID_GREENASIA is not set +# CONFIG_HID_SMARTJOYPLUS is not set +# CONFIG_HID_TIVO is not set +# CONFIG_HID_TOPSEED is not set +# CONFIG_HID_TOPRE is not set +# CONFIG_HID_UDRAW_PS3 is not set +# CONFIG_HID_XINMO is not set +# CONFIG_HID_ZEROPLUS is not set +# CONFIG_HID_ZYDACRON is not set +# CONFIG_HID_SENSOR_HUB is not set +# CONFIG_HID_ALPS is not set +# end of Special HID drivers + +# +# Intel ISH HID support +# +# CONFIG_INTEL_ISH_HID is not set +# end of Intel ISH HID support + +# +# AMD SFH HID Support +# +# CONFIG_AMD_SFH_HID is not set +# end of AMD SFH HID Support +# end of HID support + +CONFIG_USB_OHCI_LITTLE_ENDIAN=y +# CONFIG_USB_SUPPORT is not set +# CONFIG_MMC is not set +# CONFIG_MEMSTICK is not set +# CONFIG_NEW_LEDS is not set +# CONFIG_ACCESSIBILITY is not set +# CONFIG_INFINIBAND is not set +CONFIG_EDAC_ATOMIC_SCRUB=y +CONFIG_EDAC_SUPPORT=y +# CONFIG_EDAC is not set +CONFIG_RTC_LIB=y +CONFIG_RTC_MC146818_LIB=y +CONFIG_RTC_CLASS=y +CONFIG_RTC_HCTOSYS=y +CONFIG_RTC_HCTOSYS_DEVICE="rtc0" +CONFIG_RTC_SYSTOHC=y +CONFIG_RTC_SYSTOHC_DEVICE="rtc0" +# CONFIG_RTC_DEBUG is not set +CONFIG_RTC_NVMEM=y + +# +# RTC interfaces +# +CONFIG_RTC_INTF_SYSFS=y +CONFIG_RTC_INTF_PROC=y +CONFIG_RTC_INTF_DEV=y +# CONFIG_RTC_INTF_DEV_UIE_EMUL is not set +# CONFIG_RTC_DRV_TEST is not set +CONFIG_RTC_I2C_AND_SPI=y +CONFIG_DMADEVICES=y +# CONFIG_DMADEVICES_DEBUG is not set + +# +# DMA Devices +# +CONFIG_DMA_ENGINE=y +CONFIG_DMA_VIRTUAL_CHANNELS=y +CONFIG_DMA_ACPI=y +# CONFIG_ALTERA_MSGDMA is not set +# CONFIG_INTEL_IDMA64 is not set +# CONFIG_INTEL_IDXD_COMPAT is not set +# CONFIG_INTEL_IOATDMA is not set +# CONFIG_PLX_DMA is not set +# CONFIG_AMD_PTDMA is not set +# CONFIG_QCOM_HIDMA_MGMT is not set +# CONFIG_QCOM_HIDMA is not set +CONFIG_DW_DMAC_CORE=y +# CONFIG_DW_DMAC is not set +CONFIG_DW_DMAC_PCI=y +# CONFIG_DW_EDMA is not set +# CONFIG_DW_EDMA_PCIE is not set +CONFIG_HSU_DMA=y +# CONFIG_SF_PDMA is not set +# CONFIG_INTEL_LDMA is not set + +# +# DMA Clients +# +# CONFIG_ASYNC_TX_DMA is not set +# CONFIG_DMATEST is not set + +# +# DMABUF options +# +CONFIG_SYNC_FILE=y +# CONFIG_SW_SYNC is not set +# CONFIG_UDMABUF is not set +# CONFIG_DMABUF_MOVE_NOTIFY is not set +# CONFIG_DMABUF_DEBUG is not set +# CONFIG_DMABUF_SELFTESTS is not set +# CONFIG_DMABUF_HEAPS is not set +# CONFIG_DMABUF_SYSFS_STATS is not set +# end of DMABUF options + +# CONFIG_AUXDISPLAY is not set +CONFIG_UIO=y +# CONFIG_UIO_CIF is not set +CONFIG_UIO_PDRV_GENIRQ=y +CONFIG_UIO_DMEM_GENIRQ=y +# CONFIG_UIO_AEC is not set +# CONFIG_UIO_SERCOS3 is not set +# CONFIG_UIO_PCI_GENERIC is not set +# CONFIG_UIO_NETX is not set +# CONFIG_UIO_PRUSS is not set +# CONFIG_UIO_MF624 is not set +CONFIG_VFIO=y +CONFIG_VFIO_IOMMU_TYPE1=y +CONFIG_VFIO_VIRQFD=y +# CONFIG_VFIO_NOIOMMU is not set +CONFIG_VFIO_PCI_CORE=y +CONFIG_VFIO_PCI_MMAP=y +CONFIG_VFIO_PCI_INTX=y +CONFIG_VFIO_PCI=y +# CONFIG_VFIO_PCI_VGA is not set +# CONFIG_VFIO_PCI_IGD is not set +# CONFIG_VFIO_MDEV is not set +CONFIG_IRQ_BYPASS_MANAGER=y +# CONFIG_VIRT_DRIVERS is not set +CONFIG_VIRTIO_ANCHOR=y +CONFIG_VIRTIO=y +CONFIG_VIRTIO_PCI_LIB=y +CONFIG_VIRTIO_PCI_LIB_LEGACY=y +CONFIG_VIRTIO_MENU=y +CONFIG_VIRTIO_PCI=y +CONFIG_VIRTIO_PCI_LEGACY=y +CONFIG_VIRTIO_PMEM=y +CONFIG_VIRTIO_BALLOON=y +CONFIG_VIRTIO_MEM=y +CONFIG_VIRTIO_INPUT=y +CONFIG_VIRTIO_MMIO=y +CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y +CONFIG_VIRTIO_DMA_SHARED_BUFFER=y +# CONFIG_VDPA is not set +CONFIG_VHOST_MENU=y +# CONFIG_VHOST_NET is not set +# CONFIG_VHOST_VSOCK is not set +# CONFIG_VHOST_CROSS_ENDIAN_LEGACY is not set + +# +# Microsoft Hyper-V guest support +# +# end of Microsoft Hyper-V guest support + +# CONFIG_GREYBUS is not set +# CONFIG_COMEDI is not set +# CONFIG_STAGING is not set +# CONFIG_CHROME_PLATFORMS is not set +# CONFIG_MELLANOX_PLATFORM is not set +# CONFIG_SURFACE_PLATFORMS is not set +# CONFIG_X86_PLATFORM_DEVICES is not set +# CONFIG_P2SB is not set +CONFIG_HAVE_CLK=y +CONFIG_HAVE_CLK_PREPARE=y +CONFIG_COMMON_CLK=y +# CONFIG_XILINX_VCU is not set +# CONFIG_HWSPINLOCK is not set + +# +# Clock Source drivers +# +CONFIG_CLKEVT_I8253=y +CONFIG_I8253_LOCK=y +CONFIG_CLKBLD_I8253=y +# end of Clock Source drivers + +CONFIG_MAILBOX=y +CONFIG_PCC=y +# CONFIG_ALTERA_MBOX is not set +CONFIG_IOMMU_IOVA=y +CONFIG_IOMMU_API=y +CONFIG_IOMMU_SUPPORT=y + +# +# Generic IOMMU Pagetable Support +# +# end of Generic IOMMU Pagetable Support + +# CONFIG_IOMMU_DEBUGFS is not set +# CONFIG_IOMMU_DEFAULT_DMA_STRICT is not set +CONFIG_IOMMU_DEFAULT_DMA_LAZY=y +# CONFIG_IOMMU_DEFAULT_PASSTHROUGH is not set +CONFIG_IOMMU_DMA=y +# CONFIG_AMD_IOMMU is not set +# CONFIG_INTEL_IOMMU is not set +# CONFIG_IRQ_REMAP is not set +CONFIG_VIRTIO_IOMMU=y + +# +# Remoteproc drivers +# +# CONFIG_REMOTEPROC is not set +# end of Remoteproc drivers + +# +# Rpmsg drivers +# +# CONFIG_RPMSG_QCOM_GLINK_RPM is not set +# CONFIG_RPMSG_VIRTIO is not set +# end of Rpmsg drivers + +# CONFIG_SOUNDWIRE is not set + +# +# SOC (System On Chip) specific Drivers +# + +# +# Amlogic SoC drivers +# +# end of Amlogic SoC drivers + +# +# Broadcom SoC drivers +# +# end of Broadcom SoC drivers + +# +# NXP/Freescale QorIQ SoC drivers +# +# end of NXP/Freescale QorIQ SoC drivers + +# +# fujitsu SoC drivers +# +# end of fujitsu SoC drivers + +# +# i.MX SoC drivers +# +# end of i.MX SoC drivers + +# +# Enable LiteX SoC Builder specific drivers +# +# end of Enable LiteX SoC Builder specific drivers + +# +# Qualcomm SoC drivers +# +# end of Qualcomm SoC drivers + +# CONFIG_SOC_TI is not set + +# +# Xilinx SoC drivers +# +# end of Xilinx SoC drivers +# end of SOC (System On Chip) specific Drivers + +# CONFIG_PM_DEVFREQ is not set +# CONFIG_EXTCON is not set +# CONFIG_MEMORY is not set +# CONFIG_IIO is not set +# CONFIG_NTB is not set +# CONFIG_PWM is not set + +# +# IRQ chip support +# +# end of IRQ chip support + +# CONFIG_IPACK_BUS is not set +# CONFIG_RESET_CONTROLLER is not set + +# +# PHY Subsystem +# +# CONFIG_GENERIC_PHY is not set +# CONFIG_PHY_CAN_TRANSCEIVER is not set + +# +# PHY drivers for Broadcom platforms +# +# CONFIG_BCM_KONA_USB2_PHY is not set +# end of PHY drivers for Broadcom platforms + +# CONFIG_PHY_PXA_28NM_HSIC is not set +# CONFIG_PHY_PXA_28NM_USB2 is not set +# CONFIG_PHY_INTEL_LGM_EMMC is not set +# end of PHY Subsystem + +# CONFIG_POWERCAP is not set +# CONFIG_MCB is not set + +# +# Performance monitor support +# +# end of Performance monitor support + +CONFIG_RAS=y +# CONFIG_USB4 is not set + +# +# Android +# +# CONFIG_ANDROID_BINDER_IPC is not set +# end of Android + +CONFIG_LIBNVDIMM=y +CONFIG_BLK_DEV_PMEM=y +CONFIG_ND_CLAIM=y +CONFIG_ND_BTT=y +CONFIG_BTT=y +CONFIG_ND_PFN=y +CONFIG_NVDIMM_PFN=y +CONFIG_NVDIMM_DAX=y +CONFIG_DAX=y +CONFIG_DEV_DAX=y +CONFIG_DEV_DAX_PMEM=y +CONFIG_DEV_DAX_KMEM=y +CONFIG_NVMEM=y +CONFIG_NVMEM_SYSFS=y +# CONFIG_NVMEM_RMEM is not set + +# +# HW tracing support +# +# CONFIG_STM is not set +# CONFIG_INTEL_TH is not set +# end of HW tracing support + +# CONFIG_FPGA is not set +# CONFIG_SIOX is not set +# CONFIG_SLIMBUS is not set +# CONFIG_INTERCONNECT is not set +# CONFIG_COUNTER is not set +# CONFIG_MOST is not set +# CONFIG_PECI is not set +# CONFIG_HTE is not set +# end of Device Drivers + +# +# File systems +# +CONFIG_DCACHE_WORD_ACCESS=y +# CONFIG_VALIDATE_FS_PARSER is not set +CONFIG_FS_IOMAP=y +# CONFIG_EXT2_FS is not set +# CONFIG_EXT3_FS is not set +CONFIG_EXT4_FS=y +CONFIG_EXT4_USE_FOR_EXT2=y +CONFIG_EXT4_FS_POSIX_ACL=y +CONFIG_EXT4_FS_SECURITY=y +CONFIG_EXT4_DEBUG=y +CONFIG_JBD2=y +CONFIG_JBD2_DEBUG=y +CONFIG_FS_MBCACHE=y +# CONFIG_REISERFS_FS is not set +# CONFIG_JFS_FS is not set +# CONFIG_XFS_FS is not set +# CONFIG_GFS2_FS is not set +# CONFIG_OCFS2_FS is not set +# CONFIG_BTRFS_FS is not set +# CONFIG_NILFS2_FS is not set +# CONFIG_F2FS_FS is not set +CONFIG_FS_DAX=y +CONFIG_FS_DAX_PMD=y +CONFIG_FS_POSIX_ACL=y +CONFIG_EXPORTFS=y +# CONFIG_EXPORTFS_BLOCK_OPS is not set +CONFIG_FILE_LOCKING=y +CONFIG_FS_ENCRYPTION=y +CONFIG_FS_ENCRYPTION_ALGS=y +# CONFIG_FS_VERITY is not set +CONFIG_FSNOTIFY=y +CONFIG_DNOTIFY=y +CONFIG_INOTIFY_USER=y +CONFIG_FANOTIFY=y +# CONFIG_QUOTA is not set +CONFIG_AUTOFS4_FS=y +CONFIG_AUTOFS_FS=y +CONFIG_FUSE_FS=y +CONFIG_CUSE=y +CONFIG_VIRTIO_FS=y +CONFIG_FUSE_DAX=y +CONFIG_OVERLAY_FS=y +# CONFIG_OVERLAY_FS_REDIRECT_DIR is not set +CONFIG_OVERLAY_FS_REDIRECT_ALWAYS_FOLLOW=y +# CONFIG_OVERLAY_FS_INDEX is not set +# CONFIG_OVERLAY_FS_XINO_AUTO is not set +# CONFIG_OVERLAY_FS_METACOPY is not set + +# +# Caches +# +CONFIG_NETFS_SUPPORT=y +# CONFIG_NETFS_STATS is not set +CONFIG_FSCACHE=y +# CONFIG_FSCACHE_STATS is not set +# CONFIG_FSCACHE_DEBUG is not set +CONFIG_CACHEFILES=y +# CONFIG_CACHEFILES_DEBUG is not set +# CONFIG_CACHEFILES_ERROR_INJECTION is not set +# CONFIG_CACHEFILES_ONDEMAND is not set +# end of Caches + +# +# CD-ROM/DVD Filesystems +# +CONFIG_ISO9660_FS=y +CONFIG_JOLIET=y +CONFIG_ZISOFS=y +CONFIG_UDF_FS=y +# end of CD-ROM/DVD Filesystems + +# +# DOS/FAT/EXFAT/NT Filesystems +# +CONFIG_FAT_FS=y +CONFIG_MSDOS_FS=y +CONFIG_VFAT_FS=y +CONFIG_FAT_DEFAULT_CODEPAGE=437 +CONFIG_FAT_DEFAULT_IOCHARSET="ascii" +# CONFIG_FAT_DEFAULT_UTF8 is not set +# CONFIG_EXFAT_FS is not set +# CONFIG_NTFS_FS is not set +# CONFIG_NTFS3_FS is not set +# end of DOS/FAT/EXFAT/NT Filesystems + +# +# Pseudo filesystems +# +CONFIG_PROC_FS=y +CONFIG_PROC_KCORE=y +CONFIG_PROC_SYSCTL=y +CONFIG_PROC_PAGE_MONITOR=y +CONFIG_PROC_CHILDREN=y +CONFIG_PROC_PID_ARCH_STATUS=y +CONFIG_KERNFS=y +CONFIG_SYSFS=y +CONFIG_TMPFS=y +CONFIG_TMPFS_POSIX_ACL=y +CONFIG_TMPFS_XATTR=y +# CONFIG_TMPFS_INODE64 is not set +CONFIG_HUGETLBFS=y +CONFIG_HUGETLB_PAGE=y +CONFIG_ARCH_WANT_HUGETLB_PAGE_OPTIMIZE_VMEMMAP=y +CONFIG_HUGETLB_PAGE_OPTIMIZE_VMEMMAP=y +# CONFIG_HUGETLB_PAGE_OPTIMIZE_VMEMMAP_DEFAULT_ON is not set +CONFIG_MEMFD_CREATE=y +CONFIG_ARCH_HAS_GIGANTIC_PAGE=y +CONFIG_CONFIGFS_FS=y +CONFIG_EFIVAR_FS=y +# end of Pseudo filesystems + +CONFIG_MISC_FILESYSTEMS=y +# CONFIG_ORANGEFS_FS is not set +# CONFIG_ADFS_FS is not set +# CONFIG_AFFS_FS is not set +# CONFIG_ECRYPT_FS is not set +# CONFIG_HFS_FS is not set +# CONFIG_HFSPLUS_FS is not set +# CONFIG_BEFS_FS is not set +# CONFIG_BFS_FS is not set +# CONFIG_EFS_FS is not set +# CONFIG_CRAMFS is not set +CONFIG_SQUASHFS=y +CONFIG_SQUASHFS_FILE_CACHE=y +# CONFIG_SQUASHFS_FILE_DIRECT is not set +CONFIG_SQUASHFS_DECOMP_SINGLE=y +# CONFIG_SQUASHFS_DECOMP_MULTI is not set +# CONFIG_SQUASHFS_DECOMP_MULTI_PERCPU is not set +# CONFIG_SQUASHFS_XATTR is not set +CONFIG_SQUASHFS_ZLIB=y +# CONFIG_SQUASHFS_LZ4 is not set +# CONFIG_SQUASHFS_LZO is not set +CONFIG_SQUASHFS_XZ=y +# CONFIG_SQUASHFS_ZSTD is not set +# CONFIG_SQUASHFS_4K_DEVBLK_SIZE is not set +# CONFIG_SQUASHFS_EMBEDDED is not set +CONFIG_SQUASHFS_FRAGMENT_CACHE_SIZE=3 +# CONFIG_VXFS_FS is not set +# CONFIG_MINIX_FS is not set +# CONFIG_OMFS_FS is not set +# CONFIG_HPFS_FS is not set +# CONFIG_QNX4FS_FS is not set +# CONFIG_QNX6FS_FS is not set +# CONFIG_ROMFS_FS is not set +# CONFIG_PSTORE is not set +# CONFIG_SYSV_FS is not set +# CONFIG_UFS_FS is not set +# CONFIG_EROFS_FS is not set +CONFIG_NETWORK_FILESYSTEMS=y +CONFIG_NFS_FS=y +CONFIG_NFS_V2=y +CONFIG_NFS_V3=y +# CONFIG_NFS_V3_ACL is not set +CONFIG_NFS_V4=y +# CONFIG_NFS_SWAP is not set +# CONFIG_NFS_V4_1 is not set +# CONFIG_ROOT_NFS is not set +# CONFIG_NFS_FSCACHE is not set +# CONFIG_NFS_USE_LEGACY_DNS is not set +CONFIG_NFS_USE_KERNEL_DNS=y +CONFIG_NFS_DISABLE_UDP_SUPPORT=y +CONFIG_NFSD=y +# CONFIG_NFSD_V3_ACL is not set +CONFIG_NFSD_V4=y +# CONFIG_NFSD_BLOCKLAYOUT is not set +# CONFIG_NFSD_SCSILAYOUT is not set +# CONFIG_NFSD_FLEXFILELAYOUT is not set +CONFIG_GRACE_PERIOD=y +CONFIG_LOCKD=y +CONFIG_LOCKD_V4=y +CONFIG_NFS_COMMON=y +CONFIG_SUNRPC=y +CONFIG_SUNRPC_GSS=y +CONFIG_RPCSEC_GSS_KRB5=y +# CONFIG_SUNRPC_DISABLE_INSECURE_ENCTYPES is not set +# CONFIG_SUNRPC_DEBUG is not set +# CONFIG_CEPH_FS is not set +CONFIG_CIFS=y +# CONFIG_CIFS_STATS2 is not set +CONFIG_CIFS_ALLOW_INSECURE_LEGACY=y +CONFIG_CIFS_UPCALL=y +CONFIG_CIFS_XATTR=y +CONFIG_CIFS_POSIX=y +# CONFIG_CIFS_DEBUG is not set +CONFIG_CIFS_DFS_UPCALL=y +# CONFIG_CIFS_SMB_DIRECT is not set +# CONFIG_SMB_SERVER is not set +# CONFIG_CODA_FS is not set +# CONFIG_AFS_FS is not set +CONFIG_NLS=y +CONFIG_NLS_DEFAULT="utf8" +CONFIG_NLS_CODEPAGE_437=y +CONFIG_NLS_CODEPAGE_737=y +CONFIG_NLS_CODEPAGE_775=y +CONFIG_NLS_CODEPAGE_850=y +CONFIG_NLS_CODEPAGE_852=y +CONFIG_NLS_CODEPAGE_855=y +CONFIG_NLS_CODEPAGE_857=y +CONFIG_NLS_CODEPAGE_860=y +CONFIG_NLS_CODEPAGE_861=y +CONFIG_NLS_CODEPAGE_862=y +CONFIG_NLS_CODEPAGE_863=y +CONFIG_NLS_CODEPAGE_864=y +CONFIG_NLS_CODEPAGE_865=y +CONFIG_NLS_CODEPAGE_866=y +CONFIG_NLS_CODEPAGE_869=y +CONFIG_NLS_CODEPAGE_936=y +CONFIG_NLS_CODEPAGE_950=y +CONFIG_NLS_CODEPAGE_932=y +CONFIG_NLS_CODEPAGE_949=y +CONFIG_NLS_CODEPAGE_874=y +CONFIG_NLS_ISO8859_8=y +CONFIG_NLS_CODEPAGE_1250=y +CONFIG_NLS_CODEPAGE_1251=y +CONFIG_NLS_ASCII=y +CONFIG_NLS_ISO8859_1=y +CONFIG_NLS_ISO8859_2=y +CONFIG_NLS_ISO8859_3=y +CONFIG_NLS_ISO8859_4=y +CONFIG_NLS_ISO8859_5=y +CONFIG_NLS_ISO8859_6=y +CONFIG_NLS_ISO8859_7=y +CONFIG_NLS_ISO8859_9=y +CONFIG_NLS_ISO8859_13=y +CONFIG_NLS_ISO8859_14=y +CONFIG_NLS_ISO8859_15=y +CONFIG_NLS_KOI8_R=y +CONFIG_NLS_KOI8_U=y +CONFIG_NLS_MAC_ROMAN=y +CONFIG_NLS_MAC_CELTIC=y +CONFIG_NLS_MAC_CENTEURO=y +CONFIG_NLS_MAC_CROATIAN=y +CONFIG_NLS_MAC_CYRILLIC=y +CONFIG_NLS_MAC_GAELIC=y +CONFIG_NLS_MAC_GREEK=y +CONFIG_NLS_MAC_ICELAND=y +CONFIG_NLS_MAC_INUIT=y +CONFIG_NLS_MAC_ROMANIAN=y +CONFIG_NLS_MAC_TURKISH=y +CONFIG_NLS_UTF8=y +# CONFIG_DLM is not set +# CONFIG_UNICODE is not set +CONFIG_IO_WQ=y +# end of File systems + +# +# Security options +# +CONFIG_KEYS=y +# CONFIG_KEYS_REQUEST_CACHE is not set +CONFIG_PERSISTENT_KEYRINGS=y +# CONFIG_BIG_KEYS is not set +# CONFIG_TRUSTED_KEYS is not set +# CONFIG_ENCRYPTED_KEYS is not set +# CONFIG_KEY_DH_OPERATIONS is not set +# CONFIG_SECURITY_DMESG_RESTRICT is not set +# CONFIG_SECURITY is not set +CONFIG_SECURITYFS=y +CONFIG_HAVE_HARDENED_USERCOPY_ALLOCATOR=y +# CONFIG_HARDENED_USERCOPY is not set +CONFIG_FORTIFY_SOURCE=y +# CONFIG_STATIC_USERMODEHELPER is not set +# CONFIG_IMA_SECURE_AND_OR_TRUSTED_BOOT is not set +CONFIG_DEFAULT_SECURITY_DAC=y +CONFIG_LSM="yama,loadpin,safesetid,integrity" + +# +# Kernel hardening options +# + +# +# Memory initialization +# +CONFIG_INIT_STACK_NONE=y +# CONFIG_INIT_ON_ALLOC_DEFAULT_ON is not set +# CONFIG_INIT_ON_FREE_DEFAULT_ON is not set +# end of Memory initialization + +CONFIG_RANDSTRUCT_NONE=y +# end of Kernel hardening options +# end of Security options + +CONFIG_CRYPTO=y + +# +# Crypto core or helper +# +# CONFIG_CRYPTO_FIPS is not set +CONFIG_CRYPTO_ALGAPI=y +CONFIG_CRYPTO_ALGAPI2=y +CONFIG_CRYPTO_AEAD=y +CONFIG_CRYPTO_AEAD2=y +CONFIG_CRYPTO_SKCIPHER=y +CONFIG_CRYPTO_SKCIPHER2=y +CONFIG_CRYPTO_HASH=y +CONFIG_CRYPTO_HASH2=y +CONFIG_CRYPTO_RNG=y +CONFIG_CRYPTO_RNG2=y +CONFIG_CRYPTO_RNG_DEFAULT=y +CONFIG_CRYPTO_AKCIPHER2=y +CONFIG_CRYPTO_AKCIPHER=y +CONFIG_CRYPTO_KPP2=y +CONFIG_CRYPTO_ACOMP2=y +CONFIG_CRYPTO_MANAGER=y +CONFIG_CRYPTO_MANAGER2=y +# CONFIG_CRYPTO_USER is not set +# CONFIG_CRYPTO_MANAGER_DISABLE_TESTS is not set +# CONFIG_CRYPTO_MANAGER_EXTRA_TESTS is not set +CONFIG_CRYPTO_GF128MUL=y +CONFIG_CRYPTO_NULL=y +CONFIG_CRYPTO_NULL2=y +# CONFIG_CRYPTO_PCRYPT is not set +CONFIG_CRYPTO_CRYPTD=y +CONFIG_CRYPTO_AUTHENC=y +# CONFIG_CRYPTO_TEST is not set +CONFIG_CRYPTO_SIMD=y +# end of Crypto core or helper + +# +# Public-key cryptography +# +# CONFIG_CRYPTO_RSA is not set +# CONFIG_CRYPTO_DH is not set +CONFIG_CRYPTO_ECC=y +# CONFIG_CRYPTO_ECDH is not set +CONFIG_CRYPTO_ECDSA=y +# CONFIG_CRYPTO_ECRDSA is not set +# CONFIG_CRYPTO_SM2 is not set +# CONFIG_CRYPTO_CURVE25519 is not set +# end of Public-key cryptography + +# +# Block ciphers +# +CONFIG_CRYPTO_AES=y +# CONFIG_CRYPTO_AES_TI is not set +# CONFIG_CRYPTO_ARIA is not set +# CONFIG_CRYPTO_BLOWFISH is not set +# CONFIG_CRYPTO_CAMELLIA is not set +# CONFIG_CRYPTO_CAST5 is not set +# CONFIG_CRYPTO_CAST6 is not set +CONFIG_CRYPTO_DES=y +# CONFIG_CRYPTO_FCRYPT is not set +# CONFIG_CRYPTO_SERPENT is not set +# CONFIG_CRYPTO_SM4_GENERIC is not set +# CONFIG_CRYPTO_TWOFISH is not set +CONFIG_CRYPTO_TWOFISH_COMMON=y +# end of Block ciphers + +# +# Length-preserving ciphers and modes +# +# CONFIG_CRYPTO_ADIANTUM is not set +# CONFIG_CRYPTO_CHACHA20 is not set +CONFIG_CRYPTO_CBC=y +# CONFIG_CRYPTO_CFB is not set +CONFIG_CRYPTO_CTR=y +CONFIG_CRYPTO_CTS=y +CONFIG_CRYPTO_ECB=y +# CONFIG_CRYPTO_HCTR2 is not set +# CONFIG_CRYPTO_KEYWRAP is not set +CONFIG_CRYPTO_LRW=y +# CONFIG_CRYPTO_OFB is not set +CONFIG_CRYPTO_PCBC=y +CONFIG_CRYPTO_XTS=y +# end of Length-preserving ciphers and modes + +# +# AEAD (authenticated encryption with associated data) ciphers +# +# CONFIG_CRYPTO_AEGIS128 is not set +# CONFIG_CRYPTO_CHACHA20POLY1305 is not set +# CONFIG_CRYPTO_CCM is not set +CONFIG_CRYPTO_GCM=y +CONFIG_CRYPTO_SEQIV=y +CONFIG_CRYPTO_ECHAINIV=y +# CONFIG_CRYPTO_ESSIV is not set +# end of AEAD (authenticated encryption with associated data) ciphers + +# +# Hashes, digests, and MACs +# +# CONFIG_CRYPTO_BLAKE2B is not set +CONFIG_CRYPTO_CMAC=y +CONFIG_CRYPTO_GHASH=y +CONFIG_CRYPTO_HMAC=y +# CONFIG_CRYPTO_MD4 is not set +CONFIG_CRYPTO_MD5=y +# CONFIG_CRYPTO_MICHAEL_MIC is not set +CONFIG_CRYPTO_POLY1305=y +# CONFIG_CRYPTO_RMD160 is not set +CONFIG_CRYPTO_SHA1=y +CONFIG_CRYPTO_SHA256=y +CONFIG_CRYPTO_SHA512=y +# CONFIG_CRYPTO_SHA3 is not set +# CONFIG_CRYPTO_SM3_GENERIC is not set +# CONFIG_CRYPTO_STREEBOG is not set +# CONFIG_CRYPTO_VMAC is not set +# CONFIG_CRYPTO_WP512 is not set +# CONFIG_CRYPTO_XCBC is not set +# CONFIG_CRYPTO_XXHASH is not set +# end of Hashes, digests, and MACs + +# +# CRCs (cyclic redundancy checks) +# +CONFIG_CRYPTO_CRC32C=y +# CONFIG_CRYPTO_CRC32 is not set +CONFIG_CRYPTO_CRCT10DIF=y +# end of CRCs (cyclic redundancy checks) + +# +# Compression +# +CONFIG_CRYPTO_DEFLATE=y +CONFIG_CRYPTO_LZO=y +# CONFIG_CRYPTO_842 is not set +# CONFIG_CRYPTO_LZ4 is not set +# CONFIG_CRYPTO_LZ4HC is not set +# CONFIG_CRYPTO_ZSTD is not set +# end of Compression + +# +# Random number generation +# +# CONFIG_CRYPTO_ANSI_CPRNG is not set +CONFIG_CRYPTO_DRBG_MENU=y +CONFIG_CRYPTO_DRBG_HMAC=y +CONFIG_CRYPTO_DRBG_HASH=y +CONFIG_CRYPTO_DRBG_CTR=y +CONFIG_CRYPTO_DRBG=y +CONFIG_CRYPTO_JITTERENTROPY=y +# end of Random number generation + +# +# Userspace interface +# +CONFIG_CRYPTO_USER_API=y +# CONFIG_CRYPTO_USER_API_HASH is not set +# CONFIG_CRYPTO_USER_API_SKCIPHER is not set +CONFIG_CRYPTO_USER_API_RNG=y +# CONFIG_CRYPTO_USER_API_RNG_CAVP is not set +# CONFIG_CRYPTO_USER_API_AEAD is not set +# CONFIG_CRYPTO_USER_API_ENABLE_OBSOLETE is not set +# end of Userspace interface + +# +# Accelerated Cryptographic Algorithms for CPU (x86) +# +CONFIG_CRYPTO_CURVE25519_X86=y +CONFIG_CRYPTO_AES_NI_INTEL=y +# CONFIG_CRYPTO_BLOWFISH_X86_64 is not set +# CONFIG_CRYPTO_CAMELLIA_X86_64 is not set +# CONFIG_CRYPTO_CAMELLIA_AESNI_AVX_X86_64 is not set +# CONFIG_CRYPTO_CAMELLIA_AESNI_AVX2_X86_64 is not set +# CONFIG_CRYPTO_CAST5_AVX_X86_64 is not set +# CONFIG_CRYPTO_CAST6_AVX_X86_64 is not set +# CONFIG_CRYPTO_DES3_EDE_X86_64 is not set +# CONFIG_CRYPTO_SERPENT_SSE2_X86_64 is not set +# CONFIG_CRYPTO_SERPENT_AVX_X86_64 is not set +# CONFIG_CRYPTO_SERPENT_AVX2_X86_64 is not set +# CONFIG_CRYPTO_SM4_AESNI_AVX_X86_64 is not set +# CONFIG_CRYPTO_SM4_AESNI_AVX2_X86_64 is not set +CONFIG_CRYPTO_TWOFISH_X86_64=y +CONFIG_CRYPTO_TWOFISH_X86_64_3WAY=y +CONFIG_CRYPTO_TWOFISH_AVX_X86_64=y +# CONFIG_CRYPTO_ARIA_AESNI_AVX_X86_64 is not set +CONFIG_CRYPTO_CHACHA20_X86_64=y +# CONFIG_CRYPTO_AEGIS128_AESNI_SSE2 is not set +# CONFIG_CRYPTO_NHPOLY1305_SSE2 is not set +# CONFIG_CRYPTO_NHPOLY1305_AVX2 is not set +CONFIG_CRYPTO_BLAKE2S_X86=y +# CONFIG_CRYPTO_POLYVAL_CLMUL_NI is not set +CONFIG_CRYPTO_POLY1305_X86_64=y +# CONFIG_CRYPTO_SHA1_SSSE3 is not set +# CONFIG_CRYPTO_SHA256_SSSE3 is not set +# CONFIG_CRYPTO_SHA512_SSSE3 is not set +# CONFIG_CRYPTO_SM3_AVX_X86_64 is not set +# CONFIG_CRYPTO_GHASH_CLMUL_NI_INTEL is not set +CONFIG_CRYPTO_CRC32C_INTEL=y +# CONFIG_CRYPTO_CRC32_PCLMUL is not set +# CONFIG_CRYPTO_CRCT10DIF_PCLMUL is not set +# end of Accelerated Cryptographic Algorithms for CPU (x86) + +# CONFIG_CRYPTO_HW is not set +# CONFIG_ASYMMETRIC_KEY_TYPE is not set + +# +# Certificates for signature checking +# +CONFIG_SYSTEM_BLACKLIST_KEYRING=y +CONFIG_SYSTEM_BLACKLIST_HASH_LIST="" +# end of Certificates for signature checking + +CONFIG_BINARY_PRINTF=y + +# +# Library routines +# +# CONFIG_PACKING is not set +CONFIG_BITREVERSE=y +CONFIG_GENERIC_STRNCPY_FROM_USER=y +CONFIG_GENERIC_STRNLEN_USER=y +CONFIG_GENERIC_NET_UTILS=y +# CONFIG_CORDIC is not set +# CONFIG_PRIME_NUMBERS is not set +CONFIG_RATIONAL=y +CONFIG_GENERIC_PCI_IOMAP=y +CONFIG_GENERIC_IOMAP=y +CONFIG_ARCH_USE_CMPXCHG_LOCKREF=y +CONFIG_ARCH_HAS_FAST_MULTIPLIER=y +CONFIG_ARCH_USE_SYM_ANNOTATIONS=y + +# +# Crypto library routines +# +CONFIG_CRYPTO_LIB_UTILS=y +CONFIG_CRYPTO_LIB_AES=y +CONFIG_CRYPTO_ARCH_HAVE_LIB_BLAKE2S=y +CONFIG_CRYPTO_LIB_BLAKE2S_GENERIC=y +CONFIG_CRYPTO_ARCH_HAVE_LIB_CHACHA=y +CONFIG_CRYPTO_LIB_CHACHA_GENERIC=y +CONFIG_CRYPTO_LIB_CHACHA=y +CONFIG_CRYPTO_ARCH_HAVE_LIB_CURVE25519=y +CONFIG_CRYPTO_LIB_CURVE25519_GENERIC=y +CONFIG_CRYPTO_LIB_CURVE25519=y +CONFIG_CRYPTO_LIB_DES=y +CONFIG_CRYPTO_LIB_POLY1305_RSIZE=11 +CONFIG_CRYPTO_ARCH_HAVE_LIB_POLY1305=y +CONFIG_CRYPTO_LIB_POLY1305_GENERIC=y +CONFIG_CRYPTO_LIB_POLY1305=y +CONFIG_CRYPTO_LIB_CHACHA20POLY1305=y +CONFIG_CRYPTO_LIB_SHA1=y +CONFIG_CRYPTO_LIB_SHA256=y +# end of Crypto library routines + +CONFIG_CRC_CCITT=y +CONFIG_CRC16=y +CONFIG_CRC_T10DIF=y +# CONFIG_CRC64_ROCKSOFT is not set +CONFIG_CRC_ITU_T=y +CONFIG_CRC32=y +# CONFIG_CRC32_SELFTEST is not set +CONFIG_CRC32_SLICEBY8=y +# CONFIG_CRC32_SLICEBY4 is not set +# CONFIG_CRC32_SARWATE is not set +# CONFIG_CRC32_BIT is not set +# CONFIG_CRC64 is not set +# CONFIG_CRC4 is not set +# CONFIG_CRC7 is not set +CONFIG_LIBCRC32C=y +# CONFIG_CRC8 is not set +CONFIG_XXHASH=y +# CONFIG_RANDOM32_SELFTEST is not set +CONFIG_ZLIB_INFLATE=y +CONFIG_ZLIB_DEFLATE=y +CONFIG_LZO_COMPRESS=y +CONFIG_LZO_DECOMPRESS=y +CONFIG_LZ4_DECOMPRESS=y +CONFIG_ZSTD_COMMON=y +CONFIG_ZSTD_DECOMPRESS=y +CONFIG_XZ_DEC=y +CONFIG_XZ_DEC_X86=y +CONFIG_XZ_DEC_POWERPC=y +CONFIG_XZ_DEC_IA64=y +CONFIG_XZ_DEC_ARM=y +CONFIG_XZ_DEC_ARMTHUMB=y +CONFIG_XZ_DEC_SPARC=y +# CONFIG_XZ_DEC_MICROLZMA is not set +CONFIG_XZ_DEC_BCJ=y +# CONFIG_XZ_DEC_TEST is not set +CONFIG_DECOMPRESS_GZIP=y +CONFIG_DECOMPRESS_BZIP2=y +CONFIG_DECOMPRESS_LZMA=y +CONFIG_DECOMPRESS_XZ=y +CONFIG_DECOMPRESS_LZO=y +CONFIG_DECOMPRESS_LZ4=y +CONFIG_DECOMPRESS_ZSTD=y +CONFIG_TEXTSEARCH=y +CONFIG_TEXTSEARCH_KMP=y +CONFIG_TEXTSEARCH_BM=y +CONFIG_TEXTSEARCH_FSM=y +CONFIG_INTERVAL_TREE=y +CONFIG_XARRAY_MULTI=y +CONFIG_ASSOCIATIVE_ARRAY=y +CONFIG_HAS_IOMEM=y +CONFIG_HAS_IOPORT_MAP=y +CONFIG_HAS_DMA=y +CONFIG_DMA_OPS=y +CONFIG_NEED_SG_DMA_LENGTH=y +CONFIG_NEED_DMA_MAP_STATE=y +CONFIG_ARCH_DMA_ADDR_T_64BIT=y +CONFIG_SWIOTLB=y +# CONFIG_DMA_API_DEBUG is not set +# CONFIG_DMA_MAP_BENCHMARK is not set +CONFIG_SGL_ALLOC=y +# CONFIG_FORCE_NR_CPUS is not set +CONFIG_CPU_RMAP=y +CONFIG_DQL=y +CONFIG_NLATTR=y +CONFIG_IRQ_POLL=y +CONFIG_OID_REGISTRY=y +CONFIG_UCS2_STRING=y +CONFIG_HAVE_GENERIC_VDSO=y +CONFIG_GENERIC_GETTIMEOFDAY=y +CONFIG_GENERIC_VDSO_TIME_NS=y +CONFIG_FONT_SUPPORT=y +# CONFIG_FONTS is not set +CONFIG_FONT_8x8=y +CONFIG_FONT_8x16=y +CONFIG_SG_POOL=y +CONFIG_ARCH_HAS_PMEM_API=y +CONFIG_MEMREGION=y +CONFIG_ARCH_HAS_UACCESS_FLUSHCACHE=y +CONFIG_ARCH_HAS_COPY_MC=y +CONFIG_ARCH_STACKWALK=y +CONFIG_STACKDEPOT=y +CONFIG_SBITMAP=y +# end of Library routines + +# +# Kernel hacking +# + +# +# printk and dmesg options +# +CONFIG_PRINTK_TIME=y +# CONFIG_PRINTK_CALLER is not set +# CONFIG_STACKTRACE_BUILD_ID is not set +CONFIG_CONSOLE_LOGLEVEL_DEFAULT=7 +CONFIG_CONSOLE_LOGLEVEL_QUIET=4 +CONFIG_MESSAGE_LOGLEVEL_DEFAULT=4 +# CONFIG_BOOT_PRINTK_DELAY is not set +CONFIG_DYNAMIC_DEBUG=y +CONFIG_DYNAMIC_DEBUG_CORE=y +CONFIG_SYMBOLIC_ERRNAME=y +# CONFIG_DEBUG_BUGVERBOSE is not set +# end of printk and dmesg options + +CONFIG_DEBUG_KERNEL=y +CONFIG_DEBUG_MISC=y + +# +# Compile-time checks and compiler options +# +CONFIG_AS_HAS_NON_CONST_LEB128=y +CONFIG_DEBUG_INFO_NONE=y +# CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT is not set +# CONFIG_DEBUG_INFO_DWARF4 is not set +# CONFIG_DEBUG_INFO_DWARF5 is not set +CONFIG_FRAME_WARN=2048 +CONFIG_STRIP_ASM_SYMS=y +# CONFIG_READABLE_ASM is not set +# CONFIG_HEADERS_INSTALL is not set +CONFIG_DEBUG_SECTION_MISMATCH=y +CONFIG_SECTION_MISMATCH_WARN_ONLY=y +# CONFIG_DEBUG_FORCE_FUNCTION_ALIGN_64B is not set +CONFIG_FRAME_POINTER=y +CONFIG_OBJTOOL=y +CONFIG_STACK_VALIDATION=y +CONFIG_VMLINUX_MAP=y +# CONFIG_DEBUG_FORCE_WEAK_PER_CPU is not set +# end of Compile-time checks and compiler options + +# +# Generic Kernel Debugging Instruments +# +CONFIG_MAGIC_SYSRQ=y +CONFIG_MAGIC_SYSRQ_DEFAULT_ENABLE=0x1 +CONFIG_MAGIC_SYSRQ_SERIAL=y +CONFIG_MAGIC_SYSRQ_SERIAL_SEQUENCE="" +CONFIG_DEBUG_FS=y +CONFIG_DEBUG_FS_ALLOW_ALL=y +# CONFIG_DEBUG_FS_DISALLOW_MOUNT is not set +# CONFIG_DEBUG_FS_ALLOW_NONE is not set +CONFIG_HAVE_ARCH_KGDB=y +# CONFIG_KGDB is not set +CONFIG_ARCH_HAS_UBSAN_SANITIZE_ALL=y +# CONFIG_UBSAN is not set +CONFIG_HAVE_ARCH_KCSAN=y +# end of Generic Kernel Debugging Instruments + +# +# Networking Debugging +# +# CONFIG_NET_DEV_REFCNT_TRACKER is not set +# CONFIG_NET_NS_REFCNT_TRACKER is not set +# CONFIG_DEBUG_NET is not set +# end of Networking Debugging + +# +# Memory Debugging +# +# CONFIG_PAGE_EXTENSION is not set +# CONFIG_DEBUG_PAGEALLOC is not set +CONFIG_SLUB_DEBUG=y +# CONFIG_SLUB_DEBUG_ON is not set +# CONFIG_PAGE_OWNER is not set +# CONFIG_PAGE_TABLE_CHECK is not set +# CONFIG_PAGE_POISONING is not set +# CONFIG_DEBUG_RODATA_TEST is not set +CONFIG_ARCH_HAS_DEBUG_WX=y +# CONFIG_DEBUG_WX is not set +CONFIG_GENERIC_PTDUMP=y +# CONFIG_PTDUMP_DEBUGFS is not set +# CONFIG_DEBUG_OBJECTS is not set +# CONFIG_SHRINKER_DEBUG is not set +CONFIG_HAVE_DEBUG_KMEMLEAK=y +# CONFIG_DEBUG_KMEMLEAK is not set +# CONFIG_DEBUG_STACK_USAGE is not set +# CONFIG_SCHED_STACK_END_CHECK is not set +CONFIG_ARCH_HAS_DEBUG_VM_PGTABLE=y +# CONFIG_DEBUG_VM is not set +# CONFIG_DEBUG_VM_PGTABLE is not set +CONFIG_ARCH_HAS_DEBUG_VIRTUAL=y +# CONFIG_DEBUG_VIRTUAL is not set +CONFIG_DEBUG_MEMORY_INIT=y +# CONFIG_DEBUG_PER_CPU_MAPS is not set +CONFIG_ARCH_SUPPORTS_KMAP_LOCAL_FORCE_MAP=y +# CONFIG_DEBUG_KMAP_LOCAL_FORCE_MAP is not set +CONFIG_HAVE_ARCH_KASAN=y +CONFIG_HAVE_ARCH_KASAN_VMALLOC=y +CONFIG_CC_HAS_KASAN_GENERIC=y +CONFIG_CC_HAS_WORKING_NOSANITIZE_ADDRESS=y +# CONFIG_KASAN is not set +CONFIG_HAVE_ARCH_KFENCE=y +# CONFIG_KFENCE is not set +CONFIG_HAVE_ARCH_KMSAN=y +# end of Memory Debugging + +# CONFIG_DEBUG_SHIRQ is not set + +# +# Debug Oops, Lockups and Hangs +# +# CONFIG_PANIC_ON_OOPS is not set +CONFIG_PANIC_ON_OOPS_VALUE=0 +CONFIG_PANIC_TIMEOUT=0 +# CONFIG_SOFTLOCKUP_DETECTOR is not set +CONFIG_HARDLOCKUP_CHECK_TIMESTAMP=y +# CONFIG_HARDLOCKUP_DETECTOR is not set +# CONFIG_DETECT_HUNG_TASK is not set +# CONFIG_WQ_WATCHDOG is not set +# end of Debug Oops, Lockups and Hangs + +# +# Scheduler Debugging +# +# CONFIG_SCHED_DEBUG is not set +CONFIG_SCHED_INFO=y +# CONFIG_SCHEDSTATS is not set +# end of Scheduler Debugging + +# CONFIG_DEBUG_TIMEKEEPING is not set +# CONFIG_DEBUG_PREEMPT is not set + +# +# Lock Debugging (spinlocks, mutexes, etc...) +# +CONFIG_LOCK_DEBUGGING_SUPPORT=y +# CONFIG_PROVE_LOCKING is not set +# CONFIG_LOCK_STAT is not set +# CONFIG_DEBUG_RT_MUTEXES is not set +# CONFIG_DEBUG_SPINLOCK is not set +# CONFIG_DEBUG_MUTEXES is not set +# CONFIG_DEBUG_WW_MUTEX_SLOWPATH is not set +# CONFIG_DEBUG_RWSEMS is not set +# CONFIG_DEBUG_LOCK_ALLOC is not set +# CONFIG_DEBUG_ATOMIC_SLEEP is not set +# CONFIG_DEBUG_LOCKING_API_SELFTESTS is not set +# CONFIG_LOCK_TORTURE_TEST is not set +# CONFIG_WW_MUTEX_SELFTEST is not set +# CONFIG_SCF_TORTURE_TEST is not set +# CONFIG_CSD_LOCK_WAIT_DEBUG is not set +# end of Lock Debugging (spinlocks, mutexes, etc...) + +# CONFIG_DEBUG_IRQFLAGS is not set +CONFIG_STACKTRACE=y +# CONFIG_WARN_ALL_UNSEEDED_RANDOM is not set +# CONFIG_DEBUG_KOBJECT is not set + +# +# Debug kernel data structures +# +CONFIG_DEBUG_LIST=y +# CONFIG_DEBUG_PLIST is not set +# CONFIG_DEBUG_SG is not set +# CONFIG_DEBUG_NOTIFIERS is not set +CONFIG_BUG_ON_DATA_CORRUPTION=y +# CONFIG_DEBUG_MAPLE_TREE is not set +# end of Debug kernel data structures + +# CONFIG_DEBUG_CREDENTIALS is not set + +# +# RCU Debugging +# +# CONFIG_RCU_SCALE_TEST is not set +# CONFIG_RCU_TORTURE_TEST is not set +# CONFIG_RCU_REF_SCALE_TEST is not set +CONFIG_RCU_CPU_STALL_TIMEOUT=59 +CONFIG_RCU_EXP_CPU_STALL_TIMEOUT=0 +# CONFIG_RCU_TRACE is not set +# CONFIG_RCU_EQS_DEBUG is not set +# end of RCU Debugging + +# CONFIG_DEBUG_WQ_FORCE_RR_CPU is not set +# CONFIG_CPU_HOTPLUG_STATE_CONTROL is not set +# CONFIG_LATENCYTOP is not set +CONFIG_USER_STACKTRACE_SUPPORT=y +CONFIG_HAVE_RETHOOK=y +CONFIG_HAVE_FUNCTION_TRACER=y +CONFIG_HAVE_DYNAMIC_FTRACE=y +CONFIG_HAVE_DYNAMIC_FTRACE_WITH_REGS=y +CONFIG_HAVE_DYNAMIC_FTRACE_WITH_DIRECT_CALLS=y +CONFIG_HAVE_DYNAMIC_FTRACE_WITH_ARGS=y +CONFIG_HAVE_DYNAMIC_FTRACE_NO_PATCHABLE=y +CONFIG_HAVE_FTRACE_MCOUNT_RECORD=y +CONFIG_HAVE_SYSCALL_TRACEPOINTS=y +CONFIG_HAVE_FENTRY=y +CONFIG_HAVE_OBJTOOL_MCOUNT=y +CONFIG_HAVE_C_RECORDMCOUNT=y +CONFIG_HAVE_BUILDTIME_MCOUNT_SORT=y +CONFIG_TRACING_SUPPORT=y +# CONFIG_FTRACE is not set +# CONFIG_PROVIDE_OHCI1394_DMA_INIT is not set +# CONFIG_SAMPLES is not set +CONFIG_HAVE_SAMPLE_FTRACE_DIRECT=y +CONFIG_HAVE_SAMPLE_FTRACE_DIRECT_MULTI=y +CONFIG_ARCH_HAS_DEVMEM_IS_ALLOWED=y +CONFIG_STRICT_DEVMEM=y +# CONFIG_IO_STRICT_DEVMEM is not set + +# +# x86 Debugging +# +CONFIG_X86_VERBOSE_BOOTUP=y +CONFIG_EARLY_PRINTK=y +# CONFIG_EARLY_PRINTK_DBGP is not set +# CONFIG_EARLY_PRINTK_USB_XDBC is not set +# CONFIG_EFI_PGT_DUMP is not set +# CONFIG_DEBUG_TLBFLUSH is not set +CONFIG_HAVE_MMIOTRACE_SUPPORT=y +# CONFIG_X86_DECODER_SELFTEST is not set +CONFIG_IO_DELAY_0X80=y +# CONFIG_IO_DELAY_0XED is not set +# CONFIG_IO_DELAY_UDELAY is not set +# CONFIG_IO_DELAY_NONE is not set +# CONFIG_DEBUG_BOOT_PARAMS is not set +# CONFIG_CPA_DEBUG is not set +# CONFIG_DEBUG_ENTRY is not set +# CONFIG_DEBUG_NMI_SELFTEST is not set +# CONFIG_X86_DEBUG_FPU is not set +# CONFIG_PUNIT_ATOM_DEBUG is not set +# CONFIG_UNWINDER_ORC is not set +CONFIG_UNWINDER_FRAME_POINTER=y +# end of x86 Debugging + +# +# Kernel Testing and Coverage +# +# CONFIG_KUNIT is not set +# CONFIG_NOTIFIER_ERROR_INJECTION is not set +# CONFIG_FAULT_INJECTION is not set +CONFIG_ARCH_HAS_KCOV=y +CONFIG_CC_HAS_SANCOV_TRACE_PC=y +# CONFIG_KCOV is not set +CONFIG_RUNTIME_TESTING_MENU=y +# CONFIG_LKDTM is not set +# CONFIG_TEST_MIN_HEAP is not set +# CONFIG_TEST_DIV64 is not set +# CONFIG_BACKTRACE_SELF_TEST is not set +# CONFIG_TEST_REF_TRACKER is not set +# CONFIG_RBTREE_TEST is not set +# CONFIG_REED_SOLOMON_TEST is not set +# CONFIG_INTERVAL_TREE_TEST is not set +# CONFIG_ATOMIC64_SELFTEST is not set +# CONFIG_TEST_HEXDUMP is not set +# CONFIG_STRING_SELFTEST is not set +# CONFIG_TEST_STRING_HELPERS is not set +# CONFIG_TEST_STRSCPY is not set +# CONFIG_TEST_KSTRTOX is not set +# CONFIG_TEST_PRINTF is not set +# CONFIG_TEST_SCANF is not set +# CONFIG_TEST_BITMAP is not set +# CONFIG_TEST_UUID is not set +# CONFIG_TEST_XARRAY is not set +# CONFIG_TEST_MAPLE_TREE is not set +# CONFIG_TEST_RHASHTABLE is not set +# CONFIG_TEST_SIPHASH is not set +# CONFIG_TEST_IDA is not set +# CONFIG_FIND_BIT_BENCHMARK is not set +# CONFIG_TEST_FIRMWARE is not set +# CONFIG_TEST_SYSCTL is not set +# CONFIG_TEST_UDELAY is not set +# CONFIG_TEST_DYNAMIC_DEBUG is not set +# CONFIG_TEST_MEMCAT_P is not set +# CONFIG_TEST_MEMINIT is not set +# CONFIG_TEST_FREE_PAGES is not set +# CONFIG_TEST_FPU is not set +# CONFIG_TEST_CLOCKSOURCE_WATCHDOG is not set +CONFIG_ARCH_USE_MEMTEST=y +# CONFIG_MEMTEST is not set +# end of Kernel Testing and Coverage + +# +# Rust hacking +# +# end of Rust hacking +# end of Kernel hacking diff --git a/kernel/image/Dockerfile b/kernel/image/Dockerfile index 654ee978..99a98573 100644 --- a/kernel/image/Dockerfile +++ b/kernel/image/Dockerfile @@ -5,11 +5,13 @@ RUN apt-get update && apt-get install -y \ bc \ binutils-multiarch \ binutils-aarch64-linux-gnu \ + binutils-x86-64-linux-gnu \ bison \ flex \ gcc \ xz-utils \ gcc-aarch64-linux-gnu \ + gcc-x86-64-linux-gnu \ git \ libncurses-dev \ make \ @@ -22,6 +24,7 @@ COPY sources.list /etc/apt/sources.list RUN apt-get update \ && dpkg --add-architecture arm64 \ -&& apt-get install -y libelf-dev:arm64 \ +&& dpkg --add-architecture amd64 \ +&& apt-get install -y libelf-dev:arm64 libelf-dev:amd64 \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ No newline at end of file From 7e2ae5a170137bcca214e0e0f8fdaa6c918c2190 Mon Sep 17 00:00:00 2001 From: Mojtaba Hosseini Date: Tue, 16 Jun 2026 23:11:46 +0330 Subject: [PATCH 07/11] docs: fix `traversal` in a public method's documentation (#770) Seems like a typo. The change only affected a comment and does not disturb the code at all (backward compatible). Co-authored-by: J Logan --- Sources/ContainerizationArchive/ArchiveReader.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ContainerizationArchive/ArchiveReader.swift b/Sources/ContainerizationArchive/ArchiveReader.swift index 21d7dade..0477d8e4 100644 --- a/Sources/ContainerizationArchive/ArchiveReader.swift +++ b/Sources/ContainerizationArchive/ArchiveReader.swift @@ -274,7 +274,7 @@ extension ArchiveReader { /// for an existing file at a path to be extracted. public func extractContents(to directory: URL) throws -> [String] { // Create the root directory with standard permissions - // and create a FileDescriptor for secure path traveral. + // and create a FileDescriptor for secure path traversal. let fm = FileManager.default let rootFilePath = FilePath(directory.path) try fm.createDirectory(atPath: directory.path, withIntermediateDirectories: true) From d55cc188ce0dcf183b1f659e8a0da3234c233076 Mon Sep 17 00:00:00 2001 From: Aditya Ramani Date: Tue, 16 Jun 2026 18:04:31 -0700 Subject: [PATCH 08/11] Allow setting log level for vminitd (#772) The `--log-level` option when running the agent sub-command for vminitd was being silently ignored cause of the way the agent is being run. As a workaround we need to read `/proc/self/cmdline` to get the right args --- Sources/Containerization/Kernel.swift | 6 + Tests/ContainerizationTests/KernelTests.swift | 35 ++++++ examples/ctr-example/Package.resolved | 117 +++++++++++------- examples/ctr-example/Package.swift | 2 +- vminitd/Sources/VminitdCore/Logging.swift | 6 +- vminitd/Sources/vminitd/Application.swift | 15 ++- 6 files changed, 133 insertions(+), 48 deletions(-) diff --git a/Sources/Containerization/Kernel.swift b/Sources/Containerization/Kernel.swift index a7c91d39..c4c2468d 100644 --- a/Sources/Containerization/Kernel.swift +++ b/Sources/Containerization/Kernel.swift @@ -15,6 +15,7 @@ //===----------------------------------------------------------------------===// import Foundation +import Logging /// An object representing a Linux kernel used to boot a virtual machine. /// In addition to a path to the kernel itself, this type stores relevant @@ -37,6 +38,11 @@ public struct Kernel: Sendable, Codable { self.kernelArgs.append("panic=\(level)") } + // Sets the log level for the Agent + mutating public func setAgentLogLevel(level: Logger.Level) { + self.initArgs.append(contentsOf: ["--log-level", level.description]) + } + /// Additional kernel arguments. public var kernelArgs: [String] /// Additional arguments passed to the Initial Process / Agent. diff --git a/Tests/ContainerizationTests/KernelTests.swift b/Tests/ContainerizationTests/KernelTests.swift index 66b03f0e..dcc087a0 100644 --- a/Tests/ContainerizationTests/KernelTests.swift +++ b/Tests/ContainerizationTests/KernelTests.swift @@ -17,6 +17,7 @@ // import Foundation +import Logging import Testing @testable import Containerization @@ -55,4 +56,38 @@ final class KernelTests { #expect(commandLine.kernelArgs == ["console=hvc0", "debug", "panic=10"]) } + + @Test func setAgentLogLevelAppendsFlagAndValue() { + var commandLine = Kernel.CommandLine(initArgs: []) + commandLine.setAgentLogLevel(level: .debug) + #expect(commandLine.initArgs == ["--log-level", "debug"]) + } + + @Test(arguments: [ + (Logger.Level.trace, "trace"), + (.debug, "debug"), + (.info, "info"), + (.notice, "notice"), + (.warning, "warning"), + (.error, "error"), + (.critical, "critical"), + ]) + func setAgentLogLevelForEachLevel(level: Logger.Level, expected: String) { + var commandLine = Kernel.CommandLine(initArgs: []) + commandLine.setAgentLogLevel(level: level) + #expect(commandLine.initArgs == ["--log-level", expected]) + } + + @Test func setAgentLogLevelPreservesExistingInitArgs() { + var commandLine = Kernel.CommandLine(initArgs: ["--verbose"]) + commandLine.setAgentLogLevel(level: .info) + #expect(commandLine.initArgs == ["--verbose", "--log-level", "info"]) + } + + @Test func setAgentLogLevelDoesNotAffectKernelArgs() { + var commandLine = Kernel.CommandLine(debug: true, panic: 0, initArgs: []) + let kernelArgsBefore = commandLine.kernelArgs + commandLine.setAgentLogLevel(level: .warning) + #expect(commandLine.kernelArgs == kernelArgsBefore) + } } diff --git a/examples/ctr-example/Package.resolved b/examples/ctr-example/Package.resolved index d50e3654..848cc075 100644 --- a/examples/ctr-example/Package.resolved +++ b/examples/ctr-example/Package.resolved @@ -1,13 +1,13 @@ { - "originHash" : "5de11e9b526f881c570e7b65cb339765f3aa79e8646a0c1289d36f224f9f8ca0", + "originHash" : "f9b523ded39c0fa3565fd16d3b4097384449d541048751be7efee4a43af79fd4", "pins" : [ { "identity" : "async-http-client", "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "b2faff932b956df50668241d14f1b42f7bae12b4", - "version" : "1.30.0" + "revision" : "7744c2a035c68ec14726c709f031835e3e30bde1", + "version" : "1.34.0" } }, { @@ -15,17 +15,35 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/containerization.git", "state" : { - "revision" : "636eef0eff00e451de6d5d426e6a6785b90b44e2", - "version" : "0.26.5" + "revision" : "9275f365dd555c8f072e7d250d809f5eb7bdd746", + "version" : "0.33.4" } }, { - "identity" : "grpc-swift", + "identity" : "grpc-swift-2", "kind" : "remoteSourceControl", - "location" : "https://github.com/grpc/grpc-swift.git", + "location" : "https://github.com/grpc/grpc-swift-2.git", "state" : { - "revision" : "f857994e146f5146d702e9c31ac6f3c27d55d18a", - "version" : "1.27.0" + "revision" : "21fe69ab7ce0e87ac089534733c52f037e74a3eb", + "version" : "2.4.1" + } + }, + { + "identity" : "grpc-swift-nio-transport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift-nio-transport.git", + "state" : { + "revision" : "e7d463749f9037b047dcd6da4e633718ddc432b8", + "version" : "2.8.0" + } + }, + { + "identity" : "grpc-swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/grpc/grpc-swift-protobuf.git", + "state" : { + "revision" : "b05885fa9bdd88f1eab2e7162f1ee81340b0da33", + "version" : "2.4.0" } }, { @@ -42,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "cdd0ef3755280949551dc26dee5de9ddeda89f54", - "version" : "1.6.2" + "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", + "version" : "1.8.2" } }, { @@ -51,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-asn1.git", "state" : { - "revision" : "40d25bbb2fc5b557a9aa8512210bded327c0f60d", - "version" : "1.5.0" + "revision" : "a9a5efd40eaf558a2bcd48d64b1d1646be686008", + "version" : "1.7.1" } }, { @@ -60,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-async-algorithms.git", "state" : { - "revision" : "042e1c4d9d19748c9c228f8d4ebc97bb1e339b0b", - "version" : "1.0.4" + "revision" : "d0b4a06d0f173a2f3be27d3ea21b3c3aa18db440", + "version" : "1.1.4" } }, { @@ -78,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-certificates.git", "state" : { - "revision" : "c399f90e7bbe8874f6cbfda1d5f9023d1f5ce122", - "version" : "1.15.1" + "revision" : "bde8ca32a096825dfce37467137c903418c1893d", + "version" : "1.19.1" } }, { @@ -87,8 +105,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", - "version" : "1.3.0" + "revision" : "a0cb0954ecb21e4e31b0070e6ed5674e8556685a", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-configuration", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-configuration.git", + "state" : { + "revision" : "be76c4ad929eb6c4bcaf3351799f2adf9e6848a9", + "version" : "1.2.0" } }, { @@ -105,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-distributed-tracing.git", "state" : { - "revision" : "baa932c1336f7894145cbaafcd34ce2dd0b77c97", - "version" : "1.3.1" + "revision" : "dc4030184203ffafbb2ec614352487235d747fe0", + "version" : "1.4.1" } }, { @@ -114,8 +141,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-structured-headers.git", "state" : { - "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", - "version" : "1.6.0" + "revision" : "933538faa42c432d385f02e07df0ace7c5ecfc47", + "version" : "1.7.0" } }, { @@ -123,8 +150,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-http-types.git", "state" : { - "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", - "version" : "1.5.1" + "revision" : "db774a277f60063a32d854f2980299caf06da041", + "version" : "1.6.0" } }, { @@ -132,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2", - "version" : "1.6.4" + "revision" : "92448c359f00ebe36ae97d3bd9086f13c7692b5a", + "version" : "1.13.2" } }, { @@ -141,8 +168,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "56724a2b6d8e2aed1b2c5f23865b9ea5c43f9977", - "version" : "2.89.0" + "revision" : "77b84ac2cd2ac9e4ac67d19f045fd5b434f56967", + "version" : "2.101.0" } }, { @@ -150,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "7ee281d816fa8e5f3967a2c294035a318ea551c7", - "version" : "1.31.0" + "revision" : "d2eeec0339074034f11a040a74aa2a341a2c4506", + "version" : "1.34.1" } }, { @@ -159,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "c2ba4cfbb83f307c66f5a6df6bb43e3c88dfbf80", - "version" : "1.39.0" + "revision" : "61d1b44f6e4e118792be1cff88ee2bc0267c6f9a", + "version" : "1.44.0" } }, { @@ -168,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "173cc69a058623525a58ae6710e2f5727c663793", - "version" : "2.36.0" + "revision" : "407d82d5b6cc00e1c3fb83a81b1539b70c788c5e", + "version" : "2.37.1" } }, { @@ -177,8 +204,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { - "revision" : "df6c28355051c72c884574a6c858bc54f7311ff9", - "version" : "1.25.2" + "revision" : "67787bb645a5e67d2edcdfbe48a216cc549222d5", + "version" : "1.28.0" } }, { @@ -195,8 +222,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-protobuf.git", "state" : { - "revision" : "c169a5744230951031770e27e475ff6eefe51f9d", - "version" : "1.33.3" + "revision" : "f6506eaa86ed2e01cb0ae14a75035b7fdbf0918f", + "version" : "1.38.0" } }, { @@ -204,8 +231,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-service-context.git", "state" : { - "revision" : "1983448fefc717a2bc2ebde5490fe99873c5b8a6", - "version" : "1.2.1" + "revision" : "d0997351b0c7779017f88e7a93bc30a1878d7f29", + "version" : "1.3.0" } }, { @@ -213,8 +240,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/swift-service-lifecycle.git", "state" : { - "revision" : "1de37290c0ab3c5a96028e0f02911b672fd42348", - "version" : "2.9.1" + "revision" : "9829955b385e5bb88128b73f1b8389e9b9c3191a", + "version" : "2.11.0" } }, { @@ -222,8 +249,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-system.git", "state" : { - "revision" : "395a77f0aa927f0ff73941d7ac35f2b46d47c9db", - "version" : "1.6.3" + "revision" : "7502b711c92a17741fa625d722b0ccbd595d8ed1", + "version" : "1.7.2" } }, { diff --git a/examples/ctr-example/Package.swift b/examples/ctr-example/Package.swift index c90f7ebe..15b649cb 100644 --- a/examples/ctr-example/Package.swift +++ b/examples/ctr-example/Package.swift @@ -17,7 +17,7 @@ import PackageDescription -let scVersion = "0.26.5" +let scVersion = "0.33.4" let package = Package( name: "ctr-example", diff --git a/vminitd/Sources/VminitdCore/Logging.swift b/vminitd/Sources/VminitdCore/Logging.swift index 6c56da09..845d769e 100644 --- a/vminitd/Sources/VminitdCore/Logging.swift +++ b/vminitd/Sources/VminitdCore/Logging.swift @@ -23,10 +23,14 @@ import Synchronization public struct LogLevelOption: ParsableArguments { @Option(name: .long, help: "Set the log level (trace, debug, info, notice, warning, error, critical)") - var logLevel: String = "info" + public var logLevel: String = "info" public init() {} + public init(logLevel: String) { + self.logLevel = logLevel + } + public func resolvedLogLevel() -> Logger.Level { switch logLevel.lowercased() { case "trace": diff --git a/vminitd/Sources/vminitd/Application.swift b/vminitd/Sources/vminitd/Application.swift index 21009062..866f6b33 100644 --- a/vminitd/Sources/vminitd/Application.swift +++ b/vminitd/Sources/vminitd/Application.swift @@ -51,7 +51,9 @@ struct Application: AsyncParsableCommand { // so we do this synchronously before any async code runs. try mountProc() - var command = try parseAsRoot() + // When running as PID 1 with a Musl-static build, Swift's runtime + // captures argc/argv as empty. Recover argv from /proc/self/cmdline. + var command = try parseAsRoot(Self.procSelfArgv()) if let asyncCommand = command as? AsyncParsableCommand { nonisolated(unsafe) var unsafeCommand = asyncCommand try await unsafeCommand.run() @@ -85,6 +87,17 @@ struct Application: AsyncParsableCommand { try mnt.mount(createWithPerms: 0o755) } + // /proc/self/cmdline holds argv as NUL-separated bytes. Read it after + // mountProc(). Returns argv minus argv[0], suitable for parseAsRoot(_:). + private static func procSelfArgv() -> [String] { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: "/proc/self/cmdline")) else { + return [] + } + let parts = data.split(separator: 0, omittingEmptySubsequences: true) + .map { String(decoding: $0, as: UTF8.self) } + return Array(parts.dropFirst()) + } + private static func isProcMounted() -> Bool { guard let data = try? String(contentsOfFile: "/proc/mounts", encoding: .utf8) else { return false From 35b4acbc10cf63202f4838115b3f5a74c21faa65 Mon Sep 17 00:00:00 2001 From: Chris George Date: Wed, 6 May 2026 09:32:38 -0700 Subject: [PATCH 09/11] Add LinuxContainer block I/O resources --- Sources/Containerization/LinuxContainer.swift | 9 +++- .../LinuxContainerTests.swift | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index 58ffa4a3..559f2335 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -57,6 +57,8 @@ public final class LinuxContainer: Container, Sendable { public var cpus: Int = 4 /// The memory in bytes to give to the container. public var memoryInBytes: UInt64 = 1024.mib() + /// Optional block I/O resource limits for the container cgroup. + public var blockIO: LinuxBlockIO? /// The hostname for the container. public var hostname: String? /// The system control options for the container. @@ -95,6 +97,7 @@ public final class LinuxContainer: Container, Sendable { process: LinuxProcessConfiguration, cpus: Int = 4, memoryInBytes: UInt64 = 1024.mib(), + blockIO: LinuxBlockIO? = nil, hostname: String? = nil, sysctl: [String: String] = [:], interfaces: [any Interface] = [], @@ -112,6 +115,7 @@ public final class LinuxContainer: Container, Sendable { self.process = process self.cpus = cpus self.memoryInBytes = memoryInBytes + self.blockIO = blockIO self.hostname = hostname self.sysctl = sysctl self.interfaces = interfaces @@ -375,7 +379,7 @@ public final class LinuxContainer: Container, Sendable { ) } - private func generateRuntimeSpec() -> Spec { + func generateRuntimeSpec() -> Spec { var spec = Self.createDefaultRuntimeSpec(id) // Process toggles. @@ -410,7 +414,8 @@ public final class LinuxContainer: Container, Sendable { cpu: LinuxCPU( quota: Int64(config.cpus * 100_000), period: 100_000 - ) + ), + blockIO: config.blockIO ) spec.linux?.namespaces = [ diff --git a/Tests/ContainerizationTests/LinuxContainerTests.swift b/Tests/ContainerizationTests/LinuxContainerTests.swift index ddf4a678..a0a8b622 100644 --- a/Tests/ContainerizationTests/LinuxContainerTests.swift +++ b/Tests/ContainerizationTests/LinuxContainerTests.swift @@ -66,4 +66,53 @@ struct LinuxContainerTests { #expect(process.arguments == ["/bin/sh", "-c", "echo 'hello'", "&&", "sleep 10"]) } + + @Test func runtimeSpecIncludesConfiguredBlockIO() throws { + let blockIO = LinuxBlockIO( + weight: 500, + leafWeight: 300, + weightDevice: [ + LinuxWeightDevice(major: 8, minor: 0, weight: 700, leafWeight: 400) + ], + throttleReadBpsDevice: [ + LinuxThrottleDevice(major: 8, minor: 16, rate: 1_048_576) + ], + throttleWriteBpsDevice: [ + LinuxThrottleDevice(major: 8, minor: 32, rate: 2_097_152) + ], + throttleReadIOPSDevice: [ + LinuxThrottleDevice(major: 8, minor: 48, rate: 1_000) + ], + throttleWriteIOPSDevice: [ + LinuxThrottleDevice(major: 8, minor: 64, rate: 2_000) + ] + ) + + let container = try LinuxContainer( + "blkio-test", + rootfs: .block(format: "ext4", source: "/tmp/rootfs.img", destination: "/"), + vmm: StubVirtualMachineManager(), + configuration: .init(process: .init(), blockIO: blockIO) + ) + + let resources = try #require(container.generateRuntimeSpec().linux?.resources) + let specBlockIO = try #require(resources.blockIO) + + #expect(specBlockIO.weight == 500) + #expect(specBlockIO.leafWeight == 300) + #expect(specBlockIO.weightDevice.first?.major == 8) + #expect(specBlockIO.weightDevice.first?.minor == 0) + #expect(specBlockIO.weightDevice.first?.weight == 700) + #expect(specBlockIO.weightDevice.first?.leafWeight == 400) + #expect(specBlockIO.throttleReadBpsDevice.first?.rate == 1_048_576) + #expect(specBlockIO.throttleWriteBpsDevice.first?.rate == 2_097_152) + #expect(specBlockIO.throttleReadIOPSDevice.first?.rate == 1_000) + #expect(specBlockIO.throttleWriteIOPSDevice.first?.rate == 2_000) + } +} + +private struct StubVirtualMachineManager: VirtualMachineManager { + func create(config: some VMCreationConfig) async throws -> any VirtualMachineInstance { + fatalError("StubVirtualMachineManager.create should not be called by LinuxContainerTests") + } } From f361c3499672bd1df04f45142fe73b281097f938 Mon Sep 17 00:00:00 2001 From: Chris George Date: Thu, 14 May 2026 15:13:59 -0700 Subject: [PATCH 10/11] Wrap LinuxBlockIO with a Containerization type Mirrors the LinuxRLimit/LinuxCapabilities pattern so the public API can evolve independently of the OCI spec types. Configuration.blockIO now holds the wrapper and is converted via toOCI() at spec assembly. --- Sources/Containerization/LinuxBlockIO.swift | 125 ++++++++++++++++++ Sources/Containerization/LinuxContainer.swift | 2 +- .../LinuxContainerTests.swift | 3 +- 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 Sources/Containerization/LinuxBlockIO.swift diff --git a/Sources/Containerization/LinuxBlockIO.swift b/Sources/Containerization/LinuxBlockIO.swift new file mode 100644 index 00000000..486be9b0 --- /dev/null +++ b/Sources/Containerization/LinuxBlockIO.swift @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025-2026 Apple Inc. and the Containerization project authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +import ContainerizationOCI + +/// Block I/O resource limits applied to the container cgroup. +public struct LinuxBlockIO: Sendable { + /// The relative weight of the cgroup for block I/O. Valid range is 10 to 1000. + public var weight: UInt16? + /// The relative weight applied to tasks of the cgroup but not their descendant cgroups. + public var leafWeight: UInt16? + /// Per-device weight overrides. + public var weightDevice: [LinuxWeightDevice] + /// Per-device read rate limits in bytes per second. + public var throttleReadBpsDevice: [LinuxThrottleDevice] + /// Per-device write rate limits in bytes per second. + public var throttleWriteBpsDevice: [LinuxThrottleDevice] + /// Per-device read rate limits in IO operations per second. + public var throttleReadIOPSDevice: [LinuxThrottleDevice] + /// Per-device write rate limits in IO operations per second. + public var throttleWriteIOPSDevice: [LinuxThrottleDevice] + + public init( + weight: UInt16? = nil, + leafWeight: UInt16? = nil, + weightDevice: [LinuxWeightDevice] = [], + throttleReadBpsDevice: [LinuxThrottleDevice] = [], + throttleWriteBpsDevice: [LinuxThrottleDevice] = [], + throttleReadIOPSDevice: [LinuxThrottleDevice] = [], + throttleWriteIOPSDevice: [LinuxThrottleDevice] = [] + ) { + self.weight = weight + self.leafWeight = leafWeight + self.weightDevice = weightDevice + self.throttleReadBpsDevice = throttleReadBpsDevice + self.throttleWriteBpsDevice = throttleWriteBpsDevice + self.throttleReadIOPSDevice = throttleReadIOPSDevice + self.throttleWriteIOPSDevice = throttleWriteIOPSDevice + } + + /// Convert to OCI format for transport. + public func toOCI() -> ContainerizationOCI.LinuxBlockIO { + ContainerizationOCI.LinuxBlockIO( + weight: self.weight, + leafWeight: self.leafWeight, + weightDevice: self.weightDevice.map { $0.toOCI() }, + throttleReadBpsDevice: self.throttleReadBpsDevice.map { $0.toOCI() }, + throttleWriteBpsDevice: self.throttleWriteBpsDevice.map { $0.toOCI() }, + throttleReadIOPSDevice: self.throttleReadIOPSDevice.map { $0.toOCI() }, + throttleWriteIOPSDevice: self.throttleWriteIOPSDevice.map { $0.toOCI() } + ) + } +} + +/// A per-device block I/O weight override. +public struct LinuxWeightDevice: Sendable { + /// The major device number. + public var major: Int64 + /// The minor device number. + public var minor: Int64 + /// The relative weight applied to the device. Valid range is 10 to 1000. + public var weight: UInt16? + /// The relative weight applied to tasks of the cgroup but not their descendant cgroups. + public var leafWeight: UInt16? + + public init( + major: Int64, + minor: Int64, + weight: UInt16? = nil, + leafWeight: UInt16? = nil + ) { + self.major = major + self.minor = minor + self.weight = weight + self.leafWeight = leafWeight + } + + /// Convert to OCI format for transport. + public func toOCI() -> ContainerizationOCI.LinuxWeightDevice { + ContainerizationOCI.LinuxWeightDevice( + major: self.major, + minor: self.minor, + weight: self.weight, + leafWeight: self.leafWeight + ) + } +} + +/// A per-device block I/O throughput limit. +public struct LinuxThrottleDevice: Sendable { + /// The major device number. + public var major: Int64 + /// The minor device number. + public var minor: Int64 + /// The rate limit applied to the device. + public var rate: UInt64 + + public init(major: Int64, minor: Int64, rate: UInt64) { + self.major = major + self.minor = minor + self.rate = rate + } + + /// Convert to OCI format for transport. + public func toOCI() -> ContainerizationOCI.LinuxThrottleDevice { + ContainerizationOCI.LinuxThrottleDevice( + major: self.major, + minor: self.minor, + rate: self.rate + ) + } +} diff --git a/Sources/Containerization/LinuxContainer.swift b/Sources/Containerization/LinuxContainer.swift index 559f2335..0fd053be 100644 --- a/Sources/Containerization/LinuxContainer.swift +++ b/Sources/Containerization/LinuxContainer.swift @@ -415,7 +415,7 @@ public final class LinuxContainer: Container, Sendable { quota: Int64(config.cpus * 100_000), period: 100_000 ), - blockIO: config.blockIO + blockIO: config.blockIO?.toOCI() ) spec.linux?.namespaces = [ diff --git a/Tests/ContainerizationTests/LinuxContainerTests.swift b/Tests/ContainerizationTests/LinuxContainerTests.swift index a0a8b622..80bcf704 100644 --- a/Tests/ContainerizationTests/LinuxContainerTests.swift +++ b/Tests/ContainerizationTests/LinuxContainerTests.swift @@ -14,12 +14,13 @@ // limitations under the License. //===----------------------------------------------------------------------===// -import ContainerizationOCI import Foundation import Testing @testable import Containerization +import struct ContainerizationOCI.ImageConfig + struct LinuxContainerTests { @Test func processInitFromImageConfigWithAllFields() { From dffb9142178428c9226e85450284744f8bf4462d Mon Sep 17 00:00:00 2001 From: Chris George Date: Mon, 1 Jun 2026 08:47:30 -0700 Subject: [PATCH 11/11] Fix formatting and license header for block I/O changes --- Sources/Containerization/LinuxBlockIO.swift | 2 +- Tests/ContainerizationTests/LinuxContainerTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Containerization/LinuxBlockIO.swift b/Sources/Containerization/LinuxBlockIO.swift index 486be9b0..e6858040 100644 --- a/Sources/Containerization/LinuxBlockIO.swift +++ b/Sources/Containerization/LinuxBlockIO.swift @@ -1,5 +1,5 @@ //===----------------------------------------------------------------------===// -// Copyright © 2025-2026 Apple Inc. and the Containerization project authors. +// Copyright © 2026 Apple Inc. and the Containerization project authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/Tests/ContainerizationTests/LinuxContainerTests.swift b/Tests/ContainerizationTests/LinuxContainerTests.swift index 80bcf704..389e28bb 100644 --- a/Tests/ContainerizationTests/LinuxContainerTests.swift +++ b/Tests/ContainerizationTests/LinuxContainerTests.swift @@ -17,10 +17,10 @@ import Foundation import Testing -@testable import Containerization - import struct ContainerizationOCI.ImageConfig +@testable import Containerization + struct LinuxContainerTests { @Test func processInitFromImageConfigWithAllFields() {