# Private handoff: macOS AppleDatabase retention after exportable PFX imports Private draft for upstream triage; nothing has been posted. On the tested macOS build, 1,000 exportable RSA private-key imports added 1,000 `Security::AppleDatabase` heap objects despite disposal. A measured C child reproduces the growth through the installed .NET Apple crypto bridge without CLR or SDK loaded. Changing the timing of native keychain delete and initial release does not prevent it. The retained C++ owner and its share of RSS remain unresolved; this is not proof of leaked open handles or a production fix. ## Pinned environment - **macOS 26.5.2 (25F84), arm64**. The class inventory comes from Security.framework via `heap --noContent -s`. Apple’s [public AppleDatabase source](https://github.com/apple-oss-distributions/Security/blob/db15acbe6a7f257a859ad9a3bb86097bfe0679d9/OSX/libsecurity_filedb/lib/AppleDatabase.h#L485) gives class context and is not the installed framework source. - **Homebrew .NET SDK 10.0.403 / runtime 10.0.14**, shared runtime under `/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`. `libSystem.Security.Cryptography.Native.Apple.dylib`: SHA-256 `0333994e217a3d111d36d7f31460eea69a286f9fc819e2d7c746df13d2aec46c`. [Installed binary inspection](NATIVE_IMPORT_DESIGN.md) binds the actual ABI, wrapper calls and disassembly. The public [.NET v10.0.14 release](https://github.com/dotnet/runtime/releases/tag/v10.0.14) resolves to `6aa72daa24f66c01c3d7b6a32519e9d21396deec`; its [native PAL](https://github.com/dotnet/runtime/blob/6aa72daa24f66c01c3d7b6a32519e9d21396deec/src/native/libs/System.Security.Cryptography.Native.Apple/pal_x509_macos.c) is explanatory context, not asserted as the exact installed source commit. - **OPC UA SDK 2.0.1**, assembly version 2.0.1.0, exact source `07e62f72500cc0923a2bab940653c8c1116fa286`. [Directory private-key loader at that commit](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/src/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs#L758) and [eight SDK binary hashes](evidence/2026-11-10/provenance.json) bind the managed A/B and native fixture seed. The measured C processes did not load it. ## Start with the plain PFX loop The recorded [single-owner PFX control](../CertificateControl.cs) creates a fresh in-memory self-signed RSA certificate, exports PFX once, then imports and disposes each of 1,000 copies with `X509KeyStorageFlags.Exportable`. The wrapper owns the seed in that harness; the PFX loop itself only calls .NET. The [existing runner and four modes](../README.md#reproduce-the-diagnostic-subset) are reproducible from the pinned SDK references. Retained local raw snapshots: `/tmp/opcua2-rss-investigation/pfx-control/single-owner/pfx/heap-{before,after}.txt`; separate idle hold `/tmp/opcua2-rss-investigation/pfx-control/pfx-longhold/heap-{0,60,180}.txt`. The following extracts that API sequence without an OPC UA dependency. This snippet has not been separately executed; the recorded counts below are from the existing harness. Use a fresh private `TMPDIR`, a `net10.0` console app with no additional packages and a bounded parent. Take `heap --noContent -s` from a separate shell while the child is at each 15-second checkpoint. No user or system keychain setter is needed. Reap the process before removing the private directory. ```csharp using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using var rsa = RSA.Create(2048); var request = new CertificateRequest("CN=NativeMemoryControl", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); using var original = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1)); byte[] pfx = original.Export(X509ContentType.Pfx); try { Checkpoint("before"); for (int i = 1; i <= 1000; ++i) { using (X509Certificate2 copy = X509CertificateLoader.LoadPkcs12( pfx, (string?)null, X509KeyStorageFlags.Exportable)) { } if (i % 100 == 0) Collect(); } Checkpoint("after"); } finally { CryptographicOperations.ZeroMemory(pfx); } static void Collect() { GC.Collect(); GC.WaitForPendingFinalizers(); } static void Checkpoint(string stage) { Collect(); Console.WriteLine($"{stage} pid={Environment.ProcessId}"); Thread.Sleep(TimeSpan.FromSeconds(15)); } ``` ## Recorded controls and the unresolved boundary | Control | AppleDatabase inventory | What it distinguishes | |---|---:|---| | Plain PFX or SDK private `AsX509Certificate2` copy, 1,000 each | 2 → 1,002 | Import/private-copy triggers. Public DER and shared-handle controls stayed at 2; separate PFX hold stayed at 1,002 for 180 seconds. | | [SDK configured-store vs registry](README.md), 200 each | Store 3 → 203; registry 3 → 3 | Directory key reload reproduces growth despite observed SDK manager/core disposal, runtime lookup zero and owned files/FDs zero. Registry selection is a different update policy. | | [Empty native keychain](PURE_APPLE.md), 200 | 1 → 1 | Plain create/delete/CFRelease without imported cert/key did not reproduce growth. | | [Installed native bridge B](NATIVE_IMPORT.md), 200 | 1 → 2 → 201 → 201 | The DER/PKCS1 materialization, legacy import, move and identity copy sequence reproduces without managed caller/SDK; 200 deletes OSStatus 0, 2,402/2,402 returned CF owners, 3 FDs, no owned keychain files/FDs. | | [Matched deletion timing pair](NATIVE_IMPORT_TIMING.md), 200 + 200 | Both 1 → 2 → 201 → 201 | Same seed/binary; delete at logical zero with five observed references vs after all other observed references with one. Both delete status and returned owner guards passed; the timing change did not prevent growth. | Sampled server CPU stacks point primarily to configured-directory private-key loading, not the SDK's private-copy helper. The actual installed exportable path uses managed PKCS12 decoding, then native key materialization/legacy key import and move-to-keychain. The native control isolates the latter sequence; it does not use `SecPKCS12Import` or claim to model managed finalizer timing. Apple documents [separate release after keychain deletion](https://developer.apple.com/documentation/security/seckeychaindelete%28_%3A%29): installed managed handle code already calls it, and both measured C orders issued and balanced that pair. The controls do not observe internal native references or establish native delete status in the separate managed A/B. The upstream question is which native owner retains the database objects after a successful temporary-keychain delete and balanced returned owners, and whether the supported exportable PFX path can release that inventory. The installed bridge is still present in the positive native controls, so the data do not separate bridge-native ownership from Security.framework caching or claim a particular defect. [.NET documents that macOS private PFX loading requires a keychain and does not support `EphemeralKeySet`](https://learn.microsoft.com/en-us/dotnet/standard/security/cross-platform-cryptography#read-a-pkcs12pfx-file). No application-only fix preserving certificate selection, replacement, trust and ownership semantics was found. These controls do not close #799 or waive the product RSS gate. Share selected receipts only; private keys, raw native heaps, user keychain paths and raw local logs are not part of this handoff.