24 |
25 | struct exec_command_attrs {
26 | int setpgid;
27 | /// parent group id
28 | pid_t pgid;
29 | /// set the controlling terminal
30 | int setctty;
31 | /// controlling terminal fd
32 | int ctty;
33 | /// set the process as session leader
34 | int setsid;
35 | /// set the process user id
36 | uid_t uid;
37 | /// set the process group id
38 | gid_t gid;
39 | /// signal mask for the child process
40 | int mask;
41 | /// parent death signal (Linux only, 0 to disable)
42 | int pdeathSignal;
43 | };
44 |
45 | void exec_command_attrs_init(struct exec_command_attrs *attrs);
46 |
47 | /// spawn a new child process with the provided attrs
48 | int exec_command(pid_t *result, const char *executable, char *const argv[],
49 | char *const envp[], const int file_handles[],
50 | const int file_handle_count, const char *working_directory,
51 | struct exec_command_attrs *attrs);
52 |
53 | #endif /* defined(__linux__) || defined(__APPLE__) */
54 | #endif /* exec_command_h */
55 |
--------------------------------------------------------------------------------
/Sources/ContainerizationOS/BinaryInteger+Extensions.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | extension BinaryInteger {
18 | private func toUnsignedMemoryAmount(_ amount: UInt64) -> UInt64 {
19 | guard self >= 0 else {
20 | fatalError("encountered negative number during conversion to memory amount")
21 | }
22 | let val = UInt64(self)
23 | let (newVal, overflow) = val.multipliedReportingOverflow(by: amount)
24 | guard !overflow else {
25 | fatalError("UInt64 overflow when converting to memory amount")
26 | }
27 | return newVal
28 | }
29 |
30 | public func kib() -> UInt64 {
31 | self.toUnsignedMemoryAmount(1 << 10)
32 | }
33 |
34 | public func mib() -> UInt64 {
35 | self.toUnsignedMemoryAmount(1 << 20)
36 | }
37 |
38 | public func gib() -> UInt64 {
39 | self.toUnsignedMemoryAmount(1 << 30)
40 | }
41 |
42 | public func tib() -> UInt64 {
43 | self.toUnsignedMemoryAmount(1 << 40)
44 | }
45 |
46 | public func pib() -> UInt64 {
47 | self.toUnsignedMemoryAmount(1 << 50)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/scripts/make-docs.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash -e
2 | # Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # https://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 | opts=()
17 | opts+=("--allow-writing-to-directory" "$1")
18 | opts+=("generate-documentation")
19 | opts+=("--target" "Containerization")
20 | opts+=("--target" "ContainerizationArchive")
21 | opts+=("--target" "ContainerizationError")
22 | opts+=("--target" "ContainerizationEXT4")
23 | opts+=("--target" "ContainerizationExtras")
24 | opts+=("--target" "ContainerizationIO")
25 | opts+=("--target" "ContainerizationNetlink")
26 | opts+=("--target" "ContainerizationOCI")
27 | opts+=("--target" "ContainerizationOS")
28 | opts+=("--output-path" "$1")
29 | opts+=("--disable-indexing")
30 | opts+=("--transform-for-static-hosting")
31 | opts+=("--enable-experimental-combined-documentation")
32 | opts+=("--experimental-documentation-coverage")
33 |
34 | if [ ! -z "$2" ] ; then
35 | opts+=("--hosting-base-path" "$2")
36 | fi
37 |
38 | /usr/bin/swift package ${opts[@]}
39 |
40 | echo '{}' > "$1/theme-settings.json"
41 |
42 | cat > "$1/index.html" <<'EOF'
43 |
44 |
45 |
46 |
47 |
48 |
49 | If you are not redirected automatically, click here.
50 |
51 |
52 | EOF
53 |
--------------------------------------------------------------------------------
/Sources/ContainerizationOS/Reaper.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 |
19 | /// A process reaper that returns exited processes along
20 | /// with their exit status.
21 | public struct Reaper {
22 | /// Process's pid and exit status.
23 | typealias Exit = (pid: Int32, status: Int32)
24 |
25 | /// Reap all pending processes and return the pid and exit status.
26 | public static func reap() -> [Int32: Int32] {
27 | var reaped = [Int32: Int32]()
28 | while true {
29 | guard let exit = wait() else {
30 | return reaped
31 | }
32 | reaped[exit.pid] = exit.status
33 | }
34 | return reaped
35 | }
36 |
37 | /// Returns the exit status of the last process that exited.
38 | /// nil is returned when no pending processes exist.
39 | private static func wait() -> Exit? {
40 | var rus = rusage()
41 | var ws = Int32()
42 |
43 | let pid = wait4(-1, &ws, WNOHANG, &rus)
44 | if pid <= 0 {
45 | return nil
46 | }
47 | return (pid: pid, status: Command.toExitStatus(ws))
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/ContainerizationArchive/FileArchiveWriterDelegate.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 | import SystemPackage
19 |
20 | internal final class FileArchiveWriterDelegate {
21 | public let path: FilePath
22 | private var fd: FileDescriptor!
23 |
24 | public init(path: FilePath) {
25 | self.path = path
26 | }
27 |
28 | public convenience init(url: URL) {
29 | self.init(path: FilePath(url.path))
30 | }
31 |
32 | public func open(archive: ArchiveWriter) throws {
33 | self.fd = try FileDescriptor.open(
34 | self.path, .writeOnly, options: [.create, .append], permissions: [.groupRead, .otherRead, .ownerReadWrite])
35 | }
36 |
37 | public func write(archive: ArchiveWriter, buffer: UnsafeRawBufferPointer) throws -> Int {
38 | try fd.write(buffer)
39 | }
40 |
41 | public func close(archive: ArchiveWriter) throws {
42 | try self.fd.close()
43 | }
44 |
45 | public func free(archive: ArchiveWriter) {
46 | self.fd = nil
47 | }
48 |
49 | deinit {
50 | if let fd = self.fd {
51 | try? fd.close()
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Tests/ContainerizationOSTests/KeychainQueryTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | //
18 |
19 | import Foundation
20 | import Testing
21 |
22 | @testable import ContainerizationOS
23 |
24 | struct KeychainQueryTests {
25 | let id = "com.example.container-testing-keychain"
26 | let domain = "testing-keychain.example.com"
27 | let user = "containerization-test"
28 |
29 | let kq = KeychainQuery()
30 |
31 | @Test(.enabled(if: !isCI))
32 | func keychainQuery() throws {
33 | defer { try? kq.delete(id: id, host: domain) }
34 |
35 | do {
36 | try kq.save(id: id, host: domain, user: user, token: "foobar")
37 | #expect(try kq.exists(id: id, host: domain))
38 |
39 | let fetched = try kq.get(id: id, host: domain)
40 | let result = try #require(fetched)
41 | #expect(result.account == user)
42 | #expect(result.data == "foobar")
43 | } catch KeychainQuery.Error.unhandledError(status: -25308) {
44 | // ignore errSecInteractionNotAllowed
45 | }
46 | }
47 |
48 | private static var isCI: Bool {
49 | ProcessInfo.processInfo.environment["CI"] != nil
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Sources/Containerization/Image/Unpacker/Unpacker.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import ContainerizationExtras
18 | import ContainerizationOCI
19 | import Foundation
20 |
21 | /// The `Unpacker` protocol defines a standardized interface that involves
22 | /// decompressing, extracting image layers and preparing it for use.
23 | ///
24 | /// The `Unpacker` is responsible for managing the lifecycle of the
25 | /// unpacking process, including any temporary files or resources, until the
26 | /// `Mount` object is produced.
27 | public protocol Unpacker {
28 |
29 | /// Unpacks the provided image to a specified path for a given platform.
30 | ///
31 | /// This asynchronous method should handle the entire unpacking process, from reading
32 | /// the `Image` layers for the given `Platform` via its `Manifest`,
33 | /// to making the extracted contents available as a `Mount`.
34 | /// Implementations of this method may apply platform-specific optimizations
35 | /// or transformations during the unpacking.
36 | ///
37 | /// Progress updates can be observed via the optional `progress` handler.
38 | func unpack(_ image: Image, for platform: Platform, at path: URL, progress: ProgressHandler?) async throws -> Mount
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/ContainerizationTests/DNSTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 | import Testing
19 |
20 | @testable import Containerization
21 |
22 | struct DNSTests {
23 |
24 | @Test func dnsResolvConfWithAllFields() {
25 | let dns = DNS(
26 | nameservers: ["8.8.8.8", "1.1.1.1"],
27 | domain: "example.com",
28 | searchDomains: ["internal.com", "test.com"],
29 | options: ["ndots:2", "timeout:1"]
30 | )
31 |
32 | let expected = "nameserver 8.8.8.8\nnameserver 1.1.1.1\ndomain example.com\nsearch internal.com test.com\noptions ndots:2 timeout:1\n"
33 | #expect(dns.resolvConf == expected)
34 | }
35 |
36 | @Test func dnsResolvConfWithEmptyFields() {
37 | let dns = DNS(
38 | nameservers: [],
39 | domain: nil,
40 | searchDomains: [],
41 | options: []
42 | )
43 |
44 | // Should return empty string when all fields are empty
45 | #expect(dns.resolvConf == "")
46 | }
47 |
48 | @Test func dnsResolvConfWithOnlyNameservers() {
49 | let dns = DNS(nameservers: ["8.8.8.8"])
50 |
51 | let expected = "nameserver 8.8.8.8\n"
52 | #expect(dns.resolvConf == expected)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Sources/ContainerizationOS/Socket/SocketType.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | #if canImport(Musl)
18 | import Musl
19 | #elseif canImport(Glibc)
20 | import Glibc
21 | #elseif canImport(Darwin)
22 | import Darwin
23 | #else
24 | #error("SocketType not supported on this platform.")
25 | #endif
26 |
27 | /// Protocol used to describe the family of socket to be created with `Socket`.
28 | public protocol SocketType: Sendable, CustomStringConvertible {
29 | /// The domain for the socket (AF_UNIX, AF_VSOCK etc.)
30 | var domain: Int32 { get }
31 | /// The type of socket (SOCK_STREAM).
32 | var type: Int32 { get }
33 |
34 | /// Actions to perform before calling bind(2).
35 | func beforeBind(fd: Int32) throws
36 | /// Actions to perform before calling listen(2).
37 | func beforeListen(fd: Int32) throws
38 |
39 | /// Handle accept(2) for an implementation of a socket type.
40 | func accept(fd: Int32) throws -> (Int32, SocketType)
41 | /// Provide a sockaddr pointer (by casting a socket specific type like sockaddr_un for example).
42 | func withSockAddr(_ closure: (_ ptr: UnsafePointer, _ len: UInt32) throws -> Void) throws
43 | }
44 |
45 | extension SocketType {
46 | public func beforeBind(fd: Int32) {}
47 | public func beforeListen(fd: Int32) {}
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/ContainerizationEXT4/FileTimestamps.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 |
19 | public struct FileTimestamps {
20 | public var access: Date
21 | public var modification: Date
22 | public var creation: Date
23 | public var now: Date
24 |
25 | public var accessLo: UInt32 {
26 | access.fs().lo
27 | }
28 |
29 | public var accessHi: UInt32 {
30 | access.fs().hi
31 | }
32 |
33 | public var modificationLo: UInt32 {
34 | modification.fs().lo
35 | }
36 |
37 | public var modificationHi: UInt32 {
38 | modification.fs().hi
39 | }
40 |
41 | public var creationLo: UInt32 {
42 | creation.fs().lo
43 | }
44 |
45 | public var creationHi: UInt32 {
46 | creation.fs().hi
47 | }
48 |
49 | public var nowLo: UInt32 {
50 | now.fs().lo
51 | }
52 |
53 | public var nowHi: UInt32 {
54 | now.fs().hi
55 | }
56 |
57 | public init(access: Date?, modification: Date?, creation: Date?) {
58 | now = Date()
59 | self.access = access ?? now
60 | self.modification = modification ?? now
61 | self.creation = creation ?? now
62 | }
63 |
64 | public init() {
65 | self.init(access: nil, modification: nil, creation: nil)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Tests/ContainerizationExtrasTests/TestTimeout.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 | import Testing
19 |
20 | @testable import ContainerizationExtras
21 |
22 | final class TestTimeoutType {
23 | @Test
24 | func testNoCancellation() async throws {
25 | await #expect(throws: Never.self) {
26 | try await Timeout.run(
27 | for: .seconds(5),
28 | operation: {
29 | return
30 | })
31 | }
32 | }
33 |
34 | @Test
35 | func testCancellationError() async throws {
36 | await #expect(throws: CancellationError.self) {
37 | try await Timeout.run(
38 | for: .milliseconds(50),
39 | operation: {
40 | try await Task.sleep(for: .seconds(2))
41 | })
42 | }
43 | }
44 |
45 | @Test
46 | func testClosureError() async throws {
47 | // Check that we get the closures error if we don't timeout, but
48 | // the closure does throw before.
49 | await #expect(throws: POSIXError.self) {
50 | try await Timeout.run(
51 | for: .seconds(10),
52 | operation: {
53 | throw POSIXError(.E2BIG)
54 | })
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/cctl/cctl+Utils.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Containerization
18 | import ContainerizationError
19 | import ContainerizationOCI
20 | import Foundation
21 |
22 | extension Application {
23 | static func fetchImage(reference: String, store: ImageStore) async throws -> Containerization.Image {
24 | do {
25 | return try await store.get(reference: reference)
26 | } catch let error as ContainerizationError {
27 | if error.code == .notFound {
28 | return try await store.pull(reference: reference)
29 | }
30 | throw error
31 | }
32 | }
33 |
34 | static func parseKeyValuePairs(from items: [String]) -> [String: String] {
35 | var parsedLabels: [String: String] = [:]
36 | for item in items {
37 | let parts = item.split(separator: "=", maxSplits: 1)
38 | guard parts.count == 2 else {
39 | continue
40 | }
41 | let key = String(parts[0])
42 | let val = String(parts[1])
43 | parsedLabels[key] = val
44 | }
45 | return parsedLabels
46 | }
47 | }
48 |
49 | extension ContainerizationOCI.Platform {
50 | static var arm64: ContainerizationOCI.Platform {
51 | .init(arch: "arm64", os: "linux", variant: "v8")
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/vminitd/Sources/vmexec/Console.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 | import Musl
19 |
20 | class Console {
21 | let master: Int32
22 | let slavePath: String
23 |
24 | init() throws {
25 | let masterFD = open("/dev/ptmx", O_RDWR | O_NOCTTY | O_CLOEXEC)
26 | guard masterFD != -1 else {
27 | throw App.Errno(stage: "open_ptmx")
28 | }
29 |
30 | guard unlockpt(masterFD) == 0 else {
31 | throw App.Errno(stage: "unlockpt")
32 | }
33 |
34 | guard let slavePath = ptsname(masterFD) else {
35 | throw App.Errno(stage: "ptsname")
36 | }
37 |
38 | self.master = masterFD
39 | self.slavePath = String(cString: slavePath)
40 | }
41 |
42 | func configureStdIO() throws {
43 | let path = self.slavePath
44 | let slaveFD = open(path, O_RDWR)
45 | guard slaveFD != -1 else {
46 | throw App.Errno(stage: "open_pts")
47 | }
48 | defer { Musl.close(slaveFD) }
49 |
50 | for fd: Int32 in 0...2 {
51 | guard dup3(slaveFD, fd, 0) != -1 else {
52 | throw App.Errno(stage: "dup3")
53 | }
54 | }
55 | }
56 |
57 | func close() throws {
58 | guard Musl.close(self.master) == 0 else {
59 | throw App.Errno(stage: "close")
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/ContainerizationOCI/Client/Authentication.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 |
19 | /// Abstraction for returning a token needed for logging into an OCI compliant registry.
20 | public protocol Authentication: Sendable {
21 | func token() async throws -> String
22 | }
23 |
24 | /// Type representing authentication information for client to access the registry.
25 | public struct BasicAuthentication: Authentication {
26 | /// The username for the authentication.
27 | let username: String
28 | /// The password or identity token for the user.
29 | let password: String
30 |
31 | public init(username: String, password: String) {
32 | self.username = username
33 | self.password = password
34 | }
35 |
36 | /// Get a token using the provided username and password. This will be a
37 | /// base64 encoded string of the username and password delimited by a colon.
38 | public func token() async throws -> String {
39 | let credentials = "\(username):\(password)"
40 | if let authenticationData = credentials.data(using: .utf8)?.base64EncodedString() {
41 | return "Basic \(authenticationData)"
42 | }
43 | throw Error.invalidCredentials
44 | }
45 |
46 | /// `BasicAuthentication` errors.
47 | public enum Error: Swift.Error {
48 | case invalidCredentials
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Tests/ContainerizationNetlinkTests/MockNetlinkSocket.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | //
18 |
19 | @testable import ContainerizationNetlink
20 |
21 | class MockNetlinkSocket: NetlinkSocket {
22 | static let ENOMEM: Int32 = 12
23 | static let EOVERFLOW: Int32 = 75
24 |
25 | var pid: UInt32 = 0
26 |
27 | var requests: [[UInt8]] = []
28 | var responses: [[UInt8]] = []
29 |
30 | var responseIndex = 0
31 |
32 | public init() throws {}
33 |
34 | public func send(buf: UnsafeRawPointer!, len: Int, flags: Int32) throws -> Int {
35 | let ptr = buf.bindMemory(to: UInt8.self, capacity: len)
36 | requests.append(Array(UnsafeBufferPointer(start: ptr, count: len)))
37 | return len
38 | }
39 |
40 | public func recv(buf: UnsafeMutableRawPointer!, len: Int, flags: Int32) throws -> Int {
41 | guard responseIndex < responses.count else {
42 | throw NetlinkSocketError.recvFailure(rc: Self.ENOMEM)
43 | }
44 |
45 | let response = responses[responseIndex]
46 | guard len >= response.count else {
47 | throw NetlinkSocketError.recvFailure(rc: 75)
48 | }
49 |
50 | response.withUnsafeBytes { bytes in
51 | buf.copyMemory(from: bytes.baseAddress!, byteCount: response.count)
52 | }
53 |
54 | responseIndex += 1
55 | return response.count
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/Sources/Containerization/Vminitd+SocketRelay.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | extension Vminitd: SocketRelayAgent {
18 | /// Sets up a relay between a host socket to a newly created guest socket, or vice versa.
19 | public func relaySocket(port: UInt32, configuration: UnixSocketConfiguration) async throws {
20 | let request = Com_Apple_Containerization_Sandbox_V3_ProxyVsockRequest.with {
21 | $0.id = configuration.id
22 | $0.vsockPort = port
23 |
24 | if let perms = configuration.permissions {
25 | $0.guestSocketPermissions = UInt32(perms.rawValue)
26 | }
27 |
28 | switch configuration.direction {
29 | case .into:
30 | $0.guestPath = configuration.destination.path
31 | $0.action = .into
32 | case .outOf:
33 | $0.guestPath = configuration.source.path
34 | $0.action = .outOf
35 | }
36 | }
37 | _ = try await client.proxyVsock(request)
38 | }
39 |
40 | /// Stops the specified socket relay.
41 | public func stopSocketRelay(configuration: UnixSocketConfiguration) async throws {
42 | let request = Com_Apple_Containerization_Sandbox_V3_StopVsockProxyRequest.with {
43 | $0.id = configuration.id
44 | }
45 | _ = try await client.stopVsockProxy(request)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/ContainerizationExtras/AsyncLock.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 |
19 | /// `AsyncLock` provides a familiar locking API, with the main benefit being that it
20 | /// is safe to call async methods while holding the lock. This is primarily used in spots
21 | /// where an actor makes sense, but we may need to ensure we don't fall victim to actor
22 | /// reentrancy issues.
23 | public actor AsyncLock {
24 | private var busy = false
25 | private var queue: ArraySlice> = []
26 |
27 | public struct Context: Sendable {
28 | fileprivate init() {}
29 | }
30 |
31 | public init() {}
32 |
33 | /// withLock provides a scoped locking API to run a function while holding the lock.
34 | public func withLock(_ body: @Sendable @escaping (Context) async throws -> T) async rethrows -> T {
35 | while self.busy {
36 | await withCheckedContinuation { cc in
37 | self.queue.append(cc)
38 | }
39 | }
40 |
41 | self.busy = true
42 |
43 | defer {
44 | self.busy = false
45 | if let next = self.queue.popFirst() {
46 | next.resume(returning: ())
47 | } else {
48 | self.queue = []
49 | }
50 | }
51 |
52 | let context = Context()
53 | return try await body(context)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Protobuf.Makefile:
--------------------------------------------------------------------------------
1 | # Copyright © 2025 Apple Inc. and the Containerization project authors.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # https://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | LOCAL_DIR := $(ROOT_DIR)/.local
16 | LOCAL_BIN_DIR := $(LOCAL_DIR)/bin
17 |
18 | # Versions
19 | PROTOC_VERSION := 26.1
20 |
21 | # Protoc binary installation
22 | PROTOC_ZIP := protoc-$(PROTOC_VERSION)-osx-universal_binary.zip
23 | PROTOC := $(LOCAL_BIN_DIR)/protoc@$(PROTOC_VERSION)/protoc
24 | $(PROTOC):
25 | @echo Downloading protocol buffers...
26 | @mkdir -p $(LOCAL_DIR)
27 | @curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v$(PROTOC_VERSION)/$(PROTOC_ZIP)
28 | @mkdir -p $(dir $@)
29 | @unzip -jo $(PROTOC_ZIP) bin/protoc -d $(dir $@)
30 | @unzip -o $(PROTOC_ZIP) 'include/*' -d $(dir $@)
31 | @rm -f $(PROTOC_ZIP)
32 |
33 | .PHONY: protoc-gen-swift
34 | protoc-gen-swift:
35 | @$(SWIFT) build --product protoc-gen-swift
36 | @$(SWIFT) build --product protoc-gen-grpc-swift
37 |
38 | .PHONY: protos
39 | protos: $(PROTOC) protoc-gen-swift
40 | @echo Generating protocol buffers source code...
41 | @$(PROTOC) Sources/Containerization/SandboxContext/SandboxContext.proto \
42 | --plugin=protoc-gen-grpc-swift=$(BUILD_BIN_DIR)/protoc-gen-grpc-swift \
43 | --plugin=protoc-gen-swift=$(BUILD_BIN_DIR)/protoc-gen-swift \
44 | --proto_path=Sources/Containerization/SandboxContext \
45 | --grpc-swift_out="Sources/Containerization/SandboxContext" \
46 | --grpc-swift_opt=Visibility=Public \
47 | --swift_out="Sources/Containerization/SandboxContext" \
48 | --swift_opt=Visibility=Public \
49 | -I.
50 | @"$(MAKE)" update-licenses
51 |
52 | .PHONY: clean-proto-tools
53 | clean-proto-tools:
54 | @echo Cleaning proto tools...
55 | @rm -rf $(LOCAL_DIR)/bin/protoc*
56 |
--------------------------------------------------------------------------------
/scripts/check-integration-test-vm-panics.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # https://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 |
16 |
17 | # Script to scan the VM boot logs from the integration tests for kernel panics.
18 | # Looks for common kernel panic messages like "attempted to kill init" or "Kernel panic".
19 |
20 | GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
21 | if [ -z "$GIT_ROOT" ]; then
22 | echo "Error: Not in a git repository"
23 | exit 1
24 | fi
25 |
26 | BOOT_LOGS_DIR="$GIT_ROOT/bin/integration-bootlogs"
27 |
28 | if [ ! -d "$BOOT_LOGS_DIR" ]; then
29 | echo "Error: Boot logs directory not found: $BOOT_LOGS_DIR"
30 | exit 1
31 | fi
32 |
33 | echo "Scanning boot logs in: $BOOT_LOGS_DIR"
34 | echo "========================================"
35 | echo ""
36 |
37 | PANIC_FOUND=0
38 |
39 | for logfile in "$BOOT_LOGS_DIR"/*; do
40 | if [ -f "$logfile" ]; then
41 | if grep -qi "attempted to kill init\|Kernel panic\|end Kernel panic\|Attempted to kill the idle task\|Oops:" "$logfile"; then
42 | echo "🚨 PANIC DETECTED in: $(basename "$logfile")"
43 | echo "---"
44 | grep -i -B 5 -A 10 "attempted to kill init\|Kernel panic\|end Kernel panic\|Attempted to kill the idle task\|Oops:" "$logfile" | head -30
45 | echo ""
46 | echo "========================================"
47 | echo ""
48 | PANIC_FOUND=1
49 | fi
50 | fi
51 | done
52 |
53 | if [ $PANIC_FOUND -eq 0 ]; then
54 | echo "✅ No kernel panics detected in boot logs"
55 | else
56 | echo "❌ Found kernel panics - Virtual machine(s) crashed during integration tests"
57 | fi
58 |
59 | exit $PANIC_FOUND
60 |
--------------------------------------------------------------------------------
/Sources/ContainerizationOCI/Content/Content.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import ContainerizationExtras
18 | import Crypto
19 | import Foundation
20 | import NIOCore
21 |
22 | /// Protocol for defining a single OCI content
23 | public protocol Content: Sendable {
24 | /// URL to the content
25 | var path: URL { get }
26 |
27 | /// sha256 of content
28 | func digest() throws -> SHA256.Digest
29 |
30 | /// Size of content
31 | func size() throws -> UInt64
32 |
33 | /// Data representation of entire content
34 | func data() throws -> Data
35 |
36 | /// Data representation partial content
37 | func data(offset: UInt64, length: Int) throws -> Data?
38 |
39 | /// Decode the content into an object
40 | func decode() throws -> T where T: Decodable
41 | }
42 |
43 | /// Protocol defining methods to fetch and push OCI content
44 | public protocol ContentClient: Sendable {
45 | func fetch(name: String, descriptor: Descriptor) async throws -> T
46 |
47 | func fetchBlob(name: String, descriptor: Descriptor, into file: URL, progress: ProgressHandler?) async throws -> (Int64, SHA256Digest)
48 |
49 | func fetchData(name: String, descriptor: Descriptor) async throws -> Data
50 |
51 | func push(
52 | name: String,
53 | ref: String,
54 | descriptor: Descriptor,
55 | streamGenerator: () throws -> T,
56 | progress: ProgressHandler?
57 | ) async throws where T.Element == ByteBuffer
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/vminitd/Sources/vminitd/PauseCommand.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Dispatch
18 | import Logging
19 | import Musl
20 |
21 | struct PauseCommand {
22 | static func run(log: Logger) throws {
23 | if getpid() != 1 {
24 | log.warning("pause should be the first process")
25 | }
26 |
27 | // NOTE: For whatever reason, using signal() for the below causes a swift compiler issue.
28 | // Can revert whenever that is understood.
29 | let sigintSource = DispatchSource.makeSignalSource(signal: SIGINT)
30 | sigintSource.setEventHandler {
31 | log.info("Shutting down, got SIGINT")
32 | Musl.exit(0)
33 | }
34 | sigintSource.resume()
35 |
36 | let sigtermSource = DispatchSource.makeSignalSource(signal: SIGTERM)
37 | sigtermSource.setEventHandler {
38 | log.info("Shutting down, got SIGTERM")
39 | Musl.exit(0)
40 | }
41 | sigtermSource.resume()
42 |
43 | let sigchldSource = DispatchSource.makeSignalSource(signal: SIGCHLD)
44 | sigchldSource.setEventHandler {
45 | var status: Int32 = 0
46 | while waitpid(-1, &status, WNOHANG) > 0 {}
47 | }
48 | sigchldSource.resume()
49 |
50 | log.info("pause container running, waiting for signals...")
51 |
52 | while true {
53 | Musl.pause()
54 | }
55 |
56 | log.error("Error: infinite loop terminated")
57 | Musl.exit(42)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/Containerization/AttachedFilesystem.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import ContainerizationExtras
18 | import ContainerizationOCI
19 |
20 | /// A filesystem that was attached and able to be mounted inside the runtime environment.
21 | public struct AttachedFilesystem: Sendable {
22 | /// The type of the filesystem.
23 | public var type: String
24 | /// The path to the filesystem within a sandbox.
25 | public var source: String
26 | /// Destination when mounting the filesystem inside a sandbox.
27 | public var destination: String
28 | /// The options to use when mounting the filesystem.
29 | public var options: [String]
30 |
31 | #if os(macOS)
32 | public init(mount: Mount, allocator: any AddressAllocator) throws {
33 | switch mount.type {
34 | case "virtiofs":
35 | let name = try hashMountSource(source: mount.source)
36 | self.source = name
37 | case "ext4":
38 | let char = try allocator.allocate()
39 | self.source = "/dev/vd\(char)"
40 | default:
41 | self.source = mount.source
42 | }
43 | self.type = mount.type
44 | self.options = mount.options
45 | self.destination = mount.destination
46 | }
47 | #endif
48 |
49 | public init(type: String, source: String, destination: String, options: [String]) {
50 | self.type = type
51 | self.source = source
52 | self.destination = destination
53 | self.options = options
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Sources/ContainerizationOCI/Manifest.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | // Source: https://github.com/opencontainers/image-spec/blob/main/specs-go/v1/manifest.go
18 |
19 | import Foundation
20 |
21 | /// Manifest provides `application/vnd.oci.image.manifest.v1+json` mediatype structure when marshalled to JSON.
22 | public struct Manifest: Codable, Sendable {
23 | /// `schemaVersion` is the image manifest schema that this image follows.
24 | public let schemaVersion: Int
25 |
26 | /// `mediaType` specifies the type of this document data structure, e.g. `application/vnd.oci.image.manifest.v1+json`.
27 | public let mediaType: String?
28 |
29 | /// `config` references a configuration object for a container, by digest.
30 | /// The referenced configuration object is a JSON blob that the runtime uses to set up the container.
31 | public let config: Descriptor
32 |
33 | /// `layers` is an indexed list of layers referenced by the manifest.
34 | public let layers: [Descriptor]
35 |
36 | /// `annotations` contains arbitrary metadata for the image manifest.
37 | public let annotations: [String: String]?
38 |
39 | public init(
40 | schemaVersion: Int = 2, mediaType: String = MediaTypes.imageManifest, config: Descriptor, layers: [Descriptor],
41 | annotations: [String: String]? = nil
42 | ) {
43 | self.schemaVersion = schemaVersion
44 | self.mediaType = mediaType
45 | self.config = config
46 | self.layers = layers
47 | self.annotations = annotations
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/ContainerizationOS/URL+Extensions.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 |
19 | /// The `resolvingSymlinksInPath` method of the `URL` struct does not resolve the symlinks
20 | /// for directories under `/private` which include`tmp`, `var` and `etc`
21 | /// hence adding a method to build up on the existing `resolvingSymlinksInPath` that prepends `/private` to those paths
22 | extension URL {
23 | /// returns the unescaped absolutePath of a URL joined by separator
24 | func absolutePath(_ separator: String = "/") -> String {
25 | self.pathComponents
26 | .joined(separator: separator)
27 | .dropFirst("/".count)
28 | .description
29 | }
30 |
31 | public func resolvingSymlinksInPathWithPrivate() -> URL {
32 | let url = self.resolvingSymlinksInPath()
33 | #if os(macOS)
34 | let parts = url.pathComponents
35 | if parts.count > 1 {
36 | if (parts.first == "/") && ["tmp", "var", "etc"].contains(parts[1]) {
37 | if let resolved = NSURL.fileURL(withPathComponents: ["/", "private"] + parts[1...]) {
38 | return resolved
39 | }
40 | }
41 | }
42 | #endif
43 | return url
44 | }
45 |
46 | public var isDirectory: Bool {
47 | (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
48 | }
49 |
50 | public var isSymlink: Bool {
51 | (try? resourceValues(forKeys: [.isSymbolicLinkKey]))?.isSymbolicLink == true
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/vminitd/Sources/vmexec/Mount.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import ContainerizationOCI
18 | import ContainerizationOS
19 | import Foundation
20 | import Musl
21 |
22 | struct ContainerMount {
23 | private let mounts: [ContainerizationOCI.Mount]
24 | private let rootfs: String
25 |
26 | init(rootfs: String, mounts: [ContainerizationOCI.Mount]) {
27 | self.rootfs = rootfs
28 | self.mounts = mounts
29 | }
30 |
31 | func mountToRootfs() throws {
32 | for m in self.mounts {
33 | let osMount = m.toOSMount()
34 | try osMount.mount(root: self.rootfs)
35 | }
36 | }
37 |
38 | func configureConsole() throws {
39 | let ptmx = self.rootfs.standardizingPath.appendingPathComponent("dev/ptmx")
40 | guard remove(ptmx) == 0 else {
41 | throw App.Errno(stage: "remove(ptmx)")
42 | }
43 | guard symlink("pts/ptmx", ptmx) == 0 else {
44 | throw App.Errno(stage: "symlink(pts/ptmx)")
45 | }
46 | }
47 |
48 | private func mkdirAll(_ name: String, _ perm: Int16) throws {
49 | try FileManager.default.createDirectory(
50 | atPath: name,
51 | withIntermediateDirectories: true,
52 | attributes: [.posixPermissions: perm]
53 | )
54 | }
55 | }
56 |
57 | extension ContainerizationOCI.Mount {
58 | func toOSMount() -> ContainerizationOS.Mount {
59 | ContainerizationOS.Mount(
60 | type: self.type,
61 | source: self.source,
62 | target: self.destination,
63 | options: self.options
64 | )
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Sources/ContainerizationOCI/Descriptor.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | // Source: https://github.com/opencontainers/image-spec/blob/main/specs-go/v1/descriptor.go
18 |
19 | import Foundation
20 |
21 | /// Descriptor describes the disposition of targeted content.
22 | /// This structure provides `application/vnd.oci.descriptor.v1+json` mediatype
23 | /// when marshalled to JSON.
24 | public struct Descriptor: Codable, Sendable, Equatable {
25 | /// mediaType is the media type of the object this schema refers to.
26 | public let mediaType: String
27 |
28 | /// digest is the digest of the targeted content.
29 | public let digest: String
30 |
31 | /// size specifies the size in bytes of the blob.
32 | public let size: Int64
33 |
34 | /// urls specifies a list of URLs from which this object MAY be downloaded.
35 | public let urls: [String]?
36 |
37 | /// annotations contains arbitrary metadata relating to the targeted content.
38 | public var annotations: [String: String]?
39 |
40 | /// platform describes the platform which the image in the manifest runs on.
41 | ///
42 | /// This should only be used when referring to a manifest.
43 | public var platform: Platform?
44 |
45 | public init(
46 | mediaType: String, digest: String, size: Int64, urls: [String]? = nil, annotations: [String: String]? = nil,
47 | platform: Platform? = nil
48 | ) {
49 | self.mediaType = mediaType
50 | self.digest = digest
51 | self.size = size
52 | self.urls = urls
53 | self.annotations = annotations
54 | self.platform = platform
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/01-bug.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: File a bug report.
3 | title: "[Bug]: "
4 | type: "Bug"
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Thanks for taking the time to fill out this bug report!
10 | - type: checkboxes
11 | id: prereqs
12 | attributes:
13 | label: I have done the following
14 | description: Select that you have completed the following prerequisites.
15 | options:
16 | - label: I have searched the existing issues
17 | required: true
18 | - label: If possible, I've reproduced the issue using the 'main' branch of this project
19 | required: false
20 | - type: textarea
21 | id: reproduce
22 | attributes:
23 | label: Steps to reproduce
24 | description: Explain how to reproduce the incorrect behavior.
25 | validations:
26 | required: true
27 | - type: textarea
28 | id: what-happened
29 | attributes:
30 | label: Current behavior
31 | description: A concise description of what you're experiencing.
32 | validations:
33 | required: true
34 | - type: textarea
35 | id: expected
36 | attributes:
37 | label: Expected behavior
38 | description: A concise description of what you expected to happen.
39 | validations:
40 | required: true
41 | - type: textarea
42 | attributes:
43 | label: Environment
44 | description: |
45 | Examples:
46 | - **OS**: macOS 26.0 (25A354)
47 | - **Xcode**: Version 26.0 (17A324)
48 | - **Swift**: Apple Swift version 6.2 (swift-6.2-RELEASE)
49 | value: |
50 | - OS:
51 | - Xcode:
52 | - Swift:
53 | render: markdown
54 | validations:
55 | required: true
56 | - type: textarea
57 | id: logs
58 | attributes:
59 | label: Relevant log output
60 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
61 | value: |
62 | N/A
63 | render: shell
64 | - type: checkboxes
65 | id: terms
66 | attributes:
67 | label: Code of Conduct
68 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/apple/.github/blob/main/CODE_OF_CONDUCT.md).
69 | options:
70 | - label: I agree to follow this project's Code of Conduct
71 | required: true
72 |
--------------------------------------------------------------------------------
/Sources/ContainerizationExtras/AsyncMutex.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | /// `AsyncMutex` provides a mutex that protects a piece of data, with the main benefit being that it
18 | /// is safe to call async methods while holding the lock. This is primarily used in spots
19 | /// where an actor makes sense, but we may need to ensure we don't fall victim to actor
20 | /// reentrancy issues.
21 | public actor AsyncMutex {
22 | private final class Box: @unchecked Sendable {
23 | var value: T
24 | init(_ value: T) {
25 | self.value = value
26 | }
27 | }
28 |
29 | private var busy = false
30 | private var queue: ArraySlice> = []
31 | private let box: Box
32 |
33 | public init(_ initialValue: T) {
34 | self.box = Box(initialValue)
35 | }
36 |
37 | /// withLock provides a scoped locking API to run a function while holding the lock.
38 | /// The protected value is passed to the closure for safe access.
39 | public func withLock(_ body: @Sendable @escaping (inout T) async throws -> R) async rethrows -> R {
40 | while self.busy {
41 | await withCheckedContinuation { cc in
42 | self.queue.append(cc)
43 | }
44 | }
45 |
46 | self.busy = true
47 |
48 | defer {
49 | self.busy = false
50 | if let next = self.queue.popFirst() {
51 | next.resume(returning: ())
52 | } else {
53 | self.queue = []
54 | }
55 | }
56 |
57 | return try await body(&self.box.value)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/Containerization/VirtualMachineInstance.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import ContainerizationError
18 | import Foundation
19 |
20 | /// The runtime state of the virtual machine instance.
21 | public enum VirtualMachineInstanceState: Sendable {
22 | case starting
23 | case running
24 | case stopped
25 | case stopping
26 | case unknown
27 | }
28 |
29 | /// A live instance of a virtual machine.
30 | public protocol VirtualMachineInstance: Sendable {
31 | associatedtype Agent: VirtualMachineAgent
32 |
33 | // The state of the virtual machine.
34 | var state: VirtualMachineInstanceState { get }
35 |
36 | var mounts: [String: [AttachedFilesystem]] { get }
37 | /// Dial the Agent. It's up the VirtualMachineInstance to determine
38 | /// what port the agent is listening on.
39 | func dialAgent() async throws -> Agent
40 | /// Dial a vsock port in the guest.
41 | func dial(_ port: UInt32) async throws -> FileHandle
42 | /// Listen on a host vsock port.
43 | func listen(_ port: UInt32) throws -> VsockListener
44 | /// Start the virtual machine.
45 | func start() async throws
46 | /// Stop the virtual machine.
47 | func stop() async throws
48 | /// Pause the virtual machine.
49 | func pause() async throws
50 | /// Resume the virtual machine.
51 | func resume() async throws
52 | }
53 |
54 | extension VirtualMachineInstance {
55 | func pause() async throws {
56 | throw ContainerizationError(.unsupported, message: "pause")
57 | }
58 | func resume() async throws {
59 | throw ContainerizationError(.unsupported, message: "resume")
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Tests/ContainerizationTests/KernelTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | //
18 |
19 | import Foundation
20 | import Testing
21 |
22 | @testable import Containerization
23 |
24 | final class KernelTests {
25 | @Test func kernelArgs() {
26 | let commandLine = Kernel.CommandLine(debug: false, panic: 0)
27 | let kernel = Kernel(path: .init(fileURLWithPath: ""), platform: .linuxArm, commandline: commandLine)
28 |
29 | let expected = "console=hvc0 tsc=reliable panic=0"
30 | let cmdline = kernel.commandLine.kernelArgs.joined(separator: " ")
31 | #expect(cmdline == expected)
32 | }
33 |
34 | @Test func kernelDebugArgs() {
35 | let cmdLine = Kernel.CommandLine(debug: true, panic: 0)
36 | let kernel = Kernel(path: .init(fileURLWithPath: ""), platform: .linuxArm, commandline: cmdLine)
37 |
38 | let expected = "console=hvc0 tsc=reliable debug panic=0"
39 | let cmdline = kernel.commandLine.kernelArgs.joined(separator: " ")
40 | #expect(cmdline == expected)
41 | }
42 |
43 | @Test func kernelCommandLineInitWithDebugTrue() {
44 | let commandLine = Kernel.CommandLine(debug: true, panic: 5, initArgs: ["--verbose"])
45 |
46 | #expect(commandLine.kernelArgs == ["console=hvc0", "tsc=reliable", "debug", "panic=5"])
47 | #expect(commandLine.initArgs == ["--verbose"])
48 | }
49 |
50 | @Test func kernelCommandLineMutatingMethods() {
51 | var commandLine = Kernel.CommandLine(kernelArgs: ["console=hvc0"], initArgs: [])
52 |
53 | commandLine.addDebug()
54 | commandLine.addPanic(level: 10)
55 |
56 | #expect(commandLine.kernelArgs == ["console=hvc0", "debug", "panic=10"])
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Tests/ContainerizationEXT4Tests/Resources/content/blobs/sha256/ad59e9f71edceca7b1ac7c642410858489b743c97233b0a26a5e2098b1443762:
--------------------------------------------------------------------------------
1 | {"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:48a06049d3738991b011ca8b12473d712b7c40666a1462118dae3c403676afc2","size":667,"annotations":{"com.apple.container.sign.v1.certificate/ptr":"mac-Q5W6919KP6:41FB3AB2-E9B9-45CE-8252-8C17C8038670:wlan0","com.apple.container.sign.v1.signature":"MEUCIEX3psgFczBpby6sMdzBk5FF5ID5UbqM4nOpqfiVbkseAiEAlDLBr9ajHiswl8/rOyVmYdN98lakuK+dKyABEBXRXeQ="},"platform":{"architecture":"arm64","os":"linux"}}],"annotations":{"com.apple.container.info.v1.dockerfile-sha256sum":"d95983c2a8acbd4cf861c7d3b9117d3e722aebc3768ca682ddf2a427e2fd6583","com.apple.container.sign.v1.certificate/mac-Q5W6919KP6:41FB3AB2-E9B9-45CE-8252-8C17C8038670:wlan0":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURjekNDQXhpZ0F3SUJBZ0lJRW9tR0duRkFGTnd3Q2dZSUtvWkl6ajBFQXdJd2FqRWtNQ0lHQTFVRUF3d2INClFYQndiR1VnUTI5eWNHOXlZWFJsSUZCTFNVNUpWQ0JEUVNBeU1TQXdIZ1lEVlFRTERCZERaWEowYVdacFkyRjANCmFXOXVJRUYxZEdodmNtbDBlVEVUTUJFR0ExVUVDZ3dLUVhCd2JHVWdTVzVqTGpFTE1Ba0dBMVVFQmhNQ1ZWTXcNCkhoY05Nakl4TWpJd01Ua3hOREl3V2hjTk1qVXdNVEU0TVRreE5ERTVXakNCcGpFYU1CZ0dDZ21TSm9tVDhpeGsNCkFRRU1DakkzTURFd09UVTRPVFV4UWpCQUJnTlZCQU1NT1cxaFl5MVJOVmMyT1RFNVMxQTJPalF4UmtJelFVSXkNCkxVVTVRamt0TkRWRFJTMDRNalV5TFRoRE1UZERPREF6T0RZM01EcDNiR0Z1TURFVk1CTUdBMVVFQ3d3TVFYQncNCmJHVkRiMjV1WldOME1STXdFUVlEVlFRS0RBcEJjSEJzWlNCSmJtTXVNUmd3RmdZS0NaSW1pWlB5TEdRQkdSWUkNClNVUk5VeTFUVTA4d1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtqT1BRTUJCd05DQUFTcW5tSjZ3cFluWWlKRkdRaDENCjlOcGkyVnp3M1I3UFlMOXY3MnNiUDZsNy83VUt3YXV4aVk1ZkdEL29yTnhwbWNScFdPRk5mNW1JUWpFcHByMDMNCkpYUXpvNElCYVRDQ0FXVXdEQVlEVlIwVEFRSC9CQUl3QURBZkJnTlZIU01FR0RBV2dCVEx3K0x0QUcxai8rV1QNCkpsLzJJVDVVMEJ6WEZUQkVCZ2dyQmdFRkJRY0JBUVE0TURZd05BWUlLd1lCQlFVSE1BR0dLR2gwZEhBNkx5OXYNClkzTndMbUZ3Y0d4bExtTnZiUzl2WTNOd01ETXRjR3RwYm1sMFkyRXlNREV3VGdZRFZSMFJCRWN3UmFCREJnWXINCkJnRUZBZ0tnT1RBM29CZ2JGa0ZRVUV4RlEwOU9Ua1ZEVkM1QlVGQk1SUzVEVDAyaEd6QVpvQU1DQVFDaEVqQVENCkd3NXphV1JvWVhKMGFHRmZiV0Z1YVRBb0JnTlZIU1VFSVRBZkJnZ3JCZ0VGQlFjREFnWUtLd1lCQkFHQ054UUMNCkFnWUhLd1lCQlFJREJEQXpCZ05WSFI4RUxEQXFNQ2lnSnFBa2hpSm9kSFJ3T2k4dlkzSnNMbUZ3Y0d4bExtTnYNCmJTOXdhMmx1YVhSallUSXVZM0pzTUIwR0ExVWREZ1FXQkJTRlIxOExjWkgxY1laNHlEajVyWXkwMmZNeTREQU8NCkJnTlZIUThCQWY4RUJBTUNCNEF3RUFZSktvWklodmRqWkFZcEJBTU1BVEl3Q2dZSUtvWkl6ajBFQXdJRFNRQXcNClJnSWhBSk9YTnBEWE43QjhHZFZIVE1WdmEzSForeXlCTDlMcm1hZTJkeFpUUUVoekFpRUF4b25CRjhnMTNQeXYNCkhCRFVSY0p0ZURUYVdQdnJyUW9HUi9KZXQzb3B5R1U9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"}}
--------------------------------------------------------------------------------
/Sources/Containerization/DNSConfiguration.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | /// DNS configuration for a container. The values will be used to
18 | /// construct /etc/resolv.conf for a given container.
19 | public struct DNS: Sendable {
20 | /// The set of default nameservers to use if none are provided
21 | /// in the constructor.
22 | public static let defaultNameservers = ["1.1.1.1"]
23 |
24 | /// The nameservers a container should use.
25 | public var nameservers: [String]
26 | /// The DNS domain to use.
27 | public var domain: String?
28 | /// The DNS search domains to use.
29 | public var searchDomains: [String]
30 | /// The DNS options to use.
31 | public var options: [String]
32 |
33 | public init(
34 | nameservers: [String] = defaultNameservers,
35 | domain: String? = nil,
36 | searchDomains: [String] = [],
37 | options: [String] = []
38 | ) {
39 | self.nameservers = nameservers
40 | self.domain = domain
41 | self.searchDomains = searchDomains
42 | self.options = options
43 | }
44 | }
45 |
46 | extension DNS {
47 | public var resolvConf: String {
48 | var text = ""
49 |
50 | if !nameservers.isEmpty {
51 | text += nameservers.map { "nameserver \($0)" }.joined(separator: "\n") + "\n"
52 | }
53 |
54 | if let domain {
55 | text += "domain \(domain)\n"
56 | }
57 |
58 | if !searchDomains.isEmpty {
59 | text += "search \(searchDomains.joined(separator: " "))\n"
60 | }
61 |
62 | if !options.isEmpty {
63 | text += "options \(options.joined(separator: " "))\n"
64 | }
65 |
66 | return text
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/.github/workflows/build-test-images.yml:
--------------------------------------------------------------------------------
1 | name: Build and publish containerization test images
2 |
3 | permissions:
4 | contents: read
5 |
6 | on:
7 | workflow_dispatch:
8 | inputs:
9 | publish:
10 | type: boolean
11 | description: "Publish the built image"
12 | default: false
13 | version:
14 | type: string
15 | description: "Version of the image to create"
16 | default: "test"
17 | image:
18 | type: choice
19 | description: Test image to build
20 | options:
21 | - dockermanifestimage
22 | - emptyimage
23 | default: 'dockermanifestimage'
24 | useBuildx:
25 | type: boolean
26 | description: "Use docker buildx to build the image"
27 | default: false
28 |
29 | jobs:
30 | image:
31 | name: Build test images
32 | timeout-minutes: 30
33 | runs-on: ubuntu-latest
34 | permissions:
35 | contents: read
36 | packages: write
37 | steps:
38 | - name: Check branch
39 | run: |
40 | if [[ "${{ github.ref }}" != "refs/heads/main" ]] && [[ "${{ github.ref }}" != refs/heads/release* ]] && [[ "${{ inputs.publish }}" == "true" ]]; then
41 | echo "❌ Cannot publish an image if we are not on main or a release branch."
42 | exit 1
43 | fi
44 | - name: Check inputs
45 | run: |
46 | if [[ "${{ inputs.image }}" == "dockermanifestimage" ]] && [[ "${{ inputs.useBuildx }}" == "true" ]]; then
47 | echo "❌ dockermanifestimage cannot be built with buildx"
48 | exit 1
49 | fi
50 |
51 | if [[ "${{ inputs.image }}" == "emptyimage" ]] && [[ "${{ inputs.useBuildx }}" != "true" ]]; then
52 | echo "❌ emptyimage should be built with buildx"
53 | exit 1
54 | fi
55 | - name: Checkout repository
56 | uses: actions/checkout@v4
57 | - name: Login to GitHub Container Registry
58 | uses: docker/login-action@v3
59 | with:
60 | registry: ghcr.io
61 | username: ${{ github.actor }}
62 | password: ${{ secrets.GITHUB_TOKEN }}
63 | - name: Set up Docker Buildx
64 | if: ${{ inputs.useBuildx }}
65 | uses: docker/setup-buildx-action@v3
66 | - name: Build dockerfile and push image
67 | uses: docker/build-push-action@v6
68 | with:
69 | push: ${{ inputs.publish }}
70 | context: Tests/TestImages/${{ inputs.image }}
71 | tags: ghcr.io/apple/containerization/${{ inputs.image }}:${{ inputs.version }}
72 |
--------------------------------------------------------------------------------
/Tests/ContainerizationTests/HostsTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 | import Testing
19 |
20 | @testable import Containerization
21 |
22 | struct HostsTests {
23 |
24 | @Test func hostsEntryRenderedWithAllFields() {
25 | let entry = Hosts.Entry(
26 | ipAddress: "192.168.1.100",
27 | hostnames: ["myserver", "server.local"],
28 | comment: "My local server"
29 | )
30 |
31 | let expected = "192.168.1.100 myserver server.local # My local server "
32 | #expect(entry.rendered == expected)
33 | }
34 |
35 | @Test func hostsEntryRenderedWithoutComment() {
36 | let entry = Hosts.Entry(
37 | ipAddress: "10.0.0.1",
38 | hostnames: ["gateway"]
39 | )
40 |
41 | let expected = "10.0.0.1 gateway"
42 | #expect(entry.rendered == expected)
43 | }
44 |
45 | @Test func hostsEntryRenderedWithEmptyHostnames() {
46 | let entry = Hosts.Entry(
47 | ipAddress: "172.16.0.1",
48 | hostnames: [],
49 | comment: "Empty hostnames"
50 | )
51 |
52 | let expected = "172.16.0.1 # Empty hostnames "
53 | #expect(entry.rendered == expected)
54 | }
55 |
56 | @Test func hostsFileWithCommentAndEntries() {
57 | let hosts = Hosts(
58 | entries: [
59 | Hosts.Entry(ipAddress: "127.0.0.1", hostnames: ["localhost"]),
60 | Hosts.Entry(ipAddress: "192.168.1.10", hostnames: ["server"], comment: "Main server"),
61 | ],
62 | comment: "Generated hosts file"
63 | )
64 |
65 | let expected = "# Generated hosts file\n127.0.0.1 localhost\n192.168.1.10 server # Main server \n"
66 | #expect(hosts.hostsFile == expected)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/.swift-format:
--------------------------------------------------------------------------------
1 | {
2 | "fileScopedDeclarationPrivacy" : {
3 | "accessLevel" : "private"
4 | },
5 | "indentation" : {
6 | "spaces" : 4
7 | },
8 | "indentConditionalCompilationBlocks" : false,
9 | "indentSwitchCaseLabels" : false,
10 | "lineBreakAroundMultilineExpressionChainComponents" : false,
11 | "lineBreakBeforeControlFlowKeywords" : false,
12 | "lineBreakBeforeEachArgument" : false,
13 | "lineBreakBeforeEachGenericRequirement" : false,
14 | "lineLength" : 180,
15 | "maximumBlankLines" : 1,
16 | "multiElementCollectionTrailingCommas" : true,
17 | "noAssignmentInExpressions" : {
18 | "allowedFunctions" : [
19 | "XCTAssertNoThrow"
20 | ]
21 | },
22 | "prioritizeKeepingFunctionOutputTogether" : false,
23 | "respectsExistingLineBreaks" : true,
24 | "rules" : {
25 | "AllPublicDeclarationsHaveDocumentation" : false,
26 | "AlwaysUseLowerCamelCase" : true,
27 | "AmbiguousTrailingClosureOverload" : false,
28 | "BeginDocumentationCommentWithOneLineSummary" : false,
29 | "DoNotUseSemicolons" : true,
30 | "DontRepeatTypeInStaticProperties" : true,
31 | "FileScopedDeclarationPrivacy" : true,
32 | "FullyIndirectEnum" : true,
33 | "GroupNumericLiterals" : true,
34 | "IdentifiersMustBeASCII" : true,
35 | "NeverForceUnwrap" : true,
36 | "NeverUseForceTry" : true,
37 | "NeverUseImplicitlyUnwrappedOptionals" : true,
38 | "NoAccessLevelOnExtensionDeclaration" : true,
39 | "NoAssignmentInExpressions" : true,
40 | "NoBlockComments" : false,
41 | "NoCasesWithOnlyFallthrough" : true,
42 | "NoEmptyTrailingClosureParentheses" : true,
43 | "NoLabelsInCasePatterns" : true,
44 | "NoLeadingUnderscores" : false,
45 | "NoParensAroundConditions" : true,
46 | "NoPlaygroundLiterals" : true,
47 | "NoVoidReturnOnFunctionSignature" : true,
48 | "OmitExplicitReturns" : true,
49 | "OneCasePerLine" : true,
50 | "OneVariableDeclarationPerLine" : true,
51 | "OnlyOneTrailingClosureArgument" : true,
52 | "OrderedImports" : true,
53 | "ReplaceForEachWithForLoop" : true,
54 | "ReturnVoidInsteadOfEmptyTuple" : true,
55 | "TypeNamesShouldBeCapitalized" : true,
56 | "UseEarlyExits" : true,
57 | "UseLetInEveryBoundCaseVariable" : true,
58 | "UseShorthandTypeNames" : true,
59 | "UseSingleLinePropertyGetter" : true,
60 | "UseSynthesizedInitializer" : true,
61 | "UseTripleSlashForDocumentationComments" : true,
62 | "UseWhereClausesInForLoops" : false,
63 | "ValidateDocumentationComments" : true
64 | },
65 | "spacesAroundRangeFormationOperators" : false,
66 | "tabWidth" : 2,
67 | "version" : 1
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/cctl/cctl.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import ArgumentParser
18 | import Containerization
19 | import ContainerizationOCI
20 | import Foundation
21 | import Logging
22 |
23 | let log = {
24 | LoggingSystem.bootstrap(StreamLogHandler.standardError)
25 | var log = Logger(label: "com.apple.containerization")
26 | log.logLevel = .debug
27 | return log
28 | }()
29 |
30 | @main
31 | struct Application: AsyncParsableCommand {
32 | static let keychainID = "com.apple.containerization"
33 | static let appRoot: URL = {
34 | FileManager.default.urls(
35 | for: .applicationSupportDirectory,
36 | in: .userDomainMask
37 | ).first!
38 | .appendingPathComponent("com.apple.containerization")
39 | }()
40 |
41 | private static let _contentStore: ContentStore = {
42 | try! LocalContentStore(path: appRoot.appendingPathComponent("content"))
43 | }()
44 |
45 | private static let _imageStore: ImageStore = {
46 | try! ImageStore(
47 | path: appRoot,
48 | contentStore: contentStore
49 | )
50 | }()
51 |
52 | static var imageStore: ImageStore {
53 | _imageStore
54 | }
55 |
56 | static var contentStore: ContentStore {
57 | _contentStore
58 | }
59 |
60 | static let configuration = CommandConfiguration(
61 | commandName: "cctl",
62 | abstract: "Utility CLI for Containerization",
63 | version: "2.0.0",
64 | subcommands: [
65 | Images.self,
66 | Login.self,
67 | Rootfs.self,
68 | Run.self,
69 | ]
70 | )
71 | }
72 |
73 | extension String {
74 | var absoluteURL: URL {
75 | URL(fileURLWithPath: self).absoluteURL
76 | }
77 | }
78 |
79 | extension String: Swift.Error {
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/Sources/ContainerizationExtras/Timeout.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 |
19 | /// `Timeout` contains helpers to run an operation and error out if
20 | /// the operation does not finish within a provided time.
21 | public struct Timeout {
22 | /// Performs the passed in `operation` and throws a `CancellationError` if the operation
23 | /// doesn't finish in the provided `seconds` amount.
24 | public static func run(
25 | seconds: UInt32,
26 | operation: @escaping @Sendable () async throws -> T
27 | ) async throws -> T {
28 | try await withThrowingTaskGroup(of: T.self) { group in
29 | group.addTask {
30 | try await operation()
31 | }
32 |
33 | group.addTask {
34 | try await Task.sleep(for: .seconds(seconds))
35 | throw CancellationError()
36 | }
37 |
38 | guard let result = try await group.next() else {
39 | fatalError()
40 | }
41 |
42 | group.cancelAll()
43 | return result
44 | }
45 | }
46 |
47 | public static func run(
48 | for duration: Duration,
49 | operation: @escaping @Sendable () async throws -> T
50 | ) async throws -> T {
51 | try await withThrowingTaskGroup(of: T.self) { group in
52 | group.addTask {
53 | try await operation()
54 | }
55 |
56 | group.addTask {
57 | try await Task.sleep(for: duration)
58 | throw CancellationError()
59 | }
60 |
61 | guard let result = try await group.next() else {
62 | fatalError()
63 | }
64 |
65 | group.cancelAll()
66 | return result
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/ContainerizationOCI/Index.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | // Source: https://github.com/opencontainers/image-spec/blob/main/specs-go/v1/index.go
18 |
19 | import Foundation
20 |
21 | /// Index references manifests for various platforms.
22 | /// This structure provides `application/vnd.oci.image.index.v1+json` mediatype when marshalled to JSON.
23 | public struct Index: Codable, Sendable {
24 | /// schemaVersion is the image manifest schema that this image follows
25 | public let schemaVersion: Int
26 |
27 | /// mediaType specifies the type of this document data structure e.g. `application/vnd.oci.image.index.v1+json`
28 | /// This field is optional per the OCI Image Index Specification (omitempty)
29 | public let mediaType: String
30 |
31 | /// manifests references platform specific manifests.
32 | public var manifests: [Descriptor]
33 |
34 | /// annotations contains arbitrary metadata for the image index.
35 | public var annotations: [String: String]?
36 |
37 | public init(
38 | schemaVersion: Int = 2, mediaType: String = MediaTypes.index, manifests: [Descriptor],
39 | annotations: [String: String]? = nil
40 | ) {
41 | self.schemaVersion = schemaVersion
42 | self.mediaType = mediaType
43 | self.manifests = manifests
44 | self.annotations = annotations
45 | }
46 |
47 | public init(from decoder: Decoder) throws {
48 | let container = try decoder.container(keyedBy: CodingKeys.self)
49 | self.schemaVersion = try container.decode(Int.self, forKey: .schemaVersion)
50 | self.mediaType = try container.decodeIfPresent(String.self, forKey: .mediaType) ?? ""
51 | self.manifests = try container.decode([Descriptor].self, forKey: .manifests)
52 | self.annotations = try container.decodeIfPresent([String: String].self, forKey: .annotations)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/vminitd/Sources/vminitd/ContainerProcess.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import ContainerizationOS
18 | import Foundation
19 |
20 | /// Exit status information for a container process
21 | struct ContainerExitStatus: Sendable {
22 | var exitCode: Int32
23 | var exitedAt: Date
24 | }
25 |
26 | /// Protocol for managing container processes
27 | ///
28 | /// This protocol abstracts the underlying container runtime implementation,
29 | /// allowing for different backends like vmexec or runc.
30 | protocol ContainerProcess: Sendable {
31 | /// Unique identifier for the container process
32 | var id: String { get }
33 |
34 | /// Process ID of the running container (nil if not started)
35 | var pid: Int32? { get }
36 |
37 | /// Start the container process
38 | /// - Returns: The process ID of the started container
39 | /// - Throws: If the process fails to start
40 | func start() async throws -> Int32
41 |
42 | /// Wait for the container process to exit
43 | /// - Returns: Exit status information when the process exits
44 | func wait() async -> ContainerExitStatus
45 |
46 | /// Send a signal to the container process
47 | /// - Parameter signal: The signal number to send
48 | /// - Throws: If the signal cannot be sent
49 | func kill(_ signal: Int32) async throws
50 |
51 | /// Resize the terminal for the container process
52 | /// - Parameter size: The new terminal size
53 | /// - Throws: If the terminal cannot be resized or process doesn't have a terminal
54 | func resize(size: Terminal.Size) throws
55 |
56 | /// Close stdin for the container process
57 | /// - Throws: If stdin cannot be closed
58 | func closeStdin() throws
59 |
60 | /// Delete the container process and cleanup resources
61 | /// - Throws: If cleanup fails
62 | func delete() async throws
63 |
64 | /// Set the exit status of the process.
65 | func setExit(_ status: Int32)
66 | }
67 |
--------------------------------------------------------------------------------
/Tests/ContainerizationOCITests/AuthChallengeTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 | import Testing
19 |
20 | @testable import ContainerizationOCI
21 |
22 | struct AuthChallengeTests {
23 | internal struct TestCase: Sendable {
24 | let input: String
25 | let expected: AuthenticateChallenge
26 | }
27 |
28 | private static let testCases: [TestCase] = [
29 | .init(
30 | input: """
31 | Bearer realm="https://domain.io/token",service="domain.io",scope="repository:user/image:pull"
32 | """,
33 | expected: .init(type: "Bearer", realm: "https://domain.io/token", service: "domain.io", scope: "repository:user/image:pull", error: nil)),
34 | .init(
35 | input: """
36 | Bearer realm="https://foo-bar-registry.com/auth",service="Awesome Registry"
37 | """,
38 | expected: .init(type: "Bearer", realm: "https://foo-bar-registry.com/auth", service: "Awesome Registry", scope: nil, error: nil)),
39 | .init(
40 | input: """
41 | Bearer realm="users.example.com", scope="create delete"
42 | """,
43 | expected: .init(type: "Bearer", realm: "users.example.com", service: nil, scope: "create delete", error: nil)),
44 | .init(
45 | input: """
46 | Bearer realm="https://auth.server.io/token",service="registry.server.io"
47 | """,
48 | expected: .init(type: "Bearer", realm: "https://auth.server.io/token", service: "registry.server.io", scope: nil, error: nil)),
49 |
50 | ]
51 |
52 | @Test(arguments: testCases)
53 | func parseAuthHeader(testCase: TestCase) throws {
54 | let challenges = RegistryClient.parseWWWAuthenticateHeaders(headers: [testCase.input])
55 | #expect(challenges.count == 1)
56 | #expect(challenges[0] == testCase.expected)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Sources/Containerization/VsockListener.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 |
19 | #if os(macOS)
20 | import Virtualization
21 | #endif
22 |
23 | /// A stream of vsock connections.
24 | public final class VsockListener: NSObject, Sendable, AsyncSequence {
25 | public typealias Element = FileHandle
26 |
27 | /// The port the connections are for.
28 | public let port: UInt32
29 |
30 | private let connections: AsyncStream
31 | private let cont: AsyncStream.Continuation
32 | private let stopListening: @Sendable (_ port: UInt32) throws -> Void
33 |
34 | package init(port: UInt32, stopListen: @Sendable @escaping (_ port: UInt32) throws -> Void) {
35 | self.port = port
36 | let (stream, continuation) = AsyncStream.makeStream(of: FileHandle.self)
37 | self.connections = stream
38 | self.cont = continuation
39 | self.stopListening = stopListen
40 | }
41 |
42 | public func finish() throws {
43 | self.cont.finish()
44 | try self.stopListening(self.port)
45 | }
46 |
47 | public func makeAsyncIterator() -> AsyncStream.AsyncIterator {
48 | connections.makeAsyncIterator()
49 | }
50 | }
51 |
52 | #if os(macOS)
53 |
54 | extension VsockListener: VZVirtioSocketListenerDelegate {
55 | public func listener(
56 | _: VZVirtioSocketListener, shouldAcceptNewConnection conn: VZVirtioSocketConnection,
57 | from _: VZVirtioSocketDevice
58 | ) -> Bool {
59 | let fd = dup(conn.fileDescriptor)
60 | guard fd != -1 else {
61 | return false
62 | }
63 | conn.close()
64 |
65 | let fh = FileHandle(fileDescriptor: fd, closeOnDealloc: false)
66 | let result = cont.yield(fh)
67 | if case .terminated = result {
68 | try? fh.close()
69 | return false
70 | }
71 |
72 | return true
73 | }
74 | }
75 |
76 | #endif
77 |
--------------------------------------------------------------------------------
/Tests/ContainerizationTests/LinuxContainerTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import ContainerizationOCI
18 | import Foundation
19 | import Testing
20 |
21 | @testable import Containerization
22 |
23 | struct LinuxContainerTests {
24 |
25 | @Test func processInitFromImageConfigWithAllFields() {
26 | let imageConfig = ImageConfig(
27 | user: "appuser",
28 | env: ["NODE_ENV=production", "PORT=3000"],
29 | entrypoint: ["/usr/bin/node"],
30 | cmd: ["app.js", "--verbose"],
31 | workingDir: "/app"
32 | )
33 |
34 | let process = LinuxProcessConfiguration(from: imageConfig)
35 |
36 | #expect(process.workingDirectory == "/app")
37 | #expect(process.environmentVariables == ["NODE_ENV=production", "PORT=3000"])
38 | #expect(process.arguments == ["/usr/bin/node", "app.js", "--verbose"])
39 | #expect(process.user.username == "appuser")
40 | }
41 |
42 | @Test func processInitFromImageConfigWithNilValues() {
43 | let imageConfig = ImageConfig(
44 | user: nil,
45 | env: nil,
46 | entrypoint: nil,
47 | cmd: nil,
48 | workingDir: nil
49 | )
50 |
51 | let process = LinuxProcessConfiguration(from: imageConfig)
52 |
53 | #expect(process.workingDirectory == "/")
54 | #expect(process.environmentVariables == [])
55 | #expect(process.arguments == [])
56 | #expect(process.user.username == "") // Default User() has empty string username
57 | }
58 |
59 | @Test func processInitFromImageConfigEntrypointAndCmdConcatenation() {
60 | let imageConfig = ImageConfig(
61 | entrypoint: ["/bin/sh", "-c"],
62 | cmd: ["echo 'hello'", "&&", "sleep 10"]
63 | )
64 |
65 | let process = LinuxProcessConfiguration(from: imageConfig)
66 |
67 | #expect(process.arguments == ["/bin/sh", "-c", "echo 'hello'", "&&", "sleep 10"])
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/Containerization/TimeSyncer.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 | import Logging
19 |
20 | actor TimeSyncer {
21 | private var task: Task?
22 | private var context: Vminitd?
23 | private var paused: Bool
24 | private let logger: Logger?
25 |
26 | init(logger: Logger?) {
27 | self.paused = false
28 | self.logger = logger
29 | }
30 |
31 | func start(context: Vminitd, interval: Duration = .seconds(30)) {
32 | guard self.task == nil else {
33 | return
34 | }
35 |
36 | self.context = context
37 | self.task = Task {
38 | while true {
39 | do {
40 | do {
41 | try await Task.sleep(for: interval)
42 | } catch {
43 | return
44 | }
45 |
46 | guard !paused else {
47 | continue
48 | }
49 |
50 | var timeval = timeval()
51 | guard gettimeofday(&timeval, nil) == 0 else {
52 | throw POSIXError.fromErrno()
53 | }
54 |
55 | try await context.setTime(
56 | sec: Int64(timeval.tv_sec),
57 | usec: Int32(timeval.tv_usec)
58 | )
59 | } catch {
60 | self.logger?.error("failed to sync time with guest agent: \(error)")
61 | }
62 | }
63 | }
64 | }
65 |
66 | func pause() async {
67 | self.paused = true
68 | }
69 |
70 | func resume() async {
71 | self.paused = false
72 | }
73 |
74 | func close() async throws {
75 | guard let task else {
76 | // Already closed, nop.
77 | return
78 | }
79 |
80 | task.cancel()
81 | await task.value
82 |
83 | try await self.context?.close()
84 | self.task = nil
85 | self.context = nil
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/Sources/ContainerizationOCI/State.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | public enum ContainerState: String, Codable, Sendable {
18 | case creating
19 | case created
20 | case running
21 | case stopped
22 | }
23 |
24 | public struct State: Codable, Sendable {
25 | public init(
26 | version: String,
27 | id: String,
28 | status: ContainerState,
29 | pid: Int,
30 | bundle: String,
31 | annotations: [String: String]?
32 | ) {
33 | self.ociVersion = version
34 | self.id = id
35 | self.status = status
36 | self.pid = pid
37 | self.bundle = bundle
38 | self.annotations = annotations
39 | }
40 |
41 | public init(instance: State) {
42 | self.ociVersion = instance.ociVersion
43 | self.id = instance.id
44 | self.status = instance.status
45 | self.pid = instance.pid
46 | self.bundle = instance.bundle
47 | self.annotations = instance.annotations
48 | }
49 |
50 | public let ociVersion: String
51 | public let id: String
52 | public let status: ContainerState
53 | public let pid: Int
54 | public let bundle: String
55 | public var annotations: [String: String]?
56 | }
57 |
58 | public let seccompFdName: String = "seccompFd"
59 |
60 | public struct ContainerProcessState: Codable, Sendable {
61 | public init(version: String, fds: [String], pid: Int, metadata: String, state: State) {
62 | self.ociVersion = version
63 | self.fds = fds
64 | self.pid = pid
65 | self.metadata = metadata
66 | self.state = state
67 | }
68 |
69 | public init(instance: ContainerProcessState) {
70 | self.ociVersion = instance.ociVersion
71 | self.fds = instance.fds
72 | self.pid = instance.pid
73 | self.metadata = instance.metadata
74 | self.state = instance.state
75 | }
76 |
77 | public let ociVersion: String
78 | public var fds: [String]
79 | public let pid: Int
80 | public let metadata: String
81 | public let state: State
82 | }
83 |
--------------------------------------------------------------------------------
/Sources/ContainerizationExtras/AddressAllocator.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | /// Conforming objects can allocate and free various address types.
18 | public protocol AddressAllocator: Sendable {
19 | associatedtype AddressType: Sendable
20 |
21 | /// Allocate a new address.
22 | func allocate() throws -> AddressType
23 |
24 | /// Attempt to reserve a specific address.
25 | func reserve(_ address: AddressType) throws
26 |
27 | /// Free an allocated address.
28 | func release(_ address: AddressType) throws
29 |
30 | /// If no addresses are allocated, prevent future allocations and return true.
31 | func disableAllocator() -> Bool
32 | }
33 |
34 | /// Errors that a type implementing AddressAllocator should throw.
35 | public enum AllocatorError: Swift.Error, CustomStringConvertible, Equatable {
36 | case allocatorDisabled
37 | case allocatorFull
38 | case alreadyAllocated(_ address: String)
39 | case invalidAddress(_ index: String)
40 | case invalidArgument(_ msg: String)
41 | case invalidIndex(_ index: Int)
42 | case notAllocated(_ address: String)
43 | case rangeExceeded
44 |
45 | public var description: String {
46 | switch self {
47 | case .allocatorDisabled:
48 | return "the allocator is shutting down"
49 | case .allocatorFull:
50 | return "no free indices are available for allocation"
51 | case .alreadyAllocated(let address):
52 | return "cannot choose already-allocated address \(address)"
53 | case .invalidAddress(let address):
54 | return "cannot create index using address \(address)"
55 | case .invalidArgument(let msg):
56 | return "invalid argument: \(msg)"
57 | case .invalidIndex(let index):
58 | return "cannot create address using index \(index)"
59 | case .notAllocated(let address):
60 | return "cannot free unallocated address \(address)"
61 | case .rangeExceeded:
62 | return "cannot create allocator that overflows maximum address value"
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/Containerization/NATNetworkInterface.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | #if os(macOS)
18 |
19 | import vmnet
20 | import Virtualization
21 | import ContainerizationError
22 | import Foundation
23 | import Synchronization
24 |
25 | /// An interface that uses NAT to provide an IP address for a given
26 | /// container/virtual machine.
27 | @available(macOS 26, *)
28 | public final class NATNetworkInterface: Interface, Sendable {
29 | public let address: String
30 | public let gateway: String?
31 | public let macAddress: String?
32 |
33 | @available(macOS 26, *)
34 | // `reference` isn't used concurrently.
35 | public nonisolated(unsafe) let reference: vmnet_network_ref!
36 |
37 | @available(macOS 26, *)
38 | public init(
39 | address: String,
40 | gateway: String?,
41 | reference: sending vmnet_network_ref,
42 | macAddress: String? = nil
43 | ) {
44 | self.address = address
45 | self.gateway = gateway
46 | self.macAddress = macAddress
47 | self.reference = reference
48 | }
49 |
50 | @available(macOS, obsoleted: 26, message: "Use init(address:gateway:reference:macAddress:) instead")
51 | public init(
52 | address: String,
53 | gateway: String?,
54 | macAddress: String? = nil
55 | ) {
56 | self.address = address
57 | self.gateway = gateway
58 | self.macAddress = macAddress
59 | self.reference = nil
60 | }
61 | }
62 |
63 | @available(macOS 26, *)
64 | extension NATNetworkInterface: VZInterface {
65 | public func device() throws -> VZVirtioNetworkDeviceConfiguration {
66 | let config = VZVirtioNetworkDeviceConfiguration()
67 | if let macAddress = self.macAddress {
68 | guard let mac = VZMACAddress(string: macAddress) else {
69 | throw ContainerizationError(.invalidArgument, message: "invalid mac address \(macAddress)")
70 | }
71 | config.macAddress = mac
72 | }
73 |
74 | config.attachment = VZVmnetNetworkDeviceAttachment(network: self.reference)
75 | return config
76 | }
77 | }
78 |
79 | #endif
80 |
--------------------------------------------------------------------------------
/Tests/ContainerizationOCITests/OCIImageTests.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | //
18 |
19 | import Testing
20 |
21 | @testable import ContainerizationOCI
22 |
23 | struct OCITests {
24 | @Test func config() {
25 | let config = ContainerizationOCI.ImageConfig()
26 | let rootfs = ContainerizationOCI.Rootfs(type: "foo", diffIDs: ["diff1", "diff2"])
27 | let history = ContainerizationOCI.History()
28 |
29 | let image = ContainerizationOCI.Image(architecture: "arm64", os: "linux", config: config, rootfs: rootfs, history: [history])
30 | #expect(image.rootfs.type == "foo")
31 | }
32 |
33 | @Test func descriptor() {
34 | let platform = ContainerizationOCI.Platform(arch: "arm64", os: "linux")
35 | let descriptor = ContainerizationOCI.Descriptor(mediaType: MediaTypes.descriptor, digest: "123", size: 0, platform: platform)
36 |
37 | #expect(descriptor.platform?.architecture == "arm64")
38 | #expect(descriptor.platform?.os == "linux")
39 | }
40 |
41 | @Test func index() {
42 | var descriptors: [ContainerizationOCI.Descriptor] = []
43 | for i in 0..<5 {
44 | let descriptor = ContainerizationOCI.Descriptor(mediaType: MediaTypes.descriptor, digest: "\(i)", size: Int64(i))
45 | descriptors.append(descriptor)
46 | }
47 |
48 | let index = ContainerizationOCI.Index(schemaVersion: 1, manifests: descriptors)
49 | #expect(index.manifests.count == 5)
50 | }
51 |
52 | @Test func manifests() {
53 | var descriptors: [ContainerizationOCI.Descriptor] = []
54 | for i in 0..<5 {
55 | let descriptor = ContainerizationOCI.Descriptor(mediaType: MediaTypes.descriptor, digest: "\(i)", size: Int64(i))
56 | descriptors.append(descriptor)
57 | }
58 |
59 | let config = ContainerizationOCI.Descriptor(mediaType: MediaTypes.descriptor, digest: "123", size: 0)
60 |
61 | let manifest = ContainerizationOCI.Manifest(schemaVersion: 1, config: config, layers: descriptors)
62 | #expect(manifest.config.digest == "123")
63 | #expect(manifest.layers.count == 5)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/ContainerizationEXT4/EXT4+Ptr.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 |
19 | extension EXT4 {
20 | class Ptr {
21 | let underlying: UnsafeMutablePointer
22 | private var capacity: Int
23 | private var initialized: Bool
24 | private var allocated: Bool
25 |
26 | var pointee: T {
27 | underlying.pointee
28 | }
29 |
30 | init(capacity: Int) {
31 | self.underlying = UnsafeMutablePointer.allocate(capacity: capacity)
32 | self.capacity = capacity
33 | self.allocated = true
34 | self.initialized = false
35 | }
36 |
37 | static func allocate(capacity: Int) -> Ptr {
38 | Ptr(capacity: capacity)
39 | }
40 |
41 | func initialize(to value: T) {
42 | guard self.allocated else {
43 | return
44 | }
45 | if self.initialized {
46 | self.underlying.deinitialize(count: self.capacity)
47 | }
48 | self.underlying.initialize(to: value)
49 | self.allocated = true
50 | self.initialized = true
51 | }
52 |
53 | func deallocate() {
54 | guard self.allocated else {
55 | return
56 | }
57 | self.underlying.deallocate()
58 | self.allocated = false
59 | self.initialized = false
60 | }
61 |
62 | func deinitialize(count: Int) {
63 | guard self.allocated else {
64 | return
65 | }
66 | guard self.initialized else {
67 | return
68 | }
69 | self.underlying.deinitialize(count: count)
70 | self.initialized = false
71 | self.allocated = true
72 | }
73 |
74 | func move() -> T {
75 | self.initialized = false
76 | self.allocated = true
77 | return self.underlying.move()
78 | }
79 |
80 | deinit {
81 | self.deinitialize(count: self.capacity)
82 | self.deallocate()
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/Containerization/UnixSocketConfiguration.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 | import SystemPackage
19 |
20 | /// Represents a UnixSocket that can be shared into or out of a container/guest.
21 | public struct UnixSocketConfiguration: Sendable {
22 | // TODO: Realistically, we can just hash this struct and use it as the "id".
23 | package var id: String {
24 | _id
25 | }
26 |
27 | private let _id = UUID().uuidString
28 |
29 | /// The path to the socket you'd like relayed. For .into
30 | /// direction this should be the path on the host to a unix socket.
31 | /// For direction .outOf this should be the path in the container/guest
32 | /// to a unix socket.
33 | public var source: URL
34 |
35 | /// The path you'd like the socket to be relayed to. For .into
36 | /// direction this should be the path in the container/guest. For
37 | /// direction .outOf this should be the path on your host.
38 | public var destination: URL
39 |
40 | /// What to set the file permissions of the unix socket being created
41 | /// to. For .into direction this will be the socket in the guest. For
42 | /// .outOf direction this will be the socket on the host.
43 | public var permissions: FilePermissions?
44 |
45 | /// The direction of the relay. `.into` for sharing a unix socket on your
46 | /// host into the container/guest. `outOf` shares a socket in the container/guest
47 | /// onto your host.
48 | public var direction: Direction
49 |
50 | /// Type that denotes the direction of the unix socket relay.
51 | public enum Direction: Sendable {
52 | /// Share the socket into the container/guest.
53 | case into
54 | /// Share a socket in the container/guest onto the host.
55 | case outOf
56 | }
57 |
58 | public init(
59 | source: URL,
60 | destination: URL,
61 | permissions: FilePermissions? = nil,
62 | direction: Direction = .into
63 | ) {
64 | self.source = source
65 | self.destination = destination
66 | self.permissions = permissions
67 | self.direction = direction
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/ContainerizationOCI/Content/LocalContent.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import ContainerizationError
18 | import Crypto
19 | import Foundation
20 |
21 | public final class LocalContent: Content {
22 | public let path: URL
23 | private let file: FileHandle
24 |
25 | public init(path: URL) throws {
26 | guard FileManager.default.fileExists(atPath: path.path) else {
27 | throw ContainerizationError(.notFound, message: "content at path \(path.absolutePath())")
28 | }
29 |
30 | self.file = try FileHandle(forReadingFrom: path)
31 | self.path = path
32 | }
33 |
34 | public func digest() throws -> SHA256.Digest {
35 | let bufferSize = 64 * 1024 // 64 KB
36 | var hasher = SHA256()
37 |
38 | try self.file.seek(toOffset: 0)
39 | while case let data = file.readData(ofLength: bufferSize), !data.isEmpty {
40 | hasher.update(data: data)
41 | }
42 |
43 | let digest = hasher.finalize()
44 |
45 | try self.file.seek(toOffset: 0)
46 | return digest
47 | }
48 |
49 | public func data(offset: UInt64 = 0, length size: Int = 0) throws -> Data? {
50 | try file.seek(toOffset: offset)
51 | if size == 0 {
52 | return try file.readToEnd()
53 | }
54 | return try file.read(upToCount: size)
55 | }
56 |
57 | public func data() throws -> Data {
58 | try Data(contentsOf: self.path)
59 | }
60 |
61 | public func size() throws -> UInt64 {
62 | let fileAttrs = try FileManager.default.attributesOfItem(atPath: self.path.absolutePath())
63 | if let size = fileAttrs[FileAttributeKey.size] as? UInt64 {
64 | return size
65 | }
66 | throw ContainerizationError(.internalError, message: "could not determine file size for \(path.absolutePath())")
67 | }
68 |
69 | public func decode() throws -> T where T: Decodable {
70 | let json = JSONDecoder()
71 | let data = try Data(contentsOf: self.path)
72 | return try json.decode(T.self, from: data)
73 | }
74 |
75 | deinit {
76 | try? self.file.close()
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/Sources/ContainerizationEXT4/UnsafeLittleEndianBytes.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import Foundation
18 |
19 | // takes a pointer and converts its contents to native endian bytes
20 | public func withUnsafeLittleEndianBytes(of value: T, body: (UnsafeRawBufferPointer) throws -> Result)
21 | rethrows -> Result
22 | {
23 | switch Endian {
24 | case .little:
25 | return try withUnsafeBytes(of: value) { bytes in
26 | try body(bytes)
27 | }
28 | case .big:
29 | return try withUnsafeBytes(of: value) { buffer in
30 | let reversedBuffer = Array(buffer.reversed())
31 | return try reversedBuffer.withUnsafeBytes { buf in
32 | try body(buf)
33 | }
34 | }
35 | }
36 | }
37 |
38 | public func withUnsafeLittleEndianBuffer(
39 | of value: UnsafeRawBufferPointer, body: (UnsafeRawBufferPointer) throws -> T
40 | ) rethrows -> T {
41 | switch Endian {
42 | case .little:
43 | return try body(value)
44 | case .big:
45 | let reversed = Array(value.reversed())
46 | return try reversed.withUnsafeBytes { buf in
47 | try body(buf)
48 | }
49 | }
50 | }
51 |
52 | extension UnsafeRawBufferPointer {
53 | // loads littleEndian raw data, converts it native endian format and calls UnsafeRawBufferPointer.load
54 | public func loadLittleEndian(as type: T.Type) -> T {
55 | switch Endian {
56 | case .little:
57 | return self.load(as: T.self)
58 | case .big:
59 | let buffer = Array(self.reversed())
60 | return buffer.withUnsafeBytes { ptr in
61 | ptr.load(as: T.self)
62 | }
63 | }
64 | }
65 | }
66 |
67 | public enum Endianness {
68 | case little
69 | case big
70 | }
71 |
72 | // returns current endianness
73 | public var Endian: Endianness {
74 | switch CFByteOrderGetCurrent() {
75 | case CFByteOrder(CFByteOrderLittleEndian.rawValue):
76 | return .little
77 | case CFByteOrder(CFByteOrderBigEndian.rawValue):
78 | return .big
79 | default:
80 | fatalError("impossible")
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Sources/ContainerizationOCI/Client/RegistryClient+Error.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import AsyncHTTPClient
18 | import Foundation
19 | import NIOHTTP1
20 |
21 | extension RegistryClient {
22 | /// `RegistryClient` errors.
23 | public enum Error: Swift.Error, CustomStringConvertible {
24 | case invalidStatus(url: String, HTTPResponseStatus, reason: String? = nil)
25 |
26 | /// Description of the errors.
27 | public var description: String {
28 | switch self {
29 | case .invalidStatus(let u, let response, let reason):
30 | return "HTTP request to \(u) failed with response: \(response.description). Reason: \(reason ?? "Unknown")"
31 | }
32 | }
33 | }
34 |
35 | /// The container registry typically returns actionable failure reasons in the response body
36 | /// of the failing HTTP Request. This type models the structure of the error message.
37 | /// Reference: https://distribution.github.io/distribution/spec/api/#errors
38 | internal struct ErrorResponse: Codable {
39 | let errors: [RemoteError]
40 |
41 | internal struct RemoteError: Codable {
42 | let code: String
43 | let message: String
44 | let detail: String?
45 | }
46 |
47 | internal static func fromResponseBody(_ body: HTTPClientResponse.Body) async -> ErrorResponse? {
48 | guard var buffer = try? await body.collect(upTo: Int(1.mib())) else {
49 | return nil
50 | }
51 | guard let bytes = buffer.readBytes(length: buffer.readableBytes) else {
52 | return nil
53 | }
54 | let data = Data(bytes)
55 | guard let jsonError = try? JSONDecoder().decode(ErrorResponse.self, from: data) else {
56 | return nil
57 | }
58 | return jsonError
59 | }
60 |
61 | public var jsonString: String {
62 | let data = try? JSONEncoder().encode(self)
63 | guard let data else {
64 | return "{}"
65 | }
66 | return String(data: data, encoding: .utf8) ?? "{}"
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/vminitd/Sources/vminitd/Application.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | // Copyright © 2025 Apple Inc. and the Containerization project authors.
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //===----------------------------------------------------------------------===//
16 |
17 | import ContainerizationOS
18 | import Foundation
19 | import Logging
20 |
21 | @main
22 | struct Application {
23 | static func main() async throws {
24 | LoggingSystem.bootstrap(StreamLogHandler.standardError)
25 |
26 | // Parse command line arguments
27 | let args = CommandLine.arguments
28 | let command = args.count > 1 ? args[1] : "init"
29 |
30 | switch command {
31 | case "pause":
32 | let log = Logger(label: "pause")
33 |
34 | log.info("Running pause command")
35 | try PauseCommand.run(log: log)
36 | case "init":
37 | fallthrough
38 | default:
39 | let log = Logger(label: "vminitd")
40 |
41 | log.info("Running init command")
42 | try Self.mountProc(log: log)
43 | try await InitCommand.run(log: log)
44 | }
45 | }
46 |
47 | // Swift seems like it has some fun issues trying to spawn threads if /proc isn't around, so we
48 | // do this before calling our first async function.
49 | static func mountProc(log: Logger) throws {
50 | // Is it already mounted (would only be true in debug builds where we re-exec ourselves)?
51 | if isProcMounted() {
52 | return
53 | }
54 |
55 | log.info("mounting /proc")
56 |
57 | let mnt = ContainerizationOS.Mount(
58 | type: "proc",
59 | source: "proc",
60 | target: "/proc",
61 | options: []
62 | )
63 | try mnt.mount(createWithPerms: 0o755)
64 | }
65 |
66 | static func isProcMounted() -> Bool {
67 | guard let data = try? String(contentsOfFile: "/proc/mounts", encoding: .utf8) else {
68 | return false
69 | }
70 |
71 | for line in data.split(separator: "\n") {
72 | let fields = line.split(separator: " ")
73 | if fields.count >= 2 {
74 | let mountPoint = String(fields[1])
75 | if mountPoint == "/proc" {
76 | return true
77 | }
78 | }
79 | }
80 |
81 | return false
82 | }
83 | }
84 |
--------------------------------------------------------------------------------