From 1fc4c320970acb321f875afb44c3d6b9fe26608e Mon Sep 17 00:00:00 2001 From: Nucleic Date: Fri, 17 Jul 2026 16:03:36 -0700 Subject: [PATCH] Merge nucleic/quiet-opal-gecko-ah7w into dev --- PATCHES.md | 25 ++++++++++- .../Keychain/KeychainQuery.swift | 45 ++++++++++++++++--- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/PATCHES.md b/PATCHES.md index 39f1a5c..f02e490 100644 --- a/PATCHES.md +++ b/PATCHES.md @@ -100,6 +100,25 @@ in-tree means the patch can't be lost to a dependency re-resolve. paths close both ends so a failed connection fails FAST for the peer and leaks no fds. Marked `[Nucleic vendored patch]`. +13. **`Sources/ContainerizationOS/Keychain/KeychainQuery.swift` — prompt-free registry-credential + reads.** Upstream's `get`/`list`/`exists` call `SecItemCopyMatching` with the legacy login + Keychain's interactive authorization panel enabled, so any process that isn't on a registry + internet-password item's ACL raises the macOS *"'cctl' wants to use your confidential information + stored in 'ghcr.io' in your keychain"* panel when it reads that item — e.g. a `cctl` binary + re-signed ad-hoc by a fresh `make vminit-image` reading a GHCR token an earlier build stored, or + any tool linking `KeychainHelper.lookup` during an image pull/push/list. Nucleic's rule is that no + automatic credential lookup may ever raise a Keychain panel. This patch wraps the three + `SecItemCopyMatching` reads in `withoutInteractiveUI` (`SecKeychainSetUserInteractionAllowed(false)` + — the only switch that governs the legacy ACL/partition-list dialog; the data-protection + `kSecUseAuthenticationUI*` flags do NOT), so an already-trusted item reads silently while anything + else fails with `errSecInteractionNotAllowed` — which `isQuerySuccessful` now treats as "not found" + so the caller falls back to anonymous / `REGISTRY_HOST`/`USERNAME`/`TOKEN` env auth. `save` + (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]`. + ### GUEST-side patches (require rebuilding the initfs — see below) Patches #1–#7 are host-side (the `Containerization` library), shipped by a normal `swift build`. @@ -191,8 +210,10 @@ rebuild whenever a guest patch changes. Built locally, not in CI: the host frame for `[Nucleic vendored patch]` to find every site, and patch #9 (per-exec cgroups) across `Cgroup2Manager.swift` / `ManagedContainer.swift` / `ManagedProcess.swift`, patch #10 (`Socket.acceptStream` transient-error tolerance + `isTransientAcceptError`), patch #11 (the - `UnixSocketRelay` per-connection containment + fail-fast closes), and patch #12 (the `VsockProxy` - cleanup/`try!`/listener hardening in `vminitd/`). After re-applying any + `UnixSocketRelay` per-connection containment + fail-fast closes), patch #12 (the `VsockProxy` + cleanup/`try!`/listener hardening in `vminitd/`), and patch #13 (the prompt-free + `KeychainQuery` reads: `withoutInteractiveUI` + the `errSecInteractionNotAllowed` handling + + the `save` duplicate retry). After re-applying any `vminitd/` patch, rebuild + publish the custom init image with `make vminit-image` + `make vminit-image-push`, and bump `ContainerEngine.vminitReference`. 5. Update the commit hash above and in the root `Package.swift` comment. diff --git a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift index 0c45df9..3a0df5f 100644 --- a/Sources/ContainerizationOS/Keychain/KeychainQuery.swift +++ b/Sources/ContainerizationOS/Keychain/KeychainQuery.swift @@ -20,6 +20,7 @@ import FoundationEssentials #else import Foundation #endif +import Security // [Nucleic vendored patch] SecKeychain*UserInteractionAllowed for prompt-free reads /// Holds the result of a query to the keychain. public struct KeychainQueryResult { @@ -68,7 +69,15 @@ public struct KeychainQuery { query[kSecAttrAccessGroup as String] = accessGroup } - let status = SecItemAdd(query as CFDictionary, nil) + var status = SecItemAdd(query as CFDictionary, nil) + // [Nucleic vendored patch] `exists` above no longer prompts, so it can under-report an + // item this process isn't trusted to read; an explicit `login` re-save then hits + // `errSecDuplicateItem`. Deleting (no read authorization required) and retrying once keeps + // re-login working without ever raising a Keychain panel. + if status == errSecDuplicateItem { + try delete(securityDomain: securityDomain, accessGroup: accessGroup, hostname: hostname) + status = SecItemAdd(query as CFDictionary, nil) + } guard status == errSecSuccess else { throw Self.Error.unhandledError(status: status) } } @@ -114,7 +123,7 @@ public struct KeychainQuery { query[kSecAttrAccessGroup as String] = accessGroup } var item: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &item) + let status = Self.withoutInteractiveUI { SecItemCopyMatching(query as CFDictionary, &item) } let exists = try isQuerySuccessful(status) if !exists { return nil @@ -164,7 +173,7 @@ public struct KeychainQuery { query[kSecAttrAccessGroup as String] = accessGroup } var item: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &item) + let status = Self.withoutInteractiveUI { SecItemCopyMatching(query as CFDictionary, &item) } let exists = try isQuerySuccessful(status) if !exists { return [] @@ -217,12 +226,16 @@ public struct KeychainQuery { query[kSecAttrAccessGroup as String] = accessGroup } - let status = SecItemCopyMatching(query as CFDictionary, nil) + let status = Self.withoutInteractiveUI { SecItemCopyMatching(query as CFDictionary, nil) } return try isQuerySuccessful(status) } private func isQuerySuccessful(_ status: Int32) throws -> Bool { - guard status != errSecItemNotFound else { + // [Nucleic vendored patch] With interactive UI suppressed (see `withoutInteractiveUI`), a + // matching item the caller isn't trusted to read returns `errSecInteractionNotAllowed` + // instead of prompting. Treat it like "not found" so registry lookups degrade to anonymous + // / env-var auth rather than surfacing an error — and never raise a Keychain panel. + guard status != errSecItemNotFound, status != errSecInteractionNotAllowed else { return false } guard status == errSecSuccess else { @@ -230,6 +243,28 @@ public struct KeychainQuery { } return true } + + /// [Nucleic vendored patch] Run `body` with the legacy login Keychain's interactive + /// authorization panel disabled, restoring the prior setting afterward. + /// + /// Nucleic requires that no automatic registry-credential lookup (image pull / push / list, via + /// `KeychainHelper`) can ever raise the macOS " wants to use your confidential information + /// stored in 'ghcr.io' in your keychain" panel. That panel appears whenever a process that isn't + /// on an internet-password item's ACL reads it — e.g. a `cctl` binary re-signed ad-hoc by a fresh + /// `make vminit-image` reading a token an earlier build stored. The data-protection + /// `kSecUseAuthenticationUI*` flags do NOT govern that legacy ACL/partition-list dialog; the only + /// switch that does is `SecKeychainSetUserInteractionAllowed(false)`, which makes a read that + /// would otherwise prompt fail with `errSecInteractionNotAllowed`. An already-trusted item still + /// reads silently; anything else fails silently and the caller falls back to anonymous / env-var + /// auth. `SecKeychain*` is deprecated but remains the only API covering this panel. Mirrors + /// `KeychainOwnedAccess.withoutLegacyKeychainUI` in NucleicCore. + private static func withoutInteractiveUI(_ body: () -> T) -> T { + var previous = DarwinBoolean(true) + SecKeychainGetUserInteractionAllowed(&previous) + SecKeychainSetUserInteractionAllowed(false) + defer { SecKeychainSetUserInteractionAllowed(previous.boolValue) } + return body() + } } extension KeychainQuery {