//===----------------------------------------------------------------------===// // 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 ContainerizationError import ContainerizationOCI import ContainerizationOS /// A resource limit (rlimit) configuration for a container process. public struct LinuxRLimit: Sendable, Hashable { /// The kind of resource limit. public var kind: Kind /// The hard limit value. public var hard: UInt64 /// The soft limit value. public var soft: UInt64 /// Creates a new resource limit. /// /// - Parameters: /// - kind: The kind of resource limit. /// - hard: The hard limit value. /// - soft: The soft limit value. public init(kind: Kind, hard: UInt64, soft: UInt64) { self.kind = kind self.hard = hard self.soft = soft } /// Creates a new resource limit with the same value for both hard and soft limits. /// /// - Parameters: /// - kind: The kind of resource limit. /// - limit: The limit value for both hard and soft limits. public init(kind: Kind, limit: UInt64) { self.kind = kind self.hard = limit self.soft = limit } /// Convert to OCI POSIXRlimit format for transport. public func toOCI() -> POSIXRlimit { POSIXRlimit(type: self.kind.description, hard: self.hard, soft: self.soft) } } extension LinuxRLimit { /// The kind of resource limit. public struct Kind: Sendable, Hashable { private enum Value: Hashable, Sendable, CaseIterable { case addressSpace case coreFileSize case cpuTime case dataSize case fileSize case locks case lockedMemory case messageQueue case nice case openFiles case numberOfProcesses case residentSetSize case realtimePriority case realtimeTimeout case signalsPending case stackSize } private var value: Value private init(_ value: Value) { self.value = value } /// Maximum size of the process's virtual memory (address space) in bytes. public static var addressSpace: Self { Self(.addressSpace) } /// Maximum size of a core file in bytes. public static var coreFileSize: Self { Self(.coreFileSize) } /// Maximum amount of CPU time the process can consume in seconds. public static var cpuTime: Self { Self(.cpuTime) } /// Maximum size of the process's data segment in bytes. public static var dataSize: Self { Self(.dataSize) } /// Maximum size of files the process may create in bytes. public static var fileSize: Self { Self(.fileSize) } /// Maximum number of file locks. public static var locks: Self { Self(.locks) } /// Maximum number of bytes of memory that may be locked into RAM. public static var lockedMemory: Self { Self(.lockedMemory) } /// Maximum number of bytes that can be allocated for POSIX message queues. public static var messageQueue: Self { Self(.messageQueue) } /// Maximum nice value that can be set. public static var nice: Self { Self(.nice) } /// Maximum number of open file descriptors. public static var openFiles: Self { Self(.openFiles) } /// Maximum number of processes that can be created by the user. public static var numberOfProcesses: Self { Self(.numberOfProcesses) } /// Maximum size of the process's resident set (physical memory) in bytes. public static var residentSetSize: Self { Self(.residentSetSize) } /// Maximum real-time scheduling priority. public static var realtimePriority: Self { Self(.realtimePriority) } /// Maximum amount of CPU time for real-time scheduling in microseconds. public static var realtimeTimeout: Self { Self(.realtimeTimeout) } /// Maximum number of signals that may be queued. public static var signalsPending: Self { Self(.signalsPending) } /// Maximum size of the process stack in bytes. public static var stackSize: Self { Self(.stackSize) } /// Creates a Kind from its OCI string representation. /// /// - Parameter string: The OCI string representation (e.g., "RLIMIT_NOFILE"). /// - Throws: `ContainerizationError` with code `.invalidArgument` if the string doesn't match a known rlimit kind. public init(_ string: String) throws { switch string { case "RLIMIT_AS": self = .addressSpace case "RLIMIT_CORE": self = .coreFileSize case "RLIMIT_CPU": self = .cpuTime case "RLIMIT_DATA": self = .dataSize case "RLIMIT_FSIZE": self = .fileSize case "RLIMIT_LOCKS": self = .locks case "RLIMIT_MEMLOCK": self = .lockedMemory case "RLIMIT_MSGQUEUE": self = .messageQueue case "RLIMIT_NICE": self = .nice case "RLIMIT_NOFILE": self = .openFiles case "RLIMIT_NPROC": self = .numberOfProcesses case "RLIMIT_RSS": self = .residentSetSize case "RLIMIT_RTPRIO": self = .realtimePriority case "RLIMIT_RTTIME": self = .realtimeTimeout case "RLIMIT_SIGPENDING": self = .signalsPending case "RLIMIT_STACK": self = .stackSize default: throw ContainerizationError(.invalidArgument, message: "invalid rlimit kind: '\(string)'") } } } } extension LinuxRLimit.Kind: CustomStringConvertible { /// The OCI string representation of the resource limit kind. public var description: String { switch self.value { case .addressSpace: "RLIMIT_AS" case .coreFileSize: "RLIMIT_CORE" case .cpuTime: "RLIMIT_CPU" case .dataSize: "RLIMIT_DATA" case .fileSize: "RLIMIT_FSIZE" case .locks: "RLIMIT_LOCKS" case .lockedMemory: "RLIMIT_MEMLOCK" case .messageQueue: "RLIMIT_MSGQUEUE" case .nice: "RLIMIT_NICE" case .openFiles: "RLIMIT_NOFILE" case .numberOfProcesses: "RLIMIT_NPROC" case .residentSetSize: "RLIMIT_RSS" case .realtimePriority: "RLIMIT_RTPRIO" case .realtimeTimeout: "RLIMIT_RTTIME" case .signalsPending: "RLIMIT_SIGPENDING" case .stackSize: "RLIMIT_STACK" } } } /// User-friendly Linux capabilities configuration public struct LinuxCapabilities: Sendable { /// Capabilities that define the maximum set of capabilities a process can have public var bounding: [CapabilityName] = [] /// Capabilities that are actually in effect for the current process public var effective: [CapabilityName] = [] /// Capabilities that can be inherited by child processes public var inheritable: [CapabilityName] = [] /// Capabilities that are currently permitted for the process public var permitted: [CapabilityName] = [] /// Capabilities that are preserved across execve() calls public var ambient: [CapabilityName] = [] /// Grant all capabilities public static let allCapabilities = LinuxCapabilities( bounding: CapabilityName.allCases, effective: CapabilityName.allCases, inheritable: CapabilityName.allCases, permitted: CapabilityName.allCases, ambient: CapabilityName.allCases ) /// Default configuration public static let defaultOCICapabilities = LinuxCapabilities( bounding: [ .chown, .dacOverride, .fsetid, .fowner, .mknod, .netRaw, .setgid, .setuid, .setfcap, .setpcap, .netBindService, .sysChroot, .kill, .auditWrite, ], effective: [ .chown, .dacOverride, .fsetid, .fowner, .mknod, .netRaw, .setgid, .setuid, .setfcap, .setpcap, .netBindService, .sysChroot, .kill, .auditWrite, ], permitted: [ .chown, .dacOverride, .fsetid, .fowner, .mknod, .netRaw, .setgid, .setuid, .setfcap, .setpcap, .netBindService, .sysChroot, .kill, .auditWrite, ], ) public init( bounding: [CapabilityName] = [], effective: [CapabilityName] = [], inheritable: [CapabilityName] = [], permitted: [CapabilityName] = [], ambient: [CapabilityName] = [] ) { self.bounding = bounding self.effective = effective self.inheritable = inheritable self.permitted = permitted self.ambient = ambient } /// Convenience initializer that sets the same capabilities to effective, permitted, and bounding sets /// This matches the typical pattern used by containerd/runc public init(capabilities: [CapabilityName]) { self.bounding = capabilities self.effective = capabilities self.inheritable = [] self.permitted = capabilities self.ambient = [] } /// Convert to OCI format for transport public func toOCI() -> ContainerizationOCI.LinuxCapabilities { ContainerizationOCI.LinuxCapabilities( bounding: bounding.isEmpty ? nil : bounding.map { $0.description }, effective: effective.isEmpty ? nil : effective.map { $0.description }, inheritable: inheritable.isEmpty ? nil : inheritable.map { $0.description }, permitted: permitted.isEmpty ? nil : permitted.map { $0.description }, ambient: ambient.isEmpty ? nil : ambient.map { $0.description } ) } } public struct LinuxProcessConfiguration: Sendable { /// The default PATH value for a process. public static let defaultPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" /// The arguments for the container process. public var arguments: [String] = [] /// The environment variables for the container process. public var environmentVariables: [String] = ["PATH=\(Self.defaultPath)"] /// The working directory for the container process. public var workingDirectory: String = "/" /// The user the container process will run as. public var user: ContainerizationOCI.User = .init() /// The rlimits for the container process. public var rlimits: [LinuxRLimit] = [] /// Whether to set the no_new_privileges bit on the container process. When true, the /// process and its children cannot gain additional privileges via setuid/setgid binaries /// or file capabilities. public var noNewPrivileges: Bool = false /// The Linux capabilities for the container process. Defaults to /// ``LinuxCapabilities/defaultOCICapabilities`` — the restricted baseline used by /// runc/containerd, which excludes privileged capabilities such as `CAP_SYS_ADMIN`. /// Callers that require additional capabilities (for example, privileged containers) /// must opt in explicitly, e.g. by setting ``LinuxCapabilities/allCapabilities``. public var capabilities: LinuxCapabilities = .defaultOCICapabilities /// Whether to allocate a pseudo terminal for the process. If you'd like interactive /// behavior and are planning to use a terminal for stdin/out/err on the client side, /// this should likely be set to true. public var terminal: Bool = false /// The stdin for the process. public var stdin: ReaderStream? /// The stdout for the process. public var stdout: Writer? /// The stderr for the process. public var stderr: Writer? public init() {} public init( arguments: [String], environmentVariables: [String] = ["PATH=\(Self.defaultPath)"], workingDirectory: String = "/", user: ContainerizationOCI.User = .init(), rlimits: [LinuxRLimit] = [], noNewPrivileges: Bool = false, capabilities: LinuxCapabilities = .defaultOCICapabilities, terminal: Bool = false, stdin: ReaderStream? = nil, stdout: Writer? = nil, stderr: Writer? = nil ) { self.arguments = arguments self.environmentVariables = environmentVariables self.workingDirectory = workingDirectory self.user = user self.rlimits = rlimits self.noNewPrivileges = noNewPrivileges self.capabilities = capabilities self.terminal = terminal self.stdin = stdin self.stdout = stdout self.stderr = stderr } public init(from config: ImageConfig) { self.workingDirectory = config.workingDir ?? "/" self.environmentVariables = config.env ?? [] self.arguments = (config.entrypoint ?? []) + (config.cmd ?? []) self.user = { if let rawString = config.user { return User(username: rawString) } return User() }() } /// Sets up IO to be handled by the passed in Terminal, and edits the /// process configuration to set the necessary state for using a pty. mutating public func setTerminalIO(terminal: Terminal) { self.environmentVariables.append("TERM=xterm") self.terminal = true self.stdin = terminal self.stdout = terminal } func toOCI() -> ContainerizationOCI.Process { ContainerizationOCI.Process( args: self.arguments, cwd: self.workingDirectory, env: self.environmentVariables, noNewPrivileges: self.noNewPrivileges, capabilities: self.capabilities.toOCI(), user: self.user, rlimits: self.rlimits.map { $0.toOCI() }, terminal: self.terminal ) } }