test: isolate native certificate reload retention on macOS

This commit is contained in:
Joseph Doherty
2026-11-10 16:37:21 -05:00
parent 564f109143
commit af71fb11ba
13 changed files with 1108 additions and 0 deletions
@@ -0,0 +1 @@
__pycache__/
@@ -0,0 +1,213 @@
# macOS private-key reload native retention — #799
The bounded control identifies **configured-directory private-key loading** as a
trigger for continuing `Security::AppleDatabase` heap inventory on this machine,
while observed SDK certificate-core ownership and tracked .NET temporary-keychain
ownership return to zero. No narrow built-in application ownership correction was
found that preserves the current raw-session factory, Directory PKI, certificate
selection/update behavior and certificate properties. **#799 remains open.** This
is private reproduction/support material, not an approved production mitigation.
Only this new subfolder was changed. No product source, trust policy, SDK version,
certificate factory/crypto provider, exportability, key storage format, existing
PKI or resource budget was changed. No public upstream issue/message was posted.
## Reviewed, executed control
Astra reviewed the actual source and installed-runtime decompile before execution,
approved one fixed **200-acquisition** run per arm as attribution-only evidence,
and afterward independently matched all ten raw heap counts/bytes, checkpoint and
completion counts, source hashes, installed crypto hash and eight SDK DLL hashes.
The configured-store versus registry distinction below is supported by inspected
upstream regression source; that upstream regression test was **not executed**.
Both arms ran in separate fresh processes on macOS **26.5 arm64**, .NET SDK
**10.0.403**, runtime **10.0.14**. A normal SDK ApplicationInstance created one RSA
application PKI; its persisted tree was copied into separate private arm roots.
Both used the same application URI, public DER identity and private key. Normal
SDK configuration validation/certificate checks and teardown were identical. The
loop called these public APIs, disposing each returned owner before checkpoints:
| Arm | Public acquisition, repeated exactly 200 times |
|---|---|
| Store | `SecurityConfiguration.FindApplicationCertificateAsync(Basic256Sha256, privateKey: true, ...)` |
| Registry | `ICertificateRegistry.AcquireApplicationCertificateBySecurityPolicy(Basic256Sha256)` |
Each arm sampled ready, first acquisition, acquisition 200, application disposed,
and after a fixed five-second idle. Every checkpoint used the same full-GC /
finalizer / full-GC / 250 ms schedule, then a parent-controlled heap/lsof barrier.
Matched initial/first/last RSA sign-and-verify checks confirmed private-key use.
No iteration called `AsX509Certificate2`; no network, server or session was opened.
`AutoAcceptUntrustedCertificates=false` stayed configured; this key-acquisition
control makes no additional peer-validation wire claim.
Allocation-stack tracking was enabled in **both** arms, so its allocation/GC cost
is part of these observations. The 180-second child cancellation/checkpoint
limit is not a strict total wall deadline: a running heap probe (20 seconds),
lsof probe (five seconds), final wait and bounded terminate/kill reap can add
bounded overhead. Stdout is a file, with no unbounded `communicate` or pipe drain.
The recorded artifact-creation-to-result-write wall interval was **34.699 seconds**
(filesystem timestamps, not a monotonic acceptance gate). Ready-to-idle checkpoint
intervals were **21.504 seconds store** and **9.123 seconds registry**, including
probe barriers. Both process exits were zero and all children were reaped.
## Native inventory and ownership results
| Checkpoint | Store AppleDatabase | Registry AppleDatabase | Tracked runtime keychains, each arm | Outstanding SDK cores, each arm | Temporary keychain files, each arm |
|---|---:|---:|---:|---:|---:|
| Ready | 3 | 3 | 1 | 1 | 1 |
| First acquisition | 4 | 3 | 1 | 1 | 1 |
| Acquisition 200 | 203 | 3 | 1 | 1 | 1 |
| Application disposed | 203 | 3 | 0 | 0 | 0 |
| Five-second idle | 203 | 3 | 0 | 0 | 0 |
Configured PKI FDs and temporary-keychain FDs were **zero at every checkpoint**.
One other task-TMPDIR descriptor remained in each process; it is reported
separately and is not labelled a keychain descriptor. Total numeric FDs were
110 ready / 112 final in both arms. Manager disposal was observed through an
exact-SDK, read-only `m_disposed` boolean probe. Store certificate-core counters
finished **407 created / 407 disposed**; registry finished **7 / 7**. These public
counters concern underlying certificate cores, not every AddRef wrapper.
The exact installed runtime's internal temporary-keychain dictionary was read
under the same dictionary monitor, with a one-second bounded acquisition and
exact informational-version/type/field guards. Only its count was read; no native
pointer, handle, key or certificate material was enumerated or modified. Its
count returning zero establishes no remaining entries in that managed lookup.
The installed cleanup code removes the entry **before** calling native
`SecKeychainDelete`, suppresses delete errors, and then releases the CF handle.
The actual installed base `SafeKeychainHandle.ReleaseHandle` was separately
decompiled and does call `Interop.CoreFoundation.CFRelease(handle)`, then zeros
its handle. Apple's [SecKeychainDelete documentation](https://developer.apple.com/documentation/security/seckeychaindelete(_:).md)
distinguishes file/keychain deletion from releasing keychain-object memory; this
inspection finds the separate CFRelease in the installed runtime, not an obvious
missing base-release implementation. These are inspected calls, not native
return-status instrumentation. Therefore lookup zero does **not** prove that
native deletion succeeded.
| Memory observation | Store ready → final idle | Registry ready → final idle |
|---|---:|---:|
| Managed live bytes | 831,080 → 1,222,192 | 831,128 → 1,130,616 |
| RSS bytes | 87,621,632 → 115,130,368 | 87,654,400 → 97,927,168 |
| Compiled methods | 1,192 → 4,638 | 1,192 → 2,251 |
RSS includes different JIT, tracking, GC and allocator effects; this comparison
is not an exclusive native-byte attribution or a performance budget. The native
class's shallow heap bytes were 2,304 → 155,904 in store and 2,304 → 2,304 in
registry (768 bytes per classified object). Shallow class bytes exclude owned
allocations and do not explain the entire RSS difference. `PrivateMemorySize64`
returned zero on this runtime and is not used as a private-memory result.
`heap --noContent -s` reports allocations classified as the C++
`Security::AppleDatabase` class. Apple's published file-database source declares
that class as a `Database` subclass with database open/close/delete methods and
owned file/modifier state; it is not a file-descriptor counter. The published
source is conceptual evidence, **not** a claim that it is the exact source build
of this installed Security.framework. Retaining classified heap objects after
observed closure is different from proving an open handle, failed native delete,
or the identity of the retaining native owner. See the pinned
[Apple class definition](https://github.com/apple-oss-distributions/Security/blob/db15acbe6a7f257a859ad9a3bb86097bfe0679d9/OSX/libsecurity_filedb/lib/AppleDatabase.h#L485).
The leaf DER SHA-256 was identical before/after and across arms:
`fadcc11ec694732b82fe4dc47bb41bce6023d113339e53aca9fee5b079f200b2`.
After samples and process exits, every seed/arm PKI, TMPDIR and data fixture
was removed; cleanup receipt is true. Raw native snapshots stay local and private.
## Exact SDK path and public API limits
The inspected SDK checkout is the clean **2.0.1** tag at commit
`07e62f72500cc0923a2bab940653c8c1116fa286`; the control references immutable actual
published SDK 2.0.1 DLLs, with all eight hashes in `evidence/2026-11-10/provenance.json`.
| Source, pinned to the exact SDK commit | Relevant behavior |
|---|---|
| [DefaultSessionFactory.CreateChannelAsync](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/src/Opc.Ua.Client/Session/DefaultSessionFactory.cs#L406) | Loads a private certificate through the internal overload that defaults registry use to false. |
| [DefaultSessionFactory private CreateAsync](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/src/Opc.Ua.Client/Session/DefaultSessionFactory.cs#L579) | Constructs the raw Session with null certificate/chain. |
| [Session.LoadInstanceCertificateAsync](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/src/Opc.Ua.Client/Session/Session.cs#L6335) | Opening that Session independently loads its private certificate; registry preference depends on a channel manager, absent in the current raw factory. |
| [Session.LoadInstanceCertificateEntryAsync](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/src/Opc.Ua.Client/Session/Session.cs#L6432) | Documents configured-store selection for unmanaged callers and registry preference for managed callers. |
| [SecurityConfiguration.FindApplicationCertificateAsync](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/src/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs#L182) | Private-key branch calls registry-less CertificateIdentifierResolver.LoadPrivateKeyAsync. |
| [CertificateIdentifierResolver.LoadPrivateKeyCoreAsync](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/src/Opc.Ua.Core/Security/Certificates/CertificateIdentifierResolver.cs#L249) | Opens the configured store and asks it to load the private key; returned reference is caller-owned. |
| [DirectoryCertificateStore.LoadPrivateKeyAsync](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/src/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs#L758) | For PFX, macOS storage flags omit EphemeralKeySet and include Exportable, then import through X509CertificateLoader.LoadPkcs12. No public per-store private-key cache/flag setting is offered. |
| [ApplicationConfiguration.ValidateAsync](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/src/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs#L735), [ApplicationInstance.CheckOrCreateCertificateAsync](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/src/Opc.Ua.Configuration/ApplicationInstance.cs#L425), [ServerBase.OnServerStartingAsync](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/src/Opc.Ua.Core/Stack/Server/ServerBase.cs#L1887) | Application validation, normal own-certificate checking and server bring-up also load configured private certificates. Checking disposes its temporary certificate; registry replacement disposes old entries. |
| [Certificate.AsX509Certificate2](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/src/Opc.Ua.Security.Certificates/X509Certificate/Certificate.cs#L258) | Nonvirtual export/reimport copy for exportable private keys. Existing isolated controls demonstrate its PFX retention trigger, but prior sampled real-workload stacks identified directory loading as the dominant sampled import path. |
`CertificateIdentifier` has **no Certificate preload/cache property** and only a
public default constructor. Actual-binary public API inventory is retained in
`seed-api-inventory.jsonl`. `RawData` carries public encoded certificate material;
its setter parses `Certificate.FromRawData` and derives metadata. It cannot carry
the reusable private Certificate owner needed by the raw private-key load path.
The [exact-tag manager documentation](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/docs/CertificateManager.md#materializing-a-certificate-from-a-certificateidentifier)
explicitly places materialized ownership in the manager registry.
The public registry acquisition API is reference-counted and supported. However,
substituting it in the raw session path changes certificate selection semantics:
[UnmanagedCertificateLoadHonorsAnExplicitConfiguredStoreReplacementAsync](https://github.com/OPCFoundation/UA-.NETStandard/blob/07e62f72500cc0923a2bab940653c8c1116fa286/tests/Opc.Ua.Client.Tests/Session/SessionCertificateRotationRegressionTests.cs#L198)
requires a configured replacement to win over an older registry certificate and
verifies the registry API is never called. This is **inspected upstream test
source, not a test run here**. A cache/factory rewrite must additionally preserve
update/removal visibility, cancellation/fault cleanup, concurrent owners,
reconnect and certificate/issuer-chain selection; the B arm proves none of those
replacement semantics. ChannelManagerSessionFactory changes architecture and was
not used. Overriding/reimplementing channel and session construction is not a
narrow application disposal fix.
Production source at `ed60970fc294ab171a80cda1b57942a7130b4c39` was inspected:
[OpcUaApplicationHost](../../../../src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs)
assigns configuration ownership before async validation and disposes its
ApplicationInstance; Shared transfers manager lifetime to the session adapter
and disposes failed configurations; driver/browser release their owned
ApplicationInstance after session cleanup. No missing owner was identified in
those paths. The store arm already keeps its one application/configuration and
registry alive across all 200 acquisitions, so simply lengthening that
application owner cannot remove these private-key imports. Existing full-client
lifecycle disposal checks are complementary
coordinator evidence, not additional executions in this control.
## Runtime dependency and upstream-ready boundary
Actual installed `System.Security.Cryptography.dll` informational version:
`10.0.14+0427b9abbead3fa64c6655eb4290c537397131fa`.
SHA-256: `f4957d7218a004a27bf8964e45147df6a88092c6e7344582082840d62c6c579a`.
The official .NET `v10.0.14` tag resolves to
`6aa72daa24f66c01c3d7b6a32519e9d21396deec`, different from the installed binary's
informational commit. It is not presented as exact installed source. Read-only
ILSpy inspection of the **actual binary** verified the temporary-keychain
creation, dictionary/monitor and release behavior used by diagnostics. Paths and
hashes of those retained local decompilations are in `verification.json`.
The public runtime source provides matching context for
[temporary keychain creation during non-persisted import and unsupported EphemeralKeySet](https://github.com/dotnet/runtime/blob/6aa72daa24f66c01c3d7b6a32519e9d21396deec/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.macOS.cs)
and [tracked-keychain release ordering](https://github.com/dotnet/runtime/blob/6aa72daa24f66c01c3d7b6a32519e9d21396deec/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.Keychain.macOS.cs).
Microsoft documents that macOS PFX private-key loading requires a keychain and
cannot use EphemeralKeySet in its [cross-platform cryptography guide](https://learn.microsoft.com/en-us/dotnet/standard/security/cross-platform-cryptography#read-a-pkcs12pfx-file).
Changing those flags, exportability, storage format or native crypto was not an
application mitigation attempted here.
The remaining dependency is an upstream/platform investigation of why the
classified native file-database objects remain after observed SDK owner release,
tracked runtime owner release and backing-file cleanup, and whether there is a
supported correction at that boundary. Native delete status and the retaining
C++ owner are still unknown. This control does not identify a specific .NET or
Apple defect/fix. An upstream SDK improvement would also need to avoid redundant
imports **without losing the existing raw configured-store semantics**. No
supported application-only correction meeting the stated constraints was proven.
## Private reproduction and evidence
```sh
python3 scripts/validation/opcua-lifecycle-memory/native-retention/run.py \
--references /tmp/opcua2-publish-hg100-20261110/linux-arm64/host \
--output /tmp/opcua2-native-reload-FRESH
```
This builds only the standalone control against existing actual SDK DLLs, never
rebuilds product code, requires the exact installed runtime diagnostic layout,
and refuses an existing output directory. The fixed repetition count is not
configurable. Use a private fresh root and serialize with other ownership/native
controls. The checked-in selected receipts are in `evidence/2026-11-10/`;
raw artifacts are `/tmp/opcua2-native-reload-20261110a`. Raw heap/lsof output and
any failed-run private fixture material must not be redistributed wholesale.
No long run, deployment, trust weakening, SDK fork/version change, budget change,
public upstream post or #799 closure is authorized or claimed by these results.
@@ -0,0 +1,176 @@
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;
internal static class ReloadControl
{
const string Uri = "urn:localhost:NativeRetention.Control";
const int Repetitions = 200;
static readonly byte[] Challenge = Encoding.UTF8.GetBytes("native-retention-diagnostic-only");
static string Root = "";
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<ApplicationInstance> 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 int RuntimeTemporaryKeychainCount()
{
var assembly = typeof(X509Certificate2).Assembly;
Require(assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion == "10.0.14+0427b9abbead3fa64c6655eb4290c537397131fa", "Exact runtime diagnostic build changed");
var type = assembly.GetType("System.Security.Cryptography.Apple.SafeTemporaryKeychainHandle", throwOnError: true)!;
var field = type.GetField("s_lookup", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
Require(field is not null && field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(Dictionary<,>)
&& field.FieldType.GetGenericArguments()[0] == typeof(IntPtr) && field.FieldType.GetGenericArguments()[1] == type,
"Exact runtime keychain dictionary layout changed");
var lookup = (IDictionary)(field!.GetValue(null) ?? throw new InvalidOperationException("Runtime keychain dictionary absent"));
Require(Monitor.TryEnter(lookup, TimeSpan.FromSeconds(1)), "Runtime keychain monitor did not become available");
try { return lookup.Count; }
finally { Monitor.Exit(lookup); }
}
static void ValidateKey(Certificate certificate, string expected, bool sign)
{
Require(certificate.HasPrivateKey && Hash(certificate) == expected, "Wrong/missing private certificate");
if (!sign) return;
using var privateKey = certificate.GetRSAPrivateKey();
using var publicKey = certificate.GetRSAPublicKey();
Require(privateKey is not null && publicKey is not null, "Expected normal SDK RSA application certificate");
var signature = privateKey!.SignData(Challenge, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
Require(publicKey!.VerifyData(Challenge, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1), "Private key mismatch");
}
static async Task Stage(string mode, string stage, int operations, bool disposed, CancellationToken ct)
{
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
await Task.Delay(250, ct);
using var process = Process.GetCurrentProcess(); process.Refresh();
Console.WriteLine(JsonSerializer.Serialize(new { kind = "checkpoint", mode, stage, operations, processId = Environment.ProcessId,
utc = DateTimeOffset.UtcNow, managerDisposed = disposed, managedBytes = GC.GetTotalMemory(false), rssBytes = process.WorkingSet64,
privateBytes = process.PrivateMemorySize64, allocatedBytes = GC.GetTotalAllocatedBytes(), compiledMethods = JitInfo.GetCompiledMethodCount(),
gc = new[] { GC.CollectionCount(0), GC.CollectionCount(1), GC.CollectionCount(2) },
certificateCoresCreated = Certificate.InstancesCreated, certificateCoresDisposed = Certificate.InstancesDisposed,
certificateCoresOutstanding = Certificate.InstancesLeaked,
runtimeTrackedTemporaryKeychains = RuntimeTemporaryKeychainCount() }));
Console.Out.Flush();
while (!File.Exists(Path.Combine(Root, "continue-" + stage))) await Task.Delay(25, ct);
}
static async Task Arm(string mode, CancellationToken ct)
{
var telemetry = DefaultTelemetry.Create(_ => { });
var pki = Path.Combine(Root, "pki");
var before = Directory.GetFiles(pki, "*.der", SearchOption.AllDirectories).Where(p => p.Contains(Path.DirectorySeparatorChar + "own" + Path.DirectorySeparatorChar + "certs" + Path.DirectorySeparatorChar, StringComparison.Ordinal))
.Select(p => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(p)))).Order().ToArray();
Require(before.Length == 1, "Expected one copied own DER certificate");
ApplicationInstance? app = null;
try
{
app = await Open(pki, telemetry, ct);
var config = app.ApplicationConfiguration!;
var manager = config.CertificateManager;
string expected;
using (var entry = manager.AcquireApplicationCertificateBySecurityPolicy(SecurityPolicies.Basic256Sha256))
{
Require(entry is not null, "Normal initialization did not populate registry");
expected = Hash(entry!.Certificate); ValidateKey(entry.Certificate, expected, true);
}
await Stage(mode, "ready", 0, ManagerDisposed(manager), ct);
for (var i = 1; i <= Repetitions; i++)
{
if (mode == "store")
{
using var certificate = await config.SecurityConfiguration.FindApplicationCertificateAsync(
SecurityPolicies.Basic256Sha256, true, telemetry, ct).WaitAsync(ct);
Require(certificate is not null, "Configured-store acquisition failed");
ValidateKey(certificate!, expected, i is 1 or Repetitions);
}
else
{
using var entry = manager.AcquireApplicationCertificateBySecurityPolicy(SecurityPolicies.Basic256Sha256);
Require(entry is not null, "Registry acquisition failed");
ValidateKey(entry!.Certificate, expected, i is 1 or Repetitions);
}
if (i is 1 or Repetitions) await Stage(mode, i == 1 ? "first" : "repeated", i, ManagerDisposed(manager), ct);
}
await app.DisposeAsync().AsTask().WaitAsync(ct); app = null;
await Stage(mode, "disposed", Repetitions, ManagerDisposed(manager), ct);
await Task.Delay(TimeSpan.FromSeconds(5), ct);
await Stage(mode, "idle", Repetitions, ManagerDisposed(manager), ct);
// Preserve managed and native post-disposal observations before any ownership assertion rejects the arm.
Require(ManagerDisposed(manager), "Application manager not disposed");
Require(Certificate.InstancesLeaked == 0, "SDK certificate cores remain after owner disposal");
Require(RuntimeTemporaryKeychainCount() == 0, "Runtime tracked temporary keychain owners remain after application disposal");
var after = Directory.GetFiles(pki, "*.der", SearchOption.AllDirectories).Where(p => p.Contains(Path.DirectorySeparatorChar + "own" + Path.DirectorySeparatorChar + "certs" + Path.DirectorySeparatorChar, StringComparison.Ordinal))
.Select(p => Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(p)))).Order().ToArray();
Require(after.SequenceEqual(before), "Own application certificate changed");
Console.WriteLine(JsonSerializer.Serialize(new { kind = "complete", mode, operations = Repetitions, sameLeafSha256 = expected,
certificateUnchanged = true, managerDisposed = true, certificateCoresOutstanding = Certificate.InstancesLeaked, fullSoakGate = false }));
}
finally { if (app is not null) await app.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(10)); }
}
public static async Task<int> Main(string[] args)
{
Root = Path.GetFullPath(args[1]); Directory.CreateDirectory(Root);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(180));
try
{
Require(OperatingSystem.IsMacOS(), "This control requires macOS native inventory");
Require(Environment.Version.ToString() == "10.0.14", "Matched runtime must be .NET 10.0.14");
if (args[0] == "seed")
{
await using var app = await Open(Path.Combine(Root, "pki"), DefaultTelemetry.Create(_ => { }), deadline.Token);
using var entry = app.ApplicationConfiguration!.CertificateManager.AcquireApplicationCertificateBySecurityPolicy(SecurityPolicies.Basic256Sha256);
Require(entry is not null, "Seed registry not populated");
Console.WriteLine(JsonSerializer.Serialize(new { kind = "seed", leafSha256 = Hash(entry!.Certificate), runtime = Environment.Version.ToString(),
identifierPublicProperties = typeof(CertificateIdentifier).GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => p.Name).Order().ToArray(),
identifierPublicConstructors = typeof(CertificateIdentifier).GetConstructors().Select(c => c.ToString()).ToArray(),
cryptographyRuntime = new { file = typeof(X509Certificate2).Assembly.Location,
informationalVersion = typeof(X509Certificate2).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion,
sha256 = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(typeof(X509Certificate2).Assembly.Location))).ToLowerInvariant() } }));
return 0;
}
Require(args[0] is "store" or "registry", "Unknown control mode");
await Arm(args[0], deadline.Token); return 0;
}
catch (Exception error)
{
Console.WriteLine(JsonSerializer.Serialize(new { kind = "failed", errorType = error.GetType().FullName, message = error.Message }));
return 1;
}
}
}
@@ -0,0 +1,9 @@
Determining projects to restore...
Restored /private/tmp/opcua2-native-reload-20261110a/Control.csproj (in 34 ms).
Control -> /private/tmp/opcua2-native-reload-20261110a/bin/Release/net10.0/Control.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:00.63
@@ -0,0 +1,3 @@
{
"allOwnedFixtureDirectoriesRemoved": true
}
@@ -0,0 +1,34 @@
{
"controlSourceSha256": "e8c959a076bf1eecaad7e1ae71d8df3c2c86d2978a4558c98792471e880efc27",
"runnerSourceSha256": "159ba76b3048d1a594e3a28032d350e36bc8ead4c93c525c5a60b3013699aeef",
"repetitionsPerArm": 200,
"stages": [
"ready",
"first",
"repeated",
"disposed",
"idle"
],
"idleSeconds": 5,
"fullSoakGate": false,
"leakTracking": true,
"trackingOverhead": "Both A/B processes capture allocation stacks; additional allocation and GC overhead is included in observed memory.",
"nativeReferences": {
"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"
},
"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 DOTNET_ROOT [/opt/homebrew/Cellar/dotnet/10.0.403/libexec]\n\nglobal.json file:\n <user-home>/.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",
"os": "ProductName:\t\tmacOS\nProductVersion:\t\t26.5.2\nBuildVersion:\t\t25F84\n",
"architecture": "arm64",
"cryptographyRuntime": {
"file": "/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.NETCore.App/10.0.14/System.Security.Cryptography.dll",
"informationalVersion": "10.0.14+0427b9abbead3fa64c6655eb4290c537397131fa",
"sha256": "f4957d7218a004a27bf8964e45147df6a88092c6e7344582082840d62c6c579a"
}
}
@@ -0,0 +1,6 @@
{"kind":"checkpoint","mode":"registry","stage":"ready","operations":0,"processId":55851,"utc":"2026-11-10T21:30:30.043224+00:00","managerDisposed":false,"managedBytes":831128,"rssBytes":87654400,"privateBytes":0,"allocatedBytes":1761744,"compiledMethods":1192,"gc":[2,2,2],"certificateCoresCreated":7,"certificateCoresDisposed":6,"certificateCoresOutstanding":1,"runtimeTrackedTemporaryKeychains":1}
{"kind":"checkpoint","mode":"registry","stage":"first","operations":1,"processId":55851,"utc":"2026-11-10T21:30:31.093095+00:00","managerDisposed":false,"managedBytes":1055944,"rssBytes":91095040,"privateBytes":0,"allocatedBytes":2051888,"compiledMethods":1777,"gc":[4,4,4],"certificateCoresCreated":7,"certificateCoresDisposed":6,"certificateCoresOutstanding":1,"runtimeTrackedTemporaryKeychains":1}
{"kind":"checkpoint","mode":"registry","stage":"repeated","operations":200,"processId":55851,"utc":"2026-11-10T21:30:32.143169+00:00","managerDisposed":false,"managedBytes":1098664,"rssBytes":97779712,"privateBytes":0,"allocatedBytes":7937936,"compiledMethods":2093,"gc":[6,6,6],"certificateCoresCreated":7,"certificateCoresDisposed":6,"certificateCoresOutstanding":1,"runtimeTrackedTemporaryKeychains":1}
{"kind":"checkpoint","mode":"registry","stage":"disposed","operations":200,"processId":55851,"utc":"2026-11-10T21:30:33.170061+00:00","managerDisposed":true,"managedBytes":1104720,"rssBytes":97927168,"privateBytes":0,"allocatedBytes":7949840,"compiledMethods":2207,"gc":[8,8,8],"certificateCoresCreated":7,"certificateCoresDisposed":7,"certificateCoresOutstanding":0,"runtimeTrackedTemporaryKeychains":0}
{"kind":"checkpoint","mode":"registry","stage":"idle","operations":200,"processId":55851,"utc":"2026-11-10T21:30:39.166632+00:00","managerDisposed":true,"managedBytes":1130616,"rssBytes":97927168,"privateBytes":0,"allocatedBytes":7966976,"compiledMethods":2251,"gc":[10,10,10],"certificateCoresCreated":7,"certificateCoresDisposed":7,"certificateCoresOutstanding":0,"runtimeTrackedTemporaryKeychains":0}
{"kind":"complete","mode":"registry","operations":200,"sameLeafSha256":"fadcc11ec694732b82fe4dc47bb41bce6023d113339e53aca9fee5b079f200b2","certificateUnchanged":true,"managerDisposed":true,"certificateCoresOutstanding":0,"fullSoakGate":false}
@@ -0,0 +1,366 @@
{
"status": "passed",
"seed": {
"kind": "seed",
"leafSha256": "fadcc11ec694732b82fe4dc47bb41bce6023d113339e53aca9fee5b079f200b2",
"runtime": "10.0.14",
"identifierPublicProperties": [
"BinaryEncodingId",
"CertificateType",
"CertificateTypeString",
"RawData",
"StorePath",
"StoreType",
"SubjectName",
"Thumbprint",
"TypeId",
"ValidationOptions",
"XmlEncodingId"
],
"identifierPublicConstructors": [
"Void .ctor()"
],
"cryptographyRuntime": {
"file": "/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.NETCore.App/10.0.14/System.Security.Cryptography.dll",
"informationalVersion": "10.0.14+0427b9abbead3fa64c6655eb4290c537397131fa",
"sha256": "f4957d7218a004a27bf8964e45147df6a88092c6e7344582082840d62c6c579a"
}
},
"arms": {
"store": {
"exitCode": 0,
"complete": {
"kind": "complete",
"mode": "store",
"operations": 200,
"sameLeafSha256": "fadcc11ec694732b82fe4dc47bb41bce6023d113339e53aca9fee5b079f200b2",
"certificateUnchanged": true,
"managerDisposed": true,
"certificateCoresOutstanding": 0,
"fullSoakGate": false
},
"samples": [
{
"kind": "checkpoint",
"mode": "store",
"stage": "ready",
"operations": 0,
"processId": 54422,
"utc": "2026-11-10T21:30:07.200103+00:00",
"managerDisposed": false,
"managedBytes": 831080,
"rssBytes": 87621632,
"privateBytes": 0,
"allocatedBytes": 1786744,
"compiledMethods": 1192,
"gc": [
2,
2,
2
],
"certificateCoresCreated": 7,
"certificateCoresDisposed": 6,
"certificateCoresOutstanding": 1,
"runtimeTrackedTemporaryKeychains": 1,
"AppleDatabaseCount": 3,
"AppleDatabaseShallowBytes": 2304,
"fdCount": 110,
"pkiFdCount": 0,
"temporaryDirectoryFdCount": 1,
"temporaryKeychainFdCount": 0,
"temporaryKeychainFileCount": 1
},
{
"kind": "checkpoint",
"mode": "store",
"stage": "first",
"operations": 1,
"processId": 54422,
"utc": "2026-11-10T21:30:10.186803+00:00",
"managerDisposed": false,
"managedBytes": 1112360,
"rssBytes": 91619328,
"privateBytes": 0,
"allocatedBytes": 2248768,
"compiledMethods": 2073,
"gc": [
4,
4,
4
],
"certificateCoresCreated": 9,
"certificateCoresDisposed": 8,
"certificateCoresOutstanding": 1,
"runtimeTrackedTemporaryKeychains": 1,
"AppleDatabaseCount": 4,
"AppleDatabaseShallowBytes": 3072,
"fdCount": 110,
"pkiFdCount": 0,
"temporaryDirectoryFdCount": 1,
"temporaryKeychainFdCount": 0,
"temporaryKeychainFileCount": 1
},
{
"kind": "checkpoint",
"mode": "store",
"stage": "repeated",
"operations": 200,
"processId": 54422,
"utc": "2026-11-10T21:30:21.723766+00:00",
"managerDisposed": false,
"managedBytes": 1191400,
"rssBytes": 115097600,
"privateBytes": 0,
"allocatedBytes": 31247912,
"compiledMethods": 4552,
"gc": [
9,
6,
6
],
"certificateCoresCreated": 407,
"certificateCoresDisposed": 406,
"certificateCoresOutstanding": 1,
"runtimeTrackedTemporaryKeychains": 1,
"AppleDatabaseCount": 203,
"AppleDatabaseShallowBytes": 155904,
"fdCount": 112,
"pkiFdCount": 0,
"temporaryDirectoryFdCount": 1,
"temporaryKeychainFdCount": 0,
"temporaryKeychainFileCount": 1
},
{
"kind": "checkpoint",
"mode": "store",
"stage": "disposed",
"operations": 200,
"processId": 54422,
"utc": "2026-11-10T21:30:22.710567+00:00",
"managerDisposed": true,
"managedBytes": 1196408,
"rssBytes": 115212288,
"privateBytes": 0,
"allocatedBytes": 31265712,
"compiledMethods": 4618,
"gc": [
11,
8,
8
],
"certificateCoresCreated": 407,
"certificateCoresDisposed": 407,
"certificateCoresOutstanding": 0,
"runtimeTrackedTemporaryKeychains": 0,
"AppleDatabaseCount": 203,
"AppleDatabaseShallowBytes": 155904,
"fdCount": 112,
"pkiFdCount": 0,
"temporaryDirectoryFdCount": 1,
"temporaryKeychainFdCount": 0,
"temporaryKeychainFileCount": 0
},
{
"kind": "checkpoint",
"mode": "store",
"stage": "idle",
"operations": 200,
"processId": 54422,
"utc": "2026-11-10T21:30:28.704218+00:00",
"managerDisposed": true,
"managedBytes": 1222192,
"rssBytes": 115130368,
"privateBytes": 0,
"allocatedBytes": 31283272,
"compiledMethods": 4638,
"gc": [
13,
10,
10
],
"certificateCoresCreated": 407,
"certificateCoresDisposed": 407,
"certificateCoresOutstanding": 0,
"runtimeTrackedTemporaryKeychains": 0,
"AppleDatabaseCount": 203,
"AppleDatabaseShallowBytes": 155904,
"fdCount": 112,
"pkiFdCount": 0,
"temporaryDirectoryFdCount": 1,
"temporaryKeychainFdCount": 0,
"temporaryKeychainFileCount": 0
}
],
"nativeCountIsNotOpenHandleOrExclusiveRssAttribution": true,
"registryArmIsNotApprovedProductionMitigation": true
},
"registry": {
"exitCode": 0,
"complete": {
"kind": "complete",
"mode": "registry",
"operations": 200,
"sameLeafSha256": "fadcc11ec694732b82fe4dc47bb41bce6023d113339e53aca9fee5b079f200b2",
"certificateUnchanged": true,
"managerDisposed": true,
"certificateCoresOutstanding": 0,
"fullSoakGate": false
},
"samples": [
{
"kind": "checkpoint",
"mode": "registry",
"stage": "ready",
"operations": 0,
"processId": 55851,
"utc": "2026-11-10T21:30:30.043224+00:00",
"managerDisposed": false,
"managedBytes": 831128,
"rssBytes": 87654400,
"privateBytes": 0,
"allocatedBytes": 1761744,
"compiledMethods": 1192,
"gc": [
2,
2,
2
],
"certificateCoresCreated": 7,
"certificateCoresDisposed": 6,
"certificateCoresOutstanding": 1,
"runtimeTrackedTemporaryKeychains": 1,
"AppleDatabaseCount": 3,
"AppleDatabaseShallowBytes": 2304,
"fdCount": 110,
"pkiFdCount": 0,
"temporaryDirectoryFdCount": 1,
"temporaryKeychainFdCount": 0,
"temporaryKeychainFileCount": 1
},
{
"kind": "checkpoint",
"mode": "registry",
"stage": "first",
"operations": 1,
"processId": 55851,
"utc": "2026-11-10T21:30:31.093095+00:00",
"managerDisposed": false,
"managedBytes": 1055944,
"rssBytes": 91095040,
"privateBytes": 0,
"allocatedBytes": 2051888,
"compiledMethods": 1777,
"gc": [
4,
4,
4
],
"certificateCoresCreated": 7,
"certificateCoresDisposed": 6,
"certificateCoresOutstanding": 1,
"runtimeTrackedTemporaryKeychains": 1,
"AppleDatabaseCount": 3,
"AppleDatabaseShallowBytes": 2304,
"fdCount": 110,
"pkiFdCount": 0,
"temporaryDirectoryFdCount": 1,
"temporaryKeychainFdCount": 0,
"temporaryKeychainFileCount": 1
},
{
"kind": "checkpoint",
"mode": "registry",
"stage": "repeated",
"operations": 200,
"processId": 55851,
"utc": "2026-11-10T21:30:32.143169+00:00",
"managerDisposed": false,
"managedBytes": 1098664,
"rssBytes": 97779712,
"privateBytes": 0,
"allocatedBytes": 7937936,
"compiledMethods": 2093,
"gc": [
6,
6,
6
],
"certificateCoresCreated": 7,
"certificateCoresDisposed": 6,
"certificateCoresOutstanding": 1,
"runtimeTrackedTemporaryKeychains": 1,
"AppleDatabaseCount": 3,
"AppleDatabaseShallowBytes": 2304,
"fdCount": 112,
"pkiFdCount": 0,
"temporaryDirectoryFdCount": 1,
"temporaryKeychainFdCount": 0,
"temporaryKeychainFileCount": 1
},
{
"kind": "checkpoint",
"mode": "registry",
"stage": "disposed",
"operations": 200,
"processId": 55851,
"utc": "2026-11-10T21:30:33.170061+00:00",
"managerDisposed": true,
"managedBytes": 1104720,
"rssBytes": 97927168,
"privateBytes": 0,
"allocatedBytes": 7949840,
"compiledMethods": 2207,
"gc": [
8,
8,
8
],
"certificateCoresCreated": 7,
"certificateCoresDisposed": 7,
"certificateCoresOutstanding": 0,
"runtimeTrackedTemporaryKeychains": 0,
"AppleDatabaseCount": 3,
"AppleDatabaseShallowBytes": 2304,
"fdCount": 112,
"pkiFdCount": 0,
"temporaryDirectoryFdCount": 1,
"temporaryKeychainFdCount": 0,
"temporaryKeychainFileCount": 0
},
{
"kind": "checkpoint",
"mode": "registry",
"stage": "idle",
"operations": 200,
"processId": 55851,
"utc": "2026-11-10T21:30:39.166632+00:00",
"managerDisposed": true,
"managedBytes": 1130616,
"rssBytes": 97927168,
"privateBytes": 0,
"allocatedBytes": 7966976,
"compiledMethods": 2251,
"gc": [
10,
10,
10
],
"certificateCoresCreated": 7,
"certificateCoresDisposed": 7,
"certificateCoresOutstanding": 0,
"runtimeTrackedTemporaryKeychains": 0,
"AppleDatabaseCount": 3,
"AppleDatabaseShallowBytes": 2304,
"fdCount": 112,
"pkiFdCount": 0,
"temporaryDirectoryFdCount": 1,
"temporaryKeychainFdCount": 0,
"temporaryKeychainFileCount": 0
}
],
"nativeCountIsNotOpenHandleOrExclusiveRssAttribution": true,
"registryArmIsNotApprovedProductionMitigation": true
}
},
"fullSoakGate": false
}
@@ -0,0 +1 @@
{"kind":"seed","leafSha256":"fadcc11ec694732b82fe4dc47bb41bce6023d113339e53aca9fee5b079f200b2","runtime":"10.0.14","identifierPublicProperties":["BinaryEncodingId","CertificateType","CertificateTypeString","RawData","StorePath","StoreType","SubjectName","Thumbprint","TypeId","ValidationOptions","XmlEncodingId"],"identifierPublicConstructors":["Void .ctor()"],"cryptographyRuntime":{"file":"/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.NETCore.App/10.0.14/System.Security.Cryptography.dll","informationalVersion":"10.0.14\u002B0427b9abbead3fa64c6655eb4290c537397131fa","sha256":"f4957d7218a004a27bf8964e45147df6a88092c6e7344582082840d62c6c579a"}}
@@ -0,0 +1,54 @@
{
"sdkCommit": "07e62f72500cc0923a2bab940653c8c1116fa286",
"sdkCheckoutDirty": "",
"files": [
{
"path": "src/Opc.Ua.Client/Session/DefaultSessionFactory.cs",
"sha256": "366481e7f75fdaae48e2b35574a1624c685b2ae7f004c1c0204068465a737e3c"
},
{
"path": "src/Opc.Ua.Client/Session/Session.cs",
"sha256": "2af9a217a37ec35c99244983a0896277fbec616f4656e68ce9be03356b3a3f20"
},
{
"path": "src/Opc.Ua.Core/Security/Certificates/CertificateIdentifierResolver.cs",
"sha256": "2593c29171e83f8536c2923c9f8a394fadfc738a78532743dd7c1d07c6e7915f"
},
{
"path": "src/Opc.Ua.Core/Security/Certificates/DirectoryCertificateStore.cs",
"sha256": "490b75486ef3278ae6400b1ff47437b0557254ce0d0eff29ff9ebe2f29d50650"
},
{
"path": "src/Opc.Ua.Core/Security/Certificates/SecurityConfiguration.cs",
"sha256": "76222952f07c4cf97326132103684ede51bea46c43dae5f7ca1228f62255ae43"
},
{
"path": "src/Opc.Ua.Core/Schema/ApplicationConfiguration.cs",
"sha256": "0204d16915e317360f922801b59da7be03dafefe11e68672926b25e9e349db22"
},
{
"path": "src/Opc.Ua.Core/Stack/Configuration/ApplicationConfiguration.cs",
"sha256": "119b5c809154291451cfe6e4ae7b4a0c2b838e12f28231c1ac4a876a29e3efc6"
},
{
"path": "src/Opc.Ua.Core/Stack/Server/ServerBase.cs",
"sha256": "b5ba516728998b421b2573316deafbbe774792af99a10558ed0843505b5d3a09"
},
{
"path": "src/Opc.Ua.Core/Security/Certificates/CertificateManager/CertificateManager.cs",
"sha256": "e63a56b4e2cf1183e73737da925ee5f7da3d7c291ecaea361d9ddc6e58ea1695"
},
{
"path": "src/Opc.Ua.Configuration/ApplicationInstance.cs",
"sha256": "b6186e78effbc2301725914098cde8bd948ddce3147ce756b703fd76db1d32b3"
},
{
"path": "src/Opc.Ua.Security.Certificates/X509Certificate/Certificate.cs",
"sha256": "6f0be22e80e05607d1c3e35ce0fe218273446c397c18df909124be61ee437f56"
},
{
"path": "tests/Opc.Ua.Client.Tests/Session/SessionCertificateRotationRegressionTests.cs",
"sha256": "ad904d541ee2f0a08af44ba1ad58eda3db1be19305c3bab43f476fe2c0d71ae6"
}
]
}
@@ -0,0 +1,6 @@
{"kind":"checkpoint","mode":"store","stage":"ready","operations":0,"processId":54422,"utc":"2026-11-10T21:30:07.200103+00:00","managerDisposed":false,"managedBytes":831080,"rssBytes":87621632,"privateBytes":0,"allocatedBytes":1786744,"compiledMethods":1192,"gc":[2,2,2],"certificateCoresCreated":7,"certificateCoresDisposed":6,"certificateCoresOutstanding":1,"runtimeTrackedTemporaryKeychains":1}
{"kind":"checkpoint","mode":"store","stage":"first","operations":1,"processId":54422,"utc":"2026-11-10T21:30:10.186803+00:00","managerDisposed":false,"managedBytes":1112360,"rssBytes":91619328,"privateBytes":0,"allocatedBytes":2248768,"compiledMethods":2073,"gc":[4,4,4],"certificateCoresCreated":9,"certificateCoresDisposed":8,"certificateCoresOutstanding":1,"runtimeTrackedTemporaryKeychains":1}
{"kind":"checkpoint","mode":"store","stage":"repeated","operations":200,"processId":54422,"utc":"2026-11-10T21:30:21.723766+00:00","managerDisposed":false,"managedBytes":1191400,"rssBytes":115097600,"privateBytes":0,"allocatedBytes":31247912,"compiledMethods":4552,"gc":[9,6,6],"certificateCoresCreated":407,"certificateCoresDisposed":406,"certificateCoresOutstanding":1,"runtimeTrackedTemporaryKeychains":1}
{"kind":"checkpoint","mode":"store","stage":"disposed","operations":200,"processId":54422,"utc":"2026-11-10T21:30:22.710567+00:00","managerDisposed":true,"managedBytes":1196408,"rssBytes":115212288,"privateBytes":0,"allocatedBytes":31265712,"compiledMethods":4618,"gc":[11,8,8],"certificateCoresCreated":407,"certificateCoresDisposed":407,"certificateCoresOutstanding":0,"runtimeTrackedTemporaryKeychains":0}
{"kind":"checkpoint","mode":"store","stage":"idle","operations":200,"processId":54422,"utc":"2026-11-10T21:30:28.704218+00:00","managerDisposed":true,"managedBytes":1222192,"rssBytes":115130368,"privateBytes":0,"allocatedBytes":31283272,"compiledMethods":4638,"gc":[13,10,10],"certificateCoresCreated":407,"certificateCoresDisposed":407,"certificateCoresOutstanding":0,"runtimeTrackedTemporaryKeychains":0}
{"kind":"complete","mode":"store","operations":200,"sameLeafSha256":"fadcc11ec694732b82fe4dc47bb41bce6023d113339e53aca9fee5b079f200b2","certificateUnchanged":true,"managerDisposed":true,"certificateCoresOutstanding":0,"fullSoakGate":false}
@@ -0,0 +1,71 @@
{
"purpose": "attribution-only, not a production mitigation or full soak",
"controlRunsPerArm": 1,
"sourceCommit": "ed60970fc294ab171a80cda1b57942a7130b4c39",
"sdkSourceCommit": "07e62f72500cc0923a2bab940653c8c1116fa286",
"rawArtifacts": "/private/tmp/opcua2-native-reload-20261110a",
"artifactCreationToFinalResultWriteWallSeconds": 34.69896697998047,
"wallIntervalIsFilesystemBasedNotMonotonicGate": true,
"sdkFileHashes": {
"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"
},
"installedCryptography": {
"file": "/opt/homebrew/Cellar/dotnet/10.0.403/libexec/shared/Microsoft.NETCore.App/10.0.14/System.Security.Cryptography.dll",
"informationalVersion": "10.0.14+0427b9abbead3fa64c6655eb4290c537397131fa",
"sha256": "f4957d7218a004a27bf8964e45147df6a88092c6e7344582082840d62c6c579a"
},
"checkpointIntervals": {
"store": {
"readyToIdleWallSeconds": 21.504115,
"readyUtc": "2026-11-10T21:30:07.200103+00:00",
"idleUtc": "2026-11-10T21:30:28.704218+00:00"
},
"registry": {
"readyToIdleWallSeconds": 9.123408,
"readyUtc": "2026-11-10T21:30:30.043224+00:00",
"idleUtc": "2026-11-10T21:30:39.166632+00:00"
}
},
"installedRuntimeDecompile": {
"temporaryKeychainHandle": {
"localPath": "/tmp/opcua2-native-runtime-keychain-decompiled.cs",
"sha256": "96c2e996638ffdd17c35483aaece7fdbc499fd004ab549e2e128bf82a64b5dc2",
"tool": "ilspycmd 10.1.0.8386",
"readOnly": true
},
"certificateLoader": {
"localPath": "/tmp/opcua2-native-runtime-loader-decompiled.cs",
"sha256": "89434500d1723f419e83ff45fcb6e44e73b58847f4201265613587ad82a79424",
"tool": "ilspycmd 10.1.0.8386",
"readOnly": true
},
"baseKeychainHandle": {
"localPath": "/tmp/opcua2-native-runtime-keychain-base-decompiled.cs",
"sha256": "6f2abdb5779646a9bfd589d8fc765cab2e0290edee8396b2c7b2e6a4470e2bb4",
"tool": "ilspycmd 10.1.0.8386",
"readOnly": true
}
},
"review": {
"reviewer": "Astra independent security/ownership gate",
"preExecutionCodeApproved": true,
"postExecutionRawHeapAndHashReviewAccepted": true,
"productionMitigationApproved": false,
"scope": "one fixed 200-acquisition run per arm; no public upstream post or production patch"
},
"rawProvenanceSha256": "2bd55b5eddef09f7e4e2fcaab91c98649e62abc084565f88755ceba9eed29bfb",
"selectedProvenanceProjection": {
"redactedFields": [
"hostDotnet local home prefix"
],
"sha256": "36d3820fc3a858f7fc39f35237475fe2a4897a6a38ceee19efbadf9426c6dcd4",
"rawReceiptUnchanged": true
}
}
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
"""Bounded attribution-only macOS SDK private-key reload/registry controls."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import time
import xml.etree.ElementTree as ET
HERE = Path(__file__).resolve().parent
STAGES = ['ready', 'first', 'repeated', 'disposed', 'idle']
def save(path, value):
path.write_text(json.dumps(value, indent=2) + '\n')
def snapshots(pid, root, stage):
# These raw, no-content inventory files remain private/local and are not release artifacts.
heap = subprocess.run(['/usr/bin/heap', '--noContent', '-s', str(pid)], text=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, timeout=20, check=True)
(root / ('heap-' + stage + '.txt')).write_text(heap.stdout)
match = re.search(r'^\s*(\d+)\s+(\d+)\s+[\d.]+\s+Security::AppleDatabase\s', heap.stdout, re.MULTILINE)
if not match:
raise RuntimeError('AppleDatabase inventory class missing; diagnostic cannot silently report zero')
descriptors = subprocess.run(['/usr/sbin/lsof', '-a', '-p', str(pid), '-Fnf'], text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5, check=True)
(root / ('lsof-' + stage + '.txt')).write_text(descriptors.stdout)
total = pki = temporary = keychain = 0
in_descriptor = False
pki_root = (root / 'pki').resolve()
tmp_root = (root / 'tmp').resolve()
for line in descriptors.stdout.splitlines():
if line.startswith('f'):
in_descriptor = len(line) > 1 and line[1].isdigit()
total += int(in_descriptor)
elif in_descriptor and line.startswith('n/'):
target = Path(line[1:].removesuffix(' (deleted)')).resolve()
pki += int(target == pki_root or pki_root in target.parents)
in_temp = target == tmp_root or tmp_root in target.parents
temporary += int(in_temp)
keychain += int(in_temp and '.keychain' in target.name)
if pki:
raise RuntimeError('Quiescent task-owned PKI descriptors are open')
if stage in ['disposed', 'idle'] and keychain:
raise RuntimeError('Owned temporary keychain descriptors remain after application disposal')
return {'stage': stage, 'AppleDatabaseCount': int(match[1]), 'AppleDatabaseShallowBytes': int(match[2]),
'fdCount': total, 'pkiFdCount': pki, 'temporaryDirectoryFdCount': temporary,
'temporaryKeychainFdCount': keychain,
'temporaryKeychainFileCount': sum(p.is_file() and '.keychain' in p.name for p in (root / 'tmp').rglob('*'))}
def arm(app, root, mode):
(root / 'tmp').mkdir()
(root / 'data').mkdir()
env = dict(os.environ, OPCUA_CERTIFICATE_LEAK_TRACKING='1', TMPDIR=str(root / 'tmp'), XDG_DATA_HOME=str(root / 'data'))
with (root / 'samples.jsonl').open('w') as log:
process = subprocess.Popen(['dotnet', str(app), mode, str(root)], stdout=log, stderr=subprocess.STDOUT, env=env)
try:
deadline = time.monotonic() + 180
values = []
for stage in STAGES:
while True:
text = (root / 'samples.jsonl').read_text()
rows = [json.loads(line) for line in text.split('\n')[:-1]]
if any(row.get('kind') == 'failed' for row in rows):
raise RuntimeError('Control failed; see samples.jsonl')
matching = [row for row in rows if row.get('stage') == stage]
if matching:
values.append({**matching[0], **snapshots(process.pid, root, stage)})
(root / ('continue-' + stage)).touch()
break
if process.poll() is not None or time.monotonic() >= deadline:
raise RuntimeError('Control checkpoint did not complete within bound: ' + stage)
time.sleep(.05)
status = process.wait(timeout=15)
rows = [json.loads(line) for line in (root / 'samples.jsonl').read_text().splitlines()]
complete = [row for row in rows if row.get('kind') == 'complete']
if status or len(complete) != 1 or complete[0]['operations'] != 200:
raise RuntimeError('Control failed or acquisition count changed')
result = {'exitCode': status, 'complete': complete[0], 'samples': values,
'nativeCountIsNotOpenHandleOrExclusiveRssAttribution': True,
'registryArmIsNotApprovedProductionMitigation': True}
save(root / 'result.json', result)
return result
finally:
if process.poll() is None:
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
process.wait(timeout=5)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--references', required=True, type=Path, help='Actual immutable published SDK2 DLL directory')
parser.add_argument('--output', required=True, type=Path, help='Fresh private output; contains PKI and native snapshots')
options = parser.parse_args()
if sys.platform != 'darwin':
parser.error('This native attribution control requires macOS')
out = options.output.resolve()
out.mkdir(parents=True, exist_ok=False)
os.chmod(out, 0o700)
sdk = sorted(options.references.resolve().glob('Opc.Ua*.dll'))
if not sdk:
raise RuntimeError('Missing actual SDK assemblies')
project = ET.Element('Project', Sdk='Microsoft.NET.Sdk')
props = ET.SubElement(project, 'PropertyGroup')
for k, v in {'OutputType': 'Exe', 'TargetFramework': 'net10.0', 'ImplicitUsings': 'enable', 'Nullable': 'enable',
'EnableDefaultCompileItems': 'false', 'TreatWarningsAsErrors': 'true'}.items():
ET.SubElement(props, k).text = v
items = ET.SubElement(project, 'ItemGroup')
ET.SubElement(items, 'FrameworkReference', Include='Microsoft.AspNetCore.App')
ET.SubElement(items, 'Compile', Include=str(HERE / 'ReloadControl.cs'))
for dll in sdk:
ref = ET.SubElement(items, 'Reference', Include=dll.stem)
ET.SubElement(ref, 'HintPath').text = str(dll)
ET.indent(project)
ET.ElementTree(project).write(out / 'Control.csproj', encoding='unicode')
sha = lambda p: hashlib.sha256(p.read_bytes()).hexdigest()
provenance = {'controlSourceSha256': sha(HERE / 'ReloadControl.cs'), 'runnerSourceSha256': sha(HERE / 'run.py'),
'repetitionsPerArm': 200, 'stages': STAGES, 'idleSeconds': 5, 'fullSoakGate': False,
'leakTracking': True, 'trackingOverhead': 'Both A/B processes capture allocation stacks; additional allocation and GC overhead is included in observed memory.',
'nativeReferences': {p.name: sha(p) for p in sdk}, 'hostDotnet': subprocess.check_output(['dotnet', '--info'], text=True),
'os': subprocess.check_output(['sw_vers'], text=True), 'architecture': os.uname().machine}
save(out / 'provenance.json', provenance)
with (out / 'build.log').open('w') as log:
subprocess.run(['dotnet', 'build', str(out / 'Control.csproj'), '-c', 'Release', '-m:1', '--nologo'],
stdout=log, stderr=subprocess.STDOUT, timeout=120, check=True)
app = out / 'bin/Release/net10.0/Control.dll'
seed = out / 'seed'
seed.mkdir()
(seed / 'tmp').mkdir()
(seed / 'data').mkdir()
results = {}
try:
with (seed / 'seed.jsonl').open('w') as log:
subprocess.run(['dotnet', str(app), 'seed', str(seed)], stdout=log, stderr=subprocess.STDOUT, timeout=60, check=True,
env=dict(os.environ, TMPDIR=str(seed / 'tmp'), XDG_DATA_HOME=str(seed / 'data')))
seed_rows = [json.loads(x) for x in (seed / 'seed.jsonl').read_text().splitlines()]
assert len(seed_rows) == 1 and seed_rows[0]['kind'] == 'seed'
provenance['cryptographyRuntime'] = seed_rows[0]['cryptographyRuntime']
save(out / 'provenance.json', provenance)
for mode in ['store', 'registry']:
root = out / mode
root.mkdir()
shutil.copytree(seed / 'pki', root / 'pki')
results[mode] = arm(app, root, mode)
assert results['store']['complete']['sameLeafSha256'] == results['registry']['complete']['sameLeafSha256'] == seed_rows[0]['leafSha256']
save(out / 'result.json', {'status': 'passed', 'seed': seed_rows[0], 'arms': results, 'fullSoakGate': False})
finally:
for root in [seed, out / 'store', out / 'registry']:
for name in ['pki', 'tmp', 'data']:
if (root / name).exists():
shutil.rmtree(root / name)
save(out / 'fixture-cleanup.json', {'allOwnedFixtureDirectoriesRemoved': all(not (p / name).exists() for p in [seed, out / 'store', out / 'registry'] for name in ['pki', 'tmp', 'data'])})
print(out)
if __name__ == '__main__':
main()