docs: prepare private native retention handoff
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user