test: isolate native certificate reload retention on macOS
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -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
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"allOwnedFixtureDirectoriesRemoved": true
|
||||
}
|
||||
+34
@@ -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"
|
||||
}
|
||||
}
|
||||
+6
@@ -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}
|
||||
+366
@@ -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
|
||||
}
|
||||
+1
@@ -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"}}
|
||||
+54
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+6
@@ -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}
|
||||
+71
@@ -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()
|
||||
Reference in New Issue
Block a user