From 2e4905f112c3fce867c6d4a75e6bcedf2cbdebde Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 10 Nov 2026 17:28:30 -0500 Subject: [PATCH] test: record partial native bridge import control --- .../native-retention/ImportSeed.cs | 95 +++++++ .../native-retention/NATIVE_IMPORT_ATTEMPT.md | 64 +++++ .../native-retention/NATIVE_IMPORT_DESIGN.md | 179 ++++++++++++ .../native-retention/NativeImportControl.c | 264 ++++++++++++++++++ .../fixture-before-harness-cleanup.json | 3 + .../fixture-cleanup.json | 29 ++ .../native-build.log | 37 +++ .../provenance.json | 59 ++++ .../samples.jsonl | 4 + .../seed-build.log | 9 + .../seed.jsonl | 1 + .../verification.json | 45 +++ .../native-retention/native_import.py | 220 +++++++++++++++ 13 files changed, 1009 insertions(+) create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/ImportSeed.cs create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/NATIVE_IMPORT_ATTEMPT.md create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/NATIVE_IMPORT_DESIGN.md create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/NativeImportControl.c create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/fixture-before-harness-cleanup.json create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/fixture-cleanup.json create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/native-build.log create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/provenance.json create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/samples.jsonl create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/seed-build.log create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/seed.jsonl create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/verification.json create mode 100644 scripts/validation/opcua-lifecycle-memory/native-retention/native_import.py diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/ImportSeed.cs b/scripts/validation/opcua-lifecycle-memory/native-retention/ImportSeed.cs new file mode 100644 index 00000000..e48cb54e --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/ImportSeed.cs @@ -0,0 +1,95 @@ +using System.Diagnostics; +using System.Collections; +using System.Reflection; +using System.Runtime; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.Json; +using Opc.Ua; +using Opc.Ua.Configuration; +using Opc.Ua.Security.Certificates; + +[System.Runtime.Versioning.SupportedOSPlatform("macos")] +internal static class ImportSeed +{ + const string Uri = "urn:localhost:NativeRetention.Control"; + static string Hash(Certificate cert) => Convert.ToHexString(SHA256.HashData(cert.RawData)).ToLowerInvariant(); + static void Require(bool yes, string message) { if (!yes) throw new InvalidOperationException(message); } + static ApplicationConfiguration Configuration(string pki, ITelemetryContext telemetry) => new(telemetry) + { + ApplicationName = "NativeRetention.Control", ApplicationUri = Uri, ApplicationType = ApplicationType.Client, + SecurityConfiguration = new SecurityConfiguration + { + ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, + StorePath = Path.Combine(pki, "own"), SubjectName = "CN=NativeRetention.Control" }, + TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "trusted") }, + TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "issuers") }, + RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pki, "rejected") }, + AutoAcceptUntrustedCertificates = false, + }, + ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 15000 }, + }; + static async Task Open(string pki, ITelemetryContext telemetry, CancellationToken ct) + { + var app = new ApplicationInstance(telemetry) { ApplicationType = ApplicationType.Client, + ApplicationName = "NativeRetention.Control", ApplicationConfiguration = Configuration(pki, telemetry) }; + try + { + await app.ApplicationConfiguration!.ValidateAsync(ApplicationType.Client, ct); + Require(await app.CheckApplicationInstanceCertificatesAsync(true, null, ct), "Normal application certificate check failed"); + return app; + } + catch { await app.DisposeAsync(); throw; } + } + static bool ManagerDisposed(ICertificateManager manager) + { + Require(manager.GetType().FullName == "Opc.Ua.CertificateManager" && manager.GetType().Assembly.GetName().Version?.ToString() == "2.0.1.0", "Exact SDK diagnostic layout changed"); + var field = manager.GetType().GetField("m_disposed", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); + Require(field?.FieldType == typeof(bool), "Exact SDK disposal bool missing"); + return (bool)field!.GetValue(manager)!; + } + static void PrivateFile(string path, byte[] bytes) + { + using var stream = new FileStream(path, new FileStreamOptions { Mode = FileMode.CreateNew, + Access = FileAccess.Write, UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite }); + stream.Write(bytes); + } + public static async Task Main(string[] args) + { + using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + ApplicationInstance? app = null; + try + { + Require(OperatingSystem.IsMacOS() && Environment.Version.ToString() == "10.0.14", "Expected matched macOS/.NET10.0.14 seed"); + var root = Path.GetFullPath(args.Single()); + Require(Directory.Exists(root) && (File.GetUnixFileMode(root) & (UnixFileMode)63) == 0, "Expected private fresh seed directory"); + app = await Open(Path.Combine(root, "pki"), DefaultTelemetry.Create(_ => { }), deadline.Token); + var manager = app.ApplicationConfiguration!.CertificateManager; + string leaf; + using (var entry = manager.AcquireApplicationCertificateBySecurityPolicy(SecurityPolicies.Basic256Sha256)) + { + Require(entry is not null && entry.Certificate.HasPrivateKey, "Expected SDK-generated private certificate"); + var certificate = entry!.Certificate; leaf = Hash(certificate); + using var rsa = certificate.GetRSAPrivateKey(); using var publicKey = certificate.GetRSAPublicKey(); + Require(rsa is not null && publicKey is not null, "Expected normal SDK RSA certificate"); + var challenge = "native-import-seed-diagnostic"u8.ToArray(); + var signature = rsa!.SignData(challenge, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + Require(publicKey!.VerifyData(challenge, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1), "Seed key mismatch"); + var privateKey = rsa.ExportRSAPrivateKey(); + try { PrivateFile(Path.Combine(root, "leaf.der"), certificate.RawData); PrivateFile(Path.Combine(root, "private.pkcs1"), privateKey); } + finally { CryptographicOperations.ZeroMemory(privateKey); } + } + await app.DisposeAsync().AsTask().WaitAsync(deadline.Token); app = null; + Require(ManagerDisposed(manager) && Certificate.InstancesLeaked == 0, "Seed SDK ownership remains"); + Console.WriteLine(JsonSerializer.Serialize(new { kind = "seed", leafSha256 = leaf, runtime = Environment.Version.ToString(), + sdkVersion = typeof(Certificate).Assembly.GetName().Version!.ToString(), managerDisposed = true, + certificateCoresOutstanding = Certificate.InstancesLeaked, keyFormat = "PKCS1-private-RSA", privateFileMode = "0600", + cryptographyRuntime = new { informationalVersion = typeof(X509Certificate2).Assembly.GetCustomAttribute()?.InformationalVersion, + sha256 = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(typeof(X509Certificate2).Assembly.Location))).ToLowerInvariant() } })); + return 0; + } + catch(Exception error) { Console.WriteLine(JsonSerializer.Serialize(new {kind="failed", errorType=error.GetType().FullName, message=error.Message}));return 1; } + finally { if(app is not null) await app.DisposeAsync(); } + } +} diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/NATIVE_IMPORT_ATTEMPT.md b/scripts/validation/opcua-lifecycle-memory/native-retention/NATIVE_IMPORT_ATTEMPT.md new file mode 100644 index 00000000..ec696481 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/NATIVE_IMPORT_ATTEMPT.md @@ -0,0 +1,64 @@ +# Installed native bridge diagnostic — attempt A stopped at its ownership guard + +The approved fixed-200 diagnostic did **not complete**. One measured native +child started; it stopped during its first operation because the harness required +an identity keychain association that the installed .NET wrapper allows to be +absent. This is a diagnostic model error. There is no repeated native retention +result, no production mitigation or resource-budget decision, and no change to +#799's status. + +The separate SDK 2.0.1 seed completed with the expected RSA leaf/key pair, +observed application manager disposal, and private fixture files. The measured C +child loaded the pinned native bridge, checked its export image and rejected CLR +and SDK images. The C build succeeded with six unsuppressed deprecation warnings +for the exercised public keychain APIs; the seed build reported zero warnings +and errors. This was macOS **26.5.2** build **25F84** arm64; installed managed +cryptography was .NET **10.0.14**. [The inspected design](NATIVE_IMPORT_DESIGN.md) +records the exact native/managed binary paths and ABI. + +| Observation | Recorded result | +|---|---| +| Before snapshot | AppleDatabase **1 / 768 shallow bytes**; Mach RSS **7,815,168 bytes**; 3 numeric FDs | +| DER import, modern RSA creation, private keychain create/lock, external representation and legacy ephemeral import | All reached bridge calls succeeded | +| Move to private keychain | Bridge return **1**, OSStatus **0**; post-move certificate keychain copy returned a verified private association | +| Post-move identity keychain copy | Bridge-normalized OSStatus **0**, **no returned keychain reference** | +| Stop | Before identity certificate copy; its uncalled status is the `INT32_MIN` sentinel, not a native OSStatus | +| Error-path cleanup | **9** owned CF outputs / **9** releases; logical owners **0**; verified private keychain native delete OSStatus **0**; no fallback cleanup | +| Process and fixture | Native exit **1**, reported elapsed **1.172310 s**; both builds, seed and native process confirmed reaped; no remaining owned keychain files before collector cleanup; private task files overwritten and unlinked after reaping; root removed | + +Only four JSONL rows exist: before fingerprint, before checkpoint, partial +operation 1, failure. No first/repeated/idle snapshot or after fingerprint was +collected. The before-only default/search query succeeded; equality across the +attempt was **not measured**. The measured child made no setter or restore call. +No estimate of native heap growth can be drawn from its single before sample. + +## Why the guard did not match the installed wrapper + +`SafeTemporaryKeychainHandle.TrackItem` obtains a copied keychain and returns +without adding a logical owner when the copied handle is invalid. The installed +native bridge `AppleCryptoNative_SecKeychainItemCopyKeychain` at `0x8a04` normalizes +`errSecNoSuchKeychain` (`-25294`) and `errSecInvalidItemRef` (`-25304`) to success; +the receipt records the bridge result, not the underlying raw Apple status. The +installed move wrapper validates the move result and identity handle; it does +not require the identity's `TrackItem` to add an owner. Attempt A incorrectly +made that association mandatory before identity certificate copy. + +Any separately reviewed follow-up should model ownership from actual validated +associations: keep all nine expected native copy calls and their statuses; add a +logical owner only when the returned keychain matches the generated private +reference/path; record no association when it is null. Reject a disappearing +association during Untrack of a previously tracked owner. Record the actual +zero-owner deletion stage and which item/copy references remain alive, rather +than requiring deletion at identity Untrack. A single partial attempt cannot +predict the copied certificate's association or a fixed number of non-null +copies. This correction concerns the diagnostic only, not production SDK +ownership or certificate selection. + +Selected receipts and verified raw/projection hashes are in +[`evidence/2026-11-10-native-import-attempt-a`](evidence/2026-11-10-native-import-attempt-a/verification.json). +The unchanged raw directory is `/tmp/opcua2-native-import-20261110a`. It contains +raw native snapshots but no retained private fixture. The native executable +SHA-256 is `e5c5caf2a8f882f82b738396156b378fd157ebd1a825d462449cd2a0497830fd`; +the seed DLL SHA-256 is +`1fa7de542aee11acab18dcc8d4096b21fcbc16e4edc1fca2243159dd1176adc0`. +This was a single serialized execution. No retry or counterfactual arm ran. diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/NATIVE_IMPORT_DESIGN.md b/scripts/validation/opcua-lifecycle-memory/native-retention/NATIVE_IMPORT_DESIGN.md new file mode 100644 index 00000000..6e23e33f --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/NATIVE_IMPORT_DESIGN.md @@ -0,0 +1,179 @@ +# Native bridge import control — inspected design + +Status: Astra approved the refined ABI/ownership design for implementation. The +C child, separate seed and collector are implemented and submitted for actual-code +review. The original assumptions below are retained to explain the exact attempt. +[Attempt A](NATIVE_IMPORT_ATTEMPT.md) stopped at its first partial operation: the +installed wrapper permits null item associations, contrary to the original fixed +identity-association expectation. It produced no repeated retention result. The coordinator confirmed a +single staged native-bridge control; a handwritten Apple import reproduction is +deferred until the result shows whether it is useful. This is not a production +routing/crypto change, a mitigation or an acceptance decision. + +## Installed binaries and retained inspection + +Installed managed crypto: +`/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.NETCore.App/10.0.14/System.Security.Cryptography.dll`. +Informational version `10.0.14+0427b9abbead3fa64c6655eb4290c537397131fa`; +SHA-256 `f4957d7218a004a27bf8964e45147df6a88092c6e7344582082840d62c6c579a`. + +Installed native bridge: +`/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.NETCore.App/10.0.14/libSystem.Security.Cryptography.Native.Apple.dylib`. +SHA-256 `0333994e217a3d111d36d7f31460eea69a286f9fc819e2d7c746df13d2aec46c`. +Only this exact, unmodified dylib would be loaded by absolute path; required +exports and actual loaded-image identity must match. No SDK/CLR may be loaded in +the measured C child. Its native bridge dependency is explicitly part of this +experiment, unlike the preceding plain-Apple negative control. + +Retained actual-binary inspection files (read-only ILSpy 10.1.0.8386 / otool): + +| File | Relevant locations | +|---|---| +| `/tmp/opcua2-native-runtime-loader-decompiled.cs` | 836–870 load certs/keys/match, initialize import state and FromCertAndKey; 1555–1581 exportable branch; 1611–1640 keychain initialization and GetPrivateKey | +| `/tmp/opcua2-native-runtime-pkcs12key-decompiled.cs` | AsymmetricAlgorithmPkcs12PrivateKey imports PKCS8 into its RSA algorithm | +| `/tmp/opcua2-native-runtime-pkcs12key-base-decompiled.cs` | Dispose calls the algorithm's Dispose | +| `/tmp/opcua2-native-runtime-rsa-decompiled.cs` | 95–108 ImportParameters/CreateDataKey/CopyPublicKey; 431–458 PKCS1 serialization to CreateDataKey; export uses native external representation | +| `/tmp/opcua2-native-runtime-applepal-decompiled.cs` | 339–349 identity constructor/certificate copy and disposal; 555–564 MoveToKeychain | +| `/tmp/opcua2-native-runtime-appleinterop-decompiled.cs` | PAL RSA enum 544–549; CreateDataKey/external representation/public copy 1982–2127; keychain create/never-lock 3054/3209/3481; legacy key import 3563/3596; cert import 3697/3944; move 3878/4036; identity cert copy 2629/4087 | +| `/tmp/opcua2-native-runtime-certhandle-decompiled.cs` and `identityhandle-decompiled.cs` | Both actual types inherit SafeKeychainItemHandle with no release override | +| `/tmp/opcua2-native-runtime-keychainitem-decompiled.cs` | SafeKeychainItemHandle release: UntrackItem copy/release, then CFRelease; certificate and identity inherit this class | +| `/tmp/opcua2-native-runtime-keypair-decompiled.cs` | Actual SecKeyPair Dispose: modern private-key owner before modern public-key owner | +| `/tmp/opcua2-native-runtime-keyhandle-decompiled.cs` | SafeSecKeyRefHandle release: straight CFRelease, then optional managed parent release; no tracking copy | +| `/tmp/opcua2-native-runtime-keychain-decompiled.cs` | TrackItem/UntrackItem each copy/release a keychain; matching dictionary only adds/releases managed SafeHandle ownership | +| `/tmp/opcua2-native-runtime-x509contenttype-decompiled.cs` | Actual public Cert enum value 1; Pfx/Pkcs12 value 3 | +| `/tmp/opcua2-native-runtime-apple-disassembly.txt` | `_AppleCryptoNative_SecKeyImportEphemeral`, `_AddKeyToKeychain`, `_AppleCryptoNative_X509MoveToKeychain`, all required exports | + +The installed native call sites, not merely the public runtime tag, show: + +- `SecKeyImportEphemeral` at `0x92e8` calls `SecItemImport` at `0x9390`, + followed by CF ownership cleanup. +- `AddKeyToKeychain` calls `SecItemExport` at `0xa0a8` and `SecItemImport` at + `0xa11c`, then retains an optional returned key and releases local containers. +- `X509MoveToKeychain` at `0xa180` calls AddKeyToKeychain at `0xa240`, + `SecCertificateAddToKeychain` at `0xa278`, and a private-search-list identity + `SecItemCopyMatching` at `0xa388`, with CF cleanup and one returned identity. + +Public `v10.0.14` native headers/source are explanatory cross-checks, not asserted +as the exact installed source commit. In particular this **Exportable** path is +managed PKCS12 decode / RSA materialization → legacy key import → move-to-keychain. +A guessed `SecPKCS12Import` control would exercise a different path. + +## ABI and returned ownership map + +All bridge integer return values below are signed 32-bit. The installed generated +managed keychain-create declaration takes signed `int utf8PassphraseLength`; +the explanatory public native header takes uint32. The control uses the same +fixed positive bounded value 129, whose ABI representation is identical. Other +byte lengths are signed 32-bit. Native +pointers are opaque CF references and must never be printed. Every out location +starts null/zero; each non-null owned output must be released once on success or +failure. Borrowed CFData byte pointers are valid only while their owned CFData is +held and are never separately freed. CFError is an owned output, recorded only +by code and released once, without description/key logging. + +| Exact export | Argument/return contract | +|---|---| +| `AppleCryptoNative_X509ImportCertificate` | `(uint8_t*, int32 len, int32 contentType=1, CFStringRef NULL, SecKeychainRef NULL, int32 exportable=1, SecCertificateRef*, SecIdentityRef*, int32 OSStatus*)`; bridge return 1 and OSStatus 0 required; DER must yield an owned cert and no identity | +| `AppleCryptoNative_SecKeyCreateWithData` | `(uint8_t*, int32 len, uint32 PAL_RSA=2, int32 isPublic=0, SecKeyRef*, CFErrorRef*)`; bridge return 1, owned modern RSA key | +| `AppleCryptoNative_SecKeyCopyPublicKey` | `(SecKeyRef) -> SecKeyRef`; owned public-key copy, non-null required | +| `AppleCryptoNative_SecKeyCopyExternalRepresentation` | `(SecKeyRef, CFDataRef*, CFErrorRef*)`; bridge return 1, owned PKCS1 private-key data, no key bytes logged | +| `AppleCryptoNative_SecKeychainCreate` | `(const char*, int32 passwordLength=129 in installed managed ABI, const uint8_t*, SecKeychainRef*)`; OSStatus 0, owned fresh private keychain; same native false prompt flag | +| `AppleCryptoNative_SetKeychainNeverLock` | `(SecKeychainRef) -> OSStatus`; 0 required; affects only the generated private keychain, matching installed import state | +| `AppleCryptoNative_SecKeyImportEphemeral` | `(uint8_t*, int32 len, int32 isPrivate=1, SecKeyRef*, int32 OSStatus*)`; bridge return 1 / OSStatus 0, owned legacy private key | +| `AppleCryptoNative_X509MoveToKeychain` | `(SecCertificateRef, SecKeychainRef, SecKeyRef, SecIdentityRef*, int32 OSStatus*)`; bridge return 1 / OSStatus 0 and owned non-null identity required; native helper scopes its identity query to target keychain and supplied cert | +| `AppleCryptoNative_X509CopyCertFromIdentity` | `(SecIdentityRef, SecCertificateRef*) -> OSStatus`; 0 required, owned certificate copy | +| `AppleCryptoNative_SecKeychainItemCopyKeychain` | `(CFTypeRef item, SecKeychainRef*) -> int32`; bridge-normalized OSStatus 0 required; non-null returned keychain is owned and CFReleased once. The installed bridge normalizes native -25304/-25294 to 0; null output is valid for an unassociated item. Preserve normalized status and output-present flag, not raw underlying status. | +| `AppleCryptoNative_SecKeychainDelete` | `(SecKeychainRef) -> OSStatus`; recorded separately from subsequent CFRelease; only a returned keychain verified against this generated private path may be deleted | + +## Matching native interactions from managed wrapper tracking + +The child will include the bridge-normalized `SecKeychainItemCopyKeychain` calls +made by installed wrappers, not merely the primary import/move exports. Every +non-null copied keychain is released once. The map is: + +1. After DER import, TrackItem(cert): copy/release its associated keychain. + The null identity output is skipped, as the managed TrackItem does. +2. Around MoveToKeychain: acquire the outer cert-keychain copy and keep it alive + across the entire move wrapper; separately copy/release cert's old keychain + for UntrackItem; call Move; copy/release cert's new keychain and identity's + keychain for the two TrackItem calls; then release the outer copy. +3. After identity certificate copy: copy/release copied cert's keychain for + TrackItem. +4. Releasing initial cert, copied cert and identity: copy/release each item's + keychain first (UntrackItem), then CFRelease the item. This preserves the + installed PAL order: initial cert after the replacement PAL is constructed, + then replacement cert before identity at disposal. +5. Modern public/private and legacy private `SafeSecKeyRefHandle` release maps + directly to CFRelease, with no keychain tracking native call. Its optional + managed parent lifetime is not used by the inspected GetPrivateKey path. + +No managed dictionary is recreated. A small logical owner counter preserves the +known deterministic ownership/deletion order. It starts at one import-state +owner. Successful TrackItem of an item associated with the generated keychain +adds one; UntrackItem subtracts one. These logical increments are not CF retains. +After the replacement PAL is constructed and the original cert is disposed, +ImportState.Dispose relinquishes its initial logical ownership. The counter then +contains only copied-cert and identity owners. Their Untrack calls subtract one +before releasing each copied-keychain reference. The final identity Untrack +reaches zero: it calls SecKeychainDelete and releases the initial native keychain +owner **while the copied keychain ref and identity are still alive**, then +releases the copied keychain ref, then CFReleases the identity. This matches the +installed SafeHandle delayed-release ordering; deliberately postponing deletion +to after all item CFReleases would be a different reduced sequence. The legacy +private key is released explicitly after this sequence (the managed helper lacks +an explicit local using scope); exact managed finalizer timing is not emulated. +The installed bridge at `0x8a04` checks item type, calls the public Apple copy API, +and normalizes -25304/-25294 to success. The control calls that pinned export +rather than interpreting a raw public return differently. Any other returned +status fails the operation but is retained in the receipt. + +## Proposed one-child sequence and boundaries + +Prepare one fresh normal SDK-generated RSA fixture in a **separate seed process**, +not in the measured child. Export its public DER certificate and RSA PKCS1 private +key through public APIs into a private directory with files mode 0600. Preserve +only public identity/source/binary hashes, never private bytes/passwords. The seed +and measured child must be reaped before their key material is overwritten and +unlinked; cleanup is confined to their fresh owned roots. No existing PKI is used. + +One plain C child would then repeat exactly 200 times, without count/retry +extension: + +1. Import public DER through the pinned bridge. Materialize the modern RSA key + from PKCS1 through CreateWithData and obtain the owned public-key copy, matching + the inspected RSA loader prelude without reproducing managed ASN1/PFX matching. +2. Create one generated unrelated private keychain path and set its never-lock + behavior through the pinned bridge. Use generated password bytes and no prompt; + verify its returned path is exactly the generated private filename/backing file. +3. Export modern private-key representation, import the legacy ephemeral key, + then release exported CFData and modern RSA private/public owners in that order, as the + managed Pkcs12Key is disposed before MoveToKeychain. +4. Move certificate/key to that private keychain; require a typed identity. + Copy its certificate as the AppleCertificatePal identity constructor does; + release the original certificate. At first/last only, check returned public + certificate identity against the input; no secret/private-key logging. +5. Relinquish the initial import-state logical owner after original cert + disposal. Release copied cert (Untrack/copy-release then cert CFRelease), then + identity. Its final Untrack deletes/releases the verified initial keychain + owner before releasing that Untrack copy and identity, as mapped above. + Release the explicitly held legacy key. Preserve every bridge + return, OSStatus, CFError code, owned-output/release count and failure path. + +The modern/legacy distinction is explicit. The installed managed helper's local +legacy SafeSecKeyRef has no explicit using scope in FromCertAndKey; the native +control uses balanced explicit CF owners and does **not** claim that this is a +proven GC bug, or exactly emulate managed finalizer timing. + +Use the already accepted before/first/200/five-second-idle checkpoints, raw native +heap inventory, Mach RSS, total/owned-keychain FDs and backing-file counts, and +read-only successful default/search-list fingerprint equality. No list setter or +restore call. Keep fixed parent/process/probe/reap bounds, complete JSONL parsing, +private generated paths, fail-closed ABI/hash/image checks and child reaping. + +A positive result would show that managed caller participation is not necessary +for this observed import-path retention, while retaining the exact installed +native bridge code. It would **not** distinguish a bridge-internal native owner +from Apple caching; a later independently owned public-Apple sequence could be +justified then. A flat result would not by itself prove a managed bug: omitted +managed parsing/matching, registry bookkeeping or lifetime differences remain. No +production implementation or additional experiment is approved by this design. diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/NativeImportControl.c b/scripts/validation/opcua-lifecycle-memory/native-retention/NativeImportControl.c new file mode 100644 index 00000000..0f07b8f9 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/NativeImportControl.c @@ -0,0 +1,264 @@ +// Diagnostic only: pinned installed native bridge import; no measured managed caller or PFX parser. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define REPETITIONS 200 +static char root[PATH_MAX]; +static struct timespec started; +static unsigned created_refs, released_refs, query_releases; +static double elapsed(void) { + struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); + return (double)(now.tv_sec-started.tv_sec)+(double)(now.tv_nsec-started.tv_nsec)/1e9; +} +static void die(const char *message) { + fprintf(stdout,"{\"kind\":\"failed\",\"message\":\"%s\",\"createdRefs\":%u,\"releasedRefs\":%u,\"elapsedSeconds\":%.6f}\n",message,created_refs,released_refs,elapsed()); + fflush(stdout); exit(1); +} +static void hex(const unsigned char *bytes, size_t length, char *out) { + const char *alphabet="0123456789abcdef"; + for(size_t i=0;i>4];out[i*2+1]=alphabet[bytes[i]&15];} + out[length*2]=0; +} +static void wipe(void *memory, size_t length) { + volatile unsigned char *bytes=(volatile unsigned char *)memory; + while(length--)*bytes++=0; +} +static OSStatus keychain_path(SecKeychainRef keychain, char *path) { + if(!keychain||CFGetTypeID(keychain)!=SecKeychainGetTypeID())return errSecParam; + UInt32 length=PATH_MAX-1; + OSStatus status=SecKeychainGetPath(keychain,&length,path); + if(status==errSecSuccess && length=sizeof(next))die("Checkpoint path overflow"); + while(access(next,F_OK)!=0){if(elapsed()>180)die("Checkpoint deadline exceeded");struct timespec pause={0,25000000};nanosleep(&pause,NULL);} +} +static int absent(const char *path){struct stat state;if(lstat(path,&state)==0)return 0;return errno==ENOENT;} + +#define BRIDGE_SHA "0333994e217a3d111d36d7f31460eea69a286f9fc819e2d7c746df13d2aec46c" +static int32_t (*import_cert)(const uint8_t*,int32_t,int32_t,CFStringRef,SecKeychainRef,int32_t,SecCertificateRef*,SecIdentityRef*,int32_t*); +static int32_t (*create_key)(const uint8_t*,int32_t,uint32_t,int32_t,SecKeyRef*,CFErrorRef*); +static SecKeyRef (*copy_public)(SecKeyRef); +static int32_t (*external_key)(SecKeyRef,CFDataRef*,CFErrorRef*); +static int32_t (*create_chain)(const char*,int32_t,const uint8_t*,SecKeychainRef*); +static int32_t (*never_lock)(SecKeychainRef); +static int32_t (*import_key)(const uint8_t*,int32_t,int32_t,SecKeyRef*,int32_t*); +static int32_t (*move_key)(SecCertificateRef,SecKeychainRef,SecKeyRef,SecIdentityRef*,int32_t*); +static int32_t (*copy_cert)(SecIdentityRef,SecCertificateRef*); +static int32_t (*copy_chain)(CFTypeRef,SecKeychainRef*); +static int32_t (*delete_chain)(SecKeychainRef); +static unsigned all_created,all_released; +static void owned(CFTypeRef value){if(value)all_created++;} +static void drop(CFTypeRef *value){if(*value){CFRelease(*value);*value=NULL;all_released++;}} +#define DROP(value) drop((CFTypeRef*)&(value)) +static void image_guard(void){ + unsigned bridge_images=0; + for(uint32_t i=0;i<_dyld_image_count();i++){ + const char *name=_dyld_get_image_name(i); + if(strstr(name,"libcoreclr")||strstr(name,"libhostfxr")||strstr(name,"Opc.Ua"))die("Measured child loaded a managed runtime or SDK"); + if(strstr(name,"libSystem.Security.Cryptography.Native.Apple.dylib"))bridge_images++; + } + if(bridge_images!=1)die("Expected exactly one pinned native bridge image"); +} +static void load_bridge(const char *path){ + FILE *file=fopen(path,"rb");if(!file)die("Pinned bridge could not be read"); + CC_SHA256_CTX hash;CC_SHA256_Init(&hash);unsigned char bytes[8192],digest[32];size_t size; + while((size=fread(bytes,1,sizeof(bytes),file)))CC_SHA256_Update(&hash,bytes,(CC_LONG)size); + int bad=ferror(file);fclose(file);if(bad)die("Pinned bridge read failed");CC_SHA256_Final(digest,&hash); + char actual[65];hex(digest,sizeof(digest),actual);if(strcmp(actual,BRIDGE_SHA))die("Pinned bridge hash changed"); + void *library=dlopen(path,RTLD_NOW|RTLD_LOCAL);if(!library)die("Pinned bridge load failed"); +#define LOAD(variable,symbol) do { *(void**)(&variable)=dlsym(library,symbol); if(!variable)die("Pinned bridge export missing"); Dl_info info;char loaded[PATH_MAX],expected[PATH_MAX];if(!dladdr((void*)variable,&info)||!realpath(info.dli_fname,loaded)||!realpath(path,expected)||strcmp(loaded,expected))die("Bridge export image identity changed"); } while(0) + LOAD(import_cert,"AppleCryptoNative_X509ImportCertificate");LOAD(create_key,"AppleCryptoNative_SecKeyCreateWithData"); + LOAD(copy_public,"AppleCryptoNative_SecKeyCopyPublicKey");LOAD(external_key,"AppleCryptoNative_SecKeyCopyExternalRepresentation"); + LOAD(create_chain,"AppleCryptoNative_SecKeychainCreate");LOAD(never_lock,"AppleCryptoNative_SetKeychainNeverLock"); + LOAD(import_key,"AppleCryptoNative_SecKeyImportEphemeral");LOAD(move_key,"AppleCryptoNative_X509MoveToKeychain"); + LOAD(copy_cert,"AppleCryptoNative_X509CopyCertFromIdentity");LOAD(copy_chain,"AppleCryptoNative_SecKeychainItemCopyKeychain"); + LOAD(delete_chain,"AppleCryptoNative_SecKeychainDelete"); +#undef LOAD + image_guard(); +} +struct copy_result {const char *stage;int32_t status;int present,associated;}; +struct operation_state { + SecCertificateRef cert,copied;SecIdentityRef identity;SecKeyRef modern,public_key,legacy; + CFDataRef external;SecKeychainRef keychain,keychain_identity,outer;CFErrorRef error; + char path[PATH_MAX],backing[PATH_MAX]; + unsigned logical_owners;int initial_owner,cert_tracked,copied_tracked,identity_tracked; + int failed,path_verified,deleted,delete_during_identity_untrack,fallback_cleanup; + int32_t cert_return,cert_status,modern_return,create_status,lock_status,external_return,key_return,key_status,move_return,move_status,copy_cert_status,delete_status,path_status; + CFIndex modern_error,external_error;int modern_error_present,external_error_present;unsigned copy_count; + struct copy_result copies[16]; +}; +static int private_path(struct operation_state *s,SecKeychainRef keychain){ + if(!keychain||CFGetTypeID(keychain)!=SecKeychainGetTypeID())return 0; + char actual[PATH_MAX];memset(actual,0,sizeof(actual));int32_t status=keychain_path(keychain,actual); + return status==0 && (!strcmp(actual,s->path)||!strcmp(actual,s->backing)); +} +static void delete_owner(struct operation_state *s,const char *stage){ + if(!s->keychain)return; + if(s->deleted){s->failed=1;return;} + s->path_verified=private_path(s,s->keychain); + if(s->path_verified){s->delete_status=delete_chain(s->keychain);s->deleted=1;} + if(!s->path_verified||s->delete_status)s->failed=1; + s->delete_during_identity_untrack=!strcmp(stage,"release-identity")&&s->identity!=NULL&&s->copied==NULL; + DROP(s->keychain);released_refs++; +} +static void relinquish(struct operation_state *s,const char *stage){ + if(!s->logical_owners){s->failed=1;return;} + if(--s->logical_owners==0)delete_owner(s,stage); +} +static SecKeychainRef copied_chain(struct operation_state *s,CFTypeRef item,const char *stage,int *associated){ + *associated=0;if(!item)return NULL; + CFTypeID type=CFGetTypeID(item);if(type!=SecCertificateGetTypeID()&&type!=SecIdentityGetTypeID()){s->failed=1;return NULL;} + SecKeychainRef result=NULL;int32_t status=copy_chain(item,&result);owned(result); + int type_ok=!result||CFGetTypeID(result)==SecKeychainGetTypeID(); + int belongs=result&&type_ok&&s->keychain_identity==result&&private_path(s,result); + if(s->copy_count<16)s->copies[s->copy_count++]=(struct copy_result){stage,status,result!=NULL,belongs};else s->failed=1; + if(status || !type_ok || (result&&!belongs))s->failed=1; + *associated=status==0&&belongs;return result; +} +static void track(struct operation_state *s,CFTypeRef item,const char *stage,int *tracked){ + int associated=0;SecKeychainRef copy=copied_chain(s,item,stage,&associated); + if(associated){if(*tracked||!s->keychain)s->failed=1;else{s->logical_owners++;*tracked=1;}} + DROP(copy); +} +static void untrack(struct operation_state *s,CFTypeRef item,const char *stage,int *tracked){ + if(!item)return; + int associated=0;SecKeychainRef copy=copied_chain(s,item,stage,&associated); + if(*tracked){if(associated){*tracked=0;relinquish(s,stage);}else s->failed=1;} + else if(associated)s->failed=1; + // Final logical-owner release above precedes this copy's CFRelease. + DROP(copy); +} +static int leaf_equal(SecCertificateRef cert,const unsigned char *der,size_t length){ + CFDataRef data=SecCertificateCopyData(cert);owned(data); + int equal=data&&CFGetTypeID(data)==CFDataGetTypeID()&&CFDataGetLength(data)==(CFIndex)length&&!memcmp(CFDataGetBytePtr(data),der,length); + DROP(data);return equal; +} +static void import_operation(unsigned iteration,const unsigned char *der,size_t der_length,const unsigned char *key,size_t key_length){ + struct operation_state s;memset(&s,0,sizeof(s)); + s.cert_return=s.cert_status=s.modern_return=s.create_status=s.lock_status=s.external_return=s.key_return=s.key_status=s.move_return=s.move_status=s.copy_cert_status=s.delete_status=s.path_status=INT32_MIN; + unsigned before_created=all_created,before_released=all_released; + unsigned char random[144],password[129];char suffix[33]; + if(SecRandomCopyBytes(kSecRandomDefault,sizeof(random),random))die("Secure random failed"); + hex(random,16,suffix);const unsigned char choices[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@"; + for(unsigned i=0;i<128;i++)password[i]=choices[random[16+i]&63];password[128]=0;wipe(random,sizeof(random)); + int n=snprintf(s.path,sizeof(s.path),"%s/tmp/native-import-%s.keychain",root,suffix); + if(n<0||(size_t)n>=sizeof(s.path))die("Private fixture path overflow"); + n=snprintf(s.backing,sizeof(s.backing),"%s-db",s.path); + if(n<0||(size_t)n>=sizeof(s.backing)||!absent(s.path)||!absent(s.backing))die("Private fixture path already exists or overflows"); + s.cert_return=import_cert(der,(int32_t)der_length,1,NULL,NULL,1,&s.cert,&s.identity,&s.cert_status);owned(s.cert);owned(s.identity); + track(&s,s.cert,"import-cert-track",&s.cert_tracked); + if(s.identity)track(&s,s.identity,"unexpected-import-identity-track",&s.identity_tracked); + if(s.cert_return!=1||s.cert_status||!s.cert||CFGetTypeID(s.cert)!=SecCertificateGetTypeID()||s.identity||s.failed){s.failed=1;goto cleanup;} + s.modern_return=create_key(key,(int32_t)key_length,2,0,&s.modern,&s.error);owned(s.modern);owned(s.error); + s.modern_error_present=s.error!=NULL;if(s.error){if(CFGetTypeID(s.error)==CFErrorGetTypeID())s.modern_error=CFErrorGetCode(s.error);else s.failed=1;}DROP(s.error); + if(s.modern_return!=1||!s.modern||CFGetTypeID(s.modern)!=SecKeyGetTypeID()||s.modern_error_present||s.failed){s.failed=1;goto cleanup;} + s.public_key=copy_public(s.modern);owned(s.public_key);if(!s.public_key||CFGetTypeID(s.public_key)!=SecKeyGetTypeID()){s.failed=1;goto cleanup;} + s.create_status=create_chain(s.path,129,password,&s.keychain);owned(s.keychain);wipe(password,sizeof(password)); + if(s.keychain){created_refs++;s.keychain_identity=s.keychain;s.initial_owner=1;s.logical_owners=1;char actual[PATH_MAX];s.path_status=keychain_path(s.keychain,actual);s.path_verified=private_path(&s,s.keychain);} + if(s.create_status||!s.keychain||s.path_status||!s.path_verified){s.failed=1;goto cleanup;} + s.lock_status=never_lock(s.keychain);if(s.lock_status){s.failed=1;goto cleanup;} + s.external_return=external_key(s.modern,&s.external,&s.error);owned(s.external);owned(s.error); + s.external_error_present=s.error!=NULL;if(s.error){if(CFGetTypeID(s.error)==CFErrorGetTypeID())s.external_error=CFErrorGetCode(s.error);else s.failed=1;}DROP(s.error); + if(s.external_return!=1||!s.external||CFGetTypeID(s.external)!=CFDataGetTypeID()||s.external_error_present||s.failed||CFDataGetLength(s.external)<=0||CFDataGetLength(s.external)>65536){s.failed=1;goto cleanup;} + s.key_return=import_key(CFDataGetBytePtr(s.external),(int32_t)CFDataGetLength(s.external),1,&s.legacy,&s.key_status);owned(s.legacy); + DROP(s.external);DROP(s.modern);DROP(s.public_key); // Installed SecKeyPair releases private before public. + if(s.key_return!=1||s.key_status||!s.legacy||CFGetTypeID(s.legacy)!=SecKeyGetTypeID()){s.failed=1;goto cleanup;} + int associated=0;s.outer=copied_chain(&s,s.cert,"move-outer-copy",&associated); + untrack(&s,s.cert,"move-cert-untrack",&s.cert_tracked); + if(s.failed)goto cleanup; + s.move_return=move_key(s.cert,s.keychain,s.legacy,&s.identity,&s.move_status);owned(s.identity); + track(&s,s.cert,"move-cert-track",&s.cert_tracked);track(&s,s.identity,"move-identity-track",&s.identity_tracked);DROP(s.outer); + if(s.move_return!=1||s.move_status||!s.identity||CFGetTypeID(s.identity)!=SecIdentityGetTypeID()||!s.cert_tracked||!s.identity_tracked||s.failed){s.failed=1;goto cleanup;} + s.copy_cert_status=copy_cert(s.identity,&s.copied);owned(s.copied);track(&s,s.copied,"identity-cert-track",&s.copied_tracked); + if(s.copy_cert_status||!s.copied||CFGetTypeID(s.copied)!=SecCertificateGetTypeID()||!s.copied_tracked||s.failed){s.failed=1;goto cleanup;} + if((iteration==1||iteration==REPETITIONS)&&!leaf_equal(s.copied,der,der_length)){s.failed=1;goto cleanup;} +cleanup: + wipe(password,sizeof(password));DROP(s.error);DROP(s.external);DROP(s.modern);DROP(s.public_key);DROP(s.outer); + untrack(&s,s.cert,"release-original-cert",&s.cert_tracked);DROP(s.cert); + if(s.initial_owner){s.initial_owner=0;relinquish(&s,"release-import-state");} + untrack(&s,s.copied,"release-copied-cert",&s.copied_tracked);DROP(s.copied); + untrack(&s,s.identity,"release-identity",&s.identity_tracked);DROP(s.identity); + DROP(s.legacy); // Explicit lifetime; managed helper does not scope this local with using. + if(s.keychain){s.failed=1;s.fallback_cleanup=1;delete_owner(&s,"failure-fallback-after-items");} + unsigned made=all_created-before_created,released=all_released-before_released; + if(made!=released||s.logical_owners||s.cert_tracked||s.copied_tracked||s.identity_tracked)s.failed=1; + if(!s.failed&&(s.copy_count!=9||!s.delete_during_identity_untrack||!s.deleted))s.failed=1; + fprintf(stdout,"{\"kind\":\"operation\",\"iteration\":%u,\"certReturn\":%d,\"certStatus\":%d,\"modernReturn\":%d,\"modernErrorCode\":%ld,\"modernErrorPresent\":%s,\"createStatus\":%d,\"pathStatus\":%d,\"neverLockStatus\":%d,\"externalReturn\":%d,\"externalErrorCode\":%ld,\"externalErrorPresent\":%s,\"legacyReturn\":%d,\"legacyStatus\":%d,\"moveReturn\":%d,\"moveStatus\":%d,\"copyCertStatus\":%d,\"deleteStatus\":%d,\"privatePathVerified\":%s,\"deleteDuringFinalIdentityUntrack\":%s,\"fallbackCleanup\":%s,\"logicalOwnersRemaining\":%u,\"ownedOutputs\":%u,\"releasedOutputs\":%u,\"failed\":%s,\"copies\":[",iteration,s.cert_return,s.cert_status,s.modern_return,(long)s.modern_error,s.modern_error_present?"true":"false",s.create_status,s.path_status,s.lock_status,s.external_return,(long)s.external_error,s.external_error_present?"true":"false",s.key_return,s.key_status,s.move_return,s.move_status,s.copy_cert_status,s.delete_status,s.path_verified?"true":"false",s.delete_during_identity_untrack?"true":"false",s.fallback_cleanup?"true":"false",s.logical_owners,made,released,s.failed?"true":"false"); + for(unsigned i=0;i65536)die("Expected private bounded regular fixture"); + FILE *file=fopen(path,"rb");if(!file)die("Fixture read failed");*length=(size_t)state.st_size; + unsigned char *bytes=malloc(*length);if(!bytes){fclose(file);die("Fixture allocation failed");} + int bad=fread(bytes,1,*length,file)!=*length||ferror(file);fclose(file);if(bad){wipe(bytes,*length);free(bytes);die("Fixture read incomplete");}return bytes; +} +int main(int argc,char **argv){ + clock_gettime(CLOCK_MONOTONIC,&started); + if(argc!=3||!realpath(argv[1],root))die("Expected private fixture root and pinned bridge path"); + struct stat state;if(stat(root,&state)||!S_ISDIR(state.st_mode)||(state.st_mode&077))die("Fixture root must be mode0700"); + load_bridge(argv[2]);size_t der_length,key_length; + unsigned char *der=read_fixture("leaf.der",&der_length),*key=read_fixture("private.pkcs1",&key_length); + struct fingerprint before=fingerprint("before");checkpoint("before",0); + for(unsigned i=1;i<=REPETITIONS;i++){import_operation(i,der,der_length,key,key_length);if(i==1)checkpoint("first",i);} + checkpoint("repeated",REPETITIONS);struct timespec idle={5,0};while(nanosleep(&idle,&idle)){if(errno!=EINTR)die("Idle delay failed");} + struct fingerprint after=fingerprint("after");image_guard();checkpoint("idle",REPETITIONS); + wipe(key,key_length);free(key);free(der); + if(strcmp(before.default_sha,after.default_sha)||strcmp(before.search_sha,after.search_sha)||before.search_count!=after.search_count||created_refs!=REPETITIONS||released_refs!=REPETITIONS||all_created!=all_released)die("Fingerprint or ownership guard failed"); + fprintf(stdout,"{\"kind\":\"complete\",\"operations\":200,\"createdRefs\":%u,\"releasedRefs\":%u,\"ownedOutputs\":%u,\"releasedOutputs\":%u,\"readOnlyDefaultAndSearchListUnchanged\":true,\"managedRuntimePresent\":false,\"bridgeSha256\":\"%s\",\"elapsedSeconds\":%.6f,\"fullSoakGate\":false}\n",created_refs,released_refs,all_created,all_released,BRIDGE_SHA,elapsed());fflush(stdout);return 0; +} diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/fixture-before-harness-cleanup.json b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/fixture-before-harness-cleanup.json new file mode 100644 index 00000000..f63b95c8 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/fixture-before-harness-cleanup.json @@ -0,0 +1,3 @@ +{ + "remainingOwnedKeychainFiles": 0 +} diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/fixture-cleanup.json b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/fixture-cleanup.json new file mode 100644 index 00000000..234d368f --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/fixture-cleanup.json @@ -0,0 +1,29 @@ +{ + "allLaunchedChildrenConfirmedReaped": true, + "reapOutcomes": [ + { + "processId": 65330, + "confirmedReaped": true, + "exitCode": 0 + }, + { + "processId": 65334, + "confirmedReaped": true, + "exitCode": 0 + }, + { + "processId": 65338, + "confirmedReaped": true, + "exitCode": 0 + }, + { + "processId": 65339, + "confirmedReaped": true, + "exitCode": 1 + } + ], + "privateMaterialOverwrittenThenUnlinkedAfterChildReaping": true, + "physicalSecureErasureClaimed": false, + "status": "passed", + "ownedFixtureRootRemoved": true +} diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/native-build.log b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/native-build.log new file mode 100644 index 00000000..30b6f141 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/native-build.log @@ -0,0 +1,37 @@ +/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/NativeImportControl.c:41:42: warning: 'SecKeychainGetTypeID' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations] + 41 | if(!keychain||CFGetTypeID(keychain)!=SecKeychainGetTypeID())return errSecParam; + | ^ +/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:270:10: note: 'SecKeychainGetTypeID' has been explicitly marked deprecated here + 270 | CFTypeID SecKeychainGetTypeID(void) + | ^ +/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/NativeImportControl.c:43:21: warning: 'SecKeychainGetPath' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations] + 43 | OSStatus status=SecKeychainGetPath(keychain,&length,path); + | ^ +/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:474:10: note: 'SecKeychainGetPath' has been explicitly marked deprecated here + 474 | OSStatus SecKeychainGetPath(SecKeychainRef __nullable keychain, UInt32 *ioPathLength, char *pathName) + | ^ +/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/NativeImportControl.c:54:29: warning: 'SecKeychainCopyDefault' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations] + 54 | OSStatus default_status=SecKeychainCopyDefault(&default_keychain); + | ^ +/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:384:10: note: 'SecKeychainCopyDefault' has been explicitly marked deprecated here + 384 | OSStatus SecKeychainCopyDefault(SecKeychainRef * __nonnull CF_RETURNS_RETAINED keychain) + | ^ +/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/NativeImportControl.c:58:28: warning: 'SecKeychainCopySearchList' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations] + 58 | OSStatus search_status=SecKeychainCopySearchList(&search); + | ^ +/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:404:10: note: 'SecKeychainCopySearchList' has been explicitly marked deprecated here + 404 | OSStatus SecKeychainCopySearchList(CFArrayRef * __nonnull CF_RETURNS_RETAINED searchList) + | ^ +/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/NativeImportControl.c:145:42: warning: 'SecKeychainGetTypeID' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations] + 145 | if(!keychain||CFGetTypeID(keychain)!=SecKeychainGetTypeID())return 0; + | ^ +/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:270:10: note: 'SecKeychainGetTypeID' has been explicitly marked deprecated here + 270 | CFTypeID SecKeychainGetTypeID(void) + | ^ +/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/NativeImportControl.c:166:47: warning: 'SecKeychainGetTypeID' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations] + 166 | int type_ok=!result||CFGetTypeID(result)==SecKeychainGetTypeID(); + | ^ +/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:270:10: note: 'SecKeychainGetTypeID' has been explicitly marked deprecated here + 270 | CFTypeID SecKeychainGetTypeID(void) + | ^ +6 warnings generated. diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/provenance.json b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/provenance.json new file mode 100644 index 00000000..8dbfb1d6 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/provenance.json @@ -0,0 +1,59 @@ +{ + "sourcesSha256": { + "NativeImportControl.c": "d3f8d3ed0a4e25098bc42c3bb8dca8650f2557ee8c180b61877814609ced73df", + "ImportSeed.cs": "feaac1c93bdb0f4e59bd0a9fb213d6de53cb89ece7393356c9d12d318b395c1b", + "native_import.py": "2a2e8b34d351e134e91df16569437893b9bf202966ee98267a81b0030a22111d", + "pure_apple.py": "1b2148f1c20578529355379ab94b439931d7241608a5d5effce8fc86f4af7bc6" + }, + "nativeBridgeSha256": "0333994e217a3d111d36d7f31460eea69a286f9fc819e2d7c746df13d2aec46c", + "managedCryptographySha256": "f4957d7218a004a27bf8964e45147df6a88092c6e7344582082840d62c6c579a", + "sdkSeedSha256": { + "Opc.Ua.Client.dll": "620da406fabdbc75d11f9827ea7e39d2d3c762471bd2264b0d11f1d6cd9247cd", + "Opc.Ua.Configuration.dll": "6ed6a594c93c3ff35d1a16539a9fe87c1e2640549cc2f48ed6bae333dc8bf0e7", + "Opc.Ua.Core.Schema.dll": "51cfad48657d95d38074bb88be29aea6f5b361a07397247d58838d066b99772e", + "Opc.Ua.Core.Types.dll": "a7dd141b8dabe8848ff28c2fb8e16a85a0508e6a49e6b1a1c1e267175b99af96", + "Opc.Ua.Core.dll": "9d0857aa6b7f3c49037d4ea557b4da8edf07e146069464481783e66c5c9cd35a", + "Opc.Ua.Security.Certificates.dll": "7f4969aceb2f0db5328c6882cb4b117342f3b18905c38acdba600683860c4fd0", + "Opc.Ua.Server.dll": "930e714a2f6e15d393840bcdc95e1afa8012bc740c3738a655f958142721864e", + "Opc.Ua.Types.dll": "c542f69882f4e8a4ebd5ccc28a3881d6967c7ffb6dcc3c56dc6c776f39c2ede1" + }, + "compileCommand": [ + "clang", + "-std=c11", + "-O0", + "-g", + "-Wall", + "-Wextra", + "/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/NativeImportControl.c", + "-framework", + "Security", + "-framework", + "CoreFoundation", + "-o", + "/private/tmp/opcua2-native-import-20261110a/NativeImportControl" + ], + "compiler": "Apple clang version 21.0.0 (clang-2100.0.123.102)\nTarget: arm64-apple-darwin25.5.0\nThread model: posix\nInstalledDir: /Library/Developer/CommandLineTools/usr/bin\n", + "os": "ProductName:\t\tmacOS\nProductVersion:\t\t26.5.2\nBuildVersion:\t\t25F84\n", + "architecture": "arm64", + "hostDotnet": ".NET SDK:\n Version: 10.0.403\n Commit: 5a2162bea6\n Workload version: 10.0.400-manifests.a00bebb6\n MSBuild version: 18.9.15+5a2162bea\n\nRuntime Environment:\n OS Name: Mac OS X\n OS Version: 26.5\n OS Platform: Darwin\n RID: osx-arm64\n Base Path: /opt/homebrew/Cellar/dotnet/10.0.403/libexec/sdk/10.0.403/\n\n.NET workloads installed:\nThere are no installed workloads to display.\nConfigured to use workload sets when installing new manifests.\nNo workload sets are installed. Run \"dotnet workload restore\" to install a workload set.\n\nHost:\n Version: 10.0.14\n Architecture: arm64\n Commit: 0427b9abbe\n\n.NET SDKs installed:\n 10.0.403 [/opt/homebrew/Cellar/dotnet/10.0.403/libexec/sdk]\n\n.NET runtimes installed:\n Microsoft.AspNetCore.App 10.0.14 [/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.AspNetCore.App]\n Microsoft.NETCore.App 10.0.14 [/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.NETCore.App]\n\nOther architectures found:\n None\n\nEnvironment variables:\n Not set\n\nglobal.json file:\n /.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/global.json\n\nLearn more:\n https://aka.ms/dotnet/info\n\nDownload .NET:\n https://aka.ms/dotnet/download\n", + "repetitions": 200, + "stages": [ + "before", + "first", + "repeated", + "idle" + ], + "idleSeconds": 5, + "fullSoakGate": false, + "managedCallerInMeasuredChild": false, + "managedPfxParsingEmulated": false, + "legacyKeyLifetime": "Explicit CFRelease after identity disposal; managed local has no explicit using scope.", + "wrapperNativeCopyCallsPerOperation": 9, + "checkpointDeadlineSeconds": 180, + "deadlineScope": "Checkpoint/child deadline; in-progress bounded probes and bounded reap add overhead.", + "defaultOrSearchListSetters": false, + "fixtureCreationUmask": "0077", + "binarySha256": "e5c5caf2a8f882f82b738396156b378fd157ebd1a825d462449cd2a0497830fd", + "seedBinarySha256": "1fa7de542aee11acab18dcc8d4096b21fcbc16e4edc1fca2243159dd1176adc0", + "linkedLibraries": "/private/tmp/opcua2-native-import-20261110a/NativeImportControl:\n\t/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 61901.101.4)\n\t/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 4424.1.255)\n\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)\n" +} diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/samples.jsonl b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/samples.jsonl new file mode 100644 index 00000000..01ca74a6 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/samples.jsonl @@ -0,0 +1,4 @@ +{"kind":"fingerprint","stage":"before","defaultStatus":0,"defaultPathStatus":0,"searchStatus":0,"searchPathStatus":0,"defaultSha256":"08c2c77dd8eaf8042b4020991ea06c6215cc391532869bbf6ced2cf1126c5e63","searchSha256":"c05fae700986dc7a6a147a938ef2adc4a274173420e137929ed58ad986eb2a3c","searchCount":2,"ownedQueryReferencesReleased":2} +{"kind":"checkpoint","mode":"native-bridge-import","stage":"before","operations":0,"processId":65339,"utc":"2026-11-10T22:25:23.068Z","elapsedSeconds":0.006057,"rssBytes":7815168,"createdRefs":0,"releasedRefs":0,"managedRuntimePresent":false} +{"kind":"operation","iteration":1,"certReturn":1,"certStatus":0,"modernReturn":1,"modernErrorCode":0,"modernErrorPresent":false,"createStatus":0,"pathStatus":0,"neverLockStatus":0,"externalReturn":1,"externalErrorCode":0,"externalErrorPresent":false,"legacyReturn":1,"legacyStatus":0,"moveReturn":1,"moveStatus":0,"copyCertStatus":-2147483648,"deleteStatus":0,"privatePathVerified":true,"deleteDuringFinalIdentityUntrack":false,"fallbackCleanup":false,"logicalOwnersRemaining":0,"ownedOutputs":9,"releasedOutputs":9,"failed":true,"copies":[{"stage":"import-cert-track","status":0,"returnedRef":false,"privateAssociation":false},{"stage":"move-outer-copy","status":0,"returnedRef":false,"privateAssociation":false},{"stage":"move-cert-untrack","status":0,"returnedRef":false,"privateAssociation":false},{"stage":"move-cert-track","status":0,"returnedRef":true,"privateAssociation":true},{"stage":"move-identity-track","status":0,"returnedRef":false,"privateAssociation":false},{"stage":"release-original-cert","status":0,"returnedRef":true,"privateAssociation":true},{"stage":"release-identity","status":0,"returnedRef":false,"privateAssociation":false}]} +{"kind":"failed","message":"Native bridge import operation failed after ownership cleanup","createdRefs":1,"releasedRefs":1,"elapsedSeconds":1.172310} diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/seed-build.log b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/seed-build.log new file mode 100644 index 00000000..3afe07c2 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/seed-build.log @@ -0,0 +1,9 @@ + Determining projects to restore... + Restored /private/tmp/opcua2-native-import-20261110a/ImportSeed.csproj (in 31 ms). + ImportSeed -> /private/tmp/opcua2-native-import-20261110a/bin/Release/net10.0/ImportSeed.dll + +Build succeeded. + 0 Warning(s) + 0 Error(s) + +Time Elapsed 00:00:01.25 diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/seed.jsonl b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/seed.jsonl new file mode 100644 index 00000000..56cd7d6c --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/seed.jsonl @@ -0,0 +1 @@ +{"kind":"seed","leafSha256":"31a4b6181cc10f0bc862a83199688b35d3c5e2590a2c00ab4cae6c3b1dc3d179","runtime":"10.0.14","sdkVersion":"2.0.1.0","managerDisposed":true,"certificateCoresOutstanding":0,"keyFormat":"PKCS1-private-RSA","privateFileMode":"0600","cryptographyRuntime":{"informationalVersion":"10.0.14\u002B0427b9abbead3fa64c6655eb4290c537397131fa","sha256":"f4957d7218a004a27bf8964e45147df6a88092c6e7344582082840d62c6c579a"}} diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/verification.json b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/verification.json new file mode 100644 index 00000000..447b85ce --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/evidence/2026-11-10-native-import-attempt-a/verification.json @@ -0,0 +1,45 @@ +{ + "status": "failed-partial-control-model", + "projection": "Replace local user-home prefix only; raw receipts unchanged.", + "rawOutput": "/tmp/opcua2-native-import-20261110a", + "rawSha256": { + "samples.jsonl": "06ca824cf08e2c26e595a4e70091f2f5a953ac3022908966ae9ee99987c1043d", + "provenance.json": "ad87286e153b8235aa13f3a60e9bc973f9c8886a038e947ed11304361e236590", + "native-build.log": "2fd25e9c530541c224872a28cf03b7d720be28fdd0216946b533d6292aad1a75", + "seed-build.log": "bd5eb3fbfeb649aa12254fa52689a778c97364c832bee0b6968f0432da129022", + "seed.jsonl": "8b4d9cb09fc134ffed468d4a0acc12d317cb9a77c0f90569ef52e83577baf333", + "fixture-cleanup.json": "0d5c8fe16558d2a21406c88d62ed87690649d3deac7bd333d8741672a23f86c7", + "fixture-before-harness-cleanup.json": "eaaf36a5e7f7d1d3635e987d8f9a2600ce55f6482011ec9a7abf3e4dd759d9c4" + }, + "selectedProjectionSha256": { + "samples.jsonl": "06ca824cf08e2c26e595a4e70091f2f5a953ac3022908966ae9ee99987c1043d", + "provenance.json": "c6f6453edfb0c0601099e6275303359abec52f7566a47961a640bd97d554cbe9", + "native-build.log": "05fac84aff441e7a6b5ce2c3ac1d68cc3705b85495c39dd339964168335c5316", + "seed-build.log": "bd5eb3fbfeb649aa12254fa52689a778c97364c832bee0b6968f0432da129022", + "seed.jsonl": "8b4d9cb09fc134ffed468d4a0acc12d317cb9a77c0f90569ef52e83577baf333", + "fixture-cleanup.json": "0d5c8fe16558d2a21406c88d62ed87690649d3deac7bd333d8741672a23f86c7", + "fixture-before-harness-cleanup.json": "eaaf36a5e7f7d1d3635e987d8f9a2600ce55f6482011ec9a7abf3e4dd759d9c4" + }, + "rawLocalOnlySnapshotsSha256": { + "heap-before.txt": "de4896c196fcf96e58933716049772e523e268be932cef7f9a76cf270f71cd79", + "lsof-before.txt": "f12e1115e87c4110c2961cb61c0b28ea3395c2ae780a72b431d71d4bfdd2a965" + }, + "retainedInstalledBinaryInspectionSha256": { + "opcua2-native-runtime-loader-decompiled.cs": "89434500d1723f419e83ff45fcb6e44e73b58847f4201265613587ad82a79424", + "opcua2-native-runtime-pkcs12key-decompiled.cs": "60af9993d62032adf6be178efd93eef0f9c1a7a1bb2e091938d7171ab12875e2", + "opcua2-native-runtime-pkcs12key-base-decompiled.cs": "cf1e412c9752822610e6e9c96341d2b83052e2e284edc3d1c0086e95893a39b0", + "opcua2-native-runtime-rsa-decompiled.cs": "a4d9bdc8c0d78dac63f19639fb1da319e209a0f4af324edefd0fac8bc4b3becc", + "opcua2-native-runtime-applepal-decompiled.cs": "94e7ead2224719e0102983e0a13c97a9c713018640d73a7153ffea6f732312bf", + "opcua2-native-runtime-appleinterop-decompiled.cs": "da9b4ad8bd114e5b58a82607b37fbc8bc71f79a3a56dead60c88e718d8758c92", + "opcua2-native-runtime-keychainitem-decompiled.cs": "5fb58039e59d4839198fd74ed98978eed0d57008322489fd08d70d6481dd4fd4", + "opcua2-native-runtime-keyhandle-decompiled.cs": "595f2cf7d1c71a13dbdd21379a0ab072ce7d629753ca01f59e57bf789bec223e", + "opcua2-native-runtime-keypair-decompiled.cs": "623cf239bd9f755c701ba50ceffd4043591538111ca72261af4cd59c97db61c9", + "opcua2-native-runtime-certhandle-decompiled.cs": "d53053dfee192fe336e6fae055199ddcf4340da805ed8744e5e52c66f685e453", + "opcua2-native-runtime-identityhandle-decompiled.cs": "80b94acf27a0fbe38efcdc30c86e003ee2abfea30a3da5e9d535e1373ce97069", + "opcua2-native-runtime-keychain-decompiled.cs": "96c2e996638ffdd17c35483aaece7fdbc499fd004ab549e2e128bf82a64b5dc2", + "opcua2-native-runtime-keychain-base-decompiled.cs": "6f2abdb5779646a9bfd589d8fc765cab2e0290edee8396b2c7b2e6a4470e2bb4", + "opcua2-native-runtime-x509contenttype-decompiled.cs": "bed5256e025ed5b85f04b4cce132fcd7279fb06b5dd377d25f4caf39a8682b60", + "opcua2-native-runtime-apple-disassembly.txt": "75af92a3b05e4076025d43389cb3e51d8f6c8547aa485a4b5984469af7f02935" + }, + "noRepeatCompletionOrRetentionInference": true +} diff --git a/scripts/validation/opcua-lifecycle-memory/native-retention/native_import.py b/scripts/validation/opcua-lifecycle-memory/native-retention/native_import.py new file mode 100644 index 00000000..422fd228 --- /dev/null +++ b/scripts/validation/opcua-lifecycle-memory/native-retention/native_import.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +"""One fixed-200 native bridge import diagnostic; build/run only after review.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import signal +import subprocess +import sys +import time +import xml.etree.ElementTree as ET +from pure_apple import complete_rows, save, snapshot, STAGES + +HERE = Path(__file__).resolve().parent +RUNTIME = Path('/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.NETCore.App/10.0.14') +BRIDGE = RUNTIME / 'libSystem.Security.Cryptography.Native.Apple.dylib' +BRIDGE_SHA = '0333994e217a3d111d36d7f31460eea69a286f9fc819e2d7c746df13d2aec46c' +CRYPTO_SHA = 'f4957d7218a004a27bf8964e45147df6a88092c6e7344582082840d62c6c579a' +DOTNET = '/opt/homebrew/Cellar/dotnet/10.0.403/libexec/dotnet' +EXPECTED_SDK = { + 'Opc.Ua.Client.dll': '620da406fabdbc75d11f9827ea7e39d2d3c762471bd2264b0d11f1d6cd9247cd', + 'Opc.Ua.Configuration.dll': '6ed6a594c93c3ff35d1a16539a9fe87c1e2640549cc2f48ed6bae333dc8bf0e7', + 'Opc.Ua.Core.Schema.dll': '51cfad48657d95d38074bb88be29aea6f5b361a07397247d58838d066b99772e', + 'Opc.Ua.Core.Types.dll': 'a7dd141b8dabe8848ff28c2fb8e16a85a0508e6a49e6b1a1c1e267175b99af96', + 'Opc.Ua.Core.dll': '9d0857aa6b7f3c49037d4ea557b4da8edf07e146069464481783e66c5c9cd35a', + 'Opc.Ua.Security.Certificates.dll': '7f4969aceb2f0db5328c6882cb4b117342f3b18905c38acdba600683860c4fd0', + 'Opc.Ua.Server.dll': '930e714a2f6e15d393840bcdc95e1afa8012bc740c3738a655f958142721864e', + 'Opc.Ua.Types.dll': 'c542f69882f4e8a4ebd5ccc28a3881d6967c7ffb6dcc3c56dc6c776f39c2ede1', +} +CHILDREN = [] +REAP_OUTCOMES = [] +COPY_STAGES = ['import-cert-track', 'move-outer-copy', 'move-cert-untrack', 'move-cert-track', + 'move-identity-track', 'identity-cert-track', 'release-original-cert', + 'release-copied-cert', 'release-identity'] + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def reap(process): + outcome = {'processId': process.pid, 'confirmedReaped': False} + try: + if process.poll() is None: + try: + os.killpg(process.pid, signal.SIGTERM) + except ProcessLookupError: + pass + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait(timeout=5) + outcome['exitCode'] = process.wait(timeout=0) + outcome['confirmedReaped'] = True + except Exception as error: + outcome['errorType'] = type(error).__name__ + raise + finally: + REAP_OUTCOMES.append(outcome) + + +def bounded(command, log, timeout, env=None): + with log.open('w') as stream: + process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT, + env=env, start_new_session=True) + CHILDREN.append(process) + try: + status = process.wait(timeout=timeout) + if status: + raise RuntimeError('Bounded subprocess failed: ' + log.name) + finally: + reap(process) + + +def wipe_owned(root): + """Best-effort caller-owned file overwrite/unlink; no physical secure-erasure claim.""" + if not root.exists(): + return + files = [p for p in root.rglob('*') if p.is_file() and not p.is_symlink()] + if len(files) > 100 or any(p.stat().st_size > 1024 * 1024 or p.stat().st_nlink != 1 for p in files): + raise RuntimeError('Unexpected owned fixture cleanup size/link layout; preserve private root') + for path in files: + size = path.stat().st_size + with path.open('r+b') as stream: + stream.write(bytes(size)); stream.flush(); os.fsync(stream.fileno()) + shutil.rmtree(root) + + +def main(): + os.umask(0o077) # This collector and its children only; includes normal SDK seed PFX files. + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--references', required=True, type=Path) + parser.add_argument('--output', required=True, type=Path, help='Fresh private output directory') + args = parser.parse_args() + if sys.platform != 'darwin' or os.uname().machine != 'arm64': + parser.error('Pinned diagnostic requires macOS arm64') + if sha(BRIDGE) != BRIDGE_SHA or sha(RUNTIME / 'System.Security.Cryptography.dll') != CRYPTO_SHA: + raise RuntimeError('Installed native/managed cryptography identity changed') + references = args.references.resolve() + if {p.name: sha(p) for p in references.glob('Opc.Ua*.dll')} != EXPECTED_SDK: + raise RuntimeError('Pinned SDK2 seed binary set changed') + output = args.output.resolve(); output.mkdir(parents=True, exist_ok=False); os.chmod(output, 0o700) + binary = output / 'NativeImportControl' + command = ['clang', '-std=c11', '-O0', '-g', '-Wall', '-Wextra', str(HERE / 'NativeImportControl.c'), + '-framework', 'Security', '-framework', 'CoreFoundation', '-o', str(binary)] + provenance = {'sourcesSha256': {name: sha(HERE / name) for name in ['NativeImportControl.c', 'ImportSeed.cs', 'native_import.py', 'pure_apple.py']}, + 'nativeBridgeSha256': BRIDGE_SHA, 'managedCryptographySha256': CRYPTO_SHA, 'sdkSeedSha256': EXPECTED_SDK, + 'compileCommand': command, 'compiler': subprocess.check_output(['clang', '--version'], text=True, timeout=10), + 'os': subprocess.check_output(['sw_vers'], text=True, timeout=10), 'architecture': os.uname().machine, + 'hostDotnet': subprocess.check_output([DOTNET, '--info'], text=True, timeout=10), + 'repetitions': 200, 'stages': STAGES, 'idleSeconds': 5, 'fullSoakGate': False, + 'managedCallerInMeasuredChild': False, 'managedPfxParsingEmulated': False, + 'legacyKeyLifetime': 'Explicit CFRelease after identity disposal; managed local has no explicit using scope.', + 'wrapperNativeCopyCallsPerOperation': 9, 'checkpointDeadlineSeconds': 180, + 'deadlineScope': 'Checkpoint/child deadline; in-progress bounded probes and bounded reap add overhead.', + 'defaultOrSearchListSetters': False, 'fixtureCreationUmask': '0077'} + save(output / 'provenance.json', provenance) + bounded(command, output / 'native-build.log', 60) + project = ET.Element('Project', Sdk='Microsoft.NET.Sdk'); properties = ET.SubElement(project, 'PropertyGroup') + for name, value in {'OutputType': 'Exe', 'TargetFramework': 'net10.0', 'ImplicitUsings': 'enable', 'Nullable': 'enable', + 'EnableDefaultCompileItems': 'false', 'TreatWarningsAsErrors': 'true', 'UseSharedCompilation': 'false'}.items(): + ET.SubElement(properties, name).text = value + items = ET.SubElement(project, 'ItemGroup'); ET.SubElement(items, 'FrameworkReference', Include='Microsoft.AspNetCore.App') + ET.SubElement(items, 'Compile', Include=str(HERE / 'ImportSeed.cs')) + for name in EXPECTED_SDK: + ref = ET.SubElement(items, 'Reference', Include=Path(name).stem); ET.SubElement(ref, 'HintPath').text = str(references / name) + ET.indent(project); ET.ElementTree(project).write(output / 'ImportSeed.csproj', encoding='unicode') + bounded([DOTNET, 'build', str(output / 'ImportSeed.csproj'), '-c', 'Release', '-m:1', '--disable-build-servers', '--nologo'], output / 'seed-build.log', 120) + provenance['binarySha256'] = sha(binary) + provenance['seedBinarySha256'] = sha(output / 'bin/Release/net10.0/ImportSeed.dll') + provenance['linkedLibraries'] = subprocess.check_output(['otool', '-L', str(binary)], text=True, timeout=10) + save(output / 'provenance.json', provenance) + task = output / 'task'; task.mkdir(mode=0o700) + for name in ['seed', 'seed-tmp', 'seed-data', 'tmp']: + (task / name).mkdir(mode=0o700) + try: + bounded([DOTNET, str(output / 'bin/Release/net10.0/ImportSeed.dll'), str(task / 'seed')], output / 'seed.jsonl', 75, + env=dict(os.environ, TMPDIR=str(task / 'seed-tmp'), XDG_DATA_HOME=str(task / 'seed-data'))) + seeds = complete_rows(output / 'seed.jsonl') + if len(seeds) != 1 or seeds[0]['kind'] != 'seed' or seeds[0]['sdkVersion'] != '2.0.1.0' or seeds[0]['cryptographyRuntime']['sha256'] != CRYPTO_SHA: + raise RuntimeError('Seed/runtime count or identity mismatch') + if sha(task / 'seed/leaf.der') != seeds[0]['leafSha256']: + raise RuntimeError('Seed leaf file identity mismatch') + if any(p.stat().st_mode & 0o077 for p in (task / 'seed').rglob('*') if p.is_file()): + raise RuntimeError('Seed fixture file permissions are not private0600') + for name in ['pki']: + wipe_owned(task / 'seed' / name) + wipe_owned(task / 'seed-tmp'); wipe_owned(task / 'seed-data') + if sha(BRIDGE) != BRIDGE_SHA: + raise RuntimeError('Native bridge changed before measured execution') + started = time.monotonic() + with (output / 'samples.jsonl').open('w') as log: + process = subprocess.Popen([str(binary), str(task), str(BRIDGE)], stdout=log, stderr=subprocess.STDOUT, + env=dict(os.environ, TMPDIR=str(task / 'tmp')), start_new_session=True) + CHILDREN.append(process) + try: + deadline = time.monotonic() + 180; samples = [] + for stage in STAGES: + while True: + rows = complete_rows(output / 'samples.jsonl') + if any(r['kind'] == 'failed' for r in rows): + raise RuntimeError('Native import failed; preserve all status/ownership rows') + checkpoints = [r for r in rows if r['kind'] == 'checkpoint' and r.get('stage') == stage] + if checkpoints: + samples.append({**checkpoints[0], **snapshot(process.pid, task, output, stage)}) + (task / ('continue-' + stage)).touch(); break + if process.poll() is not None or time.monotonic() >= deadline: + raise RuntimeError('Native checkpoint exceeded bound: ' + stage) + time.sleep(.05) + status = process.wait(timeout=15) + rows = complete_rows(output / 'samples.jsonl') + operations = [r for r in rows if r['kind'] == 'operation']; fingerprints = [r for r in rows if r['kind'] == 'fingerprint'] + complete = [r for r in rows if r['kind'] == 'complete'] + if status or len(rows) != 207 or len(operations) != 200 or len(fingerprints) != 2 or len(complete) != 1 or [r['iteration'] for r in operations] != list(range(1, 201)): + raise RuntimeError('Native fixed operation/completion counts failed') + for row in operations: + if row['failed'] or row['fallbackCleanup'] or not row['privatePathVerified'] or not row['deleteDuringFinalIdentityUntrack'] or row['logicalOwnersRemaining'] or row['modernErrorPresent'] or row['externalErrorPresent'] or row['ownedOutputs'] != row['releasedOutputs']: + raise RuntimeError('Native ownership order/cleanup failed') + if [c['stage'] for c in row['copies']] != COPY_STAGES or any(c['status'] for c in row['copies']) or [c['returnedRef'] for c in row['copies']] != [False] * 3 + [True] * 6: + raise RuntimeError('Native wrapper-copy sequence failed') + if any(row[k] for k in ['certStatus', 'createStatus', 'pathStatus', 'neverLockStatus', 'legacyStatus', 'moveStatus', 'copyCertStatus', 'deleteStatus', 'modernErrorCode', 'externalErrorCode']) or any(row[k] != 1 for k in ['certReturn', 'modernReturn', 'externalReturn', 'legacyReturn', 'moveReturn']): + raise RuntimeError('Native bridge statuses failed') + if sha(task / 'seed/leaf.der') != seeds[0]['leafSha256'] or complete[0]['operations'] != 200 or complete[0]['bridgeSha256'] != BRIDGE_SHA or complete[0]['ownedOutputs'] != complete[0]['releasedOutputs']: + raise RuntimeError('Native identity/completion ownership guard failed') + if any(samples[-1][k] for k in ['ownedKeychainFdCount', 'ownedKeychainFileCount']): + raise RuntimeError('Private keychain files/descriptors retained') + save(output / 'result.json', {'status': 'passed', 'seed': seeds[0], 'samples': samples, 'fingerprints': fingerprints, + 'complete': complete[0], 'wallSecondsIncludingProbes': time.monotonic() - started, 'fullSoakGate': False, + 'nativeInventoryIsNotOpenHandlesOrExclusiveRss': True, 'measuredChildManagedRuntimePresent': False}) + finally: + reap(process) + finally: + save(output / 'fixture-before-harness-cleanup.json', {'remainingOwnedKeychainFiles': sum(p.is_file() and '.keychain' in p.name for p in (task / 'tmp').rglob('*'))}) + all_reaped = all(child.returncode is not None for child in CHILDREN) + cleanup = {'allLaunchedChildrenConfirmedReaped': all_reaped, 'reapOutcomes': REAP_OUTCOMES, + 'privateMaterialOverwrittenThenUnlinkedAfterChildReaping': False, 'physicalSecureErasureClaimed': False} + if not all_reaped: + cleanup.update(status='failed-preserved-private-fixture', ownedFixtureRootRemoved=False) + save(output / 'fixture-cleanup.json', cleanup) + raise RuntimeError('A launched child was not reaped; private fixture preserved without overwrite/unlink') + try: + wipe_owned(task) + cleanup.update(status='passed', ownedFixtureRootRemoved=not task.exists(), + privateMaterialOverwrittenThenUnlinkedAfterChildReaping=True) + except Exception as error: + cleanup.update(status='failed', errorType=type(error).__name__, ownedFixtureRootRemoved=not task.exists()) + raise + finally: + save(output / 'fixture-cleanup.json', cleanup) + print(output) + + +if __name__ == '__main__': + main()