diff --git a/PATCHES.md b/PATCHES.md index f02e490..158e2c1 100644 --- a/PATCHES.md +++ b/PATCHES.md @@ -116,8 +116,19 @@ in-tree means the patch can't be lost to a dependency re-resolve. (the `cctl login` write path) gains a delete-and-retry on `errSecDuplicateItem`, since the now-silent `exists` can under-report an unreadable pre-existing item. Mirrors `KeychainOwnedAccess.withoutLegacyKeychainUI` in NucleicCore. Host-side (shipped by a normal - `swift build`); `SecKeychain*` deprecation warnings are expected (built with - `WARNINGS_AS_ERRORS=false`). Marked `[Nucleic vendored patch]`. + `swift build`). The three `SecKeychain*` symbols are formally deprecated but are the only API + covering the legacy ACL panel; they're bound directly via `@_silgen_name` (`nucleic_SecKeychain*`, + top of file) so the required calls compile without deprecation warnings. Marked + `[Nucleic vendored patch]`. + +14. **`Sources/Containerization/Vminitd.swift` — configure the gRPC pipeline before the channel goes + active.** `Vminitd.init` used the now-deprecated `HTTP2ClientTransport.WrappedChannel.wrapping( + channel:config:serviceConfig:)`, which wraps an already-connected channel best-effort and may drop + early server frames such as SETTINGS. Migrated to `wrapping(config:serviceConfig:makeChannel:)`, + which invokes the transport's `configure` inside the bootstrap's channel initializer — before the + vsock channel becomes active. `init` is now `async throws` (the new overload is async); its callers + in `VZVirtualMachineInstance` (`start`/`dialAgent`, both already async) and the integration test now + `try await`. Marked `[Nucleic vendored patch]`. ### GUEST-side patches (require rebuilding the initfs — see below) diff --git a/Sources/Containerization/VZVirtualMachineInstance.swift b/Sources/Containerization/VZVirtualMachineInstance.swift index a711b2b..c05cfa5 100644 --- a/Sources/Containerization/VZVirtualMachineInstance.swift +++ b/Sources/Containerization/VZVirtualMachineInstance.swift @@ -191,7 +191,7 @@ extension VZVirtualMachineInstance: VirtualMachineInstance { try await self.vm.start(queue: self.queue) - let agent = try Vminitd( + let agent = try await Vminitd( connection: try await self.vm.waitForAgent(queue: self.queue), group: self.group ) @@ -260,7 +260,7 @@ extension VZVirtualMachineInstance: VirtualMachineInstance { port: Vminitd.port ) let handle = try conn.dupHandle() - return try Vminitd(connection: handle, group: self.group) + return try await Vminitd(connection: handle, group: self.group) } catch { if let err = error as? ContainerizationError { throw err diff --git a/Sources/Containerization/Vminitd.swift b/Sources/Containerization/Vminitd.swift index 90c516a..57157cb 100644 --- a/Sources/Containerization/Vminitd.swift +++ b/Sources/Containerization/Vminitd.swift @@ -34,18 +34,23 @@ public struct Vminitd: Sendable { public let grpcClient: GRPCClient private let connectionTask: Task - public init(connection: FileHandle, group: any EventLoopGroup) throws { - let channel = try ClientBootstrap(group: group) - .channelInitializer { channel in - channel.eventLoop.makeCompletedFuture(withResultOf: { - try channel.pipeline.syncOperations.addHandler(HTTP2ConnectBufferingHandler()) - }) - } - .withConnectedSocket(connection.fileDescriptor).wait() - let transport = HTTP2ClientTransport.WrappedChannel.wrapping( - channel: channel, + public init(connection: FileHandle, group: any EventLoopGroup) async throws { + // Configure the gRPC pipeline from inside the channel initializer — before the channel + // becomes active — so no early server frames (e.g. SETTINGS) are dropped. `configure` is + // supplied by `wrapping(config:serviceConfig:makeChannel:)` and must be called exactly once. + let fd = connection.fileDescriptor + let transport = try await HTTP2ClientTransport.WrappedChannel.wrapping( config: .defaults { $0.connection.maxIdleTime = nil } - ) + ) { configure in + try await ClientBootstrap(group: group) + .withConnectedSocket(fd) { channel in + channel.eventLoop.makeCompletedFuture { + try channel.pipeline.syncOperations.addHandler(HTTP2ConnectBufferingHandler()) + }.flatMap { _ in + configure(channel) + } + } + } let grpcClient = GRPCClient(transport: transport) self.grpcClient = grpcClient self.client = Com_Apple_Containerization_Sandbox_V3_SandboxContext.Client(wrapping: self.grpcClient) diff --git a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift index 3a0df5f..554c2ef 100644 --- a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift +++ b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift @@ -22,6 +22,15 @@ import Foundation #endif import Security // [Nucleic vendored patch] SecKeychain*UserInteractionAllowed for prompt-free reads +// [Nucleic vendored patch] `SecKeychainGet/SetUserInteractionAllowed` are formally deprecated but +// remain the ONLY API that suppresses the legacy Keychain ACL panel. Bind the C symbols directly — +// the deprecation rides on their Swift imports, not the raw symbols — so `withoutInteractiveUI` +// compiles without deprecation warnings. Mirrors `KeychainOwnedAccess` in NucleicCore. +@_silgen_name("SecKeychainGetUserInteractionAllowed") +private func nucleic_SecKeychainGetUserInteractionAllowed(_ state: UnsafeMutablePointer) -> OSStatus +@_silgen_name("SecKeychainSetUserInteractionAllowed") +private func nucleic_SecKeychainSetUserInteractionAllowed(_ state: DarwinBoolean) -> OSStatus + /// Holds the result of a query to the keychain. public struct KeychainQueryResult { public var username: String @@ -260,9 +269,9 @@ public struct KeychainQuery { /// `KeychainOwnedAccess.withoutLegacyKeychainUI` in NucleicCore. private static func withoutInteractiveUI(_ body: () -> T) -> T { var previous = DarwinBoolean(true) - SecKeychainGetUserInteractionAllowed(&previous) - SecKeychainSetUserInteractionAllowed(false) - defer { SecKeychainSetUserInteractionAllowed(previous.boolValue) } + _ = nucleic_SecKeychainGetUserInteractionAllowed(&previous) + _ = nucleic_SecKeychainSetUserInteractionAllowed(false) + defer { _ = nucleic_SecKeychainSetUserInteractionAllowed(previous) } return body() } } diff --git a/Sources/Integration/ContainerTests.swift b/Sources/Integration/ContainerTests.swift index 498aa3f..38ad16f 100644 --- a/Sources/Integration/ContainerTests.swift +++ b/Sources/Integration/ContainerTests.swift @@ -1730,7 +1730,7 @@ extension IntegrationSuite { try await assertExec(container, id: "create-fifo", cmd: "mkfifo /tmp/test-fifo") let vsock = try await container.dialVsock(port: 1024) - let vminitd = try Vminitd(connection: vsock, group: Self.eventLoop) + let vminitd = try await Vminitd(connection: vsock, group: Self.eventLoop) let root = URL(filePath: container.root)