Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
968dbe4
Downgrade swift-collections to 1.5.1. (#1984)
jglogan Jul 22, 2026
f0b2b96
Use `enum` for warmup images. (#1990)
jglogan Jul 22, 2026
9be73ed
Add missing dependencies to new ContainerTestSupport package (#1994)
katiewasnothere Jul 22, 2026
72431b0
Add OCI maskedPaths and readonlyPaths support to Container API. (#1996)
jglogan Jul 22, 2026
9af6e0e
Integration test - miscellaneous fixture and test refinements. (#1993)
jglogan Jul 23, 2026
78e2cb4
Use log instead of print for system start status messages (#1889)
adityabagchi24 Jul 23, 2026
d1d7635
Fix BuilderStart race, parallelize `container build` tests. (#2002)
jglogan Jul 23, 2026
b229cec
Allow custom kernel boot args via --kernel-arg (#1744)
arirubinstein Jul 27, 2026
13e976f
fix: Increase XPC timeout for Machine API operations (#2006)
dev-kvt Jul 27, 2026
27e5043
Update containerization import to latest 0.40.0 (#2028)
katiewasnothere Jul 27, 2026
48145ac
Fix image env vars, build context checks, TCP/UDP port forward buffer…
katiewasnothere Jul 28, 2026
6e65319
Update containerization import to 0.40.1 (#2038)
katiewasnothere Jul 28, 2026
da8bec6
[container]: add `container export` for live containers (#1630)
saehejkang Aug 2, 2026
a58c5fe
Adjust overcommit and max_map_count vm defaults in guest VMs (#2055)
adityabagchi24 Aug 2, 2026
39f12ca
[package]: bump container-builder-shim to 0.13.1 (#2056)
saehejkang Aug 3, 2026
e87d3a0
[builder]: enable ssh forwarding for container build (#1508)
saehejkang Aug 3, 2026
4ed47cb
Move SSH builder test to Serialized tests (#2061)
JaewonHur Aug 3, 2026
520371c
Increase testExecDetachProcessRunning sleep margin to avoid CI flake …
jglogan Aug 3, 2026
053190c
Merge upstream main into container clean command
recrack Aug 4, 2026
82d2df0
Fix container clean runtime identity and integration coverage
recrack Aug 4, 2026
ab40619
Merge remote-tracking branch 'upstream/main' into fix/container-clean…
recrack Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import PackageDescription

let releaseVersion = ProcessInfo.processInfo.environment["RELEASE_VERSION"] ?? "0.0.0"
let gitCommit = ProcessInfo.processInfo.environment["GIT_COMMIT"] ?? "unspecified"
let builderShimVersion = "0.13.0"
let scVersion = "0.38.0"
let builderShimVersion = "0.13.1"
let scVersion = "0.40.1"

let package = Package(
name: "container",
Expand Down Expand Up @@ -143,6 +143,7 @@ let package = Package(
.product(name: "Containerization", package: "containerization"),
.product(name: "ContainerizationArchive", package: "containerization"),
.product(name: "ContainerizationOCI", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "GRPCCore", package: "grpc-swift-2"),
.product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"),
Expand Down Expand Up @@ -574,12 +575,18 @@ let package = Package(
name: "ContainerTestSupport",
dependencies: [
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "Containerization", package: "containerization"),
.product(name: "ContainerizationArchive", package: "containerization"),
.product(name: "ContainerizationExtras", package: "containerization"),
.product(name: "Logging", package: "swift-log"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "TOML", package: "swift-toml"),
"ContainerLog",
"ContainerPersistence",
"ContainerPlugin",
"ContainerResource",
]
),
Expand Down
85 changes: 73 additions & 12 deletions Sources/ContainerBuild/BuildFSSync.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,49 @@ import CryptoKit
import Foundation
import GRPCCore

/// Handles the `fssync` stage of the build protocol.
///
/// When BuildKit needs build-context files it sends `Walk`, `Read`, and `Info`
/// requests to the shim, which proxies them over the gRPC stream to this actor.
///
/// ## Primary path: Walk (tar mode)
///
/// `Walk` is the primary data path. The host packs all requested context paths
/// into a tar archive and streams it to the shim. The shim unpacks the tar to a
/// local cache and presents the files to BuildKit via `DiffCopy`. BuildKit then
/// issues `PACKET_REQ` for regular files it needs; the shim serves those from
/// the local cache without any further calls to the host.
///
/// When a context path is a symlink whose target lies within the context root,
/// ``walk(_:_:_:)`` adds the target to the archive alongside the symlink so
/// BuildKit can dereference it during `COPY`/`ADD` processing.
///
/// ## Fallback path: Info + Read
///
/// `FS.Open()` in the shim falls back to `Info` followed by `Read` calls when
/// its local checksum cache is unpopulated (a narrow race window at the start of
/// a build). These paths are not exercised during a normal build.
///
/// ## Symlink safety
///
/// The host enforces that no file served to the builder resolves to a path
/// outside the context root. If any component of a requested path is a symlink
/// whose target lies outside the context root the request is rejected.
/// Dockerignore filtering is **not** applied here; the shim applies it after
/// unpacking the tar.
actor BuildFSSync: BuildPipelineHandler {
let contextDir: URL

init(_ contextDir: URL) throws {
let resolved = contextDir.resolvingSymlinksInPath()
guard FileManager.default.fileExists(atPath: contextDir.cleanPath) else {
throw Error.contextNotFound(contextDir.cleanPath)
}
guard try contextDir.isDir() else {
guard resolved.isDirectory else {
throw Error.contextIsNotDirectory(contextDir.cleanPath)
}

self.contextDir = contextDir
self.contextDir = resolved
}

nonisolated func accept(_ packet: ServerStream) throws -> Bool {
Expand Down Expand Up @@ -63,6 +94,11 @@ actor BuildFSSync: BuildPipelineHandler {
}
}

/// Serves the content of a single context file to the shim.
///
/// Called only via the shim's `FS.Open()` fallback path, not during a
/// normal `Walk`-based build. Rejects any path whose symlink chain resolves
/// outside the context root.
func read(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: BuildTransfer, _ buildID: String) async throws {
let offset: UInt64 = packet.offset() ?? 0
let size: Int = packet.len() ?? 0
Expand All @@ -79,6 +115,10 @@ actor BuildFSSync: BuildPipelineHandler {
path = URL(filePath: self.contextDir.cleanPath)
path.append(components: packet.source.cleanPathComponent)
}
let resolved = path.resolvingSymlinksInPath()
guard self.contextDir.parentOf(resolved) else {
throw Error.pathIsNotChild(resolved.cleanPath, self.contextDir.cleanPath)
}
let data = try {
if try path.isDir() {
return Data()
Expand All @@ -95,6 +135,12 @@ actor BuildFSSync: BuildPipelineHandler {
sender.yield(response)
}

/// Returns metadata (mode, size, modification time, uid/gid) for a single
/// context path.
///
/// Called only via the shim's `FS.Open()` fallback path, not during a
/// normal `Walk`-based build. Must reject paths that escape the context root
/// via symlinks for the same reasons as ``read(_:_:_:)``.
func info(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: BuildTransfer, _ buildID: String) async throws {
let path: URL
if packet.source.hasPrefix("/") {
Expand All @@ -105,6 +151,10 @@ actor BuildFSSync: BuildPipelineHandler {
.appendingPathComponent(packet.source)
.standardizedFileURL
}
let resolved = path.resolvingSymlinksInPath()
guard self.contextDir.parentOf(resolved) else {
throw Error.pathIsNotChild(resolved.cleanPath, self.contextDir.cleanPath)
}
let transfer = try path.buildTransfer(id: packet.id, contextDir: self.contextDir, complete: true)
var response = ClientStream()
response.buildID = buildID
Expand All @@ -127,6 +177,23 @@ actor BuildFSSync: BuildPipelineHandler {
}
}

/// Packs requested context paths into a tar archive and streams it to the shim.
///
/// This is the primary data path for build-context transfer. BuildKit sends
/// a `Walk` request whose `followpaths` field names the context paths needed
/// for the current build step (e.g. the source of a `COPY` instruction).
/// The host resolves those globs, builds an entry set, and passes it to
/// `Archiver.compress` to produce the tar.
///
/// For any symlink in the entry set whose target lies within the context
/// root, the target is added to the entry set so BuildKit can dereference
/// the symlink during `COPY`/`ADD` processing without a separate request.
/// Symlinks whose targets lie outside the context root are included as
/// symlink entries but their targets are not; BuildKit will resolve them
/// against the shim's local filesystem on Linux, not the macOS host.
///
/// Dockerignore filtering is the shim's responsibility and is applied after
/// the tar is unpacked; this method has no knowledge of `.dockerignore`.
func walk(
_ sender: AsyncStream<ClientStream>.Continuation,
_ packet: BuildTransfer,
Expand Down Expand Up @@ -334,16 +401,10 @@ actor BuildFSSync: BuildPipelineHandler {
let target: String

init(path: URL, contextDir: URL) throws {
if path.isSymlink {
let target: URL = path.resolvingSymlinksInPath()
if contextDir.parentOf(target) {
self.target = target.relativePathFrom(from: path)
} else {
self.target = target.cleanPath
}
} else {
self.target = ""
}
// Always report the literal, unresolved on-disk symlink target —
// the same value tar mode provides via Archiver's use of
// destinationOfSymbolicLink — rather than a host-resolved path.
self.target = path.isSymlink ? try FileManager.default.destinationOfSymbolicLink(atPath: path.cleanPath) : ""

self.name = try path.relativeChildPath(to: contextDir)
self.modTime = try path.modTime()
Expand Down
7 changes: 7 additions & 0 deletions Sources/ContainerBuild/BuildImageResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ import GRPCCore
import Logging
import TerminalProgress

/// Handles the `resolver` stage of the build protocol.
///
/// Resolves image references on behalf of BuildKit: authenticates with
/// registries, pulls missing base-image manifests and layers, and stores
/// them in the local content store. BuildKit delegates these operations to
/// the host because registry credentials and network access live on the
/// macOS side, not inside the builder VM.
struct BuildImageResolver: BuildPipelineHandler {
let contentStore: ContentStore
let quiet: Bool
Expand Down
50 changes: 50 additions & 0 deletions Sources/ContainerBuild/BuildPipelineHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,61 @@ import Foundation
import GRPCCore
import NIO

/// A handler for one stage of the build protocol.
///
/// The build pipeline multiplexes a single bidirectional gRPC stream between
/// the macOS host and the builder shim. Each packet carries a stage tag;
/// a handler claims packets for its stage via ``accept(_:)`` and processes
/// them via ``handle(_:_:)``.
protocol BuildPipelineHandler: Sendable {
func accept(_ packet: ServerStream) throws -> Bool
func handle(_ sender: AsyncStream<ClientStream>.Continuation, _ packet: ServerStream) async throws
}

/// Drives a build session by routing packets from the builder shim to the
/// appropriate handler.
///
/// ## Three-tier architecture
///
/// Builds involve three components with distinct responsibilities:
///
/// **macOS host (`BuildPipeline` / its handlers)**
/// Serves resources to the builder shim over a bidirectional gRPC stream.
/// Responsibilities include:
/// - Packing requested build-context files into a tar archive (``BuildFSSync``).
/// - Proxying image-layer blobs from the local content store (``BuildRemoteContentProxy``).
/// - Resolving and pulling base images (``BuildImageResolver``).
/// - Relaying builder stdout/stderr to the terminal (``BuildStdio``).
/// - Enforcing the context root boundary: directory traversal uses `openat(O_NOFOLLOW)`
/// at every descent step, and every individual file request resolves symlinks to their
/// canonical path before verifying containment within the context root.
///
/// **Builder shim (`container-builder-shim`)**
/// A Go process running inside a Linux VM that bridges the host gRPC stream
/// and BuildKit's `filesync` gRPC interface. Responsibilities include:
/// - Receiving the context tar from the host, unpacking it to a local cache,
/// and presenting the result to BuildKit via `DiffCopy`.
/// - Applying dockerignore exclusions (received from BuildKit as
/// `exclude-patterns` metadata) when walking the unpacked cache.
/// - Passing `followpaths` from BuildKit to the host so the host knows which
/// context paths to include in the tar.
///
/// **BuildKit**
/// Parses and executes the Dockerfile. Responsibilities include:
/// - Sending `Walk` requests with `followpaths` derived from each `COPY`/`ADD`
/// source and `exclude-patterns` derived from `.dockerignore`.
/// - Dereferencing symlinks, recursing into directories, and applying all
/// other COPY/ADD transfer semantics on the unpacked context the shim provides.
///
/// ## Packet flow
///
/// ```
/// BuildKit ──► shim DiffCopy ──► host Walk (tar of context files)
/// ◄── tar archive
/// ◄── PACKET_STAT per file (after shim unpacks + filters)
/// ──► PACKET_REQ for each regular file
/// ◄── PACKET_DATA (shim reads from local unpacked cache)
/// ```
public actor BuildPipeline {
let handlers: [BuildPipelineHandler]
public init(_ config: Builder.BuildConfig) async throws {
Expand Down
5 changes: 5 additions & 0 deletions Sources/ContainerBuild/BuildRemoteContentProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import ContainerizationOCI
import Foundation
import GRPCCore

/// Handles the `content-store` stage of the build protocol.
///
/// Proxies image-layer blob requests from BuildKit to the host's local
/// containerd content store. BuildKit issues these requests when it needs
/// base-image layers that are not already present in the builder VM.
struct BuildRemoteContentProxy: BuildPipelineHandler {
let local: ContentStore

Expand Down
5 changes: 5 additions & 0 deletions Sources/ContainerBuild/BuildStdio.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import Foundation
import GRPCCore
import NIO

/// Handles the stdio stage of the build protocol.
///
/// Relays builder stdout/stderr from the shim to the client terminal.
/// Build output (layer download progress, `RUN` command output, etc.) flows
/// through this handler and is written directly to the configured file handle.
actor BuildStdio: BuildPipelineHandler {
public let quiet: Bool
public let handle: FileHandle
Expand Down
6 changes: 6 additions & 0 deletions Sources/ContainerBuild/Builder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ public struct Builder: Sendable {
public let contentStore: ContentStore
public let buildArgs: [String]
public let secrets: [String: Data]
public let ssh: String
public let contextDir: String
public let dockerfile: Data
public let dockerignore: Data?
Expand All @@ -289,6 +290,7 @@ public struct Builder: Sendable {
contentStore: ContentStore,
buildArgs: [String],
secrets: [String: Data],
ssh: String,
contextDir: String,
dockerfile: Data,
dockerignore: Data?,
Expand All @@ -309,6 +311,7 @@ public struct Builder: Sendable {
self.contentStore = contentStore
self.buildArgs = buildArgs
self.secrets = secrets
self.ssh = ssh
self.contextDir = contextDir
self.dockerfile = dockerfile
self.dockerignore = dockerignore
Expand Down Expand Up @@ -356,6 +359,9 @@ public struct Builder: Sendable {
for (id, data) in config.secrets {
metadata.addString(id + "=" + data.base64EncodedString(), forKey: "secrets")
}
if config.ssh == "default" {
metadata.addString("default", forKey: "ssh")
}
for output in config.exports {
metadata.addString(try output.stringValue, forKey: "outputs")
}
Expand Down
Loading