Skip to content

Commit 4dc49f5

Browse files
authored
Merge branch 'main' into add-vminit-api-freeze-thaw
2 parents 50e7f83 + bbea928 commit 4dc49f5

27 files changed

Lines changed: 1179 additions & 249 deletions

.github/workflows/linux-build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,6 @@ jobs:
5555

5656
- name: Build vminitd (musl)
5757
run: make -C vminitd
58+
59+
- name: Run unit tests
60+
run: swift test --disable-automatic-resolution -Xswiftc -warnings-as-errors

.swift-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
6.3
1+
6.3.0

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ ifeq ($(LIBC),all)
8989
else
9090
$(call linux_run,make containerization && make -C vminitd LIBC=$(LIBC))
9191
endif
92+
93+
.PHONY: linux-test
94+
linux-test:
95+
$(call linux_run,swift test $(SWIFT_CONFIGURATION))
9296
endif
9397

9498
.PHONY: all
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 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 GRPCCore
19+
import GRPCNIOTransportCore
20+
import NIOCore
21+
import NIOPosix
22+
23+
/// Buffers incoming bytes until the full gRPC HTTP/2 pipeline is configured, then replays them.
24+
///
25+
/// This prevents the race condition where the vminitd server's initial HTTP/2 SETTINGS frame
26+
/// arrives and is discarded before `configureGRPCClientPipeline` has finished installing
27+
/// `ClientConnectionHandler`.
28+
///
29+
/// The handler is added via `ClientBootstrap.channelInitializer`, which runs before
30+
/// `registerAlreadyConfigured0` adds the fd to epoll/kqueue — guaranteeing it is in place
31+
/// before any bytes can arrive on the socket.
32+
///
33+
/// When `NIOHTTP2Handler` is added to the pipeline (inside `configureGRPCClientPipeline`), its
34+
/// `handlerAdded` fires an outbound flush (the HTTP/2 client preface). We intercept that flush
35+
/// and schedule a deferred removal via the event loop. Because `configureGRPCClientPipeline` runs
36+
/// as a single synchronous event loop task, the deferred removal is guaranteed to run after that
37+
/// entire task completes — i.e., after `ClientConnectionHandler` is also in the pipeline.
38+
/// Buffered bytes are replayed atomically as part of the pipeline removal.
39+
40+
// FIXME: This handler is needed until the swift GRPC libraries offers us a way to create a
41+
// client transport from an existing fd. Remove this type when such an API exists.
42+
public final class HTTP2ConnectBufferingHandler: ChannelDuplexHandler, RemovableChannelHandler {
43+
public typealias InboundIn = ByteBuffer
44+
public typealias InboundOut = ByteBuffer
45+
public typealias OutboundIn = ByteBuffer
46+
public typealias OutboundOut = ByteBuffer
47+
48+
private var removalScheduled = false
49+
private var bufferedReads: [NIOAny] = []
50+
51+
public init() {}
52+
53+
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
54+
bufferedReads.append(data)
55+
}
56+
57+
public func channelReadComplete(context: ChannelHandlerContext) {
58+
// Suppress while buffering; a single readComplete is emitted after replay.
59+
}
60+
61+
public func flush(context: ChannelHandlerContext) {
62+
if !removalScheduled {
63+
removalScheduled = true
64+
// Defer removal to the next event loop task. configureGRPCClientPipeline runs as a
65+
// single synchronous event loop task, so this deferred task is guaranteed to run
66+
// after that whole task completes (including ClientConnectionHandler being added).
67+
context.eventLoop.assumeIsolatedUnsafeUnchecked().execute {
68+
context.pipeline.syncOperations.removeHandler(self, promise: nil)
69+
}
70+
}
71+
context.flush()
72+
}
73+
74+
public func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) {
75+
var didRead = false
76+
while !bufferedReads.isEmpty {
77+
context.fireChannelRead(bufferedReads.removeFirst())
78+
didRead = true
79+
}
80+
if didRead {
81+
context.fireChannelReadComplete()
82+
}
83+
context.leavePipeline(removalToken: removalToken)
84+
}
85+
86+
public func channelInactive(context: ChannelHandlerContext) {
87+
bufferedReads.removeAll()
88+
context.fireChannelInactive()
89+
}
90+
}

Sources/Containerization/LinuxContainer.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,9 @@ extension LinuxContainer {
530530
let mib: UInt64 = 1.mib()
531531
let vmMemory = (self.memoryInBytes + guestAgentOverhead + mib - 1) & ~(mib - 1)
532532

533+
// Give the guest agent a core to play with outside of the container.
534+
let vmCpus = self.cpus + 1
535+
533536
// Prepare file mounts. This transforms single-file mounts into directory shares.
534537
let fileMountContext = try FileMountContext.prepare(mounts: self.config.mounts)
535538
// This is dumb, but alas.
@@ -542,7 +545,7 @@ extension LinuxContainer {
542545
}
543546

544547
let vmConfig = VMConfiguration(
545-
cpus: self.cpus,
548+
cpus: vmCpus,
546549
memoryInBytes: vmMemory,
547550
interfaces: self.interfaces,
548551
mountsByID: [self.id: containerMounts],

Sources/Containerization/VZVirtualMachineManager.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,14 @@ public struct VZVirtualMachineManager: VirtualMachineManager {
5555
// Clamp to system RAM as Virtualization.framework bounds us to this.
5656
let memoryInBytes = min(vmConfig.memoryInBytes, ProcessInfo.processInfo.physicalMemory)
5757

58+
// Clamp to system CPU count as Virtualization.framework bounds us to this.
59+
let cpus = min(vmConfig.cpus, ProcessInfo.processInfo.activeProcessorCount)
60+
5861
return try VZVirtualMachineInstance(
5962
group: self.group,
6063
logger: self.logger,
6164
with: { instanceConfig in
62-
instanceConfig.cpus = vmConfig.cpus
65+
instanceConfig.cpus = cpus
6366
instanceConfig.memoryInBytes = memoryInBytes
6467

6568
instanceConfig.kernel = self.kernel

Sources/Containerization/Vminitd.swift

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -24,70 +24,6 @@ import GRPCNIOTransportCore
2424
import NIOCore
2525
import NIOPosix
2626

27-
/// Buffers incoming bytes until the full gRPC HTTP/2 pipeline is configured, then replays them.
28-
///
29-
/// This prevents the race condition where the vminitd server's initial HTTP/2 SETTINGS frame
30-
/// arrives and is discarded before `configureGRPCClientPipeline` has finished installing
31-
/// `ClientConnectionHandler`.
32-
///
33-
/// The handler is added via `ClientBootstrap.channelInitializer`, which runs before
34-
/// `registerAlreadyConfigured0` adds the fd to epoll/kqueue — guaranteeing it is in place
35-
/// before any bytes can arrive on the socket.
36-
///
37-
/// When `NIOHTTP2Handler` is added to the pipeline (inside `configureGRPCClientPipeline`), its
38-
/// `handlerAdded` fires an outbound flush (the HTTP/2 client preface). We intercept that flush
39-
/// and schedule a deferred removal via the event loop. Because `configureGRPCClientPipeline` runs
40-
/// as a single synchronous event loop task, the deferred removal is guaranteed to run after that
41-
/// entire task completes — i.e., after `ClientConnectionHandler` is also in the pipeline.
42-
/// Buffered bytes are replayed atomically as part of the pipeline removal.
43-
private final class HTTP2ConnectBufferingHandler: ChannelDuplexHandler, RemovableChannelHandler {
44-
typealias InboundIn = ByteBuffer
45-
typealias InboundOut = ByteBuffer
46-
typealias OutboundIn = ByteBuffer
47-
typealias OutboundOut = ByteBuffer
48-
49-
private var removalScheduled = false
50-
private var bufferedReads: [NIOAny] = []
51-
52-
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
53-
bufferedReads.append(data)
54-
}
55-
56-
func channelReadComplete(context: ChannelHandlerContext) {
57-
// Suppress while buffering; a single readComplete is emitted after replay.
58-
}
59-
60-
func flush(context: ChannelHandlerContext) {
61-
if !removalScheduled {
62-
removalScheduled = true
63-
// Defer removal to the next event loop task. configureGRPCClientPipeline runs as a
64-
// single synchronous event loop task, so this deferred task is guaranteed to run
65-
// after that whole task completes (including ClientConnectionHandler being added).
66-
context.eventLoop.assumeIsolatedUnsafeUnchecked().execute {
67-
context.pipeline.syncOperations.removeHandler(self, promise: nil)
68-
}
69-
}
70-
context.flush()
71-
}
72-
73-
func removeHandler(context: ChannelHandlerContext, removalToken: ChannelHandlerContext.RemovalToken) {
74-
var didRead = false
75-
while !bufferedReads.isEmpty {
76-
context.fireChannelRead(bufferedReads.removeFirst())
77-
didRead = true
78-
}
79-
if didRead {
80-
context.fireChannelReadComplete()
81-
}
82-
context.leavePipeline(removalToken: removalToken)
83-
}
84-
85-
func channelInactive(context: ChannelHandlerContext) {
86-
bufferedReads.removeAll()
87-
context.fireChannelInactive()
88-
}
89-
}
90-
9127
/// A remote connection into the vminitd Linux guest agent via a port (vsock).
9228
/// Used to modify the runtime environment of the Linux sandbox.
9329
public struct Vminitd: Sendable {

Sources/ContainerizationEXT4/EXT4+FileTree.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ extension EXT4 {
6969
var root: Ptr<FileTreeNode>
7070

7171
init(_ root: InodeNumber, _ name: String) {
72-
self.root = Ptr<FileTreeNode>.allocate(capacity: 1)
73-
self.root.initialize(to: FileTreeNode(inode: root, name: name, parent: nil))
72+
self.root = Ptr(FileTreeNode(inode: root, name: name, parent: nil))
7473
}
7574

7675
func lookup(path: FilePath) -> Ptr<FileTreeNode>? {

0 commit comments

Comments
 (0)