chore: import upstream snapshot with attribution
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
Linux build / Linux compile check (push) Has been cancelled
Linux build / Determine Swift version (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:30 +08:00
commit 680845cb1c
445 changed files with 103779 additions and 0 deletions
@@ -0,0 +1,112 @@
//===----------------------------------------------------------------------===//
// 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 Dispatch
import Synchronization
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#endif
/// Async friendly wrapper around `DispatchSourceSignal`. Provides an `AsyncStream`
/// interface to get notified of received signals.
public final class AsyncSignalHandler: Sendable {
/// An async stream that returns the signal that was caught, if ever.
public var signals: AsyncStream<Int32> {
let (stream, cont) = AsyncStream.makeStream(of: Int32.self)
self.state.withLock {
$0.conts.append(cont)
}
cont.onTermination = { @Sendable _ in
self.cancel()
}
return stream
}
/// Cancel every AsyncStream of signals, as well as the underlying
/// DispatchSignalSource's for each registered signal.
public func cancel() {
self.state.withLock {
if $0.conts.isEmpty {
return
}
for cont in $0.conts {
cont.finish()
}
for source in $0.sources {
source.cancel()
}
$0.conts.removeAll()
$0.sources.removeAll()
}
}
struct State: Sendable {
var conts: [AsyncStream<Int32>.Continuation] = []
// `sources` isn't used concurrently.
nonisolated(unsafe) var sources: [any DispatchSourceSignal] = []
}
// We keep a reference to the continuation object that is created for
// our AsyncStream and tell our signal handler to yield a value to it
// returning a value to the consumer
private func handler(_ sig: Int32) {
self.state.withLock {
for cont in $0.conts {
cont.yield(sig)
}
}
}
private let state: Mutex<State> = .init(State())
/// Create a new `AsyncSignalHandler` that catches all signals.
public static func catchAll() -> AsyncSignalHandler {
#if os(macOS)
let range = 1...31
#else
let range = 1...64
#endif
return create(notify: range.map { Int32($0) })
}
/// Create a new `AsyncSignalHandler` for the list of given signals `notify`.
/// The default signal handlers for these signals are removed and async handlers
/// added in their place. The async signal handlers that are installed simply
/// yield to a stream if and when a signal is caught.
public static func create(notify on: [Int32]) -> AsyncSignalHandler {
let out = AsyncSignalHandler()
var sources = [any DispatchSourceSignal]()
for sig in on {
signal(sig, SIG_IGN)
let source = DispatchSource.makeSignalSource(signal: sig)
source.setEventHandler {
out.handler(sig)
}
source.resume()
// Retain a reference to our signal sources so that they
// do not go out of scope.
sources.append(source)
}
out.state.withLock { $0.sources = sources }
return out
}
}
@@ -0,0 +1,49 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
extension BinaryInteger {
private func toUnsignedMemoryAmount(_ amount: UInt64) -> UInt64 {
guard self >= 0 else {
fatalError("encountered negative number during conversion to memory amount")
}
let val = UInt64(self)
let (newVal, overflow) = val.multipliedReportingOverflow(by: amount)
guard !overflow else {
fatalError("UInt64 overflow when converting to memory amount")
}
return newVal
}
public func kib() -> UInt64 {
self.toUnsignedMemoryAmount(1 << 10)
}
public func mib() -> UInt64 {
self.toUnsignedMemoryAmount(1 << 20)
}
public func gib() -> UInt64 {
self.toUnsignedMemoryAmount(1 << 30)
}
public func tib() -> UInt64 {
self.toUnsignedMemoryAmount(1 << 40)
}
public func pib() -> UInt64 {
self.toUnsignedMemoryAmount(1 << 50)
}
}
+338
View File
@@ -0,0 +1,338 @@
//===----------------------------------------------------------------------===//
// 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 CShim
import Foundation
import Synchronization
#if canImport(Darwin)
import Darwin
private let _kill = Darwin.kill
#elseif canImport(Musl)
import Musl
private let _kill = Musl.kill
#elseif canImport(Glibc)
import Glibc
private let _kill = Glibc.kill
#endif
/// Use a command to run an executable.
public struct Command: Sendable {
/// Path to the executable binary.
public var executable: String
/// Arguments provided to the binary.
public var arguments: [String]
/// Environment variables for the process.
public var environment: [String]
/// The directory where the process should execute.
public var directory: String?
/// Additional files to pass to the process.
public var extraFiles: [FileHandle]
/// The standard input.
public var stdin: FileHandle?
/// The standard output.
public var stdout: FileHandle?
/// The standard error.
public var stderr: FileHandle?
private let state: State
/// System level attributes to set on the process.
public struct Attrs: Sendable {
/// Set pgroup for the new process.
public var setPGroup: Bool
/// Make the new process group the foreground process group (requires setPGroup).
public var setForegroundPGroup: Bool
/// Inherit the real uid/gid of the parent.
public var resetIDs: Bool
/// Reset the child's signal handlers to the default.
public var setSignalDefault: Bool
/// The initial signal mask for the process.
public var signalMask: UInt32
/// Create a new session for the process.
public var setsid: Bool
/// Set the controlling terminal for the process to fd 0.
public var setctty: Bool
/// Set the process user ID.
public var uid: UInt32?
/// Set the process group ID.
public var gid: UInt32?
/// Signal to send when parent process dies (Linux only).
public var pdeathSignal: Int32?
public init(
setPGroup: Bool = false,
setForegroundPGroup: Bool = false,
resetIDs: Bool = false,
setSignalDefault: Bool = true,
signalMask: UInt32 = 0,
setsid: Bool = false,
setctty: Bool = false,
uid: UInt32? = nil,
gid: UInt32? = nil,
pdeathSignal: Int32? = nil
) {
self.setPGroup = setPGroup
self.setForegroundPGroup = setForegroundPGroup
self.resetIDs = resetIDs
self.setSignalDefault = setSignalDefault
self.signalMask = signalMask
self.setsid = setsid
self.setctty = setctty
self.uid = uid
self.gid = gid
self.pdeathSignal = pdeathSignal
}
}
private final class State: Sendable {
let pid: Atomic<pid_t> = Atomic(-1)
}
/// Attributes to set on the process.
public var attrs = Attrs()
/// System level process identifier.
public var pid: Int32 { self.state.pid.load(ordering: .acquiring) }
public init(
_ executable: String,
arguments: [String] = [],
environment: [String] = environment(),
directory: String? = nil,
extraFiles: [FileHandle] = []
) {
self.executable = executable
self.arguments = arguments
self.environment = environment
self.extraFiles = extraFiles
self.directory = directory
self.state = State()
}
public static func environment() -> [String] {
ProcessInfo.processInfo.environment
.map { "\($0)=\($1)" }
}
}
extension Command {
public enum Error: Swift.Error, CustomStringConvertible {
case processRunning
public var description: String {
switch self {
case .processRunning:
return "the process is already running"
}
}
}
}
extension Command {
@discardableResult
public func kill(_ signal: Int32) -> Int32? {
let pid = self.pid
guard pid > 0 else {
return nil
}
return _kill(pid, signal)
}
}
extension Command {
/// Start the process.
public func start() throws {
guard self.pid == -1 else {
throw Error.processRunning
}
let child = try execute()
self.state.pid.store(child, ordering: .releasing)
}
/// Wait for the process to exit and return the exit status.
@discardableResult
public func wait() throws -> Int32 {
var rus = rusage()
var ws = Int32()
let pid = self.pid
guard pid > 0 else {
return -1
}
let result = wait4(pid, &ws, 0, &rus)
guard result == pid else {
throw POSIXError(.init(rawValue: errno)!)
}
return Self.toExitStatus(ws)
}
private func execute() throws -> pid_t {
var attrs = exec_command_attrs()
exec_command_attrs_init(&attrs)
let set = try createFileset()
defer {
for nullHandle in set.nullHandles {
try? nullHandle.close()
}
}
var fds = [Int32](repeating: 0, count: set.handles.count)
for (i, handle) in set.handles.enumerated() {
fds[i] = handle.fileDescriptor
}
attrs.setsid = self.attrs.setsid ? 1 : 0
attrs.setctty = self.attrs.setctty ? 1 : 0
attrs.setpgid = self.attrs.setPGroup ? 1 : 0
attrs.setfgpgrp = self.attrs.setForegroundPGroup ? 1 : 0
var cwdPath: UnsafeMutablePointer<CChar>?
if let chdir = self.directory {
cwdPath = strdup(chdir)
}
defer {
if let cwdPath {
free(cwdPath)
}
}
if let uid = self.attrs.uid {
attrs.uid = uid
}
if let gid = self.attrs.gid {
attrs.gid = gid
}
if let pdeathSignal = self.attrs.pdeathSignal {
attrs.pdeathSignal = pdeathSignal
}
var pid: pid_t = 0
var argv = ([executable] + arguments).map { strdup($0) } + [nil]
defer {
for arg in argv where arg != nil {
free(arg)
}
}
let env = environment.map { strdup($0) } + [nil]
defer {
for e in env where e != nil {
free(e)
}
}
let result = fds.withUnsafeBufferPointer { file_handles in
exec_command(
&pid,
argv[0],
&argv,
env,
file_handles.baseAddress!, Int32(file_handles.count),
cwdPath ?? nil,
&attrs)
}
guard result == 0 else {
throw POSIXError(.init(rawValue: errno)!)
}
return pid
}
/// Create a posix_spawn file actions set of fds to pass to the new process
private func createFileset() throws -> (nullHandles: [FileHandle], handles: [FileHandle]) {
// grab dev null handles for different purposes
let nullRead = try openDevNull(flags: O_RDONLY)
let nullWrite = try openDevNull(flags: O_WRONLY)
var files = [FileHandle]()
files.append(stdin ?? nullRead)
files.append(stdout ?? nullWrite)
files.append(stderr ?? nullWrite)
files.append(contentsOf: extraFiles)
return (nullHandles: [nullRead, nullWrite], handles: files)
}
/// Returns a file handle to /dev/null with the specified flags.
private func openDevNull(flags: Int32) throws -> FileHandle {
let fd = open("/dev/null", flags, 0)
guard fd >= 0 else {
throw POSIXError(.init(rawValue: errno)!)
}
return FileHandle(fileDescriptor: fd, closeOnDealloc: false)
}
}
extension Command {
private static let signalOffset: Int32 = 128
private static let shift: Int32 = 8
private static let mask: Int32 = 0x7F
private static let stopped: Int32 = 0x7F
private static let exited: Int32 = 0x00
static func signaled(_ ws: Int32) -> Bool {
ws & mask != stopped && ws & mask != exited
}
static func exited(_ ws: Int32) -> Bool {
ws & mask == exited
}
static func exitStatus(_ ws: Int32) -> Int32 {
let r: Int32
#if os(Linux)
r = ws >> shift & 0xFF
#else
r = ws >> shift
#endif
return r
}
public static func toExitStatus(_ ws: Int32) -> Int32 {
if signaled(ws) {
// We use the offset as that is how existing container
// runtimes minic bash for the status when signaled.
return Int32(Self.signalOffset + ws & mask)
}
if exited(ws) {
return exitStatus(ws)
}
return ws
}
}
private func WIFEXITED(_ status: Int32) -> Bool {
_WSTATUS(status) == 0
}
private func _WSTATUS(_ status: Int32) -> Int32 {
status & 0x7f
}
private func WIFSIGNALED(_ status: Int32) -> Bool {
(_WSTATUS(status) != 0) && (_WSTATUS(status) != 0x7f)
}
private func WEXITSTATUS(_ status: Int32) -> Int32 {
(status >> 8) & 0xff
}
private func WTERMSIG(_ status: Int32) -> Int32 {
status & 0x7f
}
+144
View File
@@ -0,0 +1,144 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#endif
#if canImport(FoundationEssentials)
import struct FoundationEssentials.URL
#else
import struct Foundation.URL
#endif
/// Trivial type to discover information about a given file (uid, gid, mode...).
public struct File: Sendable {
/// `File` errors.
public enum Error: Swift.Error, CustomStringConvertible {
case errno(_ e: Int32)
public var description: String {
switch self {
case .errno(let code):
return "errno \(code)"
}
}
}
/// Returns a `FileInfo` struct with information about the file.
/// - Parameters:
/// - url: The path to the file.
public static func info(_ url: URL) throws -> FileInfo {
try info(url.path)
}
/// Returns a `FileInfo` struct with information about the file.
/// - Parameters:
/// - path: The path to the file as a string.
public static func info(_ path: String) throws -> FileInfo {
var st = stat()
guard lstat(path, &st) == 0 else {
throw Error.errno(errno)
}
return FileInfo(path, stat: st)
}
}
/// `FileInfo` holds and provides easy access to stat(2) data
/// for a file.
public struct FileInfo: Sendable {
private let _stat_t: stat
private let _path: String
init(_ path: String, stat: stat) {
self._path = path
self._stat_t = stat
}
/// mode_t for the file.
public var mode: mode_t {
self._stat_t.st_mode
}
/// The files uid.
public var uid: Int {
Int(self._stat_t.st_uid)
}
/// The files gid.
public var gid: Int {
Int(self._stat_t.st_gid)
}
/// The filesystem ID the file belongs to.
public var dev: Int {
Int(self._stat_t.st_dev)
}
/// The files inode number.
public var ino: Int {
Int(self._stat_t.st_ino)
}
/// The size of the file.
public var size: Int {
Int(self._stat_t.st_size)
}
/// The path to the file.
public var path: String {
self._path
}
/// Returns if the file is a directory.
public var isDirectory: Bool {
mode & S_IFMT == S_IFDIR
}
/// Returns if the file is a pipe.
public var isPipe: Bool {
mode & S_IFMT == S_IFIFO
}
/// Returns if the file is a socket.
public var isSocket: Bool {
mode & S_IFMT == S_IFSOCK
}
/// Returns if the file is a link.
public var isLink: Bool {
mode & S_IFMT == S_IFLNK
}
/// Returns if the file is a regular file.
public var isRegularFile: Bool {
mode & S_IFMT == S_IFREG
}
/// Returns if the file is a block device.
public var isBlock: Bool {
mode & S_IFMT == S_IFBLK
}
/// Returns if the file is a character device.
public var isChar: Bool {
mode & S_IFMT == S_IFCHR
}
}
@@ -0,0 +1,346 @@
//===----------------------------------------------------------------------===//
// 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 SystemPackage
#if canImport(Darwin)
import Darwin
private let os_dup = Darwin.dup
private let os_S_IFMT = mode_t(Darwin.S_IFMT)
private let os_S_IFREG = mode_t(Darwin.S_IFREG)
private let os_S_IFDIR = mode_t(Darwin.S_IFDIR)
private let os_S_IFLNK = mode_t(Darwin.S_IFLNK)
#elseif canImport(Musl)
import CSystem
import Musl
private let os_dup = Musl.dup
private let os_S_IFMT = Musl.S_IFMT
private let os_S_IFREG = Musl.S_IFREG
private let os_S_IFDIR = Musl.S_IFDIR
private let os_S_IFLNK = Musl.S_IFLNK
#elseif canImport(Glibc)
import Glibc
private let os_dup = Glibc.dup
private let os_S_IFMT = mode_t(Glibc.S_IFMT)
private let os_S_IFREG = mode_t(Glibc.S_IFREG)
private let os_S_IFDIR = mode_t(Glibc.S_IFDIR)
private let os_S_IFLNK = mode_t(Glibc.S_IFLNK)
#endif
/// Static utility functions for secure, symlink-safe filesystem operations
/// anchored to a file descriptor.
///
/// All operations use `openat`/`mkdirat`/`unlinkat` anchored to the supplied
/// file descriptor, preventing path traversal and TOCTOU races. The type is
/// never instantiated; it exists solely as a namespace.
public enum FileDescriptorOps {
// MARK: - Nested types
public enum Error: Swift.Error, CustomStringConvertible, Equatable {
case invalidRelativePath
case invalidPathComponent
case cannotFollowSymlink
case systemError(String, Int32)
public var description: String {
switch self {
case .invalidRelativePath:
return "invalid relative path supplied to file descriptor operation"
case .invalidPathComponent:
return "an intermediate path component is missing or is not a directory"
case .cannotFollowSymlink:
return "cannot follow a symlink in a file descriptor operation"
case .systemError(let operation, let err):
return "\(operation) returned error: \(err)"
}
}
}
/// The type of a directory entry yielded by ``enumerate(_:_:)``.
public enum EntryType: Sendable, Equatable {
/// A regular file.
case regular
/// A directory. The entry is recursed into; symlinks to directories are
/// reported as `.symlink` and are never recursed.
case directory
/// A symbolic link (to a file or directory).
case symlink
/// Any other entry type (device node, named pipe, socket, etc.).
case other
}
// MARK: - Public API
/// Creates a directory relative to `fd`, rejecting paths that traverse symlinks.
///
/// - Parameters:
/// - fd: An open file descriptor for the parent directory.
/// - relativePath: The path to create, relative to `fd`.
/// - permissions: The permissions to give the directory (default 0o755).
/// - makeIntermediates: Create or replace intermediate components as needed.
/// - completion: A function that operates on the new directory fd.
/// - Throws: `FileDescriptorOps.Error` if path validation or system errors occur.
public static func mkdir(
_ fd: FileDescriptor,
_ relativePath: FilePath,
permissions: FilePermissions? = nil,
makeIntermediates: Bool = false,
completion: (FileDescriptor) throws -> Void = { _ in }
) throws {
try validateRelativePath(relativePath)
try mkdir(
fd,
relativePath.components,
permissions: permissions,
makeIntermediates: makeIntermediates,
completion: completion
)
}
/// Recursively removes a direct child of the directory at `fd`.
///
/// - Parameters:
/// - fd: An open file descriptor for the parent directory.
/// - filename: The name of the child to remove.
/// - Throws: `FileDescriptorOps.Error` if system errors occur.
public static func unlinkRecursive(_ fd: FileDescriptor, filename: FilePath.Component) throws {
guard filename.string != "." && filename.string != ".." else {
return
}
guard unlinkat(fd.rawValue, filename.string, 0) != 0 else {
return
}
guard errno != ENOENT else {
return
}
guard errno == EPERM || errno == EISDIR else {
throw Error.systemError("file removal during file descriptor unlink", errno)
}
let componentFd = openat(fd.rawValue, filename.string, O_NOFOLLOW | O_RDONLY | O_DIRECTORY)
guard componentFd >= 0 else {
throw Error.systemError("directory open during file descriptor unlink", errno)
}
let componentFileDescriptor = FileDescriptor(rawValue: componentFd)
defer { try? componentFileDescriptor.close() }
// Open the directory stream using a duplicate fd that closedir() will close.
let ownedFd = os_dup(componentFd)
guard let dir = fdopendir(ownedFd) else {
throw Error.systemError("directory opendir during file descriptor unlink", errno)
}
defer { closedir(dir) }
while let entry = readdir(dir) {
let childComponent = withUnsafePointer(to: entry.pointee.d_name) {
$0.withMemoryRebound(to: UInt8.self, capacity: Int(NAME_MAX) + 1) {
let name = String(decodingCString: $0, as: UTF8.self)
return FilePath.Component(name)
}
}
guard let childComponent else {
throw Error.systemError("directory entry processing during file descriptor unlink", errno)
}
try unlinkRecursive(componentFileDescriptor, filename: childComponent)
}
if unlinkat(fd.rawValue, filename.string, AT_REMOVEDIR) != 0 {
throw Error.systemError("directory removal during file descriptor unlink", errno)
}
}
/// Recursively enumerates the contents of `fd` without following symbolic links.
///
/// Each entry file, directory, symlink, or other type is reported to
/// `body` with a path relative to `fd`. Directories are reported before their
/// contents (pre-order) and then recursed. A symlink whose target is a directory
/// is reported as `.symlink` and is never followed, so traversal cannot escape
/// the tree rooted at `fd` regardless of where symlinks point.
///
/// `fd` must be an open file descriptor for a directory.
///
/// - Parameters:
/// - fd: An open file descriptor for the root directory to enumerate.
/// - body: Called once per entry. `path` is relative to `fd`; `type`
/// identifies the kind of entry; `parentFd` is the open file descriptor
/// for the directory that contains the entry. The last component of `path`
/// is the entry's filename; together with `parentFd` it allows the body to
/// open the entry via
/// `openat(parentFd.rawValue, path.lastComponent!.string, O_NOFOLLOW )`
/// without reconstructing an absolute path, preserving the TOCTOU safety
/// of the traversal end-to-end. `parentFd` must not be closed within the
/// body call, or used after the call returns. Throw to abort.
/// - Throws: `FileDescriptorOps.Error` on system errors; any error thrown by
/// `body` is propagated unchanged.
public static func enumerate(
_ fd: FileDescriptor,
_ body: (_ path: FilePath, _ type: EntryType, _ parentFd: FileDescriptor) throws -> Void
) throws {
try enumerateHelper(fd, relativePath: FilePath(""), body: body)
}
// MARK: - Canonical path
#if canImport(Darwin)
/// Returns the canonical path for `fd` using `F_GETPATH`.
public static func getCanonicalPath(_ fd: FileDescriptor) throws -> FilePath {
var buffer = [CChar](repeating: 0, count: Int(PATH_MAX))
guard fcntl(fd.rawValue, F_GETPATH, &buffer) != -1 else {
throw Errno(rawValue: errno)
}
let bytes = buffer.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) }
return FilePath(String(decoding: bytes, as: UTF8.self))
}
#elseif canImport(Glibc) || canImport(Musl)
/// Returns the canonical path for `fd` via `/proc/self/fd`.
public static func getCanonicalPath(_ fd: FileDescriptor) throws -> FilePath {
let fdPath = "/proc/self/fd/\(fd.rawValue)"
var buffer = [CChar](repeating: 0, count: 4096)
let len = readlink(fdPath, &buffer, buffer.count - 1)
guard len > 0 else {
throw Error.systemError("readlink", errno)
}
let bytes = buffer.prefix(len).map { UInt8(bitPattern: $0) }
return FilePath(String(decoding: bytes, as: UTF8.self))
}
#endif
// MARK: - Private helpers
private static func mkdir(
_ fd: FileDescriptor,
_ relativeComponents: FilePath.ComponentView,
permissions: FilePermissions? = nil,
makeIntermediates: Bool,
completion: (FileDescriptor) throws -> Void
) throws {
guard let currentComponent = relativeComponents.first else {
try completion(fd)
return
}
let childComponents = FilePath.ComponentView(relativeComponents.dropFirst())
var componentFd = openat(fd.rawValue, currentComponent.string, O_NOFOLLOW | O_RDONLY | O_DIRECTORY)
if componentFd < 0 {
guard makeIntermediates || childComponents.isEmpty else {
throw Error.invalidPathComponent
}
if errno != ENOENT {
try unlinkRecursive(fd, filename: currentComponent)
}
guard mkdirat(fd.rawValue, currentComponent.string, permissions?.rawValue ?? 0o755) == 0 else {
throw Error.systemError("directory creation during file descriptor mkdir", errno)
}
componentFd = openat(fd.rawValue, currentComponent.string, O_NOFOLLOW | O_RDONLY | O_DIRECTORY)
guard componentFd >= 0 else {
throw Error.systemError("directory open during file descriptor mkdir", errno)
}
}
let componentFileDescriptor = FileDescriptor(rawValue: componentFd)
defer { try? componentFileDescriptor.close() }
guard !childComponents.isEmpty else {
try completion(componentFileDescriptor)
return
}
try mkdir(
componentFileDescriptor, childComponents,
permissions: permissions, makeIntermediates: makeIntermediates, completion: completion)
}
private static func enumerateHelper(
_ fd: FileDescriptor,
relativePath: FilePath,
body: (_ path: FilePath, _ type: EntryType, _ parentFd: FileDescriptor) throws -> Void
) throws {
// fdopendir takes ownership of the fd passed to it and closes it via
// closedir. Duplicate so the caller's fd remains open.
let dupFd = os_dup(fd.rawValue)
guard dupFd >= 0 else {
throw Error.systemError("dup during file descriptor enumerate", errno)
}
guard let dir = fdopendir(dupFd) else {
let savedErrno = errno
try? FileDescriptor(rawValue: dupFd).close()
throw Error.systemError("fdopendir during file descriptor enumerate", savedErrno)
}
defer { closedir(dir) }
while let entry = readdir(dir) {
let name = withUnsafePointer(to: entry.pointee.d_name) {
$0.withMemoryRebound(to: UInt8.self, capacity: Int(NAME_MAX) + 1) {
String(decodingCString: $0, as: UTF8.self)
}
}
guard name != "." && name != ".." else { continue }
guard let component = FilePath.Component(name) else { continue }
let entryPath = relativePath.appending(component)
let entryType = resolveEntryType(parentFd: fd.rawValue, name: name, dtype: entry.pointee.d_type)
// Pass fd (the parent directory) so the body can use
// openat(parentFd.rawValue, path.lastComponent!.string, O_NOFOLLOW )
// rather than reconstructing an absolute path, keeping the fd chain unbroken.
try body(entryPath, entryType, fd)
guard entryType == .directory else { continue }
// Open the child directory with O_NOFOLLOW to guarantee we are
// entering a real directory and not a symlink that was swapped in
// between readdir and here.
let childFd = openat(fd.rawValue, name, O_NOFOLLOW | O_RDONLY | O_DIRECTORY)
guard childFd >= 0 else {
throw Error.systemError("openat during file descriptor enumerate", errno)
}
let childDescriptor = FileDescriptor(rawValue: childFd)
defer { try? childDescriptor.close() }
try enumerateHelper(childDescriptor, relativePath: entryPath, body: body)
}
}
private static func resolveEntryType(parentFd: Int32, name: String, dtype: UInt8) -> EntryType {
switch dtype {
case UInt8(DT_REG): return .regular
case UInt8(DT_DIR): return .directory
case UInt8(DT_LNK): return .symlink
case UInt8(DT_UNKNOWN):
// Some filesystems (NFS, ext2/3) report DT_UNKNOWN; fall back to fstatat.
var stbuf = stat()
guard fstatat(parentFd, name, &stbuf, AT_SYMLINK_NOFOLLOW) == 0 else { return .other }
switch stbuf.st_mode & os_S_IFMT {
case os_S_IFREG: return .regular
case os_S_IFDIR: return .directory
case os_S_IFLNK: return .symlink
default: return .other
}
default: return .other
}
}
private static func validateRelativePath(_ path: FilePath) throws {
guard !(path.components.contains { $0 == ".." }) else {
throw Error.invalidRelativePath
}
}
}
@@ -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.
//===----------------------------------------------------------------------===//
import Foundation
import SystemPackage
/// Static utility functions for path operations.
///
/// The type is never instantiated; it exists solely as a namespace.
public enum FilePathOps {
/// Returns an absolute version of `path`.
///
/// This is a purely lexical operation: it does not resolve symlinks,
/// perform tilde expansion, and does not access the file system. If `path`
/// is already absolute, this returns `path` unchanged.
public static func absolutePath(_ path: FilePath) -> FilePath {
guard !path.isAbsolute else {
return path
}
return FilePath(FileManager.default.currentDirectoryPath)
.appending(path.components)
.lexicallyNormalized()
}
}
@@ -0,0 +1,243 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
#if os(macOS)
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
/// Holds the result of a query to the keychain.
public struct KeychainQueryResult {
public var username: String
public var password: String
public var modifiedDate: Date
public var createdDate: Date
}
/// Type that facilitates interacting with the macOS keychain.
public struct KeychainQuery {
public init() {}
/// Save a value to the keychain.
/// - Parameters:
/// - securityDomain: The security domain used to fetch keychain entries.
/// - accessGroup: If present, the access group used to fetch keychain entries.
/// - hostname: The hostname for the authenticating server.
/// - username: The username to present to the server.
/// - password: The password to present to the server.
/// - Throws: An error if the keychain query fails or returns unexpected data.
public func save(
securityDomain: String,
accessGroup: String? = nil,
hostname: String,
username: String,
password: String
) throws {
if try exists(securityDomain: securityDomain, accessGroup: accessGroup, hostname: hostname) {
try delete(securityDomain: securityDomain, accessGroup: accessGroup, hostname: hostname)
}
guard let passwordEncoded = password.data(using: String.Encoding.utf8) else {
throw Self.Error.invalidPasswordConversion
}
var query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrSecurityDomain as String: securityDomain,
kSecAttrServer as String: hostname,
kSecAttrAccount as String: username,
kSecValueData as String: passwordEncoded,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
kSecAttrSynchronizable as String: false,
]
if let accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup
}
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw Self.Error.unhandledError(status: status) }
}
/// Delete a value from the keychain.
/// - Parameters:
/// - securityDomain: The security domain used to fetch keychain entries.
/// - accessGroup: If present, the access group used to fetch keychain entries.
/// - hostname: The hostname for the authenticating server.
/// - Throws: An error if the keychain query fails or returns unexpected data.
public func delete(securityDomain: String, accessGroup: String? = nil, hostname: String) throws {
var query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrSecurityDomain as String: securityDomain,
kSecAttrServer as String: hostname,
kSecMatchLimit as String: kSecMatchLimitOne,
]
if let accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup
}
let status = SecItemDelete(query as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else {
throw Self.Error.unhandledError(status: status)
}
}
/// Retrieve a value from the keychain.
/// - Parameters:
/// - securityDomain: The security domain used to fetch keychain entries.
/// - accessGroup: If present, the access group used to fetch keychain entries.
/// - hostname: The hostname for the authenticating server.
/// - Returns: The keychain entry.
/// - Throws: An error if the keychain query fails or returns unexpected data.
public func get(securityDomain: String, accessGroup: String? = nil, hostname: String) throws -> KeychainQueryResult? {
var query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrSecurityDomain as String: securityDomain,
kSecAttrServer as String: hostname,
kSecReturnAttributes as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true,
]
if let accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup
}
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
let exists = try isQuerySuccessful(status)
if !exists {
return nil
}
guard let fetched = item as? [String: Any] else {
throw Self.Error.unexpectedDataFetched
}
guard let data = fetched[kSecValueData as String] as? Data else {
throw Self.Error.keyNotPresent(key: kSecValueData as String)
}
guard let password = String(data: data, encoding: String.Encoding.utf8) else {
throw Self.Error.unexpectedDataFetched
}
guard let username = fetched[kSecAttrAccount as String] as? String else {
throw Self.Error.keyNotPresent(key: kSecAttrAccount as String)
}
guard let modifiedDate = fetched[kSecAttrModificationDate as String] as? Date else {
throw Self.Error.keyNotPresent(key: kSecAttrModificationDate as String)
}
guard let createdDate = fetched[kSecAttrCreationDate as String] as? Date else {
throw Self.Error.keyNotPresent(key: kSecAttrCreationDate as String)
}
return KeychainQueryResult(
username: username,
password: password,
modifiedDate: modifiedDate,
createdDate: createdDate
)
}
/// List all keychain entries for a domain.
/// - Parameters:
/// - securityDomain: The security domain used to fetch keychain entries.
/// - accessGroup: If present, the access group used to fetch keychain entries.
/// - Returns: An array of keychain metadata for each matching entry, or an empty array if none are found.
/// - Throws: An error if the keychain query fails or returns unexpected data.
public func list(securityDomain: String, accessGroup: String? = nil) throws -> [RegistryInfo] {
var query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrSecurityDomain as String: securityDomain,
kSecReturnAttributes as String: true,
kSecReturnData as String: false,
kSecMatchLimit as String: kSecMatchLimitAll,
]
if let accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup
}
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
let exists = try isQuerySuccessful(status)
if !exists {
return []
}
guard let fetched = item as? [[String: Any]] else {
throw Self.Error.unexpectedDataFetched
}
return try fetched.map { registry in
guard let hostname = registry[kSecAttrServer as String] as? String else {
throw Self.Error.keyNotPresent(key: kSecAttrServer as String)
}
guard let username = registry[kSecAttrAccount as String] as? String else {
throw Self.Error.keyNotPresent(key: kSecAttrAccount as String)
}
guard let modifiedDate = registry[kSecAttrModificationDate as String] as? Date else {
throw Self.Error.keyNotPresent(key: kSecAttrModificationDate as String)
}
guard let createdDate = registry[kSecAttrCreationDate as String] as? Date else {
throw Self.Error.keyNotPresent(key: kSecAttrCreationDate as String)
}
return RegistryInfo(
hostname: hostname,
username: username,
modifiedDate: modifiedDate,
createdDate: createdDate
)
}
}
/// Check if a value exists in the keychain.
/// - Parameters:
/// - securityDomain: The security domain used to fetch keychain entries.
/// - accessGroup: If present, the access group used to fetch keychain entries.
/// - hostname: The hostname for the authenticating server.
/// - Returns: `true` if the entry exists, `false` otherwise.
/// - Throws: An error if the keychain query fails.
public func exists(securityDomain: String, accessGroup: String? = nil, hostname: String) throws -> Bool {
var query: [String: Any] = [
kSecClass as String: kSecClassInternetPassword,
kSecAttrSecurityDomain as String: securityDomain,
kSecAttrServer as String: hostname,
kSecReturnAttributes as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: false,
]
if let accessGroup {
query[kSecAttrAccessGroup as String] = accessGroup
}
let status = SecItemCopyMatching(query as CFDictionary, nil)
return try isQuerySuccessful(status)
}
private func isQuerySuccessful(_ status: Int32) throws -> Bool {
guard status != errSecItemNotFound else {
return false
}
guard status == errSecSuccess else {
throw Self.Error.unhandledError(status: status)
}
return true
}
}
extension KeychainQuery {
public enum Error: Swift.Error {
case unhandledError(status: Int32)
case unexpectedDataFetched
case keyNotPresent(key: String)
case invalidPasswordConversion
}
}
#endif
@@ -0,0 +1,33 @@
//===----------------------------------------------------------------------===//
// 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 canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
/// Holds the stored attributes for a registry.
public struct RegistryInfo: Sendable {
/// The registry host as a domain name with an optional port.
public var hostname: String
/// The username used to authenticate with the registry.
public var username: String
/// The date the registry was last modified.
public let modifiedDate: Date
/// The date the registry was created.
public let createdDate: Date
}
@@ -0,0 +1,109 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
#if canImport(Musl)
import Musl
private let _mount = Musl.mount
#elseif canImport(Glibc)
import Glibc
private let _mount = Glibc.mount
#endif
/// `Binfmt` is a utility type that contains static helpers and types for
/// mounting the Linux binfmt_misc filesystem, and creating new binfmt entries.
public struct Binfmt: Sendable {
/// Default mount path for binfmt_misc.
public static let path = "/proc/sys/fs/binfmt_misc"
/// Entry models a binfmt_misc entry.
/// https://docs.kernel.org/admin-guide/binfmt-misc.html
public struct Entry {
public var name: String
public var type: String
public var offset: String
public var magic: String
public var mask: String
public var flags: String
public init(
name: String,
type: String = "M",
offset: String = "",
magic: String,
mask: String,
flags: String = "CF"
) {
self.name = name
self.type = type
self.offset = offset
self.magic = magic
self.mask = mask
self.flags = flags
}
/// Returns a binfmt `Entry` for amd64 ELF binaries.
public static func amd64() -> Self {
Binfmt.Entry(
name: "x86_64",
magic: #"\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x3e\x00"#,
mask: #"\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff"#
)
}
#if os(Linux)
/// Register the passed in `binaryPath` as the interpreter for a new binfmt_misc entry.
public func register(binaryPath: String) throws {
let registration = ":\(self.name):\(self.type):\(self.offset):\(self.magic):\(self.mask):\(binaryPath):\(self.flags)"
try registration.write(
to: URL(fileURLWithPath: Binfmt.path).appendingPathComponent("register"),
atomically: false,
encoding: .ascii
)
}
/// Deregister the binfmt_misc entry described by the current object.
public func deregister() throws {
let data = "-1"
try data.write(
to: URL(fileURLWithPath: Binfmt.path).appendingPathComponent(self.name),
atomically: false,
encoding: .ascii
)
}
#endif // os(Linux)
}
#if os(Linux)
/// Crude check to see if /proc/sys/fs/binfmt_misc/register exists.
public static func mounted() -> Bool {
FileManager.default.fileExists(atPath: "\(Self.path)/register")
}
/// Mount the binfmt_misc filesystem.
public static func mount() throws {
guard _mount("binfmt_misc", Self.path, "binfmt_misc", 0, "") == 0 else {
throw POSIXError.fromErrno()
}
}
#endif // os(Linux)
}
@@ -0,0 +1,670 @@
//===----------------------------------------------------------------------===//
// 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 CShim
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
// MARK: - Configuration Types
public enum CapabilityParsingError: Swift.Error, CustomStringConvertible {
case invalidCapabilitySet(String)
case invalidCapabilityName(String)
public var description: String {
switch self {
case .invalidCapabilitySet(let value):
return "invalid CapabilitySet value '\(value)'"
case .invalidCapabilityName(let value):
return "invalid CapabilityName '\(value)'"
}
}
}
public struct CapabilitySet: Sendable, Hashable {
private enum Value: Hashable, Sendable, CaseIterable {
case bounding
case effective
case inheritable
case permitted
case ambient
}
private var value: Value
private init(_ value: Value) {
self.value = value
}
public init(rawValue: String) throws {
let values = Value.allCases.reduce(into: [String: Value]()) {
$0[String(describing: $1).lowercased()] = $1
}
guard let match = values[rawValue.lowercased()] else {
throw CapabilityParsingError.invalidCapabilitySet(rawValue)
}
self.value = match
}
public static var bounding: Self { Self(.bounding) }
public static var effective: Self { Self(.effective) }
public static var inheritable: Self { Self(.inheritable) }
public static var permitted: Self { Self(.permitted) }
public static var ambient: Self { Self(.ambient) }
}
extension CapabilitySet: CustomStringConvertible {
public var description: String {
String(describing: self.value)
}
}
public struct CapabilityName: Sendable, Hashable {
private enum Value: Hashable, Sendable, CaseIterable {
case chown
case dacOverride
case dacReadSearch
case fowner
case fsetid
case kill
case setgid
case setuid
case setpcap
case linuxImmutable
case netBindService
case netBroadcast
case netAdmin
case netRaw
case ipcLock
case ipcOwner
case sysModule
case sysRawio
case sysChroot
case sysPtrace
case sysPacct
case sysAdmin
case sysBoot
case sysNice
case sysResource
case sysTime
case sysTtyConfig
case mknod
case lease
case auditWrite
case auditControl
case setfcap
case macOverride
case macAdmin
case syslog
case wakeAlarm
case blockSuspend
case auditRead
case perfmon
case bpf
case checkpointRestore
}
private var value: Value
private init(_ value: Value) {
self.value = value
}
public init(rawValue: String) throws {
let uppercased = rawValue.uppercased()
let normalized = uppercased.hasPrefix("CAP_") ? uppercased : "CAP_\(uppercased)"
let capNameMap: [String: Value] = [
"CAP_CHOWN": .chown,
"CAP_DAC_OVERRIDE": .dacOverride,
"CAP_DAC_READ_SEARCH": .dacReadSearch,
"CAP_FOWNER": .fowner,
"CAP_FSETID": .fsetid,
"CAP_KILL": .kill,
"CAP_SETGID": .setgid,
"CAP_SETUID": .setuid,
"CAP_SETPCAP": .setpcap,
"CAP_LINUX_IMMUTABLE": .linuxImmutable,
"CAP_NET_BIND_SERVICE": .netBindService,
"CAP_NET_BROADCAST": .netBroadcast,
"CAP_NET_ADMIN": .netAdmin,
"CAP_NET_RAW": .netRaw,
"CAP_IPC_LOCK": .ipcLock,
"CAP_IPC_OWNER": .ipcOwner,
"CAP_SYS_MODULE": .sysModule,
"CAP_SYS_RAWIO": .sysRawio,
"CAP_SYS_CHROOT": .sysChroot,
"CAP_SYS_PTRACE": .sysPtrace,
"CAP_SYS_PACCT": .sysPacct,
"CAP_SYS_ADMIN": .sysAdmin,
"CAP_SYS_BOOT": .sysBoot,
"CAP_SYS_NICE": .sysNice,
"CAP_SYS_RESOURCE": .sysResource,
"CAP_SYS_TIME": .sysTime,
"CAP_SYS_TTY_CONFIG": .sysTtyConfig,
"CAP_MKNOD": .mknod,
"CAP_LEASE": .lease,
"CAP_AUDIT_WRITE": .auditWrite,
"CAP_AUDIT_CONTROL": .auditControl,
"CAP_SETFCAP": .setfcap,
"CAP_MAC_OVERRIDE": .macOverride,
"CAP_MAC_ADMIN": .macAdmin,
"CAP_SYSLOG": .syslog,
"CAP_WAKE_ALARM": .wakeAlarm,
"CAP_BLOCK_SUSPEND": .blockSuspend,
"CAP_AUDIT_READ": .auditRead,
"CAP_PERFMON": .perfmon,
"CAP_BPF": .bpf,
"CAP_CHECKPOINT_RESTORE": .checkpointRestore,
]
guard let match = capNameMap[normalized] else {
throw CapabilityParsingError.invalidCapabilityName(rawValue)
}
self.value = match
}
public var capValue: UInt32 {
switch self.value {
case .chown: return 0
case .dacOverride: return 1
case .dacReadSearch: return 2
case .fowner: return 3
case .fsetid: return 4
case .kill: return 5
case .setgid: return 6
case .setuid: return 7
case .setpcap: return 8
case .linuxImmutable: return 9
case .netBindService: return 10
case .netBroadcast: return 11
case .netAdmin: return 12
case .netRaw: return 13
case .ipcLock: return 14
case .ipcOwner: return 15
case .sysModule: return 16
case .sysRawio: return 17
case .sysChroot: return 18
case .sysPtrace: return 19
case .sysPacct: return 20
case .sysAdmin: return 21
case .sysBoot: return 22
case .sysNice: return 23
case .sysResource: return 24
case .sysTime: return 25
case .sysTtyConfig: return 26
case .mknod: return 27
case .lease: return 28
case .auditWrite: return 29
case .auditControl: return 30
case .setfcap: return 31
case .macOverride: return 32
case .macAdmin: return 33
case .syslog: return 34
case .wakeAlarm: return 35
case .blockSuspend: return 36
case .auditRead: return 37
case .perfmon: return 38
case .bpf: return 39
case .checkpointRestore: return 40
}
}
public static var chown: Self { Self(.chown) }
public static var dacOverride: Self { Self(.dacOverride) }
public static var dacReadSearch: Self { Self(.dacReadSearch) }
public static var fowner: Self { Self(.fowner) }
public static var fsetid: Self { Self(.fsetid) }
public static var kill: Self { Self(.kill) }
public static var setgid: Self { Self(.setgid) }
public static var setuid: Self { Self(.setuid) }
public static var setpcap: Self { Self(.setpcap) }
public static var linuxImmutable: Self { Self(.linuxImmutable) }
public static var netBindService: Self { Self(.netBindService) }
public static var netBroadcast: Self { Self(.netBroadcast) }
public static var netAdmin: Self { Self(.netAdmin) }
public static var netRaw: Self { Self(.netRaw) }
public static var ipcLock: Self { Self(.ipcLock) }
public static var ipcOwner: Self { Self(.ipcOwner) }
public static var sysModule: Self { Self(.sysModule) }
public static var sysRawio: Self { Self(.sysRawio) }
public static var sysChroot: Self { Self(.sysChroot) }
public static var sysPtrace: Self { Self(.sysPtrace) }
public static var sysPacct: Self { Self(.sysPacct) }
public static var sysAdmin: Self { Self(.sysAdmin) }
public static var sysBoot: Self { Self(.sysBoot) }
public static var sysNice: Self { Self(.sysNice) }
public static var sysResource: Self { Self(.sysResource) }
public static var sysTime: Self { Self(.sysTime) }
public static var sysTtyConfig: Self { Self(.sysTtyConfig) }
public static var mknod: Self { Self(.mknod) }
public static var lease: Self { Self(.lease) }
public static var auditWrite: Self { Self(.auditWrite) }
public static var auditControl: Self { Self(.auditControl) }
public static var setfcap: Self { Self(.setfcap) }
public static var macOverride: Self { Self(.macOverride) }
public static var macAdmin: Self { Self(.macAdmin) }
public static var syslog: Self { Self(.syslog) }
public static var wakeAlarm: Self { Self(.wakeAlarm) }
public static var blockSuspend: Self { Self(.blockSuspend) }
public static var auditRead: Self { Self(.auditRead) }
public static var perfmon: Self { Self(.perfmon) }
public static var bpf: Self { Self(.bpf) }
public static var checkpointRestore: Self { Self(.checkpointRestore) }
public static var allCases: [CapabilityName] {
Value.allCases.map { CapabilityName($0) }
}
}
extension CapabilityName: CustomStringConvertible {
public var description: String {
switch self.value {
case .chown: return "CAP_CHOWN"
case .dacOverride: return "CAP_DAC_OVERRIDE"
case .dacReadSearch: return "CAP_DAC_READ_SEARCH"
case .fowner: return "CAP_FOWNER"
case .fsetid: return "CAP_FSETID"
case .kill: return "CAP_KILL"
case .setgid: return "CAP_SETGID"
case .setuid: return "CAP_SETUID"
case .setpcap: return "CAP_SETPCAP"
case .linuxImmutable: return "CAP_LINUX_IMMUTABLE"
case .netBindService: return "CAP_NET_BIND_SERVICE"
case .netBroadcast: return "CAP_NET_BROADCAST"
case .netAdmin: return "CAP_NET_ADMIN"
case .netRaw: return "CAP_NET_RAW"
case .ipcLock: return "CAP_IPC_LOCK"
case .ipcOwner: return "CAP_IPC_OWNER"
case .sysModule: return "CAP_SYS_MODULE"
case .sysRawio: return "CAP_SYS_RAWIO"
case .sysChroot: return "CAP_SYS_CHROOT"
case .sysPtrace: return "CAP_SYS_PTRACE"
case .sysPacct: return "CAP_SYS_PACCT"
case .sysAdmin: return "CAP_SYS_ADMIN"
case .sysBoot: return "CAP_SYS_BOOT"
case .sysNice: return "CAP_SYS_NICE"
case .sysResource: return "CAP_SYS_RESOURCE"
case .sysTime: return "CAP_SYS_TIME"
case .sysTtyConfig: return "CAP_SYS_TTY_CONFIG"
case .mknod: return "CAP_MKNOD"
case .lease: return "CAP_LEASE"
case .auditWrite: return "CAP_AUDIT_WRITE"
case .auditControl: return "CAP_AUDIT_CONTROL"
case .setfcap: return "CAP_SETFCAP"
case .macOverride: return "CAP_MAC_OVERRIDE"
case .macAdmin: return "CAP_MAC_ADMIN"
case .syslog: return "CAP_SYSLOG"
case .wakeAlarm: return "CAP_WAKE_ALARM"
case .blockSuspend: return "CAP_BLOCK_SUSPEND"
case .auditRead: return "CAP_AUDIT_READ"
case .perfmon: return "CAP_PERFMON"
case .bpf: return "CAP_BPF"
case .checkpointRestore: return "CAP_CHECKPOINT_RESTORE"
}
}
}
// MARK: - Linux Implementation
#if os(Linux)
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#endif
import CShim
/// Capability type flags
public struct CapType: OptionSet, Sendable {
public let rawValue: UInt32
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
// Individual capability sets (for Get/Set/Unset/etc)
public static let effective = CapType(rawValue: 1 << 0)
public static let permitted = CapType(rawValue: 1 << 1)
public static let inheritable = CapType(rawValue: 1 << 2)
public static let bounding = CapType(rawValue: 1 << 3)
public static let ambient = CapType(rawValue: 1 << 4)
// Bulk operation flags (for Apply/Fill/Clear)
public static let caps = CapType(rawValue: 1 << 8) // CAPS - effective, permitted, inheritable
public static let bounds = CapType(rawValue: 1 << 9) // BOUNDS - bounding set
public static let ambs = CapType(rawValue: 1 << 10) // AMBS - ambient capabilities
}
private struct CapabilityHeader {
var version: UInt32
var pid: Int32
init(pid: Int32 = 0) {
self.version = 0x2008_0522
self.pid = pid
}
}
private struct CapabilityData {
var effective1: UInt32
var permitted1: UInt32
var inheritable1: UInt32
var effective2: UInt32
var permitted2: UInt32
var inheritable2: UInt32
init(
effective1: UInt32 = 0,
permitted1: UInt32 = 0,
inheritable1: UInt32 = 0,
effective2: UInt32 = 0,
permitted2: UInt32 = 0,
inheritable2: UInt32 = 0
) {
self.effective1 = effective1
self.permitted1 = permitted1
self.inheritable1 = inheritable1
self.effective2 = effective2
self.permitted2 = permitted2
self.inheritable2 = inheritable2
}
}
/// Interface with Linux capabilities
/// https://linux.die.net/man/7/capabilities
public struct LinuxCapabilities: Sendable {
private var effectiveSet: UInt64 = 0
private var permittedSet: UInt64 = 0
private var inheritableSet: UInt64 = 0
private var boundingSet: UInt64 = 0
private var ambientSet: UInt64 = 0
public init() {}
/// Get the highest supported capability from the kernel
public static func getLastSupported() throws -> CapabilityName {
guard let data = try? String(contentsOfFile: "/proc/sys/kernel/cap_last_cap", encoding: .ascii),
let lastCap = UInt32(data.trimmingCharacters(in: .whitespacesAndNewlines))
else {
throw LinuxCapabilities.Error.invalidCapabilitySet("failed to read /proc/sys/kernel/cap_last_cap")
}
guard let capability = CapabilityName.allCases.first(where: { $0.capValue == lastCap }) else {
throw LinuxCapabilities.Error.invalidCapabilitySet("no capability found for kernel max cap \(lastCap)")
}
return capability
}
/// Set keep caps
public static func setKeepCaps() throws {
let result = CZ_prctl_set_keepcaps()
if result != 0 {
throw LinuxCapabilities.Error.prctlFailed(errno: errno, operation: "PR_SET_KEEPCAPS")
}
}
/// Clear keep caps
public static func clearKeepCaps() throws {
let result = CZ_prctl_clear_keepcaps()
if result != 0 {
throw LinuxCapabilities.Error.prctlFailed(errno: errno, operation: "PR_CLEAR_KEEPCAPS")
}
}
/// Load current process capabilities from kernel
public mutating func load() throws {
let data = try getCurrentCapabilities()
self.effectiveSet = UInt64(data.effective1)
self.permittedSet = UInt64(data.permitted1)
self.inheritableSet = UInt64(data.inheritable1)
}
/// Check if capability is present in the given set
public func get(which: CapType, what: CapabilityName) -> Bool {
let bit = UInt64(1) << what.capValue
if which.contains(.effective) {
return (effectiveSet & bit) != 0
} else if which.contains(.permitted) {
return (permittedSet & bit) != 0
} else if which.contains(.inheritable) {
return (inheritableSet & bit) != 0
} else if which.contains(.bounding) {
return (boundingSet & bit) != 0
} else if which.contains(.ambient) {
return (ambientSet & bit) != 0
}
return false
}
/// Set capabilities in the given sets
public mutating func set(which: CapType, caps: [CapabilityName]) {
let mask = caps.reduce(UInt64(0)) { result, cap in
result | (UInt64(1) << cap.capValue)
}
if which.contains(.effective) {
effectiveSet |= mask
}
if which.contains(.permitted) {
permittedSet |= mask
}
if which.contains(.inheritable) {
inheritableSet |= mask
}
if which.contains(.bounding) {
boundingSet |= mask
}
if which.contains(.ambient) {
ambientSet |= mask
}
}
/// Unset capabilities from the given sets
public mutating func unset(which: CapType, caps: [CapabilityName]) {
let mask = caps.reduce(UInt64(0)) { result, cap in
result | (UInt64(1) << cap.capValue)
}
if which.contains(.effective) {
effectiveSet &= ~mask
}
if which.contains(.permitted) {
permittedSet &= ~mask
}
if which.contains(.inheritable) {
inheritableSet &= ~mask
}
if which.contains(.bounding) {
boundingSet &= ~mask
}
if which.contains(.ambient) {
ambientSet &= ~mask
}
}
/// Fill all bits of given capability types
public mutating func fill(kind: CapType) {
if kind.contains(.caps) {
effectiveSet = 0xFFFF_FFFF_FFFF_FFFF
permittedSet = 0xFFFF_FFFF_FFFF_FFFF
inheritableSet = 0
}
if kind.contains(.bounds) {
boundingSet = 0xFFFF_FFFF_FFFF_FFFF
}
if kind.contains(.ambs) {
ambientSet = 0xFFFF_FFFF_FFFF_FFFF
}
}
/// Clear all bits of given capability types
public mutating func clear(kind: CapType) {
if kind.contains(.caps) {
effectiveSet = 0
permittedSet = 0
inheritableSet = 0
}
if kind.contains(.bounds) {
boundingSet = 0
}
if kind.contains(.ambs) {
ambientSet = 0
}
}
/// Apply capabilities to current process
public func apply(kind: CapType) throws {
// Apply bounding set (requires CAP_SETPCAP)
if kind.contains(.bounds) {
try applyBoundingSet()
}
// Apply main capabilities (effective, permitted, inheritable)
if kind.contains(.caps) {
try applyMainCapabilities()
}
// Apply ambient capabilities
if kind.contains(.ambs) {
try applyAmbientCapabilities()
}
}
private func applyBoundingSet() throws {
let currentData = try getCurrentCapabilities()
let hasSetPCap = (currentData.effective1 & (1 << CapabilityName.setpcap.capValue)) != 0
if hasSetPCap {
// Get the last supported capability to avoid trying to drop unsupported ones
let lastSupported = try Self.getLastSupported()
for cap in CapabilityName.allCases {
// Skip capabilities higher than what the kernel supports
guard cap.capValue <= lastSupported.capValue else { continue }
let capBit = UInt64(1) << cap.capValue
if (boundingSet & capBit) == 0 {
let result = CZ_prctl_capbset_drop(cap.capValue)
if result != 0 && errno != EINVAL {
throw Error.prctlFailed(errno: errno, operation: "PR_CAPBSET_DROP")
}
}
}
}
}
private func applyMainCapabilities() throws {
let data = CapabilityData(
effective1: UInt32(effectiveSet & 0xFFFF_FFFF),
permitted1: UInt32(permittedSet & 0xFFFF_FFFF),
inheritable1: UInt32(inheritableSet & 0xFFFF_FFFF)
)
try setCapabilities(data: data)
}
private func applyAmbientCapabilities() throws {
// Clear all ambient capabilities first
let clearResult = CZ_prctl_cap_ambient_clear_all()
if clearResult != 0 && errno != EINVAL {
throw Error.prctlFailed(errno: errno, operation: "PR_CAP_AMBIENT_CLEAR_ALL")
}
// Get the last supported capability to avoid trying to set unsupported ones
let lastSupported = try Self.getLastSupported()
// Set each ambient capability
for cap in CapabilityName.allCases {
// Skip capabilities higher than what the kernel supports
guard cap.capValue <= lastSupported.capValue else { continue }
let capBit = UInt64(1) << cap.capValue
if (ambientSet & capBit) != 0 {
let result = CZ_prctl_cap_ambient_raise(cap.capValue)
if result != 0 && errno != EINVAL {
throw Error.prctlFailed(errno: errno, operation: "PR_CAP_AMBIENT_RAISE")
}
}
}
}
private func getCurrentCapabilities() throws -> CapabilityData {
var header = CapabilityHeader()
var data = CapabilityData()
let result = withUnsafeMutablePointer(to: &header) { headerPtr in
withUnsafeMutablePointer(to: &data) { dataPtr in
CZ_capget(headerPtr, dataPtr)
}
}
if result != 0 {
throw Error.capgetFailed(errno: errno)
}
return data
}
private func setCapabilities(data: CapabilityData) throws {
var header = CapabilityHeader()
var mutableData = data
let result = withUnsafeMutablePointer(to: &header) { headerPtr in
withUnsafeMutablePointer(to: &mutableData) { dataPtr in
CZ_capset(headerPtr, dataPtr)
}
}
if result != 0 {
throw Error.capsetFailed(errno: errno)
}
}
}
extension LinuxCapabilities {
public enum Error: Swift.Error, CustomStringConvertible {
case unsupportedCapability(name: String)
case capsetFailed(errno: Int32)
case capgetFailed(errno: Int32)
case prctlFailed(errno: Int32, operation: String)
case invalidCapabilitySet(String)
public var description: String {
switch self {
case .unsupportedCapability(let name):
return "unsupported capability: \(name)"
case .capsetFailed(let errno):
return "capset failed with errno \(errno): \(String(cString: strerror(errno)))"
case .capgetFailed(let errno):
return "capget failed with errno \(errno): \(String(cString: strerror(errno)))"
case .prctlFailed(let errno, let operation):
return "prctl(\(operation)) failed with errno \(errno): \(String(cString: strerror(errno)))"
case .invalidCapabilitySet(let message):
return "invalid capability set configuration: \(message)"
}
}
}
}
#endif
@@ -0,0 +1,189 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
#if os(Linux)
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
#if canImport(Musl)
import Musl
private let _write = Musl.write
#elseif canImport(Glibc)
import Glibc
private let _write = Glibc.write
#endif
import CShim
// On glibc, epoll constants are EPOLL_EVENTS enum values. On musl they're
// plain UInt32. These helpers normalize them to UInt32/Int32.
private func epollMask(_ value: UInt32) -> UInt32 { value }
private func epollMask(_ value: Int32) -> UInt32 { UInt32(bitPattern: value) }
#if canImport(Glibc)
private func epollMask(_ value: EPOLL_EVENTS) -> UInt32 { value.rawValue }
private func epollFlag(_ value: EPOLL_EVENTS) -> Int32 { Int32(bitPattern: value.rawValue) }
#endif
/// A thin wrapper around the Linux epoll syscall surface.
public final class Epoll: Sendable {
/// A set of epoll event flags.
public struct Mask: OptionSet, Sendable {
public let rawValue: UInt32
public init(rawValue: UInt32) {
self.rawValue = rawValue
}
public static let input = Mask(rawValue: epollMask(EPOLLIN))
public static let output = Mask(rawValue: epollMask(EPOLLOUT))
public var isHangup: Bool {
!self.isDisjoint(with: Mask(rawValue: epollMask(EPOLLHUP) | epollMask(EPOLLERR)))
}
public var isRemoteHangup: Bool {
!self.isDisjoint(with: Mask(rawValue: epollMask(EPOLLRDHUP)))
}
public var readyToRead: Bool {
self.contains(.input)
}
public var readyToWrite: Bool {
self.contains(.output)
}
}
/// An event returned by `wait()`.
public struct Event: Sendable {
public let fd: Int32
public let mask: Mask
}
private let epollFD: Int32
private let eventFD: Int32
public init() throws {
let efd = epoll_create1(Int32(EPOLL_CLOEXEC))
guard efd >= 0 else {
throw POSIXError.fromErrno()
}
let evfd = eventfd(0, Int32(EFD_CLOEXEC | EFD_NONBLOCK))
guard evfd >= 0 else {
let evfdErrno = POSIXError.fromErrno()
close(efd)
throw evfdErrno
}
self.epollFD = efd
self.eventFD = evfd
// Register the eventfd with epoll for shutdown signaling.
var event = epoll_event()
event.events = epollMask(EPOLLIN)
event.data.fd = self.eventFD
let ctlResult = withUnsafeMutablePointer(to: &event) { ptr in
epoll_ctl(efd, EPOLL_CTL_ADD, self.eventFD, ptr)
}
guard ctlResult == 0 else {
let ctlErrno = POSIXError.fromErrno()
close(evfd)
close(efd)
throw ctlErrno
}
}
deinit {
close(epollFD)
close(eventFD)
}
/// Register a file descriptor for edge-triggered monitoring.
public func add(_ fd: Int32, mask: Mask) throws {
guard fcntl(fd, F_SETFL, O_NONBLOCK) == 0 else {
throw POSIXError.fromErrno()
}
let events = epollMask(EPOLLET) | mask.rawValue
var event = epoll_event()
event.events = events
event.data.fd = fd
try withUnsafeMutablePointer(to: &event) { ptr in
if epoll_ctl(self.epollFD, EPOLL_CTL_ADD, fd, ptr) == -1 {
throw POSIXError.fromErrno()
}
}
}
/// Remove a file descriptor from the monitored collection.
public func delete(_ fd: Int32) throws {
var event = epoll_event()
let result = withUnsafeMutablePointer(to: &event) { ptr in
epoll_ctl(self.epollFD, EPOLL_CTL_DEL, fd, ptr) as Int32
}
if result != 0 {
throw POSIXError.fromErrno()
}
}
/// Wait for events.
///
/// Returns ready events, an empty array on timeout, or `nil` on shutdown.
public func wait(maxEvents: Int = 128, timeout: Int32 = -1) -> [Event]? {
var events: [epoll_event] = .init(repeating: epoll_event(), count: maxEvents)
while true {
let n = epoll_wait(self.epollFD, &events, Int32(events.count), timeout)
if n < 0 {
if errno == EINTR || errno == EAGAIN {
continue
}
preconditionFailure("epoll_wait failed unexpectedly: \(POSIXError.fromErrno())")
}
if n == 0 {
return []
}
var result: [Event] = []
result.reserveCapacity(Int(n))
for i in 0..<Int(n) {
let fd = events[i].data.fd
if fd == self.eventFD {
return nil
}
result.append(Event(fd: fd, mask: Mask(rawValue: events[i].events)))
}
return result
}
}
/// Signal the epoll loop to stop waiting.
public func shutdown() {
var val: UInt64 = 1
let n = _write(eventFD, &val, MemoryLayout<UInt64>.size)
precondition(n == MemoryLayout<UInt64>.size, "eventfd write failed: \(POSIXError.fromErrno())")
}
}
#endif // os(Linux)
@@ -0,0 +1,410 @@
//===----------------------------------------------------------------------===//
// 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 CShim
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
#if canImport(Musl)
import Musl
private let _mount = Musl.mount
private let _umount = Musl.umount2
#elseif canImport(Glibc)
import Glibc
private let _mount = Glibc.mount
private let _umount = Glibc.umount2
#endif
// Mount package modeled closely from containerd's: https://github.com/containerd/containerd/tree/main/core/mount
/// `Mount` models a Linux mount (although potentially could be used on other unix platforms), and
/// provides a simple interface to mount what the type describes.
public struct Mount: Sendable {
// Type specifies the host-specific of the mount.
public var type: String
// Source specifies where to mount from. Depending on the host system, this
// can be a source path or device.
public var source: String
// Target specifies an optional subdirectory as a mountpoint.
public var target: String
// Options contains zero or more fstab-style mount options.
public var options: [String]
public init(type: String, source: String, target: String, options: [String]) {
self.type = type
self.source = source
self.target = target
self.options = options
}
}
extension Mount {
#if canImport(Glibc)
internal typealias Flag = Int
#else
internal typealias Flag = Int32
#endif
internal struct FlagBehavior {
let clear: Bool
let flag: Flag
public init(_ clear: Bool, _ flag: Flag) {
self.clear = clear
self.flag = flag
}
}
#if os(Linux)
internal static let flagsDictionary: [String: FlagBehavior] = [
"async": .init(true, MS_SYNCHRONOUS),
"atime": .init(true, MS_NOATIME),
"bind": .init(false, MS_BIND),
"defaults": .init(false, 0),
"dev": .init(true, MS_NODEV),
"diratime": .init(true, MS_NODIRATIME),
"dirsync": .init(false, MS_DIRSYNC),
"exec": .init(true, MS_NOEXEC),
"mand": .init(false, MS_MANDLOCK),
"noatime": .init(false, MS_NOATIME),
"nodev": .init(false, MS_NODEV),
"nodiratime": .init(false, MS_NODIRATIME),
"noexec": .init(false, MS_NOEXEC),
"nomand": .init(true, MS_MANDLOCK),
"norelatime": .init(true, MS_RELATIME),
"nostrictatime": .init(true, MS_STRICTATIME),
"nosuid": .init(false, MS_NOSUID),
"rbind": .init(false, MS_BIND | MS_REC),
"relatime": .init(false, MS_RELATIME),
"remount": .init(false, MS_REMOUNT),
"ro": .init(false, MS_RDONLY),
"rw": .init(true, MS_RDONLY),
"strictatime": .init(false, MS_STRICTATIME),
"suid": .init(true, MS_NOSUID),
"sync": .init(false, MS_SYNCHRONOUS),
]
internal struct MountOptions {
var flags: Int32
var data: [String]
public init(_ flags: Int32 = 0, data: [String] = []) {
self.flags = flags
self.data = data
}
}
/// Whether the mount is read only.
public var readOnly: Bool {
for option in self.options {
if option == "ro" {
return true
}
}
return false
}
/// Mount the mount relative to `root` with the current set of data in the object.
///
/// Optionally provide `createWithPerms` to set the permissions for the directory that
/// it will be mounted at.
public func mount(root: String, createWithPerms: Int16? = nil) throws {
let fd = try secureResolveInRoot(root: root)
defer { close(fd) }
let realPath = try readlinkProc(fd: fd)
try self.mountToTarget(target: realPath, createWithPerms: createWithPerms, targetResolved: true)
}
/// Open a path relative to `dirFd` using `openat2(2)` with `RESOLVE_IN_ROOT`.
///
/// All symlink resolution is confined to the directory tree beneath `dirFd`.
/// Returns the file descriptor on success, or -1 on failure (with errno set).
private func openInRoot(dirFd: Int32, path: String, flags: Int32, mode: UInt64 = 0) -> Int32 {
path.withCString { cPath in
var how = cz_open_how(
flags: UInt64(flags),
mode: mode,
resolve: UInt64(RESOLVE_IN_ROOT)
)
return CZ_openat2(dirFd, cPath, &how, MemoryLayout<cz_open_how>.size)
}
}
private func secureResolveInRoot(root: String) throws -> Int32 {
let rootFd = open(root, O_RDONLY | O_DIRECTORY | O_CLOEXEC)
guard rootFd >= 0 else {
throw Error.errno(errno, "failed to open rootfs '\(root)'")
}
// Determine if the leaf mount point should be a file or directory.
let opts = parseMountOptions()
let isBindMount = (opts.flags & Int32(MS_BIND)) != 0
var leafIsFile = false
if isBindMount {
var sourceStat = stat()
if stat(self.source, &sourceStat) == 0 {
leafIsFile = (sourceStat.st_mode & S_IFMT) != S_IFDIR
}
}
// Normalize target to a relative path for openat2.
let relativePath = self.target
.split(separator: "/", omittingEmptySubsequences: true)
.joined(separator: "/")
guard !relativePath.isEmpty else {
return rootFd
}
// Fast path: try openat2 with RESOLVE_IN_ROOT for the full path.
let openFlags: Int32 =
leafIsFile
? (O_RDONLY | O_CLOEXEC)
: (O_RDONLY | O_DIRECTORY | O_CLOEXEC)
let fd = openInRoot(dirFd: rootFd, path: relativePath, flags: openFlags)
if fd >= 0 {
close(rootFd)
return fd
}
guard errno == ENOENT else {
let savedErrno = errno
close(rootFd)
throw Error.errno(savedErrno, "failed to resolve '\(self.target)' in rootfs")
}
// Part of the path doesn't exist. Use openat2 to find the deepest
// existing ancestor, then create the missing components.
return try createMountTarget(
rootFd: rootFd, relativePath: relativePath, leafIsFile: leafIsFile
)
}
private func createMountTarget(
rootFd: Int32,
relativePath: String,
leafIsFile: Bool
) throws -> Int32 {
let components = relativePath.split(separator: "/").map(String.init)
var currentFd = rootFd
var resultFd: Int32 = -1
// Centralized cleanup. On success resultFd holds the fd we return,
// so we avoid closing it. On error resultFd is -1 and we close
// everything.
defer {
if currentFd != rootFd && currentFd != resultFd { close(currentFd) }
if rootFd != resultFd { close(rootFd) }
}
func fail(_ savedErrno: Int32, _ message: String) throws -> Never {
throw Error.errno(savedErrno, message)
}
// Find the deepest existing directory using openat2 with RESOLVE_IN_ROOT.
var firstMissing = 0
for i in 0..<components.count {
let subpath = components[0...i].joined(separator: "/")
let nextFd = openInRoot(dirFd: rootFd, path: subpath, flags: O_RDONLY | O_DIRECTORY | O_CLOEXEC)
if nextFd < 0 {
firstMissing = i
break
}
if currentFd != rootFd { close(currentFd) }
currentFd = nextFd
firstMissing = i + 1
}
// Create missing directories and the leaf mount point.
for i in firstMissing..<components.count {
let component = components[i]
let isLast = (i == components.count - 1)
if isLast && leafIsFile {
// Use mknodat to create a regular file without opening it.
let rc = mknodat(currentFd, component, S_IFREG | 0o644, 0)
if rc != 0 && errno != EEXIST {
try fail(errno, "failed to create mount point file '\(component)'")
}
let pathFd = openat(currentFd, component, O_RDONLY | O_NOFOLLOW | O_CLOEXEC)
guard pathFd >= 0 else {
try fail(errno, "failed to re-open mount point file '\(component)'")
}
resultFd = pathFd
return resultFd
}
guard mkdirat(currentFd, component, 0o755) == 0 else {
try fail(errno, "failed to create directory '\(component)'")
}
let dirFd = openat(currentFd, component, O_RDONLY | O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC)
guard dirFd >= 0 else {
try fail(errno, "failed to open created directory '\(component)'")
}
if isLast {
resultFd = dirFd
return resultFd
}
if currentFd != rootFd { close(currentFd) }
currentFd = dirFd
}
// All components already existed.
resultFd = currentFd
return resultFd
}
/// Resolve the real filesystem path for an open fd via /proc/self/fd.
private func readlinkProc(fd: Int32) throws -> String {
let procPath = "/proc/self/fd/\(fd)"
var buffer = [CChar](repeating: 0, count: Int(PATH_MAX) + 1)
let len = readlink(procPath, &buffer, buffer.count - 1)
guard len > 0 else {
throw Error.errno(errno, "readlink failed for '\(procPath)'")
}
return buffer.prefix(len).withUnsafeBufferPointer { buf in
String(decoding: buf.map { UInt8(bitPattern: $0) }, as: UTF8.self)
}
}
/// Mount the mount with the current set of data in the object. Optionally
/// provide `createWithPerms` to set the permissions for the directory that
/// it will be mounted at.
public func mount(createWithPerms: Int16? = nil) throws {
try self.mountToTarget(target: self.target, createWithPerms: createWithPerms)
}
private func mountToTarget(target: String, createWithPerms: Int16?, targetResolved: Bool = false) throws {
let pageSize = sysconf(Int32(_SC_PAGESIZE))
let opts = parseMountOptions()
let dataString = opts.data.joined(separator: ",")
if dataString.count > pageSize {
throw Error.validation("data string exceeds page size (\(dataString.count) > \(pageSize))")
}
let propagationTypes: Int32 = Int32(MS_SHARED) | Int32(MS_PRIVATE) | Int32(MS_SLAVE) | Int32(MS_UNBINDABLE)
// Ensure propagation type change flags aren't included in other calls.
let originalFlags = opts.flags & ~(propagationTypes)
// When targetResolved is true, the target path has already been securely
// resolved and the mount point created by secureResolveInRoot. Skip
// directory/file creation to avoid following symlinks in the target path.
if !targetResolved {
let targetURL = URL(fileURLWithPath: target)
let targetParent = targetURL.deletingLastPathComponent().path
if let perms = createWithPerms {
try mkdirAll(targetParent, perms)
}
// For bind mounts, check if the source is a file and create the target accordingly.
let isBindMount = (originalFlags & Int32(MS_BIND)) != 0
if isBindMount {
var sourceIsNonDir = false
var sourceStat = stat()
if stat(self.source, &sourceStat) == 0 {
sourceIsNonDir = (sourceStat.st_mode & S_IFMT) != S_IFDIR
}
if sourceIsNonDir {
// Create parent directories and touch the target file
try mkdirAll(targetParent, 0o755)
let fd = open(target, O_WRONLY | O_CREAT, 0o644)
if fd >= 0 {
close(fd)
}
} else {
try mkdirAll(target, 0o755)
}
} else {
try mkdirAll(target, 0o755)
}
}
if opts.flags & Int32(MS_REMOUNT) == 0 || !dataString.isEmpty {
guard _mount(self.source, target, self.type, UInt(originalFlags), dataString) == 0 else {
throw Error.errno(
errno,
"failed initial mount source=\(self.source) target=\(target) type=\(self.type) data=\(dataString)"
)
}
}
if opts.flags & propagationTypes != 0 {
// Change the propagation type.
let pflags = propagationTypes | Int32(MS_REC) | Int32(MS_SILENT)
guard _mount("", target, "", UInt(opts.flags & pflags), "") == 0 else {
throw Error.errno(errno, "failed propagation change mount")
}
}
let bindReadOnlyFlags = Int32(MS_BIND) | Int32(MS_RDONLY)
if originalFlags & bindReadOnlyFlags == bindReadOnlyFlags {
guard _mount("", target, "", UInt(originalFlags | Int32(MS_REMOUNT)), "") == 0 else {
throw Error.errno(errno, "failed bind mount")
}
}
}
private func mkdirAll(_ name: String, _ perm: Int16) throws {
try FileManager.default.createDirectory(
atPath: name,
withIntermediateDirectories: true,
attributes: [.posixPermissions: perm]
)
}
private func parseMountOptions() -> MountOptions {
var mountOpts = MountOptions()
for option in self.options {
if let entry = Self.flagsDictionary[option], entry.flag != 0 {
if entry.clear {
mountOpts.flags &= ~Int32(entry.flag)
} else {
mountOpts.flags |= Int32(entry.flag)
}
} else {
mountOpts.data.append(option)
}
}
return mountOpts
}
/// `Mount` errors
public enum Error: Swift.Error, CustomStringConvertible {
case errno(Int32, String)
case validation(String)
public var description: String {
switch self {
case .errno(let errno, let message):
return "mount failed with errno \(errno): \(message)"
case .validation(let message):
return "failed during validation: \(message)"
}
}
}
#endif
}
@@ -0,0 +1,38 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#endif
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
extension POSIXError {
public static func fromErrno() -> POSIXError {
guard let errCode = POSIXErrorCode(rawValue: errno) else {
fatalError("failed to convert errno to POSIXErrorCode")
}
return POSIXError(errCode)
}
}
+88
View File
@@ -0,0 +1,88 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
/// `Path` provides utilities to look for binaries in the current PATH,
/// or to return the current PATH.
public struct Path {
/// lookPath looks up an executable's path from $PATH
public static func lookPath(_ name: String) -> URL? {
lookup(name, path: getCurrentPath())
}
public static func lookPath(_ name: String, path: String) -> URL? {
lookup(name, path: path)
}
// getEnv returns the default environment of the process
// with the default $PATH added for the context of a macOS application bundle
public static func getEnv() -> [String: String] {
var env = ProcessInfo.processInfo.environment
env["PATH"] = getCurrentPath()
return env
}
private static func lookup(_ name: String, path: String) -> URL? {
// Return nil for empty names
if name.isEmpty {
return nil
}
if name.contains("/") {
if findExec(name) {
return URL(fileURLWithPath: name)
}
return nil
}
for var lookdir in path.split(separator: ":") {
if lookdir.isEmpty {
lookdir = "."
}
let file = URL(fileURLWithPath: String(lookdir)).appendingPathComponent(name)
if findExec(file.path) {
return file
}
}
return nil
}
/// getPath returns $PATH for the current process
public static func getCurrentPath() -> String {
let env = ProcessInfo.processInfo.environment
return env["PATH"] ?? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
}
// findPath returns a string containing the 'PATH' environment variable
public static func findPath(_ env: [String]?) -> String? {
guard let env = env else {
return nil
}
return env.first(where: { $0.hasPrefix("PATH=") })
.map { String($0.dropFirst(5)) }
}
// findExec returns true if the provided path is an executable
private static func findExec(_ path: String) -> Bool {
let fm = FileManager.default
return fm.isExecutableFile(atPath: path)
}
}
@@ -0,0 +1,43 @@
//===----------------------------------------------------------------------===//
// 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 Foundation
extension Pipe {
/// Close both sides of the pipe.
public func close() throws {
var err: Swift.Error?
do {
try self.fileHandleForReading.close()
} catch {
err = error
}
try self.fileHandleForWriting.close()
if let err {
throw err
}
}
/// Ensure that both sides of the pipe are set with O_CLOEXEC.
public func setCloexec() throws {
if fcntl(self.fileHandleForWriting.fileDescriptor, F_SETFD, FD_CLOEXEC) == -1 {
throw POSIXError(.init(rawValue: errno)!)
}
if fcntl(self.fileHandleForReading.fileDescriptor, F_SETFD, FD_CLOEXEC) == -1 {
throw POSIXError(.init(rawValue: errno)!)
}
}
}
+3
View File
@@ -0,0 +1,3 @@
## OS
This target contains general useful OS related definitions or wrappers.
+60
View File
@@ -0,0 +1,60 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#endif
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
/// A process reaper that returns exited processes along
/// with their exit status.
public struct Reaper {
/// Process's pid and exit status.
typealias Exit = (pid: Int32, status: Int32)
/// Reap all pending processes and return the pid and exit status.
public static func reap() -> [Int32: Int32] {
var reaped = [Int32: Int32]()
while true {
guard let exit = wait() else {
return reaped
}
reaped[exit.pid] = exit.status
}
return reaped
}
/// Returns the exit status of the last process that exited.
/// nil is returned when no pending processes exist.
private static func wait() -> Exit? {
var rus = rusage()
var ws = Int32()
let pid = wait4(-1, &ws, WNOHANG, &rus)
if pid <= 0 {
return nil
}
return (pid: pid, status: Command.toExitStatus(ws))
}
}
@@ -0,0 +1,419 @@
//===----------------------------------------------------------------------===//
// 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 ContainerizationError
import Dispatch
import Logging
import Synchronization
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#endif
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
/// Manages bidirectional data relay between two file descriptors using `DispatchSource`.
///
/// Uses non-blocking I/O with backpressure: when a destination fd's buffer is full,
/// the relay suspends reading from the source and installs a `DispatchSourceWrite`
/// to resume once the destination is writable again. This prevents blocking the
/// dispatch queue and avoids head-of-line blocking across connections.
///
/// ## Concurrency model
///
/// The class has two distinct synchronization domains:
///
/// - **Serial dispatch queue** owns all I/O state: the `Direction` objects (`d1`, `d2`)
/// and their read buffers (`buf1`, `buf2`). Every event handler, cancel handler, and
/// `stop()` call runs on this queue. No locks are needed for that state because the
/// queue is the exclusive executor. Fields in this domain are marked `nonisolated(unsafe)`.
///
/// - **Mutexes** protect the two pieces of state that cross the queue boundary:
/// `activeDirections` (written by `start()`, which may run off-queue) and
/// `completionState` (read by `waitForCompletion()` from any async context).
public final class BidirectionalRelay: Sendable {
private let fd1: Int32
private let fd2: Int32
private let log: Logger?
private let queue: DispatchQueue
private static let queueKey = DispatchSpecificKey<Void>()
/// Owns one direction of the relay: its read source, optional write source, and
/// any data buffered due to backpressure.
///
/// All methods must be called only from the relay's serial dispatch queue.
private final class Direction {
var readSource: DispatchSourceRead?
var writeSource: DispatchSourceWrite?
var pendingData: [UInt8] = []
var pendingOffset: Int = 0
private var readSuspended = false
func suspendRead() {
guard let src = readSource, !src.isCancelled, !readSuspended else { return }
readSuspended = true
src.suspend()
}
func resumeRead() {
guard let src = readSource, !src.isCancelled, readSuspended else { return }
readSuspended = false
src.resume()
}
/// Resumes the read source before cancelling it if it is suspended.
/// GCD does not deliver a cancel handler for a suspended source until it is resumed.
func cancelRead() {
guard let src = readSource, !src.isCancelled else { return }
if readSuspended {
readSuspended = false
src.resume()
}
src.cancel()
}
}
private enum CompletionState {
case pending
case waiting(CheckedContinuation<Void, Never>)
case completed
}
private enum CopyResult {
case ok
case blocked
case eof
}
// Queue-owned state. Written by start() before activate(), so all subsequent
// accesses from event/cancel handlers observe the initialized values without
// additional synchronization. nonisolated(unsafe) declares that we are taking
// responsibility for this; the serial queue is the enforcing mechanism.
private nonisolated(unsafe) let d1 = Direction() // fd1 fd2
private nonisolated(unsafe) let d2 = Direction() // fd2 fd1
private nonisolated(unsafe) let buf1: UnsafeMutableBufferPointer<UInt8>
private nonisolated(unsafe) let buf2: UnsafeMutableBufferPointer<UInt8>
// Counts active read sources. Set to 2 in start() (possibly off-queue) and
// decremented in cancel handlers (always on the queue). The Mutex provides the
// cross-thread visibility guarantee for the initial write from start(). Whichever
// cancel handler drives the count to zero calls closeBothFds() exactly once
// no cross-source isCancelled checks, no possibility of double-close.
private let activeDirections: Mutex<Int>
// May be read from any async context (waitForCompletion) and written from the
// queue (closeBothFds), so it needs a Mutex rather than queue-only protection.
private let completionState: Mutex<CompletionState>
/// Creates a new bidirectional relay between two file descriptors.
///
/// - Parameters:
/// - fd1: The first file descriptor.
/// - fd2: The second file descriptor.
/// - queue: The dispatch queue to use for I/O operations. If nil, a new queue is created.
/// - log: The optional logger for debugging.
public init(
fd1: Int32,
fd2: Int32,
queue: DispatchQueue? = nil,
log: Logger? = nil
) {
self.fd1 = fd1
self.fd2 = fd2
self.queue = queue ?? DispatchQueue(label: "com.apple.containerization.bidirectional-relay")
self.queue.setSpecific(key: Self.queueKey, value: ())
self.log = log
self.activeDirections = Mutex(0)
self.completionState = Mutex(.pending)
let pageSize = Int(getpagesize())
self.buf1 = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: pageSize)
self.buf2 = UnsafeMutableBufferPointer<UInt8>.allocate(capacity: pageSize)
}
deinit {
buf1.deallocate()
buf2.deallocate()
}
private static func setNonBlocking(_ fd: Int32) throws {
let flags = fcntl(fd, F_GETFL)
guard flags != -1 else {
throw ContainerizationError(
.internalError,
message: "fcntl F_GETFL failed on fd \(fd): errno \(errno)"
)
}
guard fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1 else {
throw ContainerizationError(
.internalError,
message: "fcntl F_SETFL O_NONBLOCK failed on fd \(fd): errno \(errno)"
)
}
}
/// Starts the bidirectional relay to copy data between fd1 and fd2.
public func start() throws {
try Self.setNonBlocking(fd1)
try Self.setNonBlocking(fd2)
let src1 = DispatchSource.makeReadSource(fileDescriptor: fd1, queue: queue)
let src2 = DispatchSource.makeReadSource(fileDescriptor: fd2, queue: queue)
d1.readSource = src1
d2.readSource = src2
activeDirections.withLock { $0 = 2 }
src1.setEventHandler { [self] in handleRead(d1, from: fd1, to: fd2, buffer: buf1) }
src2.setEventHandler { [self] in handleRead(d2, from: fd2, to: fd1, buffer: buf2) }
src1.setCancelHandler { [self] in
d1.writeSource?.cancel()
d1.writeSource = nil
directionFinished()
}
src2.setCancelHandler { [self] in
d2.writeSource?.cancel()
d2.writeSource = nil
directionFinished()
}
src1.activate()
src2.activate()
}
/// Stops the relay and closes both file descriptors.
public func stop() {
runOnQueue {
d1.cancelRead()
d2.cancelRead()
}
}
/// Waits for the relay to complete.
public func waitForCompletion() async {
await withCheckedContinuation { c in
completionState.withLock { state in
switch state {
case .pending:
state = .waiting(c)
case .waiting:
fatalError("waitForCompletion called multiple times")
case .completed:
c.resume()
}
}
}
}
private func runOnQueue(_ work: () -> Void) {
if DispatchQueue.getSpecific(key: Self.queueKey) != nil {
work()
} else {
queue.sync(execute: work)
}
}
private func directionFinished() {
let remaining = activeDirections.withLock { count -> Int in
count -= 1
return count
}
if remaining == 0 {
closeBothFds()
}
}
private func handleRead(
_ dir: Direction,
from srcFd: Int32,
to dstFd: Int32,
buffer: UnsafeMutableBufferPointer<UInt8>
) {
do {
switch try Self.copy(buffer: buffer, from: srcFd, to: dstFd, direction: dir) {
case .ok:
break
case .eof:
log?.debug(
"source EOF",
metadata: ["sourceFd": "\(srcFd)", "destinationFd": "\(dstFd)"]
)
dir.cancelRead()
if shutdown(dstFd, Int32(SHUT_WR)) != 0 {
log?.debug(
"shutdown(SHUT_WR) failed",
metadata: ["fd": "\(dstFd)", "errno": "\(errno)"]
)
}
case .blocked:
log?.debug(
"write blocked, applying backpressure",
metadata: [
"sourceFd": "\(srcFd)",
"destinationFd": "\(dstFd)",
"pendingBytes": "\(dir.pendingData.count)",
]
)
dir.suspendRead()
installWriteSource(for: dir, from: srcFd, to: dstFd)
}
} catch {
log?.warning(
"I/O error",
metadata: [
"sourceFd": "\(srcFd)",
"destinationFd": "\(dstFd)",
"error": "\(error)",
]
)
dir.cancelRead()
if shutdown(dstFd, Int32(SHUT_RDWR)) != 0 {
log?.warning(
"shutdown(SHUT_RDWR) failed",
metadata: ["fd": "\(dstFd)", "errno": "\(errno)"]
)
}
}
}
private func installWriteSource(for dir: Direction, from srcFd: Int32, to dstFd: Int32) {
let ws = DispatchSource.makeWriteSource(fileDescriptor: dstFd, queue: queue)
dir.writeSource = ws
ws.setEventHandler { [self] in drainPending(dir: dir, from: srcFd, to: dstFd) }
// No cancel handler: clearing pendingData from a cancel handler would race with
// a newly installed write source if drainPending completes and the read source
// immediately produces another blocked write, installing a fresh write source
// before the old cancel handler fires. pendingData is cleared explicitly by
// drainPending on success, and freed with Direction when the relay is torn down.
ws.activate()
}
private func drainPending(dir: Direction, from srcFd: Int32, to dstFd: Int32) {
let remaining = dir.pendingData.count - dir.pendingOffset
guard remaining > 0 else { return }
let n = dir.pendingData.withUnsafeBufferPointer { buf in
write(dstFd, buf.baseAddress!.advanced(by: dir.pendingOffset), remaining)
}
if n > 0 {
dir.pendingOffset += n
if dir.pendingOffset >= dir.pendingData.count {
dir.writeSource?.cancel()
dir.writeSource = nil
dir.pendingData = []
dir.pendingOffset = 0
log?.debug(
"backpressure relieved, resuming reads",
metadata: ["sourceFd": "\(srcFd)", "destinationFd": "\(dstFd)"]
)
dir.resumeRead()
}
} else if n == -1 && errno == EAGAIN {
// Spurious write-ready notification; wait for the next one.
} else {
log?.warning(
"write error during pending drain",
metadata: ["destinationFd": "\(dstFd)", "errno": "\(errno)"]
)
dir.writeSource?.cancel()
dir.writeSource = nil
dir.cancelRead()
if shutdown(dstFd, Int32(SHUT_RDWR)) != 0 {
log?.warning(
"shutdown(SHUT_RDWR) failed after drain error",
metadata: ["fd": "\(dstFd)", "errno": "\(errno)"]
)
}
}
}
/// Drains srcFd into dstFd in a loop until EAGAIN/EWOULDBLOCK or EOF.
///
/// Looping until EAGAIN is required on Linux: libdispatch uses FIONREAD to decide
/// whether to fire the read event, so when the only remaining readable condition is
/// EOF (FIONREAD == 0), the event is suppressed. Reading in a loop here ensures we
/// observe read() == 0 on the same handler invocation that drained the last bytes.
private static func copy(
buffer: UnsafeMutableBufferPointer<UInt8>,
from srcFd: Int32,
to dstFd: Int32,
direction: Direction
) throws -> CopyResult {
guard let base = buffer.baseAddress else {
throw ContainerizationError(.invalidState, message: "buffer has no base address")
}
readLoop: while true {
let nr = read(srcFd, base, buffer.count)
if nr == 0 { return .eof }
if nr < 0 {
if errno == EAGAIN || errno == EWOULDBLOCK { return .ok }
if errno == EINTR { continue readLoop }
throw ContainerizationError(
.internalError,
message: "read failed: fd \(srcFd), errno \(errno)"
)
}
var offset = 0
while offset < nr {
let nw = write(dstFd, base.advanced(by: offset), nr - offset)
if nw > 0 {
offset += nw
} else if nw < 0 {
if errno == EINTR { continue }
if errno == EAGAIN || errno == EWOULDBLOCK {
direction.pendingData = Array(
UnsafeBufferPointer(start: base.advanced(by: offset), count: nr - offset)
)
direction.pendingOffset = 0
return .blocked
}
throw ContainerizationError(
.internalError,
message: "write failed: fd \(dstFd), errno \(errno)"
)
} else {
throw ContainerizationError(
.internalError,
message: "zero-byte write on fd \(dstFd)"
)
}
}
}
}
private func closeBothFds() {
log?.debug("closing fds", metadata: ["fd1": "\(fd1)", "fd2": "\(fd2)"])
close(fd1)
close(fd2)
completionState.withLock { state in
if case .waiting(let c) = state { c.resume() }
state = .completed
}
}
}
@@ -0,0 +1,494 @@
//===----------------------------------------------------------------------===//
// 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 CShim
import Foundation
import Synchronization
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#else
#error("Socket not supported on this platform.")
#endif
#if !os(Windows)
let sysFchmod = fchmod
let sysRead = read
let sysUnlink = unlink
let sysSend = send
let sysClose = close
let sysShutdown = shutdown
let sysBind = bind
let sysSocket = socket
let sysSetsockopt = setsockopt
let sysGetsockopt = getsockopt
let sysListen = listen
let sysAccept = accept
let sysConnect = connect
let sysIoctl: @convention(c) (CInt, CUnsignedLong, UnsafeMutableRawPointer) -> CInt = ioctl
let sysRecvmsg = recvmsg
#endif
/// Thread-safe socket wrapper.
public final class Socket: Sendable {
public enum TimeoutOption {
case send
case receive
}
public enum ShutdownOption {
case read
case write
case readWrite
}
private enum SocketState {
case created
case connected
case listening
}
private struct State {
let socketState: SocketState
let handle: FileHandle?
let type: SocketType
let acceptSource: DispatchSourceRead?
}
private let _closeOnDeinit: Bool
private let _queue: DispatchQueue
private let state: Mutex<State>
public var fileDescriptor: Int32 {
guard let handle = state.withLock({ $0.handle }) else {
return -1
}
return handle.fileDescriptor
}
public convenience init(type: SocketType, closeOnDeinit: Bool = true) throws {
let sockFD = sysSocket(type.domain, type.type, 0)
if sockFD < 0 {
throw SocketError.withErrno("failed to create socket: \(sockFD)", errno: errno)
}
self.init(fd: sockFD, type: type, closeOnDeinit: closeOnDeinit)
}
init(fd: Int32, type: SocketType, closeOnDeinit: Bool) {
_queue = DispatchQueue(label: "com.apple.containerization.socket")
_closeOnDeinit = closeOnDeinit
let state = State(
socketState: .created,
handle: FileHandle(fileDescriptor: fd, closeOnDealloc: false),
type: type,
acceptSource: nil
)
self.state = Mutex(state)
}
/// Internal initializer for wrapping already-connected file descriptors (e.g., from socketpair)
/// Ideally we just get rid of the state machine in this class. Not sure how much value it provides..
init(fd: Int32, type: SocketType, closeOnDeinit: Bool, connected: Bool) {
_queue = DispatchQueue(label: "com.apple.containerization.socket")
_closeOnDeinit = closeOnDeinit
let state = State(
socketState: connected ? .connected : .created,
handle: FileHandle(fileDescriptor: fd, closeOnDealloc: false),
type: type,
acceptSource: nil
)
self.state = Mutex(state)
}
deinit {
if _closeOnDeinit {
try? close()
}
}
}
extension Socket {
static func errnoToError(msg: String) -> SocketError {
SocketError.withErrno("\(msg) (\(_errnoString(errno)))", errno: errno)
}
public func connect() throws {
try state.withLock { currentState in
guard currentState.socketState == .created else {
throw SocketError.invalidOperationOnSocket("connect")
}
guard let handle = currentState.handle else {
throw SocketError.closed
}
var res: Int32 = 0
try currentState.type.withSockAddr { (ptr, length) in
res = Syscall.retrying {
sysConnect(handle.fileDescriptor, ptr, length)
}
}
if res == -1 {
throw Socket.errnoToError(msg: "could not connect to socket \(currentState.type)")
}
currentState = State(
socketState: .connected,
handle: handle,
type: currentState.type,
acceptSource: currentState.acceptSource
)
}
}
public func listen() throws {
try state.withLock { currentState in
guard currentState.socketState == .created else {
throw SocketError.invalidOperationOnSocket("listen")
}
guard let handle = currentState.handle else {
throw SocketError.closed
}
try currentState.type.beforeBind(fd: handle.fileDescriptor)
var rc: Int32 = 0
try currentState.type.withSockAddr { (ptr, length) in
rc = sysBind(handle.fileDescriptor, ptr, length)
}
if rc < 0 {
throw Socket.errnoToError(msg: "could not bind to \(currentState.type)")
}
try currentState.type.beforeListen(fd: handle.fileDescriptor)
if sysListen(handle.fileDescriptor, SOMAXCONN) < 0 {
throw Socket.errnoToError(msg: "listen failed on \(currentState.type)")
}
currentState = State(
socketState: .listening,
handle: handle,
type: currentState.type,
acceptSource: currentState.acceptSource
)
}
}
public func close() throws {
try state.withLock { currentState in
guard let handle = currentState.handle else {
// Already closed.
return
}
let acceptSource = currentState.acceptSource
acceptSource?.cancel()
try handle.close()
currentState = State(
socketState: currentState.socketState,
handle: nil,
type: currentState.type,
acceptSource: nil
)
}
}
public func write(data: any DataProtocol) throws -> Int {
let handle = try state.withLock { currentState in
guard currentState.socketState == .connected else {
throw SocketError.invalidOperationOnSocket("write")
}
guard let handle = currentState.handle else {
throw SocketError.closed
}
return handle
}
if data.isEmpty {
return 0
}
try handle.write(contentsOf: data)
return data.count
}
public func acceptStream(closeOnDeinit: Bool = true) throws -> AsyncThrowingStream<Socket, Swift.Error> {
let source = try state.withLock { currentState -> DispatchSourceRead in
guard currentState.socketState == .listening else {
throw SocketError.invalidOperationOnSocket("accept")
}
guard let handle = currentState.handle else {
throw SocketError.closed
}
guard currentState.acceptSource == nil else {
throw SocketError.acceptStreamExists
}
let source = DispatchSource.makeReadSource(
fileDescriptor: handle.fileDescriptor,
queue: _queue
)
currentState = State(
socketState: currentState.socketState,
handle: handle,
type: currentState.type,
acceptSource: source
)
return source
}
return AsyncThrowingStream { cont in
source.setCancelHandler {
cont.finish()
}
source.setEventHandler(handler: {
if source.data == 0 {
source.cancel()
return
}
do {
let connection = try self.accept(closeOnDeinit: closeOnDeinit)
cont.yield(connection)
} catch SocketError.closed {
source.cancel()
} catch {
cont.yield(with: .failure(error))
source.cancel()
}
})
source.activate()
}
}
public func accept(closeOnDeinit: Bool = true) throws -> Socket {
let (handle, socketType) = try state.withLock { currentState in
guard currentState.socketState == .listening else {
throw SocketError.invalidOperationOnSocket("accept")
}
guard let handle = currentState.handle else {
throw SocketError.closed
}
return (handle, currentState.type)
}
let (clientFD, newSocketType) = try socketType.accept(fd: handle.fileDescriptor)
return Socket(
fd: clientFD,
type: newSocketType,
closeOnDeinit: closeOnDeinit,
connected: true
)
}
/// Receive a file descriptor via SCM_RIGHTS control message.
/// This is commonly used for passing file descriptors between processes via Unix domain sockets.
public func receiveFileDescriptor() throws -> Int32 {
let handle = try state.withLock { currentState in
guard currentState.socketState == .connected else {
throw SocketError.invalidOperationOnSocket("receiveFileDescriptor")
}
guard let handle = currentState.handle else {
throw SocketError.closed
}
return handle
}
var msg = msghdr()
var iov = iovec()
var buf: UInt8 = 0
iov.iov_base = withUnsafeMutablePointer(to: &buf) { UnsafeMutableRawPointer($0) }
iov.iov_len = 1
msg.msg_iov = withUnsafeMutablePointer(to: &iov) { $0 }
msg.msg_iovlen = 1
var cmsgBuf = [UInt8](repeating: 0, count: Int(CZ_CMSG_SPACE(Int(MemoryLayout<Int32>.size))))
msg.msg_control = withUnsafeMutablePointer(to: &cmsgBuf[0]) { UnsafeMutableRawPointer($0) }
msg.msg_controllen = numericCast(cmsgBuf.count)
let recvResult = withUnsafeMutablePointer(to: &msg) { msgPtr in
sysRecvmsg(handle.fileDescriptor, msgPtr, 0)
}
guard recvResult >= 0 else {
throw Socket.errnoToError(msg: "recvmsg failed")
}
// Extract file descriptor from control message
let cmsgPtr = withUnsafeMutablePointer(to: &msg) { CZ_CMSG_FIRSTHDR($0) }
guard let cmsg = cmsgPtr else {
throw SocketError.invalidFileDescriptor
}
guard cmsg.pointee.cmsg_level == SOL_SOCKET,
cmsg.pointee.cmsg_type == SCM_RIGHTS
else {
throw SocketError.invalidFileDescriptor
}
guard let dataPtr = CZ_CMSG_DATA(cmsg) else {
throw SocketError.invalidFileDescriptor
}
let fdPtr = dataPtr.assumingMemoryBound(to: Int32.self)
let fd = fdPtr.pointee
guard fd >= 0 else {
throw SocketError.invalidFileDescriptor
}
return fd
}
public func read(buffer: inout Data) throws -> Int {
let handle = try state.withLock { currentState in
guard currentState.socketState == .connected else {
throw SocketError.invalidOperationOnSocket("read")
}
guard let handle = currentState.handle else {
throw SocketError.closed
}
return handle
}
var bytesRead = 0
let bufferSize = buffer.count
try buffer.withUnsafeMutableBytes { pointer in
guard let baseAddress = pointer.baseAddress else {
throw SocketError.missingBaseAddress
}
bytesRead = Syscall.retrying {
sysRead(handle.fileDescriptor, baseAddress, bufferSize)
}
if bytesRead < 0 {
throw Socket.errnoToError(msg: "error reading from connection")
} else if bytesRead == 0 {
throw SocketError.closed
}
}
return bytesRead
}
public func shutdown(how: ShutdownOption) throws {
let handle = try state.withLock { currentState in
guard let handle = currentState.handle else {
throw SocketError.closed
}
return handle
}
var howOpt: Int32 = 0
switch how {
case .read:
howOpt = Int32(SHUT_RD)
case .write:
howOpt = Int32(SHUT_WR)
case .readWrite:
howOpt = Int32(SHUT_RDWR)
}
if sysShutdown(handle.fileDescriptor, howOpt) < 0 {
throw Socket.errnoToError(msg: "shutdown failed")
}
}
public func setSockOpt(sockOpt: Int32 = 0, ptr: UnsafeRawPointer, stride: UInt32) throws {
let handle = try state.withLock { currentState in
guard let handle = currentState.handle else {
throw SocketError.closed
}
return handle
}
if setsockopt(handle.fileDescriptor, SOL_SOCKET, sockOpt, ptr, stride) < 0 {
throw Socket.errnoToError(msg: "failed to set sockopt")
}
}
public func setTimeout(option: TimeoutOption, seconds: Int) throws {
let handle = try state.withLock { currentState in
guard let handle = currentState.handle else {
throw SocketError.closed
}
return handle
}
var sockOpt: Int32 = 0
switch option {
case .receive:
sockOpt = SO_RCVTIMEO
case .send:
sockOpt = SO_SNDTIMEO
}
var timer = timeval()
timer.tv_sec = seconds
timer.tv_usec = 0
if setsockopt(
handle.fileDescriptor,
SOL_SOCKET,
sockOpt,
&timer,
socklen_t(MemoryLayout<timeval>.size)
) < 0 {
throw Socket.errnoToError(msg: "failed to set read timeout")
}
}
static func _errnoString(_ err: Int32?) -> String {
String(validatingCString: strerror(errno)) ?? "error: \(errno)"
}
}
public enum SocketError: Error, Equatable, CustomStringConvertible {
case closed
case acceptStreamExists
case invalidOperationOnSocket(String)
case missingBaseAddress
case withErrno(_ msg: String, errno: Int32)
case invalidFileDescriptor
public var description: String {
switch self {
case .closed:
return "socket: closed"
case .acceptStreamExists:
return "accept stream already exists"
case .invalidOperationOnSocket(let operation):
return "socket: invalid operation on socket '\(operation)'"
case .missingBaseAddress:
return "socket: missing base address"
case .withErrno(let msg, _):
return "socket: error \(msg)"
case .invalidFileDescriptor:
return "socket: invalid file descriptor received"
}
}
}
@@ -0,0 +1,48 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#else
#error("SocketType not supported on this platform.")
#endif
/// Protocol used to describe the family of socket to be created with `Socket`.
public protocol SocketType: Sendable, CustomStringConvertible {
/// The domain for the socket (AF_UNIX, AF_VSOCK etc.)
var domain: Int32 { get }
/// The type of socket (SOCK_STREAM).
var type: Int32 { get }
/// Actions to perform before calling bind(2).
func beforeBind(fd: Int32) throws
/// Actions to perform before calling listen(2).
func beforeListen(fd: Int32) throws
/// Handle accept(2) for an implementation of a socket type.
func accept(fd: Int32) throws -> (Int32, SocketType)
/// Provide a sockaddr pointer (by casting a socket specific type like sockaddr_un for example).
func withSockAddr(_ closure: (_ ptr: UnsafePointer<sockaddr>, _ len: UInt32) throws -> Void) throws
}
extension SocketType {
public func beforeBind(fd: Int32) {}
public func beforeListen(fd: Int32) {}
}
@@ -0,0 +1,160 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
#if canImport(Musl)
import Musl
let _SOCK_STREAM = SOCK_STREAM
#elseif canImport(Glibc)
import Glibc
let _SOCK_STREAM = Int32(SOCK_STREAM.rawValue)
#elseif canImport(Darwin)
import Darwin
let _SOCK_STREAM = SOCK_STREAM
#else
#error("UnixType not supported on this platform.")
#endif
/// Unix domain socket variant of `SocketType`.
public struct UnixType: SocketType, Sendable, CustomStringConvertible {
public var domain: Int32 { AF_UNIX }
public var type: Int32 { _SOCK_STREAM }
public var description: String {
path
}
public let path: String
public let perms: mode_t?
private let _addr: sockaddr_un
private let _unlinkExisting: Bool
private init(sockaddr: sockaddr_un) {
let pathname: String = withUnsafePointer(to: sockaddr.sun_path) { ptr in
let charPtr = UnsafeRawPointer(ptr).assumingMemoryBound(to: CChar.self)
return String(cString: charPtr)
}
self._addr = sockaddr
self.path = pathname
self._unlinkExisting = false
self.perms = nil
}
/// Mode and unlinkExisting only used if the socket is going to be a listening socket.
public init(
path: String,
perms: mode_t? = nil,
unlinkExisting: Bool = false
) throws {
self.path = path
self.perms = perms
self._unlinkExisting = unlinkExisting
var addr = sockaddr_un()
addr.sun_family = sa_family_t(AF_UNIX)
let socketName = path
let nameLength = socketName.utf8.count
#if os(macOS)
// Funnily enough, this isn't limited by sun path on macOS even though
// it's stated as so.
let lengthLimit = 253
#elseif os(Linux)
let lengthLimit = MemoryLayout.size(ofValue: addr.sun_path)
#endif
guard nameLength < lengthLimit else {
throw Error.nameTooLong(path)
}
_ = withUnsafeMutablePointer(to: &addr.sun_path.0) { ptr in
socketName.withCString { strncpy(ptr, $0, nameLength) }
}
#if os(macOS)
addr.sun_len = UInt8(MemoryLayout<UInt8>.size + MemoryLayout<sa_family_t>.size + socketName.utf8.count + 1)
#endif
self._addr = addr
}
public func accept(fd: Int32) throws -> (Int32, SocketType) {
var clientFD: Int32 = -1
var addr = sockaddr_un()
clientFD = Syscall.retrying {
var size = socklen_t(MemoryLayout<sockaddr_un>.stride)
return withUnsafeMutablePointer(to: &addr) { pointer in
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { pointer in
sysAccept(fd, pointer, &size)
}
}
}
if clientFD < 0 {
throw Socket.errnoToError(msg: "accept failed")
}
return (clientFD, UnixType(sockaddr: addr))
}
public func beforeBind(fd: Int32) throws {
#if os(Linux)
// Only Linux supports setting the mode of a socket before binding.
if let perms = self.perms {
guard fchmod(fd, perms) == 0 else {
throw Socket.errnoToError(msg: "fchmod failed")
}
}
#endif
var rc: Int32 = 0
if self._unlinkExisting {
rc = sysUnlink(self.path)
if rc != 0 && errno != ENOENT {
throw Socket.errnoToError(msg: "failed to remove old socket at \(self.path)")
}
}
}
public func beforeListen(fd: Int32) throws {
#if os(macOS)
if let perms = self.perms {
guard chmod(self.path, perms) == 0 else {
throw Socket.errnoToError(msg: "chmod failed")
}
}
#endif
}
public func withSockAddr(_ closure: (UnsafePointer<sockaddr>, UInt32) throws -> Void) throws {
var addr = self._addr
try withUnsafePointer(to: &addr) {
let addrBytes = UnsafeRawPointer($0).assumingMemoryBound(to: sockaddr.self)
try closure(addrBytes, UInt32(MemoryLayout<sockaddr_un>.stride))
}
}
}
extension UnixType {
/// `UnixType` errors.
public enum Error: Swift.Error, CustomStringConvertible {
case nameTooLong(_: String)
public var description: String {
switch self {
case .nameTooLong(let name):
return "\(name) is too long for a Unix Domain Socket path"
}
}
}
}
@@ -0,0 +1,108 @@
//===----------------------------------------------------------------------===//
// 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 CShim
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#else
#error("VsockType not supported on this platform.")
#endif
/// Vsock variant of `SocketType`.
public struct VsockType: SocketType, Sendable {
public var domain: Int32 { AF_VSOCK }
public var type: Int32 { _SOCK_STREAM }
public var description: String {
"\(cid):\(port)"
}
public static let anyCID: UInt32 = UInt32(bitPattern: -1)
public static let hypervisorCID: UInt32 = 0x0
// Supported on Linux 5.6+; otherwise, will need to use getLocalCID().
public static let localCID: UInt32 = 0x1
public static let hostCID: UInt32 = 0x2
// socketFD is unused on Linux.
public static func getLocalCID(socketFD: Int32) throws -> UInt32 {
let request = VsockLocalCIDIoctl
#if os(Linux)
let fd = open("/dev/vsock", O_RDONLY | O_CLOEXEC)
if fd == -1 {
throw Socket.errnoToError(msg: "failed to open /dev/vsock")
}
defer { close(fd) }
#else
let fd = socketFD
#endif
var cid: UInt32 = 0
guard sysIoctl(fd, numericCast(request), &cid) != -1 else {
throw Socket.errnoToError(msg: "failed to get local cid")
}
return cid
}
public let port: UInt32
public let cid: UInt32
private let _addr: sockaddr_vm
public init(port: UInt32, cid: UInt32) {
self.cid = cid
self.port = port
var sockaddr = sockaddr_vm()
sockaddr.svm_family = sa_family_t(AF_VSOCK)
sockaddr.svm_cid = cid
sockaddr.svm_port = port
self._addr = sockaddr
}
private init(sockaddr: sockaddr_vm) {
self._addr = sockaddr
self.cid = sockaddr.svm_cid
self.port = sockaddr.svm_port
}
public func accept(fd: Int32) throws -> (Int32, SocketType) {
var clientFD: Int32 = -1
var addr = sockaddr_vm()
while clientFD < 0 {
var size = socklen_t(MemoryLayout<sockaddr_vm>.stride)
clientFD = withUnsafeMutablePointer(to: &addr) { pointer in
pointer.withMemoryRebound(to: sockaddr.self, capacity: 1) { pointer in
sysAccept(fd, pointer, &size)
}
}
if clientFD < 0 && errno != EINTR {
throw Socket.errnoToError(msg: "accept failed")
}
}
return (clientFD, VsockType(sockaddr: addr))
}
public func withSockAddr(_ closure: (UnsafePointer<sockaddr>, UInt32) throws -> Void) throws {
var addr = self._addr
try withUnsafePointer(to: &addr) {
let addrBytes = UnsafeRawPointer($0).assumingMemoryBound(to: sockaddr.self)
try closure(addrBytes, UInt32(MemoryLayout<sockaddr_vm>.stride))
}
}
}
+88
View File
@@ -0,0 +1,88 @@
//===----------------------------------------------------------------------===//
// 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 timestamp with second and nanosecond precision.
public struct TimeSpec: Sendable, Hashable {
/// Seconds since the Unix epoch.
public var seconds: Int64
/// Nanoseconds past the second.
public var nanoseconds: Int32
public init(seconds: Int64, nanoseconds: Int32) {
self.seconds = seconds
self.nanoseconds = nanoseconds
}
}
/// File metadata returned by a `stat` call.
public struct Stat: Sendable, Hashable {
/// ID of device containing file (`st_dev`).
public var dev: UInt64
/// Inode number (`st_ino`).
public var ino: UInt64
/// File type and mode (`st_mode`).
public var mode: UInt32
/// Number of hard links (`st_nlink`).
public var nlink: UInt64
/// User ID of owner (`st_uid`).
public var uid: UInt32
/// Group ID of owner (`st_gid`).
public var gid: UInt32
/// Device ID, if special file (`st_rdev`).
public var rdev: UInt64
/// Total size in bytes (`st_size`).
public var size: Int64
/// Preferred I/O block size (`st_blksize`).
public var blksize: Int64
/// Number of 512-byte blocks allocated (`st_blocks`).
public var blocks: Int64
/// Time of last access (`st_atim`).
public var atime: TimeSpec
/// Time of last modification (`st_mtim`).
public var mtime: TimeSpec
/// Time of last status change (`st_ctim`).
public var ctime: TimeSpec
public init(
dev: UInt64,
ino: UInt64,
mode: UInt32,
nlink: UInt64,
uid: UInt32,
gid: UInt32,
rdev: UInt64,
size: Int64,
blksize: Int64,
blocks: Int64,
atime: TimeSpec,
mtime: TimeSpec,
ctime: TimeSpec
) {
self.dev = dev
self.ino = ino
self.mode = mode
self.nlink = nlink
self.uid = uid
self.gid = gid
self.rdev = rdev
self.size = size
self.blksize = blksize
self.blocks = blocks
self.atime = atime
self.mtime = mtime
self.ctime = ctime
}
}
+39
View File
@@ -0,0 +1,39 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#else
#error("retryingSyscall not supported on this platform.")
#endif
/// Helper type to deal with running system calls.
public struct Syscall {
/// Retry a syscall on EINTR.
public static func retrying<T: FixedWidthInteger>(_ closure: () -> T) -> T {
while true {
let res = closure()
if res == -1 && errno == EINTR {
continue
}
return res
}
}
}
+39
View File
@@ -0,0 +1,39 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
#if canImport(Darwin)
import Darwin
#endif
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
/// Helper type to deal with system control functionalities.
public struct Sysctl {
#if os(macOS)
/// Simple `sysctlbyname` wrapper.
public static func byName(_ name: String) throws -> Int64 {
var num: Int64 = 0
var size = MemoryLayout<Int64>.size
if sysctlbyname(name, &num, &size, nil, 0) != 0 {
throw POSIXError.fromErrno()
}
return num
}
#endif
}
+209
View File
@@ -0,0 +1,209 @@
//===----------------------------------------------------------------------===//
// 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 Foundation
/// `Terminal` provides a clean interface to deal with terminal interactions on Unix platforms.
public struct Terminal: Sendable {
private let initState: termios?
private var descriptor: Int32 {
handle.fileDescriptor
}
public let handle: FileHandle
public init(descriptor: Int32, setInitState: Bool = true) throws {
if setInitState {
self.initState = try Self.getattr(descriptor)
} else {
initState = nil
}
self.handle = .init(fileDescriptor: descriptor, closeOnDealloc: false)
}
/// Write the provided data to the tty device.
public func write(_ data: Data) throws {
try handle.write(contentsOf: data)
}
/// The winsize for a pty.
public struct Size: Sendable {
let size: winsize
/// The width or `col` of the pty.
public var width: UInt16 {
size.ws_col
}
/// The height or `rows` of the pty.
public var height: UInt16 {
size.ws_row
}
init(_ size: winsize) {
self.size = size
}
/// Set the size for use with a pty.
public init(width cols: UInt16, height rows: UInt16) {
self.size = winsize(ws_row: rows, ws_col: cols, ws_xpixel: 0, ws_ypixel: 0)
}
}
/// Return the current pty attached to any of the STDIO descriptors.
public static var current: Terminal {
get throws {
for i in [STDERR_FILENO, STDOUT_FILENO, STDIN_FILENO] {
do {
return try Terminal(descriptor: i)
} catch {}
}
throw Error.notAPty
}
}
/// The current window size for the pty.
public var size: Size {
get throws {
var ws = winsize()
try fromSyscall(ioctl(descriptor, UInt(TIOCGWINSZ), &ws))
return Size(ws)
}
}
/// Create a new pty pair.
/// - Parameter initialSize: An initial size of the child pty.
public static func create(initialSize: Size? = nil) throws -> (parent: Terminal, child: Terminal) {
var parent: Int32 = 0
var child: Int32 = 0
let size = initialSize ?? Size(width: 120, height: 40)
var ws = size.size
try fromSyscall(openpty(&parent, &child, nil, nil, &ws))
return (
parent: try Terminal(descriptor: parent, setInitState: false),
child: try Terminal(descriptor: child, setInitState: false)
)
}
}
// MARK: Errors
extension Terminal {
public enum Error: Swift.Error, CustomStringConvertible {
case notAPty
public var description: String {
switch self {
case .notAPty:
return "the provided fd is not a pty"
}
}
}
}
extension Terminal {
/// Resize the current pty from the size of the provided pty.
/// - Parameter pty: A pty to resize from.
public func resize(from pty: Terminal) throws {
var ws = try pty.size
try fromSyscall(ioctl(descriptor, UInt(TIOCSWINSZ), &ws))
}
/// Resize the pty to the provided window size.
/// - Parameter size: A window size for a pty.
public func resize(size: Size) throws {
var ws = size.size
try fromSyscall(ioctl(descriptor, UInt(TIOCSWINSZ), &ws))
}
/// Resize the pty to the provided window size.
/// - Parameter width: A width or cols of the terminal.
/// - Parameter height: A height or rows of the terminal.
public func resize(width: UInt16, height: UInt16) throws {
var ws = Size(width: width, height: height)
try fromSyscall(ioctl(descriptor, UInt(TIOCSWINSZ), &ws))
}
}
extension Terminal {
/// Enable raw mode for the pty.
public func setraw() throws {
var attr = try Self.getattr(descriptor)
cfmakeraw(&attr)
attr.c_oflag = attr.c_oflag | tcflag_t(OPOST)
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
}
/// Enable echo support.
/// Chars typed will be displayed to the terminal.
public func enableEcho() throws {
var attr = try Self.getattr(descriptor)
attr.c_iflag &= ~tcflag_t(ICRNL)
attr.c_lflag &= ~tcflag_t(ICANON | ECHO)
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
}
/// Disable echo support.
/// Chars typed will not be displayed back to the terminal.
public func disableEcho() throws {
var attr = try Self.getattr(descriptor)
attr.c_lflag &= ~tcflag_t(ECHO)
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
}
private static func getattr(_ fd: Int32) throws -> termios {
var attr = termios()
try fromSyscall(tcgetattr(fd, &attr))
return attr
}
}
// MARK: Reset
extension Terminal {
/// Close this pty's file descriptor.
public func close() throws {
do {
// Use FileHandle's close directly as it sets the underlying fd in the object
// to -1 for us.
try self.handle.close()
} catch {
if let error = error as NSError?, error.domain == NSPOSIXErrorDomain {
throw POSIXError(.init(rawValue: Int32(error.code))!)
}
throw error
}
}
/// Reset the pty to its initial state.
public func reset() throws {
if var attr = initState {
try fromSyscall(tcsetattr(descriptor, TCSANOW, &attr))
}
}
/// Reset the pty to its initial state masking any errors.
/// This is commonly used in a `defer` body to reset the current pty where the error code is not generally useful.
public func tryReset() {
try? reset()
}
}
private func fromSyscall(_ status: Int32) throws {
guard status == 0 else {
throw POSIXError(.init(rawValue: errno)!)
}
}
@@ -0,0 +1,71 @@
//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#elseif canImport(Darwin)
import Darwin
#endif
/// The `resolvingSymlinksInPath` method of the `URL` struct does not resolve the symlinks
/// for directories under `/private` which include`tmp`, `var` and `etc`
/// hence adding a method to build up on the existing `resolvingSymlinksInPath` that prepends `/private` to those paths
extension URL {
/// returns the unescaped absolutePath of a URL joined by separator
func absolutePath(_ separator: String = "/") -> String {
self.pathComponents
.joined(separator: separator)
.dropFirst("/".count)
.description
}
public func resolvingSymlinksInPathWithPrivate() -> URL {
let url = self.resolvingSymlinksInPath()
#if os(macOS)
let parts = url.pathComponents
if parts.count > 1 {
if (parts.first == "/") && ["tmp", "var", "etc"].contains(parts[1]) {
var resolved = URL(filePath: "/private")
for part in parts[1...] {
resolved.append(path: part)
}
return resolved
}
}
#endif
return url
}
public var isDirectory: Bool {
var st = stat()
guard stat(self.path, &st) == 0 else { return false }
return (st.st_mode & S_IFMT) == S_IFDIR
}
public var isSymlink: Bool {
var st = stat()
guard lstat(self.path, &st) == 0 else { return false }
return (st.st_mode & S_IFMT) == S_IFLNK
}
}
+284
View File
@@ -0,0 +1,284 @@
//===----------------------------------------------------------------------===//
// 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
#if canImport(FoundationEssentials)
import FoundationEssentials
#else
import Foundation
#endif
/// `User` provides utilities to ensure that a given username exists in
/// /etc/passwd (and /etc/group). Largely inspired by runc (and moby's)
/// `user` packages.
public enum User {
public static let passwdFilePath = URL(filePath: "/etc/passwd")
public static let groupFilePath = URL(filePath: "/etc/group")
private static let minID: UInt32 = 0
private static let maxID: UInt32 = 2_147_483_647
public struct ExecUser: Sendable {
public var uid: UInt32
public var gid: UInt32
public var sgids: [UInt32]
public var home: String
public var shell: String
public init(uid: UInt32, gid: UInt32, sgids: [UInt32], home: String, shell: String) {
self.uid = uid
self.gid = gid
self.sgids = sgids
self.home = home
self.shell = shell
}
}
public struct User {
public var name: String
public var password: String
public var uid: UInt32
public var gid: UInt32
public var gecos: String
public var home: String
public var shell: String
/// The argument `rawString` must follow the below format.
/// Name:Password:Uid:Gid:Gecos:Home:Shell
init(rawString: String) throws {
let args = rawString.split(separator: ":", omittingEmptySubsequences: false)
guard args.count == 7 else {
throw Error.parseError("cannot parse User from '\(rawString)'")
}
guard let uid = UInt32(args[2]) else {
throw Error.parseError("cannot parse uid from '\(args[2])'")
}
guard let gid = UInt32(args[3]) else {
throw Error.parseError("cannot parse gid from '\(args[3])'")
}
self.name = String(args[0])
self.password = String(args[1])
self.uid = uid
self.gid = gid
self.gecos = String(args[4])
self.home = String(args[5])
self.shell = String(args[6])
}
}
struct Group {
var name: String
var password: String
var gid: UInt32
var users: [String]
/// The argument `rawString` must follow the below format.
/// Name:Password:Gid:user1,user2
init(rawString: String) throws {
let args = rawString.split(separator: ":", omittingEmptySubsequences: false)
guard args.count == 4 else {
throw Error.parseError("cannot parse Group from '\(rawString)'")
}
guard let gid = UInt32(args[2]) else {
throw Error.parseError("cannot parse gid from '\(args[2])'")
}
self.name = String(args[0])
self.password = String(args[1])
self.gid = gid
self.users = args[3].split(separator: ",").map { String($0) }
}
}
}
// MARK: Private methods
extension User {
private static func parse(file: URL, handler: (_ line: String) throws -> Void) throws {
let fm = FileManager.default
guard fm.fileExists(atPath: file.absolutePath()) else {
throw Error.missingFile(file.absolutePath())
}
let content = try String(contentsOf: file, encoding: .ascii)
let lines = content.components(separatedBy: .newlines)
for line in lines {
let trimmed = line.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else {
continue
}
guard !trimmed.hasPrefix("#") else {
continue
}
try handler(trimmed)
}
}
/// Parse the contents of the passwd file with a provided filter function.
static func parsePasswd(passwdFile: URL, filter: ((User) -> Bool)? = nil) throws -> [User] {
var users: [User] = []
try self.parse(file: passwdFile) { line in
let user = try User(rawString: line)
if let filter {
guard filter(user) else {
return
}
}
users.append(user)
}
return users
}
/// Parse the contents of the group file with a provided filter function.
static func parseGroup(groupFile: URL, filter: ((Group) -> Bool)? = nil) throws -> [Group] {
var groups: [Group] = []
try self.parse(file: groupFile) { line in
let group = try Group(rawString: line)
if let filter {
guard filter(group) else {
return
}
}
groups.append(group)
}
return groups
}
}
// MARK: Public methods
extension User {
/// Looks up uid in the password file specified by `passwdPath`.
public static func lookupUid(passwdPath: URL = Self.passwdFilePath, uid: UInt32) throws -> User {
let users = try parsePasswd(
passwdFile: passwdPath,
filter: { u in
u.uid == uid
})
if users.count == 0 {
throw Error.noPasswdEntries
}
return users[0]
}
/// Parses a user string in any of the following formats:
/// "user, uid, user:group, uid:gid, uid:group, user:gid"
/// and returns an ExecUser type from the information.
public static func getExecUser(
userString: String,
defaults: ExecUser? = nil,
passwdPath: URL = Self.passwdFilePath,
groupPath: URL = Self.groupFilePath
) throws -> ExecUser {
let defaults = defaults ?? ExecUser(uid: 0, gid: 0, sgids: [], home: "/", shell: "")
var user = ExecUser(
uid: defaults.uid,
gid: defaults.gid,
sgids: defaults.sgids,
home: defaults.home,
shell: defaults.shell
)
let parts = userString.split(
separator: ":",
maxSplits: 1,
omittingEmptySubsequences: false
)
let userArg = parts.isEmpty ? "" : String(parts[0])
let groupArg = parts.count > 1 ? String(parts[1]) : ""
let uidArg = UInt32(userArg)
let notUID = uidArg == nil
let gidArg = UInt32(groupArg)
let notGID = gidArg == nil
let users: [User]
do {
users = try parsePasswd(passwdFile: passwdPath) { u in
if userArg.isEmpty {
return u.uid == user.uid
}
if !notUID {
return uidArg! == u.uid
}
return u.name == userArg
}
} catch Error.missingFile {
users = []
}
var matchedUserName = ""
if !users.isEmpty {
let matchedUser = users[0]
matchedUserName = matchedUser.name
user.uid = matchedUser.uid
user.gid = matchedUser.gid
user.home = matchedUser.home
user.shell = matchedUser.shell
} else if !userArg.isEmpty {
if notUID {
throw Error.noPasswdEntries
}
user.uid = uidArg!
if user.uid < minID || user.uid > maxID {
throw Error.range
}
}
if !groupArg.isEmpty || !matchedUserName.isEmpty {
let groups: [Group]
do {
groups = try parseGroup(groupFile: groupPath) { g in
if groupArg.isEmpty {
return g.users.contains(matchedUserName)
}
if !notGID {
return gidArg! == g.gid
}
return g.name == groupArg
}
} catch Error.missingFile {
groups = []
}
if !groupArg.isEmpty {
if !groups.isEmpty {
user.gid = groups[0].gid
} else {
if notGID {
throw Error.noGroupEntries
}
user.gid = gidArg!
if user.gid < minID || user.gid > maxID {
throw Error.range
}
}
}
user.sgids = groups.map { $0.gid }
}
return user
}
public enum Error: Swift.Error {
case missingFile(String)
case range
case noPasswdEntries
case noGroupEntries
case parseError(String)
}
}