7.2 KiB
Deferring native keychain deletion did not prevent inventory growth
The fixed paired diagnostic reproduced the same Security::AppleDatabase
inventory in both measured processes: 1 → 2 → 201 → 201. Deferring native
SecKeychainDelete and the initial keychain CFRelease until after all observed
returned item, key, data, error and copy owners were released did not prevent the
increase on this pinned native bridge path. This rules out changing only that
observed deletion timing as a remedy in this control. It does not identify
bridge-internal or Apple ownership, establish whether any unobserved native
references remain, or assign a platform defect.
The two arms used the same fresh private RSA seed, same measured C binary and installed native bridge, and separate processes. The fixed normal arm ran first and reproduced attempt B, whose source and receipts remain unchanged. The deferred arm ran next. There was one paired execution, 200 operations per arm, with no retry, count change, reversed order or managed caller in either measured child. These are bounded diagnostics on macOS 26.5.2 build 25F84 arm64. No production mitigation, RSS budget decision, full-soak/G4 pass or #799 closure follows.
What changed between the arms
Both arms ran the same pinned native imports, nine ordered wrapper keychain copies per operation, and item, copy and legacy-key release sequence. The logical registry retired at the same point in all 400 operations: the final copied certificate Untrack after original certificate disposal and initial import-state logical release. At logical zero there were five observed returned references: the initial keychain, current keychain copy, copied certificate, identity and legacy key. The original certificate reference was already released.
| Arm | Actual native delete and initial release | Observed returned CF owners at delete |
|---|---|---|
| Normal | At logical zero, during copied certificate Untrack | 5: initial keychain and four other references |
| Deferred | After all item, copy and legacy-key releases | 1: initial keychain only |
In the deferred arm the initial native owner stayed held after the logical
registry retired; its delete and CFRelease ran only after all other observed
returned owners were gone. The collector replayed every logical owner transition
and independently verified the physical keychain owner and actual deletion state.
This ledger covers references returned to the child. It does not enumerate
unobservable references within the installed bridge or Security.framework.
All 200 keychain deletes in each arm returned native status zero. All bridge operations that use one as success returned one; the reached OSStatus fields were zero. Each arm observed 1,800 keychain copy calls with normalized status zero and 800 returned private associations. The four associated copy stages were identical: post-move original certificate Track, copied certificate Track, original certificate Untrack and copied certificate Untrack. Both identity calls returned null with success. Each arm balanced 2,402/2,402 returned CF owners, including 200/200 initial keychain refs, with zero remaining logical owners and no fallback cleanup. First and last certificate identity checks matched the one unchanged shared seed in both arms.
Native measurements and cleanup
| Arm | Checkpoint | AppleDatabase count / shallow bytes | Numeric FDs | Owned keychain files / FDs | RSS bytes |
|---|---|---|---|---|---|
| Normal | Before | 1 / 768 | 3 | 0 / 0 | 7,667,712 |
| Normal | First | 2 / 1,536 | 3 | 0 / 0 | 10,895,360 |
| Normal | 200 | 201 / 154,368 | 3 | 0 / 0 | 14,548,992 |
| Normal | Five-second idle | 201 / 154,368 | 3 | 0 / 0 | 14,581,760 |
| Deferred | Before | 1 / 768 | 3 | 0 / 0 | 7,536,640 |
| Deferred | First | 2 / 1,536 | 3 | 0 / 0 | 10,878,976 |
| Deferred | 200 | 201 / 154,368 | 3 | 0 / 0 | 14,123,008 |
| Deferred | Five-second idle | 201 / 154,368 | 3 | 0 / 0 | 14,139,392 |
Native child times were 17.403 and 17.290 seconds, respectively. Parent
wall including the seed and both arms was 35.545 seconds. Heap inventory
came from heap --noContent -s; RSS was Mach task resident size. Numeric FD
counts did not rise. A classified native heap object is not an open handle and
its shallow bytes do not explain the full RSS change. The one fixed-order pair
does not estimate a timing effect on RSS or generalize beyond this control.
The single .NET 10.0.14 SDK 2.0.1 seed ran outside both measured children. Its private DER/PKCS1 fixture files were mode 0600 in a private root, and its SDK PKI was removed after seed exit. Both measured children read the same seed files; the public leaf hash and private file identity metadata remained stable. The native children checked the pinned bridge image and rejected CLR/SDK images. No PFX parser or managed finalizer behavior was emulated. The explicit legacy key release order was unchanged between the arms.
All four read-only default/search-list fingerprints matched; all query statuses were zero and the search-list count was two. No setter or restore API ran. Both builds, seed and both measured children exited zero and were reaped. Both owned keychain directories already had zero keychain files before harness cleanup. The remaining private fixture files were overwritten and unlinked only after all reaping, and the private root was removed. No physical secure erasure guarantee is claimed. Native compilation emitted six unsuppressed deprecation warnings for the exercised public keychain APIs; the seed build reported zero warnings and errors.
The result narrows the remaining question: the installed native bridge plus Security.framework import path retained the database inventory with both tested release orders, while the empty keychain control did not. It remains unresolved which native component owns the retained objects. There is no supported application-only correction demonstrated by this experiment.
Pinned evidence
Raw private output is /tmp/opcua2-native-import-timing-20261110a.
Selected receipts
retain the source/runtime/binary hashes, build logs, shared seed and cleanup
receipts, both arm summaries and nine exact raw JSONL rows per arm. The 207 raw
rows per arm and all native heap/FD snapshots stay local and are hash-bound.
- Pair result SHA-256:
f0b4195fe445259934b91e9c6743437c8427844b8fe5e3a9aafe28847f495986 - Normal raw JSONL SHA-256:
73288755c768065ad0a8b2dad6964696d707f1c6be5a6b8bd531902cb3b4bf51 - Deferred raw JSONL SHA-256:
61691ff4c500d2696f6998cd225d19677a0cb8eb47ed8f7545b01f76e4acc456 - Measured binary SHA-256:
cec035355aea998d6dd9311561b23e1617a1096191847cf99f1a0a90a7b6d112 - Seed DLL SHA-256:
36e648ac37b61cb5dd65f03dcf5db555fc297d23243c42e282ef522f15a8a7a1
The fixed collector accepts only the pinned source and binary set and a fresh private output. Any further run needs its own scope and serialization review.
PYTHONDONTWRITEBYTECODE=1 python3 \
scripts/validation/opcua-lifecycle-memory/native-retention/native_import_timing.py \
--references /tmp/opcua2-native-reload-20261110a/bin/Release/net10.0 \
--output /tmp/opcua2-native-import-timing-FRESH