Compare commits

..

1 Commits

Author SHA1 Message Date
Joseph Doherty 91bcd70ab1 test(opcua): preserve original canonical adapter partial ownership proof 2026-11-10 16:08:24 -05:00
61 changed files with 53 additions and 12477 deletions
+1 -1
View File
@@ -63,5 +63,5 @@ lib/
# Subagent-driven-development git worktrees
.worktrees/
# Local OPC UA validation runs; selected receipts belong under docs/plans/evidence.
# OPC UA SDK upgrade verification artifacts
/artifacts/opcua2/
+5 -125
View File
@@ -68,7 +68,7 @@ No optional ManagedSession/DI/AOT/cluster redesign, trust relaxation, silent war
- G1 restored 2.0 graph / break inventory: passed; compilation convergence subsequently passed G2.
- G2 compilation convergence: passed; whole-solution Release builds have zero warnings/errors and temporary migration assets are removed.
- G3 behavioral/security/interoperability: pending.
- G4 lifecycle/performance/publish: failed/open for the canonical RSS gate (#799); scoped client lifecycle/publish results are recorded below.
- G4 lifecycle/performance/publish: pending.
- Astra security/ownership source findings: closed for the reviewed identity, history, audit and browse changes; final release review remains pending.
No upgraded artifact is approved for release.
@@ -396,7 +396,7 @@ only a **prospective diagnostic saturation design**, recorded in the report:
384 fixed canonical cycles/96 restarts plus one settling cycle within1,800 seconds;
fixed traced windows321–352 and353–384; zero generated-factory compilations,
new SDK assembly loads and lost events; ownership checks active throughout.
That experiment had not run at that review; its later diagnostic result is recorded below and cannot pass G4. Matched full acceptance requires
That experiment has not run and cannot pass G4. Matched full acceptance requires
an original-SDK adapter for the identical canonical workload and at least7,200
seconds **after** a prospectively reviewed fixed warmup. Historical failures and
macOS native retention remain open in #799. No release approval was given.
@@ -418,8 +418,9 @@ master push; [immutable package/test receipt](evidence/2026-11-10-historiangw-13
Independent actual-package SDK2 Gateway verification also passed 523/0/6.
The integrated SDK2/HG 1.0 solution built with 0 warnings/errors in 46.91 seconds.
CI3 and the corrected follow-up run are recorded below; every command preserves
test/guard exits and the 12,000 execution floor. CI2 remains historical pre-HG 1.0 evidence.
Fresh CI3 is running under `artifacts/opcua2/f2a8ce13/solution-tests3/`, preserving
its test/guard exits and the 12,000 execution floor. CI2 remains the historical
pre-HG 1.0 result; CI3 final counters will be recorded after completion.
All four self-contained Host/CLI publishes and actual Linux Host two-start/stop
checks were repeated on the HG 1.0 graph at `f2a8ce13`: all passed. Both Host
@@ -428,124 +429,3 @@ SDK family remains 2.0.1. Six strict HMAC/peer cases, two Good published-CLI rea
preserved certificate identity and zero task-secret matches passed. See the
[fresh publication/runtime report](evidence/2026-11-10-opcua2/publish-smoke-hg100.md).
Windows actual install/runtime #800 and live/paired-environment #801 remain pending.
## CI3 investigation and corrected final verification
The post-HG 1.0 full CI3 run at `f2a8ce13` produced **63 TRX, 19,124 total,
18,489 executed, 18,472 passed, 17 failed and 635 existing skipped** results.
Test exit 1 and execution guard 0 are retained in
[the complete summary](evidence/2026-11-10-opcua2/solution-tests3.json).
Eleven failures match the established baseline issues #796/#798. Five browser
failures came from a task simulator with differing advertised/internal and host
ports. Matching those ports restores browser 5/5 and driver 5/5 with no source
change. Actual SDK1.5.378.182 browser binaries also fail the different-port
advertisement with the same DNS error: [baseline NAT parity](evidence/2026-11-10-opcua2/browser-nat-parity.md).
The preexisting browser capability gap is [#806](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/806),
not a new SDK2 behavior or a security-policy workaround.
The final failure was a preexisting Runtime test-ordering race: an EF-presence
wait captured the first apply before its later NodeAdded announcement. A gated
controlled reproduction proves 1→2 with one EF and one announcement. The narrow
repair awaits correlated clean completion replies for both rebuilds, retaining
all idempotence assertions and existing budgets. Astra reviewed source causality
and RED/GREEN proof. SDK2 and independent SDK1.5 owning Runtime suites each pass
**1,002 tests, zero failures and 3 existing skips**. The test-only fix landed on
master `52d1373e`, merged at `75057d8c`; [#805](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/805)
closed with validation. [Exact repair evidence](evidence/2026-11-10-opcua2/rebuild-test-barrier.md).
No product code, expected result, timing budget or skip was changed.
The complete solution rebuilt at `75057d8c` with **zero warnings/errors**, 26.61 s.
Fresh full CI4 with the corrected simulator and completion-barrier repair completed
at `75057d8c`: **63 TRX, 19,124 total, 18,489 executed, 18,478 passed, 11 failed,
635 existing skips**. Test exit **1**, execution guard **0** at floor12,000.
All eleven remaining failures are the established ten PercentGood cases (#796)
and one ScriptWorker timing assertion (#798). The fixture and rebuild-ordering
cases pass in this same full run, with no additional failure classes or new skips.
[Versioned counters, exact failures and per-project hashes](evidence/2026-11-10-opcua2/solution-tests4.json)
retain the nonzero status. CI3 remains available; this baseline-matching result
is **not a green solution run**. Task-owned simulator was removed after completion.
## Canonical candidate generated-factory saturation result
The exact immutable bundle reviewed by Astra completed **384 canonical cycles,
96 restarts and one settling cycle in 1,425.242 seconds**, inside the fixed 1,800-second
cutoff. Both fixed trace windows and the continuous attached interval had zero
SDK generated-factory JIT/load events, zero new SDK assembly loads and zero lost
events. The observer detached/drained before settling; shutdown ownership checks
passed. This meets only the selected generated-factory readiness criterion.
The unchanged cold cap rejected at cycle 248: RSS 362,409,984 >358,838,272 bytes.
The original assertion was rethrown; test/container/launcher exit 1, no ordinary
completion or full-duration flag. Peak RSS 394,956,800 bytes; final managed memory
19,974,280 bytes. Allocation StackTrace tracking was enabled, so this is additional
instrumented diagnostic overhead and cannot serve as uninstrumented performance
or acceptance evidence. [Scoped report and hashed receipts](evidence/2026-11-10-opcua2/saturation-candidate.md).
#799 and G4 remain failed/open. A matched original-SDK canonical adapter is being
prepared before any further acceptance-protocol review; no warmed replacement
baseline, budget waiver, factory/runtime setting or production patch was adopted.
## Original canonical adapter limitation and final review boundary
The [original-SDK canonical adapter](../../scripts/validation/opcua-lifecycle-memory/baseline-canonical/README.md)
compiles against the unchanged `83ed880e` publications with zero warnings/errors;
its package/input/source hashes are preserved. A short normal-cycle control
completed encrypted sessions, typed writes/readbacks, all ten history calls,
revocation, surgery/rebuild, stalled-read recovery, subscription cleanup and both
client closes. The retained closed Session object still held one browse and six
history points while SessionManager had zero active sessions; the copied assertion
fails with exit1, including after a separate fixed250ms observation. Exact1.5
binary inspection shows Close removes diagnostics, while Dispose clears those
lists. Held-object state does not prove live server retention or an unbounded leak.
A separate active-session restart control cleared the observable session/
subscription/item/CP counts, then returned **exit2: unsupported ownership parity**.
Original ApplicationInstance/CertificateValidator have no SDK2 manager-disposal
observable; cached store references1→1 are not file-handle counts. Unsupported
metrics were not fabricated as zeros. This is not a passing four-cycle sequence,
a matched process-memory host, an accepted RSS denominator or a long-run result.
Astra independently verified208 reference hashes, all short-run projections and
the exact immutable close/dispose behavior. The diagnostic package is reviewable;
shared semantic/token/weak-root/resource-owner checks remain prerequisites to a
new acceptance procedure.
Astra's final integrated verdict is **source/security review closed, release HOLD**.
It independently verified all63 final CI4 TRX hashes/counters and exact baseline
failure attribution, the two-hour client receipts and428-file recursive manifest,
the candidate saturation result and original failure preservation, and the
original adapter's explicit limitations. No new must-fix migration source/security
finding remains. **CI remains red on11 baseline cases; G3 external evidence and
G4 resource/performance acceptance remain unresolved.** #799, #800, #801 and
ScadaBridge#121 stay open. Neither an SDK version substitution, production crypto/
session-factory workaround, resource-cap waiver nor warmed acceptance baseline
was approved. SDK2 remains on draft PR#802; it is not merged to master or deployed.
## Follow-on #799 close ownership and native attribution
The [candidate bounded close control](evidence/2026-11-10-opcua2/close-ownership-candidate.md)
at `564f1091` passed with one abandoned browse and six abandoned history points.
Old authentication returns exactly BadSessionIdInvalid; old continuation tokens
are rejected without a history-backend read while fresh-session positives work.
All 23 old-object weak targets collect with the same host still running; all 13
application/server/manager/core/store targets collect after normal shutdown.
SDK2's stronger `(0,0)` closed-list check, two manager-disposal checks, empty
caches and zero PKI descriptors remain enforced. Astra verified the actual TRX.
Six affected history tests also pass; their original launcher mistakenly expected
seven, and the retained false guard receipt has a separate independently reviewed
six-test source/TRX correction. No failed test was relabelled or rerun for that fix.
The [fixed macOS store-versus-registry control](../../scripts/validation/opcua-lifecycle-memory/native-retention/README.md)
completed its execution/ownership checks, **not native-retention acceptance**.
Two hundred configured-store acquisitions leave AppleDatabase inventory 3→203;
registry AddRefs remain 3→3. After normal disposal and five seconds idle, both
arms have zero SDK certificate owners, runtime-tracked temporary keychains,
temporary-keychain files, keychain descriptors and PKI descriptors. The installed
runtime calls CFRelease after native deletion; a missing managed release is not
indicated. Native ownership/delete status, complete RSS attribution and a supported
mitigation remain unresolved. Registry substitution would change deliberate
configured-store replacement semantics and was not adopted.
These results leave the original cold RSS failures and #799/G4 unchanged. The
[prospective resolution procedure](../../scripts/validation/opcua-lifecycle-memory/RESOLUTION.md)
is explicitly a design draft, not approval to launch a warmed acceptance run.
+10 -22
View File
@@ -77,17 +77,13 @@ unexecuted operator or duration gates. Keep secrets out of logs and release reco
| Production client lifecycle | **7,200.984 seconds,5,420 cycles,43,360 verified manager disposals**, zero weak product survivors and PKI descriptors across543 samples. [Exact source/runtime/counter receipts](../../scripts/validation/opcua-client-lifecycle/evidence/2026-11-10-full/verification.json). This is a client lifecycle pass, not a server RSS or full G4 pass. |
| Source review | Independent reviews covered identity, ownership, aggregate preservation, hierarchy and filtered audit paths. [Review scope](2026-11-10-opcua-sdk-2-evidence.md#integrated-implementation-and-source-reviews). Source review is not release approval. |
The final corrected CI-equivalent run at `75057d8c` completed with **63 TRX,
19,124 total, 18,489 executed, 18,478 passed, 11 failed and 635 existing skips**.
Test exit **1**; execution guard **0** at floor12,000. It is not green: the eleven
failures are ten PercentGood cases and one ShadowMode250ms assertion, independently
reproduced on baseline. [Final counters/failures/project hashes](evidence/2026-11-10-opcua2/solution-tests4.json).
CI3's extra five simulator-advertisement failures were corrected by a directly
reachable matching-port fixture; the baseline Runtime test-ordering race was fixed
through correlated completion barriers (#805, master52d1373e). Both corrections
pass in the final full run. The retained CI3 report and original SDK1.5 NAT parity
are in the [main evidence](2026-11-10-opcua-sdk-2-evidence.md#ci3-investigation-and-corrected-final-verification);
preexisting browser NAT behavior remains #806.
The second full CI-equivalent run completed with **63 TRX, 19,121 discovered,
18,486 executed, 18,475 passed, 11 failed and 635 existing skips**. Test exit was
**1**; execution guard exit was **0** at floor 12,000. It is not green: the exact
failures are ten unchanged PercentGood cases and one ShadowMode 250 ms timing
assertion (585.7377 ms), independently reproduced on baseline, with no new failure
classes. [Versioned CI2 counters and exact failures](evidence/2026-11-10-opcua2/solution-tests2.json); provenance is in the
[main evidence](2026-11-10-opcua-sdk-2-evidence.md#solution-performance-and-package-evidence).
The ten PercentGood failures are tracked in
[#796](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/796), and the separately
reproduced baseline ScriptWorker timing failure is
@@ -107,13 +103,6 @@ Shared/driver/reinitialize/browser Linux lifecycle run passed its actual two-hou
duration and cleanup checks, as recorded above; it does not replace this failed
server workload. See [long-run scope](2026-11-10-opcua-sdk-2-evidence.md#resource-finding-and-long-run-scope)
for instrumentation limitations and final-result updates.
The fixed candidate saturation diagnostic later met its selected generated-factory
criterion, but retained the cold RSS rejection and exit1. The original-SDK adapter
records partial wire coverage and unsupported ownership/process-memory parity;
it supplies no approved performance denominator. See [scoped saturation result](evidence/2026-11-10-opcua2/saturation-candidate.md)
and [original adapter limitations](../../scripts/validation/opcua-lifecycle-memory/baseline-canonical/README.md).
Astra's final source/security review is closed; release remains on HOLD.
| Operator release field | Still required before its release decision |
|---|---|
@@ -336,8 +325,7 @@ RSS gate is failed (#799), full CI retains the 11 baseline failures (#796/#798),
the client lifecycle run completed two hours with cleanup checks passed, secure
ScadaBridge interoperability is blocked (ScadaBridge#121), and
Windows/live AVEVA/pair/operator gates remain pending (#800/#801). No fleet rollout
or rollback has been executed. The final post-HG1.0 corrected full run retains
only the eleven independently reproduced baseline failures; earlier failed runs
remain recorded. The coordinator records remaining decisions in #782 and the main
evidence; none is inferred from a
or rollback has been executed. The fresh post-HG1.0 solution CI3 run remains in
progress; its final counters will supplement the retained CI2 failures. The
coordinator records remaining decisions in #782 and the main evidence; none is inferred from a
passing subset or cross-published artifact.
@@ -1,23 +0,0 @@
# SDK1.5 Browser different-port NAT parity
Read-only application validation using existing Release Browser integration DLLs in `/Users/dohertj2/.codex/worktrees/historiangw-client-040/OtOpcUa` (current HEAD `52d1373e8129b7f4f0ea2e35f86a8a5d0ed1de36`). No repository edits, build, or restore. DLL dependency graph confirms all five OPCFoundation packages `1.5.378.182`; exact DLL hashes/package versions are in `metadata.json`.
One task-owned fixture was started from cached `mcr.microsoft.com/iotedge/opc-plc:2.14.10` with `--pn=50000 --ut --aa --alm`, hostname `otopcua-sdk1-browser-nat-parity`, and localhost ephemeral mapping `127.0.0.1:32773 -> 50000`. Image ID/digest and Docker configuration are recorded in `container-metadata.txt` and `image-digest.txt`. No shared fixture, volume, or port was changed.
All five original SDK1.5 Browser integration cases failed: **0 passed, 5 failed, 0 skipped**; test exit1, execution guard exit0. Each failed with `System.Net.Sockets.SocketException: nodename nor servname provided, or not known` in the SDK1.5 `DefaultSessionFactory.CreateChannelAsync/CreateAsync` path. `browser-sdk1-nat.trx` and `test.log` contain the actual failures.
A standalone scratch discovery probe referencing those exact baseline `Opc.Ua.*` DLLs called public `DiscoveryClient.CreateAsync` and `GetEndpointsAsync` against localhost:32773. It succeeded (exit0) and captured all seven advertised endpoints at `opc.tcp://otopcua-sdk1-browser-nat-parity:50000/`, including SecurityPolicy.None/MessageSecurityMode.None. See `get-endpoints.log` and `discovery-probe/{Program.cs,probe.csproj}`. The advertised hostname/port are therefore the actual discovery response, not inferred from configuration alone.
This establishes the same browser failure on SDK1.5 when discovery advertises an unreachable Docker hostname/internal port. It supports classification as a preexisting NAT/advertisement limitation exposed by the test fixture mapping, rather than an SDK2-only regression. Root's matching-port SDK2 fixture pass is separate evidence.
Command:
```sh
OPCUA_SIM_ENDPOINT=opc.tcp://127.0.0.1:32773 dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests.csproj -c Release --no-build --no-restore --logger 'trx;LogFileName=browser-sdk1-nat.trx' --results-directory /tmp/opcua-browser-sdk1-nat-parity
MIN_EXECUTED=1 bash scripts/ci/assert-not-all-skipped.sh /tmp/opcua-browser-sdk1-nat-parity/browser-sdk1-nat.trx
```
Cleanup removed only `otopcua-sdk1-browser-nat-parity`; cleanup exit0 and exact-name absence recorded in `cleanup.log`. No source/issue changes or commits.
The baseline browser limitation remains open in [#806](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/806). This migration retains its behavior; the fixture is configured as a directly reachable endpoint with identical host/internal/advertised ports. Focused SDK2 browser5/5 and driver5/5 passed after that correction, with no source changes.
@@ -1,200 +0,0 @@
{
"issues": {
"ordering": "#805 closed; master52d1373e",
"nat": "#806 open; SDK1.5 parity confirms preexisting limitation"
},
"evidence": {
"controlledRed": {
"sha256": "986e32c042f8b8525af44647f52d2defbf759ab122eb7494a05d77a0e1e4fbf9",
"counters": {
"total": "1",
"executed": "1",
"passed": "0",
"failed": "1",
"error": "0",
"timeout": "0",
"aborted": "0",
"inconclusive": "0",
"passedButRunAborted": "0",
"notRunnable": "0",
"notExecuted": "0",
"disconnected": "0",
"warning": "0",
"completed": "0",
"inProgress": "0",
"pending": "0"
},
"outcomes": {
"Passed": 0,
"Failed": 1,
"NotExecuted": 0
}
},
"controlledGreen": {
"sha256": "cbc3bf65bc7ad915f8b8697c260181f3cc01321a4904137dcca6fd4ac09b047d",
"counters": {
"total": "1",
"executed": "1",
"passed": "1",
"failed": "0",
"error": "0",
"timeout": "0",
"aborted": "0",
"inconclusive": "0",
"passedButRunAborted": "0",
"notRunnable": "0",
"notExecuted": "0",
"disconnected": "0",
"warning": "0",
"completed": "0",
"inProgress": "0",
"pending": "0"
},
"outcomes": {
"Passed": 1,
"Failed": 0,
"NotExecuted": 0
}
},
"sdk2Runtime": {
"sha256": "238ecd1b10df3d9e173d428f2760c35b5e293b4db67f3e9a2a46d2d9988a27f2",
"counters": {
"total": "1005",
"executed": "1002",
"passed": "1002",
"failed": "0",
"error": "0",
"timeout": "0",
"aborted": "0",
"inconclusive": "0",
"passedButRunAborted": "0",
"notRunnable": "0",
"notExecuted": "0",
"disconnected": "0",
"warning": "0",
"completed": "0",
"inProgress": "0",
"pending": "0"
},
"outcomes": {
"Passed": 1002,
"Failed": 0,
"NotExecuted": 3
}
},
"sdk1Runtime": {
"sha256": "e6b378d324a91d08159b59f675d10af2207a745afc6e0c846564c39ac144b58a",
"counters": {
"total": "1005",
"executed": "1002",
"passed": "1002",
"failed": "0",
"error": "0",
"timeout": "0",
"aborted": "0",
"inconclusive": "0",
"passedButRunAborted": "0",
"notRunnable": "0",
"notExecuted": "0",
"disconnected": "0",
"warning": "0",
"completed": "0",
"inProgress": "0",
"pending": "0"
},
"outcomes": {
"Passed": 1002,
"Failed": 0,
"NotExecuted": 3
}
},
"browserCorrectedFixture": {
"sha256": "ed9db0d9545fef17e8b43429a962a36936b551c2de03535be4b7983343d42ecd",
"counters": {
"total": "5",
"executed": "5",
"passed": "5",
"failed": "0",
"error": "0",
"timeout": "0",
"aborted": "0",
"inconclusive": "0",
"passedButRunAborted": "0",
"notRunnable": "0",
"notExecuted": "0",
"disconnected": "0",
"warning": "0",
"completed": "0",
"inProgress": "0",
"pending": "0"
},
"outcomes": {
"Passed": 5,
"Failed": 0,
"NotExecuted": 0
}
},
"driverCorrectedFixture": {
"sha256": "dc2ac55ca296474d767b29ddca2feaa027cec5793ab6ac6a28a10c98dfb2cb56",
"counters": {
"total": "5",
"executed": "5",
"passed": "5",
"failed": "0",
"error": "0",
"timeout": "0",
"aborted": "0",
"inconclusive": "0",
"passedButRunAborted": "0",
"notRunnable": "0",
"notExecuted": "0",
"disconnected": "0",
"warning": "0",
"completed": "0",
"inProgress": "0",
"pending": "0"
},
"outcomes": {
"Passed": 5,
"Failed": 0,
"NotExecuted": 0
}
},
"sdk1BrowserNatBaseline": {
"sha256": "7111ac68dd46bb1931b0e8c3b4bbb3b85588dbcd94758aec5fcb3e6e205b0487",
"counters": {
"total": "5",
"executed": "5",
"passed": "0",
"failed": "5",
"error": "0",
"timeout": "0",
"aborted": "0",
"inconclusive": "0",
"passedButRunAborted": "0",
"notRunnable": "0",
"notExecuted": "0",
"disconnected": "0",
"warning": "0",
"completed": "0",
"inProgress": "0",
"pending": "0"
},
"outcomes": {
"Passed": 0,
"Failed": 5,
"NotExecuted": 0
}
},
"actorMasterCandidateSourceDiff": {
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"bytes": 0
},
"sdk1GetEndpoints": {
"sha256": "c4400f696ce3a8e8cdc49a4dedc06e4969584cfb47a6ada28c28bf91f75189f7",
"bytes": 858
}
},
"expectedRedFailureRetained": true,
"newProductChanges": false
}
File diff suppressed because it is too large Load Diff
@@ -1,41 +0,0 @@
# Candidate close and weak-root control
The final bounded control at `564f10914357fc34db303b2faf030907663e6779`
passed: one test executed, one passed, no skips; runner elapsed 10.082 seconds.
It creates one abandoned browse point and six abandoned history points through
the same encrypted canonical fixture helpers. Before close, the retained server
session has exactly `(1,6)` points; SDK2's stronger post-close `(0,0)` check remains.
The old authentication token returns exactly `BadSessionIdInvalid` (`0x80250000`).
A fresh session rejects both old continuation tokens with
`BadContinuationPointInvalid`; invalid history does not call the historian.
Fresh reads and the session's own browse/history roundtrips succeed around the
denials, so a generally broken channel cannot satisfy this control.
After the session helper and its completed async state machine leave the isolated
boundary, **all 23 old client/session/point/value/state targets are collectible
while the same server is still running**, in each of five fixed GC observations.
After normal host/application disposal and the fixture boundary, all 13 observed
application/configuration/manager/validation-core/store/server/node-manager/
backend targets are collectible in every round. Independent retained/collectible
observer controls behave as expected. Both SDK2 certificate managers are disposed,
cached stores are empty and actual PKI descriptors are zero.
Earlier one-history-point control receipts remain recorded: `b5986c61` passed
with the original two-status auth whitelist; `6737e367` passed after narrowing to
the observed exact status. Six affected history tests also passed at `6737e367`.
Their launcher mistakenly expected seven tests. The original false guard receipt
is retained; a separate source inventory and TRX review confirms four session
history tests plus two event-paging tests, all executed/passed. No seventh source
test is missing and the tests were not rerun to correct that bookkeeping error.
[Machine-readable observations, commands and raw hashes](close-ownership-candidate.json)
retain all four executions. The current build has zero warnings/errors. Astra
reviewed the design and actual code before each diagnostic execution; it verified
all three close-control TRX files, the aligned 23/13 weak-target result and the
six-test inventory correction independently.
This establishes bounded candidate close semantics and managed collectibility.
It does **not** establish full canonical operation/host parity with the original
standalone adapter, a steady RSS denominator, a resource-budget waiver, macOS
native-retention resolution or G4 acceptance. No production code changed.
@@ -1,43 +0,0 @@
# CI3 Runtime rebuild idempotence assertion investigation
Base: `04d5513af2620f277a56acbb19ae466623fc054f` (SDK2, actual HG1.0), branch `codex/opcua2-rebuild-test-barrier`. SDK `10.0.403`. No production edits, budget changes, expected result changes, skips, or issue actions.
## Cause and classification evidence
`RecordingSink.EnsureFolder` queues `EF:eq-1` before the actor finishes materialisation and calls `AnnounceAddedNodes`, which queues `NA:line-1`. The old test waited only for EF, captured a possibly incomplete first-call count, then slept after sending the second request. Thus it could attribute the first apply's trailing announcement to the second apply. `FinishRebuild` sends the public `RebuildCompleted` outcome after `CompleteAddressSpaceApply` and deferred writes, so it supplies the needed completion barrier.
An independent source comparison against landed SDK1.5 master `230cef4e` found no actor/test changes: `git diff 230cef4e 04d5513a -- src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/OpcUa/OpcUaPublishActorRebuildTests.cs` has zero bytes (`master-candidate-source.diff`). This supports a preexisting test-ordering failure, not an SDK2 production idempotence regression.
## Controlled reproduction
Temporary test-only `AfterEnsureFolder` gate blocked the first apply immediately after EF was recorded. The original EF-presence wait and snapshot therefore captured exactly one call. The second request was queued, the gate released, and both apply-completion counters awaited before the unchanged count assertion. The exact CI failure reproduced: expected 1, actual 2. Diagnostic records `completed calls=EF:eq-1,NA:line-1; completions=2`. There was only one EF and one announcement: no repeated materialisation.
- RED: `controlled-red/controlled-red-diagnostic.trx`, `test-diagnostic.log`, `controlled-red-source.cs`; build exit0, test exit1, guard exit0; 1 failed/0 passed/0 skipped (expected reproduction).
- GREEN: same first-apply gate with explicit count1 while blocked; release, then await first correlated clean `RebuildCompleted` before snapshot and second correlated clean reply before assertion. `controlled-green/controlled-green.trx`, `controlled-green-source.cs`; build0/test0/guard0; 1 passed/0 failed/0 skipped.
- Temporary gates are absent from final source. Only the target test changes: both requests ask for outcomes, both replies must match correlation and be clean, original EF/no-full-rebuild/unchanged-call-count assertions remain, and fixed sleep is removed. Existing `PresenceBudget` and cancellation token are retained.
## Reproduction commands
From the owned worktree, test project is `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/ZB.MOM.WW.OtOpcUa.Runtime.Tests.csproj`.
```sh
dotnet build "$PROJECT" -c Release --no-restore
dotnet test "$PROJECT" -c Release --no-build --no-restore --filter 'FullyQualifiedName~Second_rebuild_with_same_artifact_is_empty_plan_no_op' --logger 'trx;LogFileName=focused.trx' --results-directory "$RESULTS"
# Preserve dotnet exit before guard; guard does not replace test status.
MIN_EXECUTED=1 bash scripts/ci/assert-not-all-skipped.sh "$RESULTS/focused.trx"
# Owning suite omits --filter and uses its own fresh results directory/TRX.
```
## Final validation
Commit: `b5f51406ffc7dcf5b40baa52156ea85e862ab5b4` (one test file; worktree clean).
Final owning project build: exit0, 0 warnings, 0 errors, 2.97s (`final/build.log`, `final/build-status.txt`). Full unfiltered Runtime suite: 1,005 total, 1,002 passed, 0 failed, 3 existing skips, duration1m25s. Test exit0, nonzero execution guard exit0 (`final/runtime-final.trx`, `final/test.log`, `final/guard.log`, `final/status.txt`). Target test is passed in that full-suite TRX. DLL SHA256 is recorded in `final/runtime-dll.sha256`.
The three existing skips are the retired-v3 artifact/composer parity tests `Composer_and_artifact_agree_on_equipment_node_device_host`, `ParseComposition_scoped_flags_cross_cluster_orphan_equipment`, and `Driverbound_equipment_kept_when_driver_in_cluster`. No skips were added or changed. No remaining diagnostics from this work.
Host concurrently ran coordinator validation; this task makes no performance claim. Root owns broader CI/fixture classification and issue writes.
Coordinator landing: master `52d1373e`, merged into SDK2 at `75057d8c`; [#805](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/805) closed after independent SDK1.5 master Runtime validation:1,002passed/0failed/3existing skips, test/guard exits0. No product source changed. Fresh full CI4 follows this repair and fixture correction; it is not assumed green from the owning-suite pass.
@@ -1,352 +0,0 @@
{
"scope": "Instrumented candidate-only canonical saturation diagnostic. No acceptance protocol or baseline change.",
"allocationTracking": {
"enabled": true,
"environment": "OPCUA_CERTIFICATE_LEAK_TRACKING=1",
"overhead": "Exact SDK captures a StackTrace and tracker entry per certificate wrapper. Not an uninstrumented performance/acceptance baseline."
},
"bundle": {
"sourceRevision": "2738d7cc8675298ceb38ed19ca58a9f643036ff1",
"receiptSha256": "e4eed693c80baa29266f58cee4473b1181bc6de8f4c460843bd6b1993bcf7179",
"sdk": "2.0.1",
"gateway": "1.0.0",
"runtime": "self-contained Linux ARM64 10.0.14 for target and observer",
"sha256": "e4eed693c80baa29266f58cee4473b1181bc6de8f4c460843bd6b1993bcf7179"
},
"run": {
"acceptance": false,
"containerExitStatus": 1,
"wallSeconds": 1429.7275004169205,
"sourceRevision": "2738d7cc8675298ceb38ed19ca58a9f643036ff1",
"receiptSha256": "e4eed693c80baa29266f58cee4473b1181bc6de8f4c460843bd6b1993bcf7179",
"descriptorTests": {
"executed": 2,
"skipped": 0
},
"experimentTests": {
"executed": 1,
"skipped": 0
},
"diagnosticOutcome": {
"kind": "saturation-result",
"utc": "2026-11-10T20:30:58.5817344+00:00",
"canonicalCycles": 384,
"settlingCycles": 1,
"restarts": 96,
"phaseSeconds": 1425.2416632,
"readiness": true,
"coldBudgetRejected": true,
"shutdownOwnershipVerified": true,
"acceptance": false
},
"protocolCompleted": true
},
"start": {
"kind": "start",
"utc": "2026-11-10T20:07:13.3303503+00:00",
"processId": 200,
"mode": "saturation",
"requestedSeconds": 1800,
"minimumFullSeconds": 7200,
"build": "2738d7cc8675298ceb38ed19ca58a9f643036ff1",
"sdk": "2.0.1+07e62f72500cc0923a2bab940653c8c1116fa286",
"diagnosticInstrumentation": "SDK2.0.1 CP lists and certificate-validation cached stores; read-only",
"limits": {
"retainedManagedGrowthBytes": 67108864,
"rssGrowthBytes": 134217728,
"fdGrowth": 32
},
"coverage": "encrypted sessions; scalar/Int16[] Raw+UNS; subscription removal; browse abandonment; history release/cross-session/close; surgical changes/rebuild; revocation; paused TCP read/recovery; same-PKI restart"
},
"coldRejection": {
"kind": "budget-rejected",
"cycle": 248,
"seconds": 913.8748808,
"resources": {
"Cycle": 248,
"Seconds": 913.830416,
"Sessions": 0,
"Subscriptions": 0,
"Items": 0,
"BrowsePoints": 0,
"HistoryPoints": 0,
"LiveDataItems": 0,
"LiveEventItems": 0,
"MonitoredNodes": 0,
"Variables": 19,
"Folders": 4,
"Conditions": 1,
"CachedCertificateStores": 2,
"Descriptors": 193,
"PkiDescriptors": 0,
"ManagedBytes": 19961096,
"RssBytes": 362409984,
"AllocatedBytes": 5662357112,
"Gen0": 619,
"Gen1": 496,
"Gen2": 496,
"GcCommittedBytes": 48226304,
"GcHeapBytes": 22015464,
"GcFragmentedBytes": 2054368,
"PinnedObjects": 1,
"PendingFinalizers": 17,
"JitMethods": 75848,
"JitIlBytes": 11437192,
"ActiveTimers": 16,
"Threads": 21
},
"baseline": {
"Cycle": 3,
"Seconds": 7.6451075,
"Sessions": 0,
"Subscriptions": 0,
"Items": 0,
"BrowsePoints": 0,
"HistoryPoints": 0,
"LiveDataItems": 0,
"LiveEventItems": 0,
"MonitoredNodes": 0,
"Variables": 19,
"Folders": 4,
"Conditions": 1,
"CachedCertificateStores": 2,
"Descriptors": 191,
"PkiDescriptors": 0,
"ManagedBytes": 19878208,
"RssBytes": 224620544,
"AllocatedBytes": 86490920,
"Gen0": 7,
"Gen1": 6,
"Gen2": 6,
"GcCommittedBytes": 41967616,
"GcHeapBytes": 20716064,
"GcFragmentedBytes": 837856,
"PinnedObjects": 1,
"PendingFinalizers": 16,
"JitMethods": 35741,
"JitIlBytes": 4635402,
"ActiveTimers": 18,
"Threads": 27
},
"failure": "Shouldly.ShouldAssertException: 362409984L\r\n should be less than or equal to\r\n358838272L\r\n but was not\n at ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.Sdk2LifecycleSoakTests.Repeated_lifecycles_return_resources_to_a_steady_baseline() in tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/Sdk2LifecycleSoakTests.cs:line 133"
},
"observer": {
"protocol": "sdk2-saturation-v1",
"runId": "545b3093-5eab-4348-a72d-926b9d1da7da",
"targetPid": 200,
"observerPid": 109,
"active": false,
"drained": true,
"markersValid": true,
"eventsLost": 0,
"sdkFactoryJit": 0,
"sdkFactoryLoads": 0,
"sdkAssemblyLoads": 0,
"windows": [
{
"name": "window1",
"firstCycle": 321,
"lastCycle": 352,
"startRestarts": 80,
"endRestarts": 88,
"sdkFactoryJit": 0,
"sdkFactoryLoads": 0,
"sdkAssemblyLoads": 0
},
{
"name": "window2",
"firstCycle": 353,
"lastCycle": 384,
"startRestarts": 88,
"endRestarts": 96,
"sdkFactoryJit": 0,
"sdkFactoryLoads": 0,
"sdkAssemblyLoads": 0
}
],
"otherJit": 287,
"loadedNativeBytes": 60760,
"runtimeControlJit": 0,
"configuration": {
"runtimeProvider": "Microsoft-Windows-DotNETRuntime",
"level": "Verbose",
"keywords": "0x18",
"markerProvider": "OtOpcUa-Sdk2-Saturation",
"circularBufferMiB": 16,
"requestRundown": false
},
"acceptance": false
},
"tests": {
"descriptor-controls": {
"counters": {
"total": "2",
"executed": "2",
"passed": "2",
"failed": "0",
"error": "0",
"timeout": "0",
"aborted": "0",
"inconclusive": "0",
"passedButRunAborted": "0",
"notRunnable": "0",
"notExecuted": "0",
"disconnected": "0",
"warning": "0",
"completed": "0",
"inProgress": "0",
"pending": "0"
},
"sha256": "0de2cd05f9f1dbcaee3cff33e3e625723f5dc19b8f2dbcf3fbf1376792bc489c",
"errors": []
},
"saturation": {
"counters": {
"total": "1",
"executed": "1",
"passed": "0",
"failed": "1",
"error": "0",
"timeout": "0",
"aborted": "0",
"inconclusive": "0",
"passedButRunAborted": "0",
"notRunnable": "0",
"notExecuted": "0",
"disconnected": "0",
"warning": "0",
"completed": "0",
"inProgress": "0",
"pending": "0"
},
"sha256": "1b5cba9e1b3c348d0dcbb79ba4c715d0e837602c78afc2c221c64c2e776e825b",
"errors": [
"Shouldly.ShouldAssertException : 362409984L\r\n should be less than or equal to\r\n358838272L\r\n but was not"
]
}
},
"firstSample": {
"Cycle": 1,
"Seconds": 2.7631819,
"Sessions": 0,
"Subscriptions": 0,
"Items": 0,
"BrowsePoints": 0,
"HistoryPoints": 0,
"LiveDataItems": 0,
"LiveEventItems": 0,
"MonitoredNodes": 0,
"Variables": 19,
"Folders": 4,
"Conditions": 1,
"CachedCertificateStores": 2,
"Descriptors": 191,
"PkiDescriptors": 0,
"ManagedBytes": 19275376,
"RssBytes": 222228480,
"AllocatedBytes": 62054576,
"Gen0": 3,
"Gen1": 2,
"Gen2": 2,
"GcCommittedBytes": 47521792,
"GcHeapBytes": 20204744,
"GcFragmentedBytes": 929368,
"PinnedObjects": 1,
"PendingFinalizers": 19,
"JitMethods": 29401,
"JitIlBytes": 4142214,
"ActiveTimers": 12,
"Threads": 27
},
"baselineSample": {
"Cycle": 3,
"Seconds": 7.6451075,
"Sessions": 0,
"Subscriptions": 0,
"Items": 0,
"BrowsePoints": 0,
"HistoryPoints": 0,
"LiveDataItems": 0,
"LiveEventItems": 0,
"MonitoredNodes": 0,
"Variables": 19,
"Folders": 4,
"Conditions": 1,
"CachedCertificateStores": 2,
"Descriptors": 191,
"PkiDescriptors": 0,
"ManagedBytes": 19878208,
"RssBytes": 224620544,
"AllocatedBytes": 86490920,
"Gen0": 7,
"Gen1": 6,
"Gen2": 6,
"GcCommittedBytes": 41967616,
"GcHeapBytes": 20716064,
"GcFragmentedBytes": 837856,
"PinnedObjects": 1,
"PendingFinalizers": 16,
"JitMethods": 35741,
"JitIlBytes": 4635402,
"ActiveTimers": 18,
"Threads": 27
},
"lastSample": {
"Cycle": 385,
"Seconds": 1425.221077,
"Sessions": 0,
"Subscriptions": 0,
"Items": 0,
"BrowsePoints": 0,
"HistoryPoints": 0,
"LiveDataItems": 0,
"LiveEventItems": 0,
"MonitoredNodes": 0,
"Variables": 19,
"Folders": 4,
"Conditions": 1,
"CachedCertificateStores": 2,
"Descriptors": 200,
"PkiDescriptors": 0,
"ManagedBytes": 19974280,
"RssBytes": 367771648,
"AllocatedBytes": 8790684304,
"Gen0": 963,
"Gen1": 770,
"Gen2": 770,
"GcCommittedBytes": 46555136,
"GcHeapBytes": 20348120,
"GcFragmentedBytes": 373840,
"PinnedObjects": 1,
"PendingFinalizers": 27,
"JitMethods": 80193,
"JitIlBytes": 12208985,
"ActiveTimers": 15,
"Threads": 22
},
"peakResources": {
"ManagedBytes": 20501376,
"RssBytes": 394956800,
"Descriptors": 201,
"PkiDescriptors": 0,
"JitMethods": 80193,
"GcCommittedBytes": 48549888
},
"evidenceSha256": {
"run-receipt.json": "842eacf845af019df087c69c55bb527d83c6e0d6f42057c8dd63d65eb5f86511",
"resources.jsonl": "eab640b684908d6978ca323c919d40b8ec6496a483484e3f86a396511e28dbc3",
"control/drained.json": "5a61b090086f379f3cc96b1efe328225efdafb027795a57f7636acfbf2bc5594",
"results/descriptor-controls.trx": "0de2cd05f9f1dbcaee3cff33e3e625723f5dc19b8f2dbcf3fbf1376792bc489c",
"results/saturation.trx": "1b5cba9e1b3c348d0dcbb79ba4c715d0e837602c78afc2c221c64c2e776e825b",
"test-exit-status.txt": "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865",
"observer-exit-status.txt": "9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa",
"target-runtime.txt": "1287d31fb4a49b24e82c346e2b9d2a4ef956d151e08f0497c3aef78617d2a012",
"observer-runtime.txt": "38c764d4f77a4bfa9c27905c5f8feb1076d30ea626f28d0f188e4bf86bb0c456"
},
"noOrdinaryCompleteRecord": true,
"privatePkiAndRawTraceExcluded": true,
"sanitization": {
"checkoutPrefixRedacted": true,
"rawResourceHashPreserved": true
}
}
@@ -1,35 +0,0 @@
# Candidate canonical saturation diagnostic
The prospectively fixed experiment completed 384 canonical cycles / 96 restarts
plus one settling cycle in **1,425.242 seconds**, within the 1,800-second cutoff.
Both traced windows, 321–352 and 353–384, recorded **zero generated-factory JIT/load
events, zero new SDK assembly loads and zero lost events**; the continuous attached
interval also had zero such events. Other JIT starts: 287. Summed CLR `MethodSize`
values: 60,760 bytes; this is not an RSS/PSS measurement. The observer was active
before cycle 321 and stopped/drained before cycle 385. Shutdown ownership checks
passed.
**The cold resource failure remains:** cycle 248 RSS 362,409,984 exceeded
358,838,272 bytes (cycle 3 + 128 MiB). The first assertion was retained and rethrown;
test/container/launcher exit 1, one failed executed lifecycle test, no ordinary
completion/full-duration result. Descriptor controls passed 2/2. Readiness=true
describes the selected generated-factory criterion on the canonical workload,
**not general runtime/performance readiness, G4 acceptance or #799 closure**.
Source `2738d7cc`, exact SDK 2.0.1 and HG 1.0.0; both participants use self-contained
Linux ARM64 runtime 10.0.14. Astra approved the exact immutable 1,275-file bundle
before launch and independently verified 1,064 module identities. The production/
central-package source tree matched the candidate. Outer output permissions were
0700; private PKI and raw trace stay unversioned.
`OPCUA_CERTIFICATE_LEAK_TRACKING=1` was enabled before startup. Exact SDK tracking
captures an allocation StackTrace and tracker entry per wrapper. This additional
diagnostic overhead prevents reuse as an uninstrumented performance/acceptance
baseline. Caps and measurement baseline were unchanged.
[Hashed result, windows, failure and runtime receipts](saturation-candidate.json)
preserve cold/warm/peak observations. A matched original-SDK canonical adapter,
prospectively reviewed acceptance procedure and at least 7,200 seconds after fixed
warmup are still required before a replacement resource gate could be considered.
Continued macOS native certificate-object accumulation remains separate and
unresolved. No production workaround or acceptance waiver was applied.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,349 +0,0 @@
{
"issue": 807,
"scope": "SDK1 host terminal-disposal repair; independent of SDK2 migration release acceptance.",
"originalMaster": "52d1373e8129b7f4f0ea2e35f86a8a5d0ed1de36",
"redSource": "84bfe6cd20285ccf136998998ef5c051917d8201",
"greenSource": "10edc44f94afc42300518f1504091c58217a89a9",
"redProductionEqualsOriginalMaster": true,
"redOriginalProductionSha256": "346362c8c18b5bcdb90757f654c02c68bad44fc5875abc8b4672f4c7ac49b381",
"greenProductionSha256": "c4934b15e695a4110e8cea8fbdef8faabad80aadeca4ccfb007b11400057ff9b",
"resolvedSdkPackages": [
"OPCFoundation.NetStandard.Opc.Ua.Configuration/1.5.378.182",
"OPCFoundation.NetStandard.Opc.Ua.Core/1.5.378.182",
"OPCFoundation.NetStandard.Opc.Ua.Security.Certificates/1.5.378.182",
"OPCFoundation.NetStandard.Opc.Ua.Server/1.5.378.182",
"OPCFoundation.NetStandard.Opc.Ua.Types/1.5.378.182"
],
"builds": [
{
"rawLogPath": "/tmp/opcua-sdk1-terminal-red-build.log",
"sha256": "38f5561a0db2a2d26f8462f36e114ced057c189effcd6e87dc5a62a42c06d52e",
"warnings": 0,
"errors": 0
},
{
"rawLogPath": "/tmp/opcua-sdk1-terminal-green-build.log",
"sha256": "5fae318d45d9b5b8b4ba2812ef87869edf6d851e09848154d5f4a51f47ed68d2",
"warnings": 0,
"errors": 0
},
{
"rawLogPath": "/tmp/opcua-sdk1-terminal-solution-build.log",
"sha256": "13317ee89238730216874531fad82e3591a6d0d1b5503c8bd896dcc95c885daf",
"warnings": 0,
"errors": 0
}
],
"runs": [
{
"source": "84bfe6cd20285ccf136998998ef5c051917d8201",
"rawDirectory": "artifacts/opcua2/84bfe6cd/sdk1-terminal-red",
"command": [
"dotnet",
"test",
"tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.csproj",
"-c",
"Release",
"--no-build",
"--no-restore",
"--filter",
"FullyQualifiedName~OpcUaApplicationHostDisposalTests",
"--logger",
"trx;LogFileName=close.trx",
"--results-directory",
"artifacts/opcua2/84bfe6cd/sdk1-terminal-red"
],
"testExit": 1,
"guardPassed": true,
"runnerSeconds": 3.8918902919394895,
"counters": {
"total": "5",
"executed": "5",
"passed": "0",
"failed": "5",
"error": "0",
"timeout": "0",
"aborted": "0",
"inconclusive": "0",
"passedButRunAborted": "0",
"notRunnable": "0",
"notExecuted": "0",
"disconnected": "0",
"warning": "0",
"completed": "0",
"inProgress": "0",
"pending": "0"
},
"selectedResults": [
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Host_terminally_disposes_its_booted_server_once",
"outcome": "Failed",
"error": "Shouldly.ShouldAssertException : server.TerminalDisposals\r\n should be\r\n1\r\n but was\r\n0"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Concurrent_disposers_share_completion_and_one_cleanup_owner",
"outcome": "Failed",
"error": "Shouldly.ShouldAssertException : server.StopCalls\r\n should be\r\n1\r\n but was\r\n4"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Disposed_host_rejects_start_without_taking_server_ownership",
"outcome": "Failed",
"error": "Shouldly.ShouldAssertException : Task `host.StartAsync(server, Ct)`\r\n should throw\r\nSystem.ObjectDisposedException\r\n but did not"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Stop_failure_still_terminally_disposes_the_server",
"outcome": "Failed",
"error": "Shouldly.ShouldAssertException : server.TerminalDisposals\r\n should be\r\n1\r\n but was\r\n0"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Partial_startup_failure_does_not_strand_the_server",
"outcome": "Failed",
"error": "Shouldly.ShouldAssertException : server.TerminalDisposals\r\n should be\r\n1\r\n but was\r\n0"
}
],
"rawHashes": {
"close.trx": "ec832633adf184a07f31075ff342bb2c1b9d3f3893c518f5737dc02526f886bb",
"run.log": "cdb7c4982d45807de53f70dfc22fad69ccda1db740db05407cef8df8b6b6d904",
"receipt.json": "4469c071c0d178446ccfdbdca4bf11ebad43c33feb91d8a8c5f2fa891579f2dc"
}
},
{
"source": "10edc44f94afc42300518f1504091c58217a89a9",
"rawDirectory": "artifacts/opcua2/10edc44f/sdk1-terminal-green",
"command": [
"dotnet",
"test",
"tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.csproj",
"-c",
"Release",
"--no-build",
"--no-restore",
"--filter",
"FullyQualifiedName~OpcUaApplicationHostDisposalTests",
"--logger",
"trx;LogFileName=close.trx",
"--results-directory",
"artifacts/opcua2/10edc44f/sdk1-terminal-green"
],
"testExit": 0,
"guardPassed": true,
"runnerSeconds": 3.336757709039375,
"counters": {
"total": "5",
"executed": "5",
"passed": "5",
"failed": "0",
"error": "0",
"timeout": "0",
"aborted": "0",
"inconclusive": "0",
"passedButRunAborted": "0",
"notRunnable": "0",
"notExecuted": "0",
"disconnected": "0",
"warning": "0",
"completed": "0",
"inProgress": "0",
"pending": "0"
},
"selectedResults": [
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Partial_startup_failure_does_not_strand_the_server",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Concurrent_disposers_share_completion_and_one_cleanup_owner",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Host_terminally_disposes_its_booted_server_once",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Stop_failure_still_terminally_disposes_the_server",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Disposed_host_rejects_start_without_taking_server_ownership",
"outcome": "Passed"
}
],
"rawHashes": {
"close.trx": "c44ba287659af567fcba1209c147aa5bfb6f2fc27753c0923d08b92372c007b4",
"run.log": "7f68723461a5ac4154c247b8bdfcc6d3770a5c1c1a88016051ef492f1684c88e",
"receipt.json": "bea02609e11dcc4569a26add9ae5836b101bf107d0346dd34514fc1d114ba525"
}
},
{
"source": "10edc44f94afc42300518f1504091c58217a89a9",
"rawDirectory": "artifacts/opcua2/10edc44f/sdk1-server-suite",
"command": [
"dotnet",
"test",
"tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.csproj",
"-c",
"Release",
"--no-build",
"--no-restore",
"--logger",
"trx;LogFileName=close.trx",
"--results-directory",
"artifacts/opcua2/10edc44f/sdk1-server-suite"
],
"testExit": 0,
"guardPassed": true,
"runnerSeconds": 119.36682745895814,
"counters": {
"total": "1033",
"executed": "1032",
"passed": "1032",
"failed": "0",
"error": "0",
"timeout": "0",
"aborted": "0",
"inconclusive": "0",
"passedButRunAborted": "0",
"notRunnable": "0",
"notExecuted": "0",
"disconnected": "0",
"warning": "0",
"completed": "0",
"inProgress": "0",
"pending": "0"
},
"selectedResults": [
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.AddressSpaceComposerDeviceHostTests.Compose_resolves_driver_device_and_device_host",
"outcome": "NotExecuted",
"error": "v3: equipment no longer binds a driver/device \u2014 Equipment lost DriverInstanceId/DeviceId and Compose no longer takes a devices list, so EquipmentNode's DriverInstanceId/DeviceId/DeviceHost projection is always null. Equipment references raw tags via UnsTagReference. The device-endpoint merge this suite characterized now lives in Runtime.Tests DeploymentArtifactRawPathTests; the host-extraction unit is Commons.Tests DeviceConfigIntentTests."
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Host_terminally_disposes_its_booted_server_once",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.BuildSecurityPolicies_empty_input_falls_back_to_none",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostTests.StartAsync_creates_application_certificate_in_pki_own",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostQuotaTests.Configured_transport_quotas_replace_only_what_is_set",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.BuildSecurityPolicies_default_set_emits_all_three_baseline_profiles",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Partial_startup_failure_does_not_strand_the_server",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.BuildUserTokenPolicies_withholds_anonymous_when_disallowed",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.NullOpcUaUserAuthenticator_always_denies",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostQuotaTests.Configured_request_quotas_are_applied",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_anonymous_token_is_rejected_when_disallowed",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_username_success_sets_identity_and_no_validation_error",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Disposed_host_rejects_start_without_taking_server_ownership",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostServerArrayTests.ServerArray_contains_local_uri_and_configured_peers_after_start",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostQuotaTests.Default_request_quotas_are_the_former_hard_coded_values",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.StartAsync_with_only_signandencrypt_omits_None_endpoint",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Concurrent_disposers_share_completion_and_one_cleanup_owner",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostQuotaTests.Unset_transport_quotas_keep_the_sdk_defaults",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostDisposalTests.Stop_failure_still_terminally_disposes_the_server",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_anonymous_token_is_admitted_with_a_role_carrying_identity",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.StartAsync_populates_ServerConfiguration_with_all_enabled_profiles",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostTests.StartAsync_reuses_existing_certificate_on_second_boot",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_username_denial_sets_validation_error_and_no_identity",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostQuotaTests.History_read_batch_cap_default_scales_with_the_machine",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.BuildSecurityPolicies_dedupes_repeated_profiles",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_authenticator_throwing_results_in_rejection",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_null_username_treated_as_empty_string",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostImpersonationTests.HandleImpersonation_username_success_carries_roles_on_identity",
"outcome": "Passed"
},
{
"name": "ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests.OpcUaApplicationHostSecurityTests.BuildUserTokenPolicies_emits_anonymous_and_username",
"outcome": "Passed"
}
],
"rawHashes": {
"close.trx": "800ac4134f48fc901da2c3519573184337c3f9a22cc5e7a5f4859a6b586fbf70",
"run.log": "253457c47f8801d37ce5fafc5c95cb9f5f0a9bbcead363c4ad0f5256e18fc7df",
"receipt.json": "b0db91f846ca883c8b8d48a946021f5e99785a26c37ff1087187b83575f62c96"
}
}
],
"existingSkip": "AddressSpaceComposerDeviceHostTests.Compose_resolves_driver_device_and_device_host",
"newSkips": 0,
"ownership": "One caller owns detach/Stop/terminal Dispose; all Dispose callers await shared completion; Start after disposal begins is rejected.",
"limits": [
"Does not serialize an already-running asynchronous Start against Dispose.",
"Does not alter immutable83ed880e baseline publications or aggregate regression oracles.",
"Does not establish any relative RSS denominator, #799 acceptance or SDK2 release approval."
],
"review": "Astra approved the isolated SDK1 source/result/package gate: byte-identical original production RED; five expected REDs and five GREENs; 1032 owning-suite passes with one existing skip; all build hashes and selected security/PKI results verified. No SDK2 release or performance-baseline approval."
}
@@ -1,96 +0,0 @@
# Issue #799 resolution gates
Status: prospective procedure under review, **not approved for launch**. The
retained cold failures remain failures. This document does not change the plan's
resource budgets, establish a replacement baseline, or authorize release.
## Separate the evidence
| Question | Evidence required | Current boundary |
|---|---|---|
| Finite generated-code startup | Fixed canonical operation sequence and bounded, lossless trace windows | Candidate saturation met the selected factory criterion. Summed native MethodSize does not explain all RSS. |
| Cross-version lifecycle ownership and memory | Equivalent operations, token invalidation, dropped harness roots, resource-owner checks and matched executable hosts | Original closed objects retain continuation lists while held by the harness; usable steady-state comparison is not established. |
| Continuing macOS native retention | Private-key import attribution, bounded native inventory, supported ownership correction and verification | Balanced managed disposal alone has not stopped Apple Security object accumulation. Linux results cannot close this component. |
The coordinator owns integration and long executions. Two independent Sol agent
lanes own `baseline-canonical/` and `native-retention/` respectively; use high
effort for control implementation and ownership analysis. Astra at xhigh reviews
each control design, its result, any production correction, and the complete
acceptance procedure before execution. No worker changes the other lane's files,
production factory architecture, immutable baseline, or acceptance denominator.
## Short controls before a new long run
The original-SDK lane must demonstrate removal from SessionManager, rejection of
the old authentication token, and rejection of old browse/history continuation
tokens from a fresh session. A helper boundary must drop all strong Session,
continuation and harness references before bounded GC/weak-reference observations.
Observe closed-session/point weak references while the same host remains live,
then separately observe application/validator/store roots after normal host stop;
teardown must not hide retention of closed sessions. Inventory separately
identifies which resource owner releases each object.
Retain the earlier held-object failure. Do not explicitly dispose abandoned
objects or clear caches merely to make the baseline pass. Unsupported original
ownership probes remain unsupported, rather than being reported as zero.
The native lane must attribute configured-store private-key loads separately
from public certificate and shared-handle operations using fixed counts and
identical certificates. Include a separate bounded store-update semantic control
so registry reuse cannot silently bypass configured-store rotation. Review exact
SDK 2.0.1 public ownership seams before any
application correction. A supported correction needs a failing-before/passing-
after ownership control and preserved trust, private-key availability, reconnect
and shutdown behavior. Custom crypto, copied transport negotiation, SDK forks,
factory substitution and certificate-policy changes require a separate design;
they are not fixes authorized by this procedure. Keep private PKI out of Git.
## Prerequisites for reviewing warmed acceptance
1. Establish operation/status/order parity and semantically valid close/ownership
checks across versions. Match executable host, runtime, architecture and
instrumentation. A growing original process is not an accepted denominator.
2. Freeze source commits, all artifact hashes, runtime/image identities, private
PKI identity, fixtures and the serial same-host schedule before launch.
3. Submit the actual harness change, negative controls and immutable artifact
receipts to Astra. Approval of diagnostics is not approval of acceptance.
4. Resolve continuing macOS native growth independently. A Linux acceptance run
cannot waive it, even if the Linux resource guard passes.
Before this draft becomes executable, define version-appropriate standard-node
loading/factory trace coverage for the original binary. The absence of SDK2's
`Opc.Ua.OpcUaExtensions` type must not let the original pass an irrelevant zero
counter. Also predeclare identical retained-managed/GC and RSS sampling cadence
and statistics, including exactly which samples feed the relative 15% comparison.
These details remain pending; observed results cannot select them retroactively.
## Proposed fixed warmup and measurement
This section is a review proposal only. If the prerequisites and Astra gate pass,
qualification uses exactly 384 canonical cycles / 96 restarts plus one settling
cycle within 1,800 seconds. The fixed trace windows are cycles 321–352 and
353–384, with the continuous veto across their boundary. Require zero generated-
factory JIT/load events, zero new SDK assembly loads and zero lost events. Reject
missing markers, wrong identity or observer faults. Do not extend or retry the
qualification until a favorable sample appears.
Prospectively select exactly the post-cycle-385 sample as the measurement
baseline; do not choose a minimum, median or later sample from observed RSS.
Retain all cold, peak and rejected rows. Measurement must then last at least
7,200 actual seconds **after** warmup, retaining the +64 MiB managed, +128 MiB
RSS and +32 descriptor caps, all canonical operations and semantic/native/
ownership guards. Latch the first failure permanently. No resource resets,
skipped operations or completion claim on a rejected run.
Setup, warmup and final cleanup do not count toward measurement duration. Final
shutdown and ownership checks are mandatory after that duration completes.
For uninstrumented acceptance, first perform an identical fresh paired
qualification rehearsal, then use the fixed warmup without EventPipe or
allocation StackTrace tracking. The earlier tracked candidate run cannot be
relabelled as this evidence. Predefine sampling and statistics for at least three
matched representative runs to assess the separate plan-relative throughput,
latency, allocation and RSS budgets. A two-hour absolute-cap pass alone does not
satisfy those comparisons.
Only verified results meeting these distinct requirements can support closing
#799. Keep #782 G4 and draft PR #802 on hold until their remaining gates also
pass; Windows/runtime and licensed interoperability evidence remain separate.
@@ -18,7 +18,6 @@ internal sealed class BaselineRig : IAsyncDisposable
?? throw new InvalidOperationException("An ephemeral fixture credential is required");
private readonly OpcUaApplicationHostOptions _options;
private OpcUaApplicationHost _host = null!;
private bool _disposed;
internal OtOpcUaSdkServer Server { get; private set; } = null!;
internal OtOpcUaNodeManager Nm => Server.NodeManager!;
internal SdkAddressSpaceSink Sink { get; private set; } = null!;
@@ -89,13 +88,9 @@ internal sealed class BaselineRig : IAsyncDisposable
}
public async ValueTask DisposeAsync()
{
if (_disposed) return;
_disposed = true;
try
{
if (_host is not null) { await _host.DisposeAsync(); Canonical.Ledger.Record("host-stop"); }
}
finally { if (Directory.Exists(PkiRoot)) Directory.Delete(PkiRoot, true); }
await _host.DisposeAsync();
Canonical.Ledger.Record("host-stop");
if (Directory.Exists(PkiRoot)) Directory.Delete(PkiRoot, true);
}
private sealed class Auth : IOpcUaUserAuthenticator
{
@@ -1,167 +0,0 @@
# Original close semantics and bounded retention controls
These are separately reviewed short controls against the unchanged 83ed880e
publication, SDK 1.5.378.182. They do not replace the historical `(1 browse,
6 history)` held-list port comparison, change an oracle, approve a memory
baseline, or claim an unbounded leak. The operation deadline is 120 seconds;
normal shutdown/private-directory cleanup has independent 20/20/5/5-second
bounds, and the semantic runner has a final 180-second process bound.
## What was tested
A fresh fixture creates the original production host, encrypted opc.tcp client,
one abandoned browse point and six event-history points. After normal client
CloseAsync, the control verifies removal from the same running SessionManager.
The old closed object still exposes `(1,6)` while explicitly held, as previously
recorded; that observation is not treated as live protocol ownership.
A fresh valid session supplies the same open raw transport for all following
requests. `ITransportChannel.SendRequestAsync` receives explicit request headers;
no closed Session client guard or ClientBase header substitution can satisfy the
negative checks. Each response must carry the exact sent RequestHandle.
| Request | Verified outcome |
|---|---|
| Fresh raw read before old-token request | Good service/item status |
| Read with old authentication token | Correlated ServiceFault, BadSessionIdInvalid (2149908480) |
| Fresh raw read after the fault, same transport | Good service/item status |
| Fresh browse point creation | Good, nonempty point |
| Old browse point from the fresh session | BadContinuationPointInvalid (2152333312) |
| Fresh own browse point resume and release | Good; resume returns references and another point |
| Fresh history page | Good; nonempty point; backend dispatched |
| Old history point from the fresh session | BadContinuationPointInvalid; backend counter remains 7 to 7 |
| Fresh own history point resume and release | Good; resume dispatches backend and returns another point |
All 11 raw responses were correlated. Generic transport failures/timeouts are not
accepted as token denial. The exact .182 ClientBase independently preserves a
nonnull AuthenticationToken in UpdateRequestHeader; the raw path bypasses that
method entirely. The protected token getter is read through an exact-type-checked
diagnostic accessor before close; tokens and payloads are never logged.
## Two distinct weak-reference boundaries
A NoInlining synchronous helper completes the async closed-session scope from a
thread-pool context, returning only weak references and primitive/type metadata.
No Session, point, response, token array or backend is returned as a strong
reference. The first GC phase runs while the SAME original host/configuration
remain alive. Their weak targets are explicitly expected alive. A second
NoInlining fixture boundary completes normal host shutdown, private-directory
cleanup and the complete async fixture scope before the outer GC phase.
Each phase records round 0 followed by exactly five forced blocking/compacting
collections with fixed 50/100/250/500/1000 ms delays. Each GC/finalizer worker is
bounded at five seconds. Observers use WeakReference.IsAlive rather than keeping
TryGetTarget values across collections. A deliberately retained positive object
must stay alive and an isolated collectible negative must disappear. No delay is
extended to obtain collection.
In the ordinary run all **16 old client/server Session, browse point/Data,
history-slot wrapper and history-value targets** disappeared during the first
live-host forced collection. This separates the held-list implementation detail
from those objects' actual reclamation in this fixture. After normal stop and
all fixture boundaries, five server-graph targets remained across all five
observations: server, node manager, backend, server validator and server cached
store. Rig/client configuration/client application/client validator/client cached
store targets disappeared. Positive and negative controls behaved correctly.
PKI descriptors were zero in live-host and stopped-but-harness-held observations.
The ordinary run preserves **exit 1**, with unsupported SDK2 manager-disposal
observables still explicit.
For this single fixture, live-host final managed bytes were 94,550,728 and timers
10; after-stop final managed bytes were 7,774,200 and timers 7. This drop does not
establish that the earlier multi-restart managed/timer growth disappears, nor is
it a usable performance denominator. Full process/framework parity remains open.
## Exact point-resource ownership
Runtime inspection identifies browse `ContinuationPoint.Data` as **Opc.Ua.NodeBrowser**
from the original Opc.Ua.Types binary. Its exact Dispose(bool) implementation is
empty. Its declared storage is managed IReference/List/HashSet/index state plus
managed context/view properties, with no owned IDisposable field. The containing
ContinuationPoint.Dispose delegates to Data, so omission of that call on this
exact browser does not skip a direct cleanup action. This is not a statement
about every INodeBrowser implementation or arbitrary driver browser resources.
The six history values are the original event HistoryContinuationState records;
the original store saves each record directly in the SDK slot. They have scalar,
string/date and managed paging/set state, no IDisposable contract or backend/store
handle ownership. This event fixture has no pending raw end-bound resource. Both
the SDK slot wrapper and its Value are weak-tracked; all collected while the
original host remained running.
## Stop is distinct from terminal Dispose
Read-only inspection of the pinned original product/SDK establishes:
1. Archived OtOpcUa OpcUaApplicationHost.DisposeAsync awaits only
ApplicationInstance.StopAsync.
2. Original ApplicationInstance.StopAsync delegates to Server.StopAsync.
3. ServerBase.StopAsync stops listeners/service hosts and invokes the standard
stopping hook. StandardServer's stopping hook DOES shut down subscriptions,
sessions and node managers and dispose internal server data.
4. Terminal ServerBase.Dispose separately disposes its RequestQueue.
RequestQueue owns a ServerBase reference and starts minimum worker tasks;
those workers await SemaphoreSlim with a 15,000 ms timeout and cancellation
token. At the minimum worker count they continue waiting. Queue.Dispose
cancels that token, releases/disposes its signal and terminates its ownership.
5. StandardServer.Dispose also unregisters its CertificateUpdate callback and
disposes remaining terminal resources. The SDK2 ApplicationInstance.DisposeAsync
explicitly performs StopAsync THEN Server.Dispose; the archived original host
never calls that terminal API.
This is a source-backed original application lifecycle gap and a plausible
retention path consistent with the observed server graph. It is not a measured
heap dominator/root chain or attribution of any particular timer. No original
production code was patched, and no extra disposal was added to ordinary mode.
A separate, gated `semantic-terminal-dispose` attribution mode first records the
ordinary result. Only afterward does a NoInlining helper briefly acquire the
known weak original server target, invoke its PUBLIC terminal Dispose once,
drop that strong reference, then repeat the same fixed GC schedule. No private
timer disposal, cache clearing, oracle adjustment or delay extension is used.
The original ordinary exit is returned even if terminal disposal clears all
observed targets. Its results are recorded separately in `semantic-evidence.json`.
It cannot turn the archived original behavior or failed RSS gates into a pass.
That separately approved run reproduced the same normal-stop five survivors and
returned **exit 1**. After the single public Dispose, all five disappeared at the
first next forced collection and remained absent through round 5. At the final
observations, timers changed from 7 to 2 and managed bytes from 7,692,144 to
4,513,760. Positive/negative GC controls remained valid. This controlled intervention
supports missing terminal server disposal as the ownership distinction; it does
not identify which queue task, timer or event subscription was the actual GC root.
It does not establish an unbounded leak from one fixture, eliminate unrelated
native retention, or make the original process a performance denominator.
Both immutable adapter builds completed with zero warnings/errors, using the
same 208 reference hashes. The ordinary run uses build 3 and the separate
counterfactual uses build 4; their exact source, binary, build-log and raw-result
hashes are retained in `semantic-evidence.json`. Raw evidence remains in private
external directories. No additional control was run to adjust these findings.
The exact original test seams are public virtual
`ServerBase.StopAsync(CancellationToken)`, protected virtual `Dispose(bool)`,
`OnServerStarting(ApplicationConfiguration)`,
`StartApplicationAsync(ApplicationConfiguration, CancellationToken)`, and
`OnServerStoppingAsync(CancellationToken)`. Public `Dispose()` and `StartAsync`
are nonvirtual. StandardServer overrides those protected lifecycle hooks and
also exposes protected virtual `OnServerStarted(IServerInternal)`. A separate
reviewed application repair can test stop failure/partial startup using these
supported seams; this diagnostic does not repair or rebuild the archived product.
## Smallest future matched-host path (proposal only)
A standalone SDK2 driver mirroring this original standalone driver is the smallest
path: same .NET runtime, assertions, telemetry/logging configuration, PKI/security,
operation recipe/counts, raw request/correlation probes, NoInlining boundaries,
weak controls, sampling schedule and module provenance. Keep SDK-specific value
representations and read-only ownership observers explicit. Both xUnit hosts
would require creating/porting a larger original test-host graph and introduce
more loader/framework differences. Neither path has been implemented or approved
as an acceptance protocol. The current candidate xUnit control is semantic
corroboration, not process-memory comparability.
Any correction of archived app ownership must be a separate reviewed change and
new provenance; these controls do not substitute a repaired baseline or use its
post-counterfactual memory as a denominator. Long original runs, reset baselines,
RSS caps and acceptance remain outside this work.
@@ -11,8 +11,7 @@ using Shouldly;
using ZB.MOM.WW.OtOpcUa.Commons.OpcUa;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using CoreHistoryReadResult = ZB.MOM.WW.OtOpcUa.Core.Abstractions.HistoryReadResult;
return args.Length == 3 && (args[2] == "--semantic-retention-control" || args[2] == "--semantic-terminal-dispose-control")
? await Canonical.SemanticRetentionAsync(args) : await Canonical.RunAsync(args);
return await Canonical.RunAsync(args);
internal static partial class Canonical
{
private const string Scalar = BaselineRig.RawDeviceFolder + "/SoakScalar";
@@ -104,15 +103,12 @@ internal static partial class Canonical
{public Task<NodeWriteOutcome> WriteAsync(string id,object? value,AddressSpaceRealm realm,CancellationToken ct)=>Task.FromResult(new NodeWriteOutcome(true,null));}
internal sealed class SqlLikeEventSource(List<HistoricalEvent> events) : IHistorianDataSource
{
private int _eventReads;
internal int EventReads => Volatile.Read(ref _eventReads);
private readonly List<HistoricalEvent> _all =
events.OrderBy(e => e.EventTimeUtc).ThenBy(e => e.EventId, StringComparer.Ordinal).ToList();
public async Task<HistoricalEventsResult> ReadEventsAsync(
string? sourceName, DateTime startUtc, DateTime endUtc, int maxEvents, CancellationToken ct)
{
Interlocked.Increment(ref _eventReads);
await Task.Delay(1, ct).ConfigureAwait(false); // complete on a pool thread, like a real backend
var window = _all.Where(e => e.EventTimeUtc >= startUtc && e.EventTimeUtc < endUtc).ToList();
var cap = maxEvents <= 0 ? 1000 : maxEvents;
@@ -82,14 +82,13 @@ as the candidate; those observations cannot invent the missing manager ownership
proof. No manual Session.Dispose, store replacement or cache clearing was added
to make an assertion pass.
The historical partial proof above did not test token/continuation rejection or
collection after dropping its diagnostic references. Later, separately reviewed
short controls established those semantics and observed the remaining server
graph retention after normal shutdown. Their direct point-resource inspection,
two weak-reference phases and separate terminal-disposal attribution are recorded
in [OWNERSHIP.md](OWNERSHIP.md) and `semantic-evidence.json`. Both ordinary failure
results remain intact. Certificate-store/native ownership gaps remain explicit;
these findings do not approve a common lifecycle/acceptance protocol.
Before any common lifecycle/acceptance protocol can be proposed, a separately
reviewed short control must establish original SessionManager removal, rejection
of the old authentication token, rejection of old CP tokens from a fresh session,
then drop every strong session/point/test reference at an isolated helper boundary
and perform bounded GC/weak-reference observation. Any retained IDisposable point
resource needs direct source/ownership inspection. Certificate-store/native
ownership gaps must remain explicit. This proposal has **not** been executed.
The standalone adapter also lacks the candidate's xUnit/VSTest host; equal process
memory overhead is not proven. Earlier original-subset managed/timer growth is an
@@ -121,10 +120,5 @@ Add `--inventory` to build the read-only metadata inventory instead. The runner
uses a fresh private directory and a temporary environment credential with the
same prefix/byte length as the canonical fixture; it preserves the original child
exit. Exit 1 is an observed control failure; exit 2 is operations completed with
unsupported ownership parity. Neither is a passing baseline. Normal/restart modes
have a hard 120-second bound and no long-run option.
The later semantic modes are `--mode semantic-retention` and the separately gated
`--mode semantic-terminal-dispose` attribution control. They use the 120-second
operation deadline plus bounded independent cleanup and a 180-second outer runner
limit. Neither changes normal/active-restart results or authorizes a long run.
unsupported ownership parity. Neither is a passing baseline. The smoke driver
has a hard 120-second bound and no long-run option.
@@ -1,385 +0,0 @@
using System.Collections;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Text.Json;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using Shouldly;
using ServerSession = Opc.Ua.Server.ISession;
using ClientSession = Opc.Ua.Client.ISession;
internal static partial class Canonical
{
internal static async Task<int> SemanticRetentionAsync(string[] args)
{
if (args.Length != 3 || args[1] != "1") throw new InvalidOperationException("One bounded semantic control only");
var root = Path.GetFullPath(args[0]);
var counterfactual = args[2] == "--semantic-terminal-dispose-control";
if (Directory.Exists(root)) throw new InvalidOperationException("Use a fresh private directory");
Directory.CreateDirectory(root);
if (!OperatingSystem.IsWindows()) File.SetUnixFileMode(root, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
await using var output = new StreamWriter(new FileStream(Path.Combine(root, "resources.jsonl"), FileMode.CreateNew, FileAccess.Write, FileShare.Read));
CurrentEvidence = output;
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(120));
var retainedPositive = new object();
var positive = Witness("positive-retained", retainedPositive);
var negative = EphemeralNegative();
try
{
await LogAsync(output, new { kind = "semantic-process", phase = "before-fixture", process = ProcessCounters() });
// Task.Run has no captured synchronization context. Its closure contains only path/writer/token.
var result = await Task.Run(() => FixtureBoundary(root, output, positive, negative, deadline.Token), deadline.Token);
var phase2 = await ObserveAsync("after-stop", result.All, positive, negative, output, deadline.Token);
var liveOld = result.LiveHostSurvivors;
var stopped = phase2.Where(x => x.alive && x.label != "positive-retained").Select(x => x.label).ToArray();
await LogAsync(output, new { kind = "semantic-retention-result", semanticsComplete = true,
liveHostOldSurvivors = liveOld, afterStopSurvivors = stopped, acceptance = false, ownershipParity = false,
unsupportedObservables = new[] { "SDK2 CertificateManager disposed flag", "SDK2 manager-owned cache-clear proof", "ApplicationInstance.DisposeAsync" } });
var ordinaryExitCode = liveOld.Length == 0 && stopped.Length == 0 ? 2 : 1;
if (counterfactual)
{
// Separate attribution only, AFTER preserving the ordinary stop result above.
var server = result.All.Single(x => x.Label == "fixture-server");
var invoked = await Task.Run(() => TerminalDisposeBoundary(server), deadline.Token).WaitAsync(TimeSpan.FromSeconds(20), deadline.Token);
await LogAsync(output, new { kind = "counterfactual-terminal-dispose", invoked, ordinaryExitCode,
publicMethod = "ServerBase.Dispose", acceptance = false });
var terminal = await ObserveAsync("after-terminal-dispose", result.All, positive, negative, output, deadline.Token);
await LogAsync(output, new { kind = "counterfactual-attribution-result", ordinaryExitCode,
survivors = terminal.Where(x => x.alive && x.label != "positive-retained").Select(x => x.label).ToArray(), acceptance = false });
}
GC.KeepAlive(retainedPositive);
// Even collection of every observed target does not invent the unsupported ownership proof.
return ordinaryExitCode;
}
catch (Exception error)
{
await LogAsync(output, new { kind = "semantic-control-failed", exceptionType = error.GetType().FullName, acceptance = false });
Console.Error.WriteLine(error);
GC.KeepAlive(retainedPositive);
return 1;
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static FixtureWitness FixtureBoundary(string root, StreamWriter output, WeakWitness positive, WeakWitness negative, CancellationToken ct) =>
FixtureScopeAsync(root, output, positive, negative, ct).GetAwaiter().GetResult();
private static async Task<FixtureWitness> FixtureScopeAsync(string root, StreamWriter output, WeakWitness positive, WeakWitness negative, CancellationToken ct)
{
var sdk = typeof(NodeId).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
if (sdk is null || !sdk.StartsWith("1.5.378.182+", StringComparison.Ordinal)) throw new InvalidOperationException("Immutable .182 SDK required");
var clientRoot = Path.Combine(root, "private-fixture", "client-pki");
BaselineRig? rig = null;
ApplicationConfiguration? config = null;
ApplicationInstance? application = null;
ExceptionDispatchInfo? failure = null;
FixtureWitness? result = null;
var booted = false;
try
{
var telemetry = DefaultTelemetry.Create(_ => { });
config = new ApplicationConfiguration(telemetry)
{
ApplicationName = "SDK2LifecycleSoak", ApplicationUri = $"urn:SDK2LifecycleSoak:{Guid.NewGuid():N}", ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(clientRoot, "own"), SubjectName = "CN=SDK2LifecycleSoak" },
TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(clientRoot, "issuer") },
TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(clientRoot, "trusted") },
RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(clientRoot, "rejected") }, AutoAcceptUntrustedCertificates = true
},
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 15000 }, TransportQuotas = new TransportQuotas { OperationTimeout = 5000 }
};
application = new ApplicationInstance(telemetry) { ApplicationConfiguration = config };
await config.ValidateAsync(ApplicationType.Client, ct);
await application.CheckApplicationInstanceCertificatesAsync(true, null, ct);
rig = new BaselineRig(Path.Combine(root, "private-fixture"));
await rig.BootAsync(ct);
Configure(rig);
booted = true;
await LogAsync(output, new { kind = "semantic-start", sdk, acceptance = false, productRebuilt = false,
maximumSeconds = 120, gcDelaysMilliseconds = new[] { 50, 100, 250, 500, 1000 }, gcRoundTimeoutSeconds = 5 });
var fixture = new List<WeakWitness>
{
Witness("fixture-rig", rig), Witness("fixture-server", rig.Server), Witness("fixture-node-manager", rig.Nm),
Witness("fixture-backend", rig.Nm.HistorianDataSource), Witness("fixture-client-config", config),
Witness("fixture-client-application", application), Witness("fixture-server-validator", rig.Validator),
Witness("fixture-client-validator", config.CertificateValidator)
};
// This synchronous NoInlining boundary drops the completed async state machine before phase1 GC.
var closed = ClosedBoundary(rig, config, output, ct);
PublicCounts(rig).ShouldBe((0, 0, 0));
// Validation may populate stores on first connection; inspect after those operations.
CaptureStores(rig.Validator, "fixture-server-store", fixture);
CaptureStores(config.CertificateValidator, "fixture-client-store", fixture);
var all = closed.Concat(fixture).ToArray();
await WriteShapes(all, output);
var phase1 = await ObserveAsync("live-host", all, positive, negative, output, ct);
var survivors = phase1.Where(x => x.alive && x.label.StartsWith("old-", StringComparison.Ordinal)).Select(x => x.label).ToArray();
foreach (var live in phase1.Where(x => x.label.StartsWith("fixture-", StringComparison.Ordinal)))
live.alive.ShouldBeTrue("the host/configuration are intentionally still owned in phase1");
var descriptors = await BaselineMetrics.DescriptorsAsync(ct, clientRoot, rig.PkiRoot);
descriptors.Pki.ShouldBe(0);
await LogAsync(output, new { kind = "semantic-owned-resources", phase = "live-host", pkiDescriptors = descriptors.Pki,
serverCachedStoreReferences = BaselineMetrics.CachedStoreReferences(rig.Validator),
clientCachedStoreReferences = BaselineMetrics.CachedStoreReferences(config.CertificateValidator),
cachedReferenceIsOpenHandle = false });
result = new FixtureWitness(all, survivors);
}
catch (Exception error) { failure = ExceptionDispatchInfo.Capture(error); }
finally
{
// These bounds do not reuse the canceled 120-second operation token.
async Task Cleanup(string step, Func<Task> operation, int seconds)
{
try { await operation().WaitAsync(TimeSpan.FromSeconds(seconds), CancellationToken.None); }
catch (Exception error)
{
failure ??= ExceptionDispatchInfo.Capture(error);
await LogAsync(output, new { kind = "semantic-cleanup-failed", step, exceptionType = error.GetType().FullName });
}
}
if (rig is not null)
await Cleanup("normal-host-stop", () => rig.DisposeAsync().AsTask(), 20);
if (booted && rig is not null && config is not null)
await Cleanup("post-stop-observations", async () =>
{
using var probe = new CancellationTokenSource(TimeSpan.FromSeconds(15));
var after = await BaselineMetrics.DescriptorsAsync(probe.Token, clientRoot, rig.PkiRoot);
after.Pki.ShouldBe(0);
await LogAsync(output, new { kind = "semantic-owned-resources", phase = "stopped-but-harness-held", pkiDescriptors = after.Pki,
serverCachedStoreReferences = BaselineMetrics.CachedStoreReferences(rig.Validator),
clientCachedStoreReferences = BaselineMetrics.CachedStoreReferences(config.CertificateValidator), disposalFlagObservable = false });
}, 20);
await Cleanup("client-private-directory", () => Task.Run(() => { if (Directory.Exists(clientRoot)) Directory.Delete(clientRoot, true); }), 5);
if (rig is not null)
await Cleanup("server-private-directory", () => Task.Run(() => { if (Directory.Exists(rig.PkiRoot)) Directory.Delete(rig.PkiRoot, true); }), 5);
GC.KeepAlive(application); GC.KeepAlive(config); GC.KeepAlive(rig);
}
failure?.Throw();
return result ?? throw new InvalidOperationException("The fixture did not produce diagnostic witnesses");
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static WeakWitness[] ClosedBoundary(BaselineRig rig, ApplicationConfiguration config, StreamWriter output, CancellationToken ct) =>
ClosedScopeAsync(rig, config, output, ct).GetAwaiter().GetResult();
private static async Task<WeakWitness[]> ClosedScopeAsync(BaselineRig rig, ApplicationConfiguration config, StreamWriter output, CancellationToken ct)
{
var weak = new List<WeakWitness>();
using var owner = await ConnectAsync(rig, config, null, ct);
weak.Add(Witness("old-client-session", owner));
var oldServerSession = rig.Server.CurrentInstance.SessionManager.GetSessions().Single(x => x.Id == owner.SessionId);
weak.Add(Witness("old-server-session", oldServerSession));
var oldAuthenticationToken = Token(owner);
var oldBrowse = (await BrowseAsync(owner, ct))!;
RequirePoint(oldBrowse);
byte[]? oldHistory = null;
for (var i = 0; i < 6; i++)
{
var page = await PageAsync(owner, null, false, ct);
RequirePoint(page.ContinuationPoint);
oldHistory ??= page.ContinuationPoint;
}
CapturePoints(oldServerSession, weak);
ContinuationCounts(oldServerSession).ShouldBe((1, 6));
await owner.CloseAsync(ct);
rig.Server.CurrentInstance.SessionManager.GetSessions().Any(x => ReferenceEquals(x, oldServerSession)).ShouldBeFalse();
rig.Server.CurrentInstance.SessionManager.GetSessions().Count.ShouldBe(0);
await LogAsync(output, new { kind = "semantic-closed-state", activeSessions = 0, rawClosedBrowse = ContinuationCounts(oldServerSession).Browse,
rawClosedHistory = ContinuationCounts(oldServerSession).History, diagnosticReferenceHeld = true });
using var fresh = await ConnectAsync(rig, config, null, ct);
var channel = fresh.TransportChannel;
var freshToken = Token(fresh);
freshToken.ShouldNotBe(oldAuthenticationToken);
uint sequence = 10000;
RequestHeader Header(NodeId token) => new() { AuthenticationToken = token, RequestHandle = ++sequence, Timestamp = DateTime.UtcNow, TimeoutHint = 5000 };
ReadRequest Read(NodeId token) => new() { RequestHeader = Header(token), TimestampsToReturn = TimestampsToReturn.Neither,
NodesToRead = [new ReadValueId { NodeId = Raw(fresh, Scalar), AttributeId = Attributes.Value }] };
await RequireReadGood(channel, Read(freshToken), "fresh-read-before", output, ct);
var oldRequest = Read(oldAuthenticationToken);
var denied = await Send(channel, oldRequest, "old-authentication-token", output, ct);
var deniedCode = denied.ResponseHeader.ServiceResult;
(deniedCode == StatusCodes.BadSessionIdInvalid || deniedCode == StatusCodes.BadSessionClosed).ShouldBeTrue();
await RequireReadGood(channel, Read(freshToken), "fresh-read-after", output, ct);
var ownBrowseRequest = new BrowseRequest { RequestHeader = Header(freshToken), RequestedMaxReferencesPerNode = 1,
NodesToBrowse = [new BrowseDescription { NodeId = Raw(fresh, BaselineRig.RawDeviceFolder), BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, IncludeSubtypes = true, ResultMask = (uint)BrowseResultMask.All }] };
var ownBrowse = (BrowseResponse)await Send(channel, ownBrowseRequest, "fresh-own-browse", output, ct);
ownBrowse.Results[0].StatusCode.ShouldBe(StatusCodes.Good); RequirePoint(ownBrowse.Results[0].ContinuationPoint);
var oldBrowseResponse = (BrowseNextResponse)await Send(channel, new BrowseNextRequest { RequestHeader = Header(freshToken),
ContinuationPoints = [oldBrowse], ReleaseContinuationPoints = false }, "old-browse-point", output, ct);
oldBrowseResponse.Results[0].StatusCode.ShouldBe(StatusCodes.BadContinuationPointInvalid);
await LogAsync(output, new { kind = "semantic-item-status", name = "old-browse-point", statusCode = (uint)oldBrowseResponse.Results[0].StatusCode });
var ownBrowseNext = (BrowseNextResponse)await Send(channel, new BrowseNextRequest { RequestHeader = Header(freshToken),
ContinuationPoints = [ownBrowse.Results[0].ContinuationPoint], ReleaseContinuationPoints = false }, "fresh-own-browse-resume", output, ct);
ownBrowseNext.Results[0].StatusCode.ShouldBe(StatusCodes.Good);
ownBrowseNext.Results[0].References.Count.ShouldBeGreaterThan(0); RequirePoint(ownBrowseNext.Results[0].ContinuationPoint);
var ownBrowseRelease = (BrowseNextResponse)await Send(channel, new BrowseNextRequest { RequestHeader = Header(freshToken),
ContinuationPoints = [ownBrowseNext.Results[0].ContinuationPoint], ReleaseContinuationPoints = true }, "fresh-own-browse-release", output, ct);
ownBrowseRelease.Results[0].StatusCode.ShouldBe(StatusCodes.Good);
HistoryReadRequest History(byte[]? point, bool release) => new()
{
RequestHeader = Header(freshToken), TimestampsToReturn = TimestampsToReturn.Both, ReleaseContinuationPoints = release,
HistoryReadDetails = new ExtensionObject(new ReadEventDetails { StartTime = Epoch.AddDays(-1), EndTime = Epoch.AddDays(1), NumValuesPerNode = 4,
Filter = new EventFilter { SelectClauses = [new SimpleAttributeOperand(ObjectTypeIds.BaseEventType, new QualifiedName(BrowseNames.EventId))] } }),
NodesToRead = [new HistoryReadValueId { NodeId = Raw(fresh, BaselineRig.RawDeviceFolder), ContinuationPoint = point }]
};
var backend = (SqlLikeEventSource)rig.Nm.HistorianDataSource;
var beforeOwn = backend.EventReads;
var ownHistory = (HistoryReadResponse)await Send(channel, History(null, false), "fresh-own-history", output, ct);
ownHistory.Results[0].StatusCode.ShouldBe(StatusCodes.Good); RequirePoint(ownHistory.Results[0].ContinuationPoint);
backend.EventReads.ShouldBeGreaterThan(beforeOwn);
var beforeOld = backend.EventReads;
var oldHistoryResponse = (HistoryReadResponse)await Send(channel, History(oldHistory, false), "old-history-point", output, ct);
oldHistoryResponse.Results[0].StatusCode.ShouldBe(StatusCodes.BadContinuationPointInvalid);
backend.EventReads.ShouldBe(beforeOld);
await LogAsync(output, new { kind = "semantic-backend", operation = "old-history-point", before = beforeOld, after = backend.EventReads,
statusCode = (uint)oldHistoryResponse.Results[0].StatusCode });
var ownHistoryNext = (HistoryReadResponse)await Send(channel, History(ownHistory.Results[0].ContinuationPoint, false), "fresh-own-history-resume", output, ct);
ownHistoryNext.Results[0].StatusCode.ShouldBe(StatusCodes.Good); RequirePoint(ownHistoryNext.Results[0].ContinuationPoint);
backend.EventReads.ShouldBeGreaterThan(beforeOld);
var releaseHistory = (HistoryReadResponse)await Send(channel, History(ownHistoryNext.Results[0].ContinuationPoint, true), "fresh-own-history-release", output, ct);
releaseHistory.Results[0].StatusCode.ShouldBe(StatusCodes.Good);
await fresh.CloseAsync(ct);
rig.Server.CurrentInstance.SessionManager.GetSessions().Count.ShouldBe(0);
return weak.ToArray();
}
private static NodeId Token(ClientSession session)
{
var property = typeof(ClientBase).GetProperty("AuthenticationToken", BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException("Exact .182 token getter is missing");
return property.PropertyType == typeof(NodeId) && property.GetValue(session) is NodeId value && !NodeId.IsNull(value)
? NodeId.Parse(value.ToString()) : throw new InvalidOperationException("Exact .182 token getter changed");
}
private static async Task<IServiceResponse> Send(ITransportChannel channel, IServiceRequest request, string name, StreamWriter output, CancellationToken ct)
{
// No Session closed guard or ClientBase header update runs on this public raw transport path.
var response = await channel.SendRequestAsync(request, ct);
response.ResponseHeader.RequestHandle.ShouldBe(request.RequestHeader.RequestHandle);
await LogAsync(output, new { kind = "semantic-wire", name, requestHandle = request.RequestHeader.RequestHandle,
responseHandle = response.ResponseHeader.RequestHandle, responseType = response.GetType().Name,
serviceStatusCode = (uint)response.ResponseHeader.ServiceResult });
return response;
}
private static async Task RequireReadGood(ITransportChannel channel, ReadRequest request, string name, StreamWriter output, CancellationToken ct)
{
var response = (ReadResponse)await Send(channel, request, name, output, ct);
response.ResponseHeader.ServiceResult.ShouldBe(StatusCodes.Good);
response.Results[0].StatusCode.ShouldBe(StatusCodes.Good);
}
private static FieldInfo ExactField(object owner, string name)
{
for (var type = owner.GetType(); type is not null; type = type.BaseType)
if (type.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly) is { } field) return field;
throw new InvalidOperationException("Missing exact .182 diagnostic field: " + name);
}
private static void CapturePoints(ServerSession session, List<WeakWitness> target)
{
var gate = ExactField(session, "m_lock").GetValue(session) as System.Threading.Lock ?? throw new InvalidOperationException("Unexpected session lock");
using (gate.EnterScope())
{
var browse = ExactField(session, "m_browseContinuationPoints").GetValue(session) as IList ?? throw new InvalidOperationException("Missing populated browse list");
foreach (var value in browse)
{
var point = value as Opc.Ua.Server.ContinuationPoint ?? throw new InvalidOperationException("Unexpected browse point type");
target.Add(Witness("old-browse-point", point));
if (point.Data is { } data) target.Add(Witness("old-browse-data", data));
}
var history = ExactField(session, "m_historyContinuationPoints").GetValue(session) as IList ?? throw new InvalidOperationException("Missing populated history list");
var index = 0;
foreach (var value in history)
{
var slot = value ?? throw new InvalidOperationException("Null history slot");
target.Add(Witness("old-history-slot-" + index, slot));
var state = ExactField(slot, "Value").GetValue(slot) ?? throw new InvalidOperationException("Null history state");
target.Add(Witness("old-history-value-" + index++, state));
}
}
}
private static void CaptureStores(CertificateValidator validator, string label, List<WeakWitness> target)
{
var gate = ExactField(validator, "m_semaphore").GetValue(validator) as SemaphoreSlim ?? throw new InvalidOperationException("Unexpected validator lock");
if (!gate.Wait(TimeSpan.FromSeconds(5))) throw new TimeoutException("Validator witness lock timed out");
try
{
var index = 0;
foreach (var name in new[] { "m_trustedCertificateStore", "m_issuerCertificateStore", "m_rejectedCertificateStore" })
if (ExactField(validator, name).GetValue(validator) is CertificateStoreIdentifier identifier
&& ExactField(identifier, "m_store").GetValue(identifier) is ICertificateStore store)
target.Add(Witness(label + "-" + index++, store));
}
finally { gate.Release(); }
}
private static WeakWitness Witness(string label, object target) => new(label, target);
[MethodImpl(MethodImplOptions.NoInlining)]
private static WeakWitness EphemeralNegative() => Witness("negative-collectible", new object());
[MethodImpl(MethodImplOptions.NoInlining)]
private static bool TerminalDisposeBoundary(WeakWitness server) => server.DisposeOriginalServerForAttribution();
private static async Task WriteShapes(IEnumerable<WeakWitness> witnesses, StreamWriter output)
{ foreach (var witness in witnesses) await LogAsync(output, witness.Shape); }
private static async Task<WeakObservation[]> ObserveAsync(string phase, WeakWitness[] witnesses, WeakWitness positive, WeakWitness negative, StreamWriter output, CancellationToken ct)
{
var delays = new[] { 0, 50, 100, 250, 500, 1000 };
WeakObservation[] last = [];
for (var round = 0; round < delays.Length; round++)
{
if (round > 0)
{
await Task.Delay(delays[round], ct);
await Task.Run(ForcedCollection, ct).WaitAsync(TimeSpan.FromSeconds(5), ct);
}
last = witnesses.Concat([positive, negative]).Select(x => x.Observe(phase == "live-host" && x.Label.StartsWith("fixture-", StringComparison.Ordinal)
|| x.Label == "positive-retained")).ToArray();
await LogAsync(output, new { kind = "weak-observation", phase, round, groups = last, process = ProcessCounters() });
positive.IsAlive().ShouldBeTrue("the retained positive must remain observable");
if (round > 0) negative.IsAlive().ShouldBeFalse("the isolated collectible negative proves GC observation is effective");
}
return last;
}
private static void ForcedCollection() { GC.Collect(2, GCCollectionMode.Forced, true, true); GC.WaitForPendingFinalizers(); GC.Collect(2, GCCollectionMode.Forced, true, true); }
private static object ProcessCounters()
{
using var process = Process.GetCurrentProcess(); process.Refresh();
return new { managedBytes = GC.GetTotalMemory(false), rssBytes = process.WorkingSet64,
activeTimers = Timer.ActiveCount, gcCommittedBytes = GC.GetGCMemoryInfo().TotalCommittedBytes,
jitMethods = System.Runtime.JitInfo.GetCompiledMethodCount() };
}
private sealed class WeakWitness
{
private readonly WeakReference _reference;
internal string Label { get; }
internal object Shape { get; }
private readonly string _type;
internal WeakWitness(string label, object target)
{
Label = label; _type = target.GetType().FullName ?? target.GetType().Name; _reference = new WeakReference(target);
var fields = new List<object>();
for (var type = target.GetType(); type is not null; type = type.BaseType)
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
if (typeof(IDisposable).IsAssignableFrom(field.FieldType)) fields.Add(new { name = field.Name, type = field.FieldType.FullName });
Shape = new { kind = "weak-resource-shape", label, type = _type, isDisposable = target is IDisposable, resourceFields = fields };
}
[MethodImpl(MethodImplOptions.NoInlining)] internal bool IsAlive() => _reference.IsAlive;
[MethodImpl(MethodImplOptions.NoInlining)]
internal bool DisposeOriginalServerForAttribution()
{
var value = _reference.Target;
if (value is null) return false;
if (Label != "fixture-server" || value is not ZB.MOM.WW.OtOpcUa.OpcUaServer.OtOpcUaSdkServer server)
throw new InvalidOperationException("Only the known original server may receive terminal attribution disposal");
server.Dispose(); // Public terminal lifecycle API, once; no private timer/cache manipulation.
return true;
}
internal WeakObservation Observe(bool expectedAlive) => new(Label, _type, IsAlive(), expectedAlive);
}
private sealed record WeakObservation(string label, string type, bool alive, bool expectedAlive);
private sealed record FixtureWitness(WeakWitness[] All, string[] LiveHostSurvivors);
}
@@ -32,7 +32,7 @@ items = ET.SubElement(project,'ItemGroup')
ET.SubElement(items,'FrameworkReference',Include='Microsoft.AspNetCore.App')
for name,path in references.items():
reference=ET.SubElement(items,'Reference',Include=path.stem); ET.SubElement(reference,'HintPath').text=str(path)
files = ['Inventory.cs'] if args.inventory else ['Program.cs','BaselineRig.cs','BaselineMetrics.cs','CanonicalOperations.cs','SemanticRetention.cs']
files = ['Inventory.cs'] if args.inventory else ['Program.cs','BaselineRig.cs','BaselineMetrics.cs','CanonicalOperations.cs']
for name in files: ET.SubElement(items,'Compile',Include=str(source/name))
path=root/'adapter.csproj'; ET.indent(project); ET.ElementTree(project).write(path,encoding='unicode')
manifest={'diagnosticOnly':True,'productRebuilt':False,'references':{name:hashlib.sha256(p.read_bytes()).hexdigest() for name,p in references.items()},
@@ -10,7 +10,7 @@ import subprocess
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--adapter',type=Path,required=True)
parser.add_argument('--output',type=Path,required=True)
parser.add_argument('--mode',choices=['normal','active-restart','semantic-retention','semantic-terminal-dispose'],required=True)
parser.add_argument('--mode',choices=['normal','active-restart'],required=True)
args=parser.parse_args()
output=args.output.resolve()
if output.exists():raise RuntimeError('Use a fresh private output directory')
@@ -19,14 +19,11 @@ environment=os.environ.copy()
environment['OTOPCUA_BASELINE_CANONICAL_KEY']='otua_k-server-events_'+secrets.token_hex(3)
command=['dotnet',str(args.adapter.resolve(strict=True)),str(output),'1']
if args.mode=='active-restart':command+=['--active-restart-control']
if args.mode=='semantic-retention':command+=['--semantic-retention-control']
if args.mode=='semantic-terminal-dispose':command+=['--semantic-terminal-dispose-control']
outer_timeout=180 if args.mode.startswith('semantic-') else 120
with os.fdopen(os.open(log,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600),'w') as stream:
try:result=subprocess.run(command,env=environment,stdout=stream,stderr=subprocess.STDOUT,timeout=outer_timeout);exit_code=result.returncode
try:result=subprocess.run(command,env=environment,stdout=stream,stderr=subprocess.STDOUT,timeout=120);exit_code=result.returncode
except subprocess.TimeoutExpired:exit_code=124
if output.exists():
(output/'runner-receipt.json').write_text(json.dumps({'mode':args.mode,'exitCode':exit_code,'acceptance':False,
'productRebuilt':False,'timeoutSeconds':outer_timeout,'credentialBytes':len(environment['OTOPCUA_BASELINE_CANONICAL_KEY'].encode())},indent=2)+'\n')
'productRebuilt':False,'timeoutSeconds':120,'credentialBytes':len(environment['OTOPCUA_BASELINE_CANONICAL_KEY'].encode())},indent=2)+'\n')
print(json.dumps({'mode':args.mode,'exitCode':exit_code,'acceptance':False}))
raise SystemExit(exit_code)
@@ -1,71 +0,0 @@
# Linux late-slope diagnostic
The [protocol](../LATE_SLOPE.md) fixes 7,200 seconds of canonical cycling from
startup completion, with the existing active restart every fourth cycle. The
cycle-three baseline and resource caps remain in force. A latched rejection
keeps the original failing test and container exit. The result records
`acceptance=false` even when the diagnostic completes its duration and shutdown.
The analyzer requires an ordered active, recovered-stall, resource-observation,
quiescent and complete-cycle ledger. It checks the 25-second restart helper
bound, 30-second complete cycle bound and 60-second shutdown bound. It
recomputes the first cold rejection and verifies its assertion against the
actual TRX. It checks each fixed time window and the entire 1,800–7,200 second
range before reporting RSS and retained-managed slopes in bytes per hour.
The report includes sample counts, quiescent peaks, final values and coverage.
It identifies whether the earlier saturation workload had completed by 1,800
seconds, which bounds any interpretation relative to the previously observed
restart step. The raw JSONL keeps every sample and the first assertion stack.
Certificate allocation stack tracking remains enabled and contributes to the
measured process. The slope report is descriptive and carries no resource
pass threshold. Quiescent peaks refer to the sampled points.
## Offline controls and artifact preparation
The synthetic controls exercise the parser, receipts and bundle manifest with
temporary files. Run them with Python 3.11 or later.
```bash
python3 scripts/validation/opcua-lifecycle-memory/late-slope/test_offline.py
```
Prepare from the reviewed clean commit containing the canonical test and tools.
The image argument is the immutable ID of a locally cached Linux ARM64 SDK
image. The explicitly selected Docker context must use a local Unix socket.
The preparation command publishes the self-contained .NET 10.0.14 app and
records the exact SDK 2.0.1 and HistorianGateway 1.0.0 graph, source hashes, file
hashes and managed module MVIDs. It uses the repository's read-only PE metadata
inspector. Preparation completes before any descriptor or lifecycle test runs.
```bash
python3 scripts/validation/opcua-lifecycle-memory/late-slope/prepare.py \
--source-revision "$reviewedRevision" --docker-context "$localContext" \
--image "$cachedImageId" --output "$bundle"
python3 "$bundle/tools/launch.py" --bundle "$bundle" \
--receipt-sha256 "$reviewedBundleReceiptHash" --verify-only
```
## Reviewed run and analysis
Root owns launch after review of the exact bundle receipt and controls. The
bundled launcher uses the pinned Unix endpoint, immutable image, network-none
container, read-only root filesystem and read-only application and tools mounts.
A fresh private task directory holds results and fixture files. The two
descriptor controls must execute successfully before the canonical test starts. The launcher verifies
the actual test process's apphost, mapped CLR and CLR hash. Its outer deadline
is 7,440 seconds, with cleanup limited to the uniquely labeled container.
```bash
python3 "$bundle/tools/launch.py" --bundle "$bundle" \
--receipt-sha256 "$reviewedBundleReceiptHash" --output "$run"
python3 "$bundle/tools/analyze.py" --run "$run" \
--bundle-receipt "$bundle/receipt.json" --bundle-receipt-sha256 "$reviewedBundleReceiptHash"
```
A completed diagnostic with a cold-cap rejection has a nonzero launcher exit.
Its receipt records diagnostic completion separately from the actual test and
container status. The offline analyzer verifies the reviewed bundle receipt,
raw evidence hashes, test identities and outcomes, fixed duration and verified
shutdown before emitting a report. It can run from copies of the small bundle
receipt and run evidence on a machine without Docker.
@@ -1,423 +0,0 @@
#!/usr/bin/env python3
"""Validate a recorded Linux late-slope diagnostic and describe its quiescent samples."""
import argparse
import hashlib
import json
import math
from pathlib import Path
import re
from statistics import median
import sys
import xml.etree.ElementTree as ET
PROTOCOL = "sdk2-late-slope-v1"
DOCKER_LABEL = "org.otopcua.late-slope-run"
MODE = "late-slope"
SECONDS = 7200
MAX_CYCLE_SECONDS = 30
RESTART_SECONDS = 25
SHUTDOWN_SECONDS = 60
CONTAINER_SECONDS = 7440
RUNTIME = "10.0.14"
SDK = "2.0.1+07e62f72500cc0923a2bab940653c8c1116fa286"
APP = "ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests"
TEST_CLASS = APP + ".Sdk2LifecycleSoakTests."
TEST_NAME = TEST_CLASS + "Repeated_lifecycles_return_resources_to_a_steady_baseline"
DESCRIPTOR_TESTS = {
TEST_CLASS + "Descriptor_probe_observes_an_open_PKI_file_through_a_symlink_alias_and_its_close",
TEST_CLASS + "Descriptor_probe_times_out_and_reaps_a_process_that_keeps_both_streams_open",
}
STALL_STATUSES = {"OperationCanceled", "BadTimeout", "BadRequestCancelledByClient", "BadRequestTimeout", "BadRequestInterrupted"}
WINDOWS = ((1800, 2400), (3600, 4200), (6600, 7200))
BUDGETS = {"fdGrowth": 32, "retainedManagedGrowthBytes": 64 << 20, "rssGrowthBytes": 128 << 20}
SETTINGS = {
"protocol": PROTOCOL, "mode": MODE, "requestedSeconds": SECONDS, "rid": "linux-arm64",
"runtimeVersion": RUNTIME, "selfContained": True, "acceptance": False,
"startupTimeoutSeconds": 120, "cycleTimeoutSeconds": MAX_CYCLE_SECONDS,
"cycleDeadlineSeconds": SECONDS + MAX_CYCLE_SECONDS, "restartTimeoutSeconds": RESTART_SECONDS,
"ownerTimeoutSeconds": 20, "shutdownTimeoutSeconds": SHUTDOWN_SECONDS,
"containerTimeoutSeconds": CONTAINER_SECONDS, "certificateLeakTracking": True,
"windowsSeconds": [list(window) for window in WINDOWS], "limits": BUDGETS,
}
CAP_FIELDS = (("Descriptors", "fdGrowth"), ("ManagedBytes", "retainedManagedGrowthBytes"), ("RssBytes", "rssGrowthBytes"))
ZERO_FIELDS = (
"Sessions", "Subscriptions", "Items", "BrowsePoints", "HistoryPoints",
"LiveDataItems", "LiveEventItems", "MonitoredNodes", "PkiDescriptors",
)
STEADY_FIELDS = ("Variables", "Folders", "Conditions", "CachedCertificateStores")
MEASUREMENTS = (
"Descriptors", "ManagedBytes", "RssBytes", "AllocatedBytes", "Gen0", "Gen1", "Gen2",
"GcCommittedBytes", "GcHeapBytes", "GcFragmentedBytes", "PinnedObjects", "PendingFinalizers",
"JitMethods", "JitIlBytes", "ActiveTimers", "Threads",
)
SAMPLE_FIELDS = {"Cycle", "Seconds", *ZERO_FIELDS, *STEADY_FIELDS, *MEASUREMENTS}
EVIDENCE_FILES = (
"launch.json", "container.log", "container-exit-status.txt", "controller-runtime.txt",
"descriptor-controls.log", "test.log", "target-runtime.txt", "resources.jsonl",
"test-exit-status.txt", "results/descriptor-controls.trx", "results/late-slope.trx",
)
TRX = {"t": "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"}
def require(condition, message):
if not condition:
raise ValueError(message)
def local_context(context):
return (type(context) is dict and set(context) == {"name", "endpoint"}
and isinstance(context["name"], str) and re.fullmatch(r"[A-Za-z0-9_.-]+", context["name"])
and isinstance(context["endpoint"], str) and context["endpoint"].startswith("unix:///")
and not any(character in context["endpoint"] for character in "\r\n\0"))
def container_command(root, output, bundle, run_id):
require(all(path.is_absolute() and ":" not in str(path) for path in (root, output)), "Invalid Docker bind paths")
require(local_context(bundle["dockerContext"]), "Expected the reviewed local Unix Docker context")
return ["docker", "--host", bundle["dockerContext"]["endpoint"], "run", "--rm", "--init", "--pull", "never",
"--network", "none", "--platform", "linux/arm64", "--read-only", "--workdir", "/task",
"--name", f"opcua-late-slope-{run_id}",
"--label", f"{DOCKER_LABEL}={run_id}", "-e", "OTOPCUA_SOAK_BUILD_SHA=" + bundle["sourceRevision"],
"-v", f"{root / 'app'}:/app:ro", "-v", f"{root / 'tools'}:/tools:ro", "-v", f"{output}:/task",
bundle["image"], "bash", "/tools/container.sh"]
def digest(path):
with path.open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def unique_object(pairs):
result = {}
for key, value in pairs:
require(key not in result, f"Duplicate JSON key {key}")
result[key] = value
return result
def reject_constant(value):
raise ValueError(f"Invalid JSON constant {value}")
def json_object(text):
value = json.loads(text, object_pairs_hook=unique_object, parse_constant=reject_constant)
require(type(value) is dict, "Expected a JSON object")
return value
def read_object(path):
return json_object(path.read_text(encoding="utf-8"))
def read_rows(path):
with path.open(encoding="utf-8") as stream:
for line_number, line in enumerate(stream, 1):
require(line.endswith("\n"), f"Truncated JSONL row {line_number}")
try:
yield json_object(line)
except ValueError as error:
raise ValueError(f"Invalid JSONL row {line_number}: {error}") from error
def count(value, label, minimum=0):
require(type(value) is int and minimum <= value <= (1 << 63) - 1, f"Invalid {label}")
return value
def seconds(value, label, minimum=0, maximum=CONTAINER_SECONDS):
require(type(value) in (int, float) and minimum <= value <= maximum, f"Invalid {label}")
return value
def assertion_message(text):
require(isinstance(text, str), "Missing original resource-cap assertion")
message = text.replace("\r", "").split("\n at ", 1)[0].strip()
normalized = re.sub(r"^Shouldly\.ShouldAssertException\s*:\s*", "Shouldly.ShouldAssertException: ", message)
require(normalized.startswith("Shouldly.ShouldAssertException: ") and len(normalized) > 32,
"The recorded failure is not a resource-cap assertion")
return normalized
def sample(row, cycle):
value = row["resources"]
require(type(value) is dict and set(value) == SAMPLE_FIELDS, f"Invalid sample fields at cycle {cycle}")
require(count(value["Cycle"], "sample cycle", 1) == cycle, f"Out-of-order sample cycle {cycle}")
seconds(value["Seconds"], "sample time", maximum=SECONDS + MAX_CYCLE_SECONDS)
for field in SAMPLE_FIELDS - {"Cycle", "Seconds"}:
count(value[field], field)
require(all(value[field] == 0 for field in ZERO_FIELDS), f"Quiescent ownership failure at cycle {cycle}")
return value
def validate_rows(rows, revision):
"""Validate the canonical row order, original budget and fixed-duration outcome."""
rows = iter(rows)
def take(kind):
row = next(rows, {})
require(row.get("kind") == kind, f"Expected {kind}, found {row.get('kind')!r}")
require(row.get("acceptance", False) is False and "satisfiesTwoHourDuration" not in row,
"An ordinary acceptance result is forbidden")
return row
start = take("start")
require(start.get("mode") == MODE and start.get("requestedSeconds") == SECONDS
and type(start["requestedSeconds"]) is int and start.get("minimumFullSeconds") == SECONDS
and start.get("acceptance") is False, "Wrong fixed-duration diagnostic mode")
require(start.get("build") == revision and re.fullmatch(r"[0-9a-f]{40}", revision), "Wrong source revision")
require(start.get("runtime") == RUNTIME and start.get("architecture") == "Arm64" and start.get("sdk") == SDK,
"Wrong Linux ARM64 runtime or SDK version")
count(start.get("processId"), "test process ID", 1)
seconds(start.get("startupSeconds"), "startup duration", maximum=SETTINGS["startupTimeoutSeconds"])
limits = start.get("limits")
require(type(limits) is dict and limits == BUDGETS and all(type(value) is int for value in limits.values()),
"Original cold resource caps changed")
samples = []
first_rejection = None
completions = []
# Buffer only a single row so the actual record order cannot be reconstructed by sorting.
pending = next(rows, {})
while pending.get("kind") == "active":
cycle = len(samples) + 1
require(count(pending.get("cycle"), "active cycle", 1) == cycle, "Missing or duplicate active cycle")
active_counts = tuple(count(pending.get(field), field) for field in ("sessions", "subscriptions", "items"))
require(active_counts == (2, 1, 3), f"Incomplete active session, subscription or item exercise at cycle {cycle}")
for field in ("browsePoints", "historyPoints"):
count(pending.get(field), field, 1)
stall = take("network-stall-recovered")
require(count(stall.get("cycle"), "recovered stall cycle", 1) == cycle, "Missing or duplicate recovered stall")
seconds(stall.get("seconds"), "network stall time", maximum=MAX_CYCLE_SECONDS)
require(isinstance(stall.get("status"), str) and stall["status"] in STALL_STATUSES,
"Unexpected recovered network stall status")
value = sample(take("resource-observation"), cycle)
require(not samples or value["Seconds"] > samples[-1]["Seconds"], "Nonmonotonic or duplicate sample time")
require(not completions or value["Seconds"] > completions[-1]["elapsedSeconds"],
"Sample precedes completion of the previous cycle")
samples.append(value)
if cycle >= 3:
baseline = samples[2]
require(all(value[field] == baseline[field] for field in STEADY_FIELDS),
f"Quiescent model or certificate-store inventory changed at cycle {cycle}")
exceeded = [field for field, cap in CAP_FIELDS if value[field] > baseline[field] + BUDGETS[cap]]
if exceeded and first_rejection is None:
rejection = take("budget-rejected")
require(count(rejection.get("cycle"), "rejection cycle", 1) == cycle
and sample(rejection, cycle) == value
and sample({"resources": rejection.get("baseline")}, 3) == baseline,
"First rejection does not match the failing sample and original cycle-three baseline")
seconds(rejection.get("seconds"), "rejection time", minimum=value["Seconds"],
maximum=SECONDS + MAX_CYCLE_SECONDS)
first_rejection = {**rejection, "exceededFields": exceeded,
"assertionMessage": assertion_message(rejection.get("failure"))}
if cycle >= 26:
retained = [row["ManagedBytes"] for row in samples[-24:]]
require(not (all(a < b for a, b in zip(retained, retained[1:]))
and retained[-1] - retained[0] > (16 << 20)),
f"Managed growth guard failed at cycle {cycle}")
quiescent = take("quiescent-budget-rejected" if first_rejection else "quiescent")
require(sample(quiescent, cycle) == value, "Quiescent sample differs from its pre-assertion observation")
quiescent_seconds = seconds(quiescent.get("cycleSeconds"), "quiescent cycle duration", maximum=MAX_CYCLE_SECONDS)
restart_seconds = 0
if cycle % 4 == 0:
restart = take("restart")
require(count(restart.get("cycles"), "restart cycle", 1) == cycle
and count(restart.get("restarts"), "restart count", 1) == cycle // 4, "Missing or duplicate active restart")
bounded = seconds(restart.get("boundedRestartSeconds"), "bounded restart duration", maximum=RESTART_SECONDS)
restart_seconds = seconds(restart.get("seconds"), "combined restart duration", minimum=bounded, maximum=MAX_CYCLE_SECONDS)
completion = take("late-slope-cycle")
require(count(completion.get("cycle"), "completed cycle", 1) == cycle
and count(completion.get("restarts"), "completed restarts") == cycle // 4, "Invalid completed cycle ledger")
elapsed = seconds(completion.get("elapsedSeconds"), "cycle completion time", minimum=value["Seconds"],
maximum=SECONDS + MAX_CYCLE_SECONDS)
seconds(completion.get("cycleSeconds"), "complete cycle duration", minimum=quiescent_seconds + restart_seconds,
maximum=MAX_CYCLE_SECONDS)
if first_rejection and first_rejection["cycle"] == cycle:
require(first_rejection["seconds"] <= elapsed, "Rejection follows its completed cycle")
require(not completions or completions[-1]["elapsedSeconds"] < SECONDS,
"Another cycle began after the fixed duration")
completions.append(completion)
pending = next(rows, {})
result = pending
require(result.get("kind") == "late-slope-result" and next(rows, None) is None,
"Expected a unique final diagnostic result with no ordinary completion or trailing failure")
require(result.get("mode") == MODE and result.get("acceptance") is False
and "satisfiesTwoHourDuration" not in result and result.get("durationCompleted") is True
and result.get("shutdownVerified") is True and result.get("fatalFailure") is False and result.get("failureTypes") == [],
"Duration, shutdown or diagnostic outcome was incomplete")
cycles = count(result.get("cycles"), "result cycles", 4)
require(cycles == len(samples) and count(result.get("restarts"), "result restarts", 1) == cycles // 4,
"Final cycle or restart count differs from the ledger")
phase = seconds(result.get("cycleSeconds"), "canonical cycle phase", minimum=SECONDS, maximum=SECONDS + MAX_CYCLE_SECONDS)
require(phase >= completions[-1]["elapsedSeconds"] and completions[-1]["elapsedSeconds"] >= SECONDS,
"The final cycle does not cover the fixed duration")
seconds(result.get("shutdownSeconds"), "shutdown duration", maximum=SHUTDOWN_SECONDS)
require(result.get("coldBudgetRejected") is (first_rejection is not None), "The first cold-cap rejection was not preserved")
return start, samples, completions, first_rejection, result
def sample_summary(samples, fields):
times = [value["Seconds"] for value in samples]
summary = {"sampleCount": len(samples), "firstSeconds": times[0], "lastSeconds": times[-1],
"spanSeconds": times[-1] - times[0], "maximumGapSeconds": max((b - a for a, b in zip(times, times[1:])), default=0)}
for field in fields:
values = [value[field] for value in samples]
summary[field] = {"first": values[0], "last": values[-1], "min": min(values), "max": max(values), "median": median(values)}
return summary
def slope(samples, field):
times = [value["Seconds"] for value in samples]
mean = math.fsum(times) / len(times)
mean_value = math.fsum(value[field] for value in samples) / len(samples)
variance = math.fsum((time - mean) ** 2 for time in times)
return 3600 * math.fsum((time - mean) * (value[field] - mean_value)
for time, value in zip(times, samples)) / variance
def describe(samples, completions):
windows = []
for begin, end in WINDOWS:
subset = [value for value in samples if begin <= value["Seconds"] <= end]
require(len(subset) >= 10, f"Insufficient observations in {begin}–{end} seconds")
summary = sample_summary(subset, MEASUREMENTS)
require(summary["firstSeconds"] <= begin + 60 and summary["lastSeconds"] >= end - 60
and summary["spanSeconds"] >= 480 and summary["maximumGapSeconds"] <= 60,
f"Incomplete coverage in {begin}–{end} seconds")
windows.append({"windowSeconds": [begin, end], **summary})
late = [value for value in samples if 1800 <= value["Seconds"] <= SECONDS]
coverage = sample_summary(late, ())
require(coverage["maximumGapSeconds"] <= 60, "An observation gap exceeds 60 seconds across the late phase")
previous = [row for row in completions if row["elapsedSeconds"] <= 1800]
cycles = len(previous)
restarts = previous[-1]["restarts"] if previous else 0
work = {"cycles": cycles, "restarts": restarts, "earlierSaturationWorkloadReached": cycles >= 385 and restarts >= 96}
if not work["earlierSaturationWorkloadReached"]:
work["interpretation"] = "Post-restart-step attribution withheld because the earlier workload counts fell short by 1800 seconds."
return {
"allQuiescent": sample_summary(samples, (*ZERO_FIELDS, *STEADY_FIELDS, *MEASUREMENTS)),
"finalQuiescentSample": samples[-1],
"windows": windows,
"leastSquares": {**coverage, "rangeSeconds": [1800, SECONDS], "units": "bytes/hour",
"rss": slope(late, "RssBytes"), "retainedManaged": slope(late, "ManagedBytes")},
"workCompletedBy1800Seconds": work,
}
def check_trx(path, expected_names, failure=None):
document = ET.parse(path).getroot()
counters = document.findall("./t:ResultSummary/t:Counters", TRX)
require(len(counters) == 1, f"Missing unique counters in {path.name}")
counters = counters[0].attrib
total = len(expected_names)
failed = 1 if failure else 0
expected = {"total": total, "executed": total, "passed": total - failed, "failed": failed,
"error": 0, "timeout": 0, "aborted": 0, "notExecuted": 0}
for key, value in expected.items():
require(counters.get(key) == str(value), f"Unexpected {key} count in {path.name}")
for key in ("inconclusive", "passedButRunAborted", "notRunnable", "disconnected", "inProgress", "pending"):
require(counters.get(key, "0") == "0", f"Unexpected {key} count in {path.name}")
results = document.findall("./t:Results/t:UnitTestResult", TRX)
require(len(results) == total and {row.get("testName") for row in results} == expected_names,
f"Wrong test identities in {path.name}")
require(all(row.get("outcome") == ("Failed" if failure else "Passed") for row in results),
f"Unexpected test outcome in {path.name}")
if failure:
actual = results[0].findtext("./t:Output/t:ErrorInfo/t:Message", namespaces=TRX)
require(assertion_message(actual) == failure, "TRX failure differs from the first cold-cap assertion")
return {"executed": total, "passed": total - failed, "failed": failed, "skipped": 0}
def exit_status(path):
value = path.read_text(encoding="ascii").strip()
require(re.fullmatch(r"0|[1-9][0-9]{0,2}", value) and int(value) <= 255, f"Invalid {path.name}")
return int(value)
def inspect_run(path, provenance, wall_seconds):
"""Check actual run evidence. This function neither launches Docker nor changes the evidence."""
start, samples, completions, rejection, result = validate_rows(read_rows(path / "resources.jsonl"), provenance["sourceRevision"])
wall = seconds(wall_seconds, "container wall time", minimum=SECONDS)
require(start["startupSeconds"] + result["cycleSeconds"] + result["shutdownSeconds"] <= wall,
"Startup, cycle and shutdown phases exceed measured wall time")
test_exit = exit_status(path / "test-exit-status.txt")
container_exit = exit_status(path / "container-exit-status.txt")
require(test_exit == container_exit and ((test_exit != 0) == bool(rejection)), "Original test/container exit was not preserved")
mapping = (path / "target-runtime.txt").read_text(encoding="ascii").splitlines()
require(mapping == [f"targetPid={start['processId']}", f"actualExecutable=/app/{APP}",
"mappedCoreClr=/app/libcoreclr.so", f"coreClrSha256={provenance['coreClrSha256']}"],
"Published Linux apphost or mapped CLR was not verified for the recorded PID")
controls = check_trx(path / "results/descriptor-controls.trx", DESCRIPTOR_TESTS)
workload = check_trx(path / "results/late-slope.trx", {TEST_NAME}, rejection and rejection["assertionMessage"])
return {"diagnosticOutcome": result, "firstColdRejection": rejection, "descriptorTests": controls,
"experimentTests": workload, "testExitStatus": test_exit, "containerExitStatus": container_exit,
**describe(samples, completions)}
def evidence_hashes(path):
result = {}
for name in EVIDENCE_FILES:
file = path / name
require(file.is_file() and file.resolve() == file, f"Missing or linked evidence file {name}")
result[name] = digest(file)
return result
def analyze_run(path, bundle_receipt_path, bundle_hash):
require(re.fullmatch(r"[0-9a-f]{64}", bundle_hash) and digest(bundle_receipt_path) == bundle_hash,
"Reviewed bundle receipt hash differs")
bundle = read_object(bundle_receipt_path)
require(bundle.get("kind") == "bundle" and all(type(bundle.get(key)) is type(value) and bundle[key] == value
for key, value in SETTINGS.items()),
"Invalid immutable bundle protocol")
receipt = read_object(path / "run-receipt.json")
provenance = read_object(path / "launch.json")
require(receipt.get("kind") == "run" and provenance.get("kind") == "launch", "Wrong receipt types")
for record in (receipt, provenance):
require(record.get("protocol") == PROTOCOL and record.get("acceptance") is False
and record.get("bundleReceiptSha256") == bundle_hash, "Wrong diagnostic provenance")
for field in ("runId", "sourceRevision", "image", "coreClrSha256", "dockerContext", "command", "containerTimeoutSeconds"):
require(receipt[field] == provenance[field], f"Launch and run receipts disagree on {field}")
require(receipt["sourceRevision"] == bundle["sourceRevision"] and receipt["image"] == bundle["image"]
and receipt["coreClrSha256"] == bundle["files"]["app/libcoreclr.so"]
and receipt["dockerContext"] == bundle["dockerContext"],
"Recorded source, image or runtime differs from the reviewed bundle")
require(re.fullmatch(r"[0-9a-f]{32}", receipt["runId"])
and re.fullmatch(r"sha256:[0-9a-f]{64}", receipt["image"])
and re.fullmatch(r"[0-9a-f]{64}", receipt["coreClrSha256"]), "Invalid run identity or immutable provenance")
require(receipt.get("diagnosticCompleted") is True and receipt.get("containerRemoved") is True,
"The launcher did not verify duration, shutdown and container cleanup")
require("validationErrors" not in receipt and provenance["containerTimeoutSeconds"] == CONTAINER_SECONDS
and provenance["command"] == container_command(Path(provenance["bundlePath"]), Path(provenance["outputPath"]),
bundle, receipt["runId"]), "The recorded container invocation differs")
require(receipt.get("evidenceSha256") == evidence_hashes(path), "Recorded evidence hashes differ")
report = inspect_run(path, provenance, receipt["wallSeconds"])
for field in ("diagnosticOutcome", "testExitStatus", "containerExitStatus", "descriptorTests", "experimentTests"):
require(receipt[field] == report[field], f"Run receipt differs from the raw {field}")
require(receipt.get("launcherExitStatus") == report["testExitStatus"], "The launcher did not preserve the original exit")
return {"protocol": PROTOCOL, "acceptance": False, "bundleReceiptSha256": bundle_hash,
"sourceRevision": receipt["sourceRevision"], "evidenceSha256": receipt["evidenceSha256"], **report}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--run", type=Path, required=True)
parser.add_argument("--bundle-receipt", type=Path, required=True)
parser.add_argument("--bundle-receipt-sha256", required=True)
options = parser.parse_args()
try:
print(json.dumps(analyze_run(options.run.resolve(strict=True), options.bundle_receipt,
options.bundle_receipt_sha256), indent=2, allow_nan=False))
except (OSError, ValueError, KeyError, TypeError, ET.ParseError) as error:
print(f"Incomplete late-slope evidence: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,129 +0,0 @@
"""Checks shared by immutable artifact preparation and launch."""
import json
import os
from pathlib import Path
import re
import stat
import subprocess
import sys
from uuid import UUID
sys.dont_write_bytecode = True
from analyze import APP, RUNTIME, SETTINGS, digest, local_context, read_object, require
TOOLS = ("analyze.py", "bundle.py", "launch.py", "container.sh", "README.md")
TEST_DIRECTORY = f"tests/Server/{APP}"
CANONICAL_SOURCES = tuple(f"{TEST_DIRECTORY}/{name}" for name in (
"Sdk2LifecycleSoakTests.cs", "Sdk2LifecycleLateSlopeTests.cs", f"{APP}.csproj"))
SDK_PACKAGES = {
f"OPCFoundation.NetStandard.Opc.Ua.{component}/2.0.1"
for component in ("Client", "Configuration", "Core", "Core.Schema", "Core.Types",
"Security.Certificates", "Server", "Types")
}
GATEWAY_PACKAGES = {f"ZB.MOM.WW.HistorianGateway.{component}/1.0.0" for component in ("Client", "Contracts")}
def context_identity(name):
require(re.fullmatch(r"[A-Za-z0-9_.-]+", name), "Invalid local Docker context name")
require(not any(os.environ.get(key) for key in ("DOCKER_HOST", "DOCKER_TLS", "DOCKER_TLS_VERIFY", "DOCKER_CERT_PATH"))
and os.environ.get("DOCKER_CONTEXT", name) in ("", name), "Docker endpoint or TLS override is set")
result = subprocess.check_output(["docker", "context", "inspect", name], text=True, timeout=15)
contexts = json.loads(result)
require(len(contexts) == 1 and contexts[0]["Name"] == name, "Expected one explicit Docker context")
identity = {"name": name, "endpoint": contexts[0]["Endpoints"]["docker"]["Host"]}
require(local_context(identity) and Path(identity["endpoint"][len("unix://"):]).is_socket(),
"The reviewed Docker context must use a local Unix socket")
return identity
def image_identity(image, context):
require(re.fullmatch(r"sha256:[0-9a-f]{64}", image), "Use a locally cached immutable image ID")
result = subprocess.check_output(["docker", "--host", context["endpoint"], "image", "inspect", image], text=True, timeout=15)
images = json.loads(result)
require(len(images) == 1, "Expected one cached image")
value = images[0]
require((value["Id"], value["Os"], value["Architecture"]) == (image, "linux", "arm64"),
"The immutable image must be Linux ARM64")
return {"id": image, "os": "linux", "architecture": "arm64"}
def manifest(root):
"""Hash the exact regular-file tree and exclude only the receipt that contains it."""
result = {}
for path in sorted(root.rglob("*")):
kind = path.lstat().st_mode
require(stat.S_ISDIR(kind) or stat.S_ISREG(kind), f"Unsupported artifact type {path.relative_to(root)}")
if stat.S_ISREG(kind) and path != root / "receipt.json":
result[path.relative_to(root).as_posix()] = digest(path)
return result
def check_elf(path):
with path.open("rb") as stream:
header = stream.read(20)
require(header[:6] == b"\x7fELF\x02\x01" and header[18:20] == b"\xb7\x00",
f"{path.name} is not ELF64 AArch64")
def check_application(app):
check_elf(app / APP)
require(os.access(app / APP, os.X_OK), "The Linux apphost is not executable")
check_elf(app / "libcoreclr.so")
options = read_object(app / f"{APP}.runtimeconfig.json")["runtimeOptions"]
frameworks = options["includedFrameworks"]
require(options["tfm"] == "net10.0" and "framework" not in options and "frameworks" not in options
and isinstance(frameworks, list) and any(value["name"] == "Microsoft.NETCore.App" for value in frameworks)
and all(value["version"] == RUNTIME for value in frameworks), "Wrong self-contained application runtime")
dependencies = read_object(app / f"{APP}.deps.json")
require(dependencies["runtimeTarget"]["name"] == ".NETCoreApp,Version=v10.0/linux-arm64", "Wrong application RID")
libraries = dependencies["libraries"]
sdk = {key for key in libraries if key.startswith("OPCFoundation.")}
gateway = {key for key in libraries if key.startswith("ZB.MOM.WW.HistorianGateway.")}
require(sdk == SDK_PACKAGES and gateway == GATEWAY_PACKAGES, "Expected exact SDK 2.0.1 and HistorianGateway 1.0.0 graph")
return sorted(sdk), sorted(gateway)
def check_modules(modules, files):
require(isinstance(modules, list), "Missing managed module identities")
seen = set()
for module in modules:
name = module["file"]
require(name in files and name.startswith("app/") and name.endswith(".dll") and name not in seen,
"Unexpected or duplicate managed module file")
require(module["assembly"] and re.fullmatch(r"\d+(?:\.\d+){3}", module["version"])
and UUID(module["mvid"]).int != 0, "Invalid assembly version or MVID")
seen.add(name)
required = {f"app/{APP}.dll", "app/Opc.Ua.Server.dll", "app/System.Private.CoreLib.dll",
"app/ZB.MOM.WW.HistorianGateway.Client.dll", "app/ZB.MOM.WW.HistorianGateway.Contracts.dll"}
require(required <= seen, "Missing application, runtime or dependency module identity")
def verify_bundle(root, expected_hash):
path = root / "receipt.json"
require(path.is_file() and not path.is_symlink() and re.fullmatch(r"[0-9a-f]{64}", expected_hash)
and digest(path) == expected_hash, "Reviewed bundle receipt hash differs")
receipt = read_object(path)
require(receipt.get("kind") == "bundle" and all(type(receipt.get(key)) is type(value) and receipt[key] == value
for key, value in SETTINGS.items()), "Invalid fixed diagnostic settings")
require(re.fullmatch(r"[0-9a-f]{40}", receipt["sourceRevision"]), "Invalid committed source revision")
files = manifest(root)
require(receipt["files"] == files, "The immutable bundle file set or a file hash changed")
require({name for name in files if name.startswith("tools/")} == {f"tools/{name}" for name in TOOLS},
"Unexpected bundled tools")
sdk, gateway = check_application(root / "app")
require(receipt["sdkLibraries"] == sdk and receipt["gatewayLibraries"] == gateway,
"Dependency receipt differs from the published application")
check_modules(receipt["managedModules"], files)
require(receipt["imageIdentity"] == {"id": receipt["image"], "os": "linux", "architecture": "arm64"}
and re.fullmatch(r"sha256:[0-9a-f]{64}", receipt["image"]), "Invalid immutable image identity")
require(local_context(receipt["dockerContext"]), "Invalid reviewed local Docker context")
require(receipt["sourceFiles"] and all(re.fullmatch(r"[0-9a-f]{64}", value) for value in receipt["sourceFiles"].values()),
"Missing committed source hashes")
require(set(CANONICAL_SOURCES) <= receipt["sourceFiles"].keys(), "Missing canonical test or project hash")
for name in TOOLS:
source = "scripts/validation/opcua-lifecycle-memory/late-slope/" + name
require(receipt["sourceFiles"].get(source) == files[f"tools/{name}"], f"Bundled {name} differs from source")
return receipt
@@ -1,83 +0,0 @@
#!/usr/bin/env bash
# Execute the immutable self-contained apphost through the cached Linux VSTest controller.
verify_mapped_coreclr() {
awk -v identity="$2" '
$6 ~ /\/libcoreclr.so$/ {
if (NF != 6 || ($5 " " $6) != identity) bad = 1
else matched = 1
}
END { exit (matched && !bad) ? 0 : 1 }
' "$1"
}
# The offline controls source only the mapping check above.
if [[ ${BASH_SOURCE[0]} != "$0" ]]; then return 0; fi
set -euo pipefail
umask 077
app=ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests
test_class="$app.Sdk2LifecycleSoakTests"
mkdir -p /task/tmp /task/data /task/cli /task/results
export TMPDIR=/task/tmp XDG_DATA_HOME=/task/data DOTNET_CLI_HOME=/task/cli
export OTOPCUA_SDK2_SOAK=1 OTOPCUA_SDK2_SOAK_MODE=late-slope OTOPCUA_SDK2_SOAK_SECONDS=7200
export OTOPCUA_SDK2_SOAK_OUTPUT=/task/resources.jsonl OPCUA_CERTIFICATE_LEAK_TRACKING=1
export DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1
[[ $(uname -s) == Linux && $(uname -m) == aarch64 && -d /proc/self/fd ]]
[[ ${OTOPCUA_SOAK_BUILD_SHA:-} =~ ^[0-9a-f]{40}$ ]]
dotnet --info > /task/controller-runtime.txt
dotnet vstest "/app/$app.dll" \
--TestCaseFilter:"FullyQualifiedName~$test_class.Descriptor_probe_" \
--ResultsDirectory:/task/results '--Logger:trx;LogFileName=descriptor-controls.trx' > /task/descriptor-controls.log 2>&1
# Verify actual execution before starting the long test. The host parses the complete TRX independently.
controls=/task/results/descriptor-controls.trx
counters=$(grep -E '^[[:space:]]*<Counters ' "$controls")
[[ $(grep -Ec '^[[:space:]]*<Counters ' "$controls") == 1 ]]
for attribute in total=2 executed=2 passed=2 failed=0 notExecuted=0 error=0 timeout=0 aborted=0; do
[[ $counters == *" ${attribute%=*}=\"${attribute#*=}\""* ]]
done
[[ $(grep -Ec '^[[:space:]]*<UnitTestResult ' "$controls") == 2 ]]
for name in Descriptor_probe_observes_an_open_PKI_file_through_a_symlink_alias_and_its_close \
Descriptor_probe_times_out_and_reaps_a_process_that_keeps_both_streams_open; do
result=$(grep -E '^[[:space:]]*<UnitTestResult ' "$controls" | grep -F "testName=\"$test_class.$name\"")
[[ $result == *' outcome="Passed"'* && $result != *$'\n'* ]]
done
vstest_pid=
cleanup() {
if [[ -n "$vstest_pid" ]]; then kill "$vstest_pid" 2>/dev/null || true; fi
}
trap cleanup EXIT
dotnet vstest "/app/$app.dll" \
--TestCaseFilter:"FullyQualifiedName=$test_class.Repeated_lifecycles_return_resources_to_a_steady_baseline" \
--ResultsDirectory:/task/results '--Logger:trx;LogFileName=late-slope.trx' > /task/test.log 2>&1 &
vstest_pid=$!
# The canonical startup deadline is 120 seconds. Allow controller startup before observing its first complete row.
proof_deadline=$((SECONDS + 150))
observed=0
while ((SECONDS < proof_deadline)) && kill -0 "$vstest_pid" 2>/dev/null; do
if [[ -s /task/resources.jsonl ]] && IFS= read -r header < /task/resources.jsonl; then
[[ $header == *'"kind":"start"'* && $header == *'"mode":"late-slope"'* ]]
pid=$(sed -n 's/.*"processId":\([1-9][0-9]*\),.*/\1/p' <<< "$header")
[[ $pid =~ ^[1-9][0-9]*$ ]]
executable=$(readlink "/proc/$pid/exe")
[[ $executable == "/app/$app" ]]
runtime=/app/libcoreclr.so
verify_mapped_coreclr "/proc/$pid/maps" "$(stat -c '%i %n' "$runtime")"
runtime_hash=$(sha256sum "$runtime")
printf 'targetPid=%s\nactualExecutable=%s\nmappedCoreClr=%s\ncoreClrSha256=%s\n' \
"$pid" "$executable" "$runtime" "${runtime_hash%% *}" > /task/target-runtime.txt
observed=1
break
fi
sleep 0.1
done
if [[ $observed != 1 ]]; then kill "$vstest_pid" 2>/dev/null || true; fi
test_status=0
wait "$vstest_pid" || test_status=$?
vstest_pid=
printf '%s\n' "$test_status" > /task/test-exit-status.txt
if [[ $test_status != 0 ]]; then exit "$test_status"; fi
[[ $observed == 1 ]]
@@ -1,130 +0,0 @@
#!/usr/bin/env python3
"""Verify an immutable bundle or launch its fixed-duration Linux diagnostic."""
import argparse
import json
from pathlib import Path
import re
import subprocess
import sys
import time
import uuid
import xml.etree.ElementTree as ET
sys.dont_write_bytecode = True
from analyze import CONTAINER_SECONDS, DOCKER_LABEL, PROTOCOL, container_command, evidence_hashes, inspect_run, require
from bundle import context_identity, image_identity, verify_bundle
def remove_container(context, run_id):
docker = ["docker", "--host", context["endpoint"]]
def containers():
result = subprocess.check_output([*docker, "ps", "--all", "--quiet", "--no-trunc",
"--filter", f"label={DOCKER_LABEL}={run_id}"], text=True, timeout=10)
ids = result.split()
require(all(re.fullmatch(r"[0-9a-f]{64}", value) for value in ids), "Unexpected container ID")
return ids
ids = containers()
if ids:
subprocess.run([*docker, "rm", "--force", *ids], stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=15)
require(not containers(), "The task-owned container is still present")
def write(path, value):
with path.open("x", encoding="utf-8") as stream:
json.dump(value, stream, indent=2, allow_nan=False)
stream.write("\n")
def verify_local_image(bundle):
context = bundle["dockerContext"]
require(context_identity(context["name"]) == context, "Reviewed Docker context endpoint changed")
require(image_identity(bundle["image"], context) == bundle["imageIdentity"], "Wrong cached image identity")
def launch(root, bundle, bundle_hash, output):
require(Path(__file__).resolve() == root / "tools/launch.py", "Invoke the reviewed bundled launcher")
require(not output.is_relative_to(root), "Run output must be outside the immutable bundle")
run_id = uuid.uuid4().hex
invocation = container_command(root, output, bundle, run_id)
output.mkdir(mode=0o700, parents=True, exist_ok=False)
provenance = {"kind": "launch", "protocol": PROTOCOL, "acceptance": False, "runId": run_id,
"bundleReceiptSha256": bundle_hash, "sourceRevision": bundle["sourceRevision"], "image": bundle["image"],
"coreClrSha256": bundle["files"]["app/libcoreclr.so"], "command": invocation,
"dockerContext": bundle["dockerContext"], "bundlePath": str(root), "outputPath": str(output),
"containerTimeoutSeconds": CONTAINER_SECONDS}
write(output / "launch.json", provenance)
errors = []
removed = False
started = time.monotonic()
try:
with (output / "container.log").open("x", encoding="utf-8") as log:
process = subprocess.run(invocation, stdout=log, stderr=subprocess.STDOUT, timeout=CONTAINER_SECONDS)
status = process.returncode
except subprocess.TimeoutExpired:
status = 124
errors.append("The container reached the fixed outer deadline")
except KeyboardInterrupt:
status = 130
errors.append("Container execution was interrupted")
except OSError as error:
status = 125
errors.append(str(error))
finally:
wall = time.monotonic() - started
try:
remove_container(bundle["dockerContext"], run_id)
removed = True
except (OSError, subprocess.SubprocessError, ValueError) as error:
errors.append(f"Container cleanup failed: {error}")
(output / "container-exit-status.txt").write_text(f"{status}\n", encoding="ascii")
receipt = {**provenance, "kind": "run", "containerExitStatus": status, "wallSeconds": wall,
"containerRemoved": removed, "diagnosticCompleted": False}
try:
verify_bundle(root, bundle_hash)
verify_local_image(bundle)
receipt["evidenceSha256"] = evidence_hashes(output)
inspected = inspect_run(output, provenance, wall)
for field in ("diagnosticOutcome", "testExitStatus", "descriptorTests", "experimentTests"):
receipt[field] = inspected[field]
except (OSError, KeyError, TypeError, ValueError, ET.ParseError, subprocess.SubprocessError) as error:
errors.append(f"Evidence validation failed: {error}")
receipt["diagnosticCompleted"] = not errors
if errors:
receipt["validationErrors"] = errors
# Return the original container status when the test rethrows its first resource cap.
receipt["launcherExitStatus"] = (128 - status if status < 0 else status) or int(bool(errors))
write(output / "run-receipt.json", receipt)
print(json.dumps(receipt, indent=2, allow_nan=False))
return receipt["launcherExitStatus"]
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--bundle", type=Path, required=True)
parser.add_argument("--receipt-sha256", required=True)
action = parser.add_mutually_exclusive_group(required=True)
action.add_argument("--verify-only", action="store_true")
action.add_argument("--output", type=Path)
options = parser.parse_args()
try:
root = options.bundle.resolve(strict=True)
bundle = verify_bundle(root, options.receipt_sha256)
verify_local_image(bundle)
if options.verify_only:
print(json.dumps({"protocol": PROTOCOL, "sourceRevision": bundle["sourceRevision"],
"receiptSha256": options.receipt_sha256, "verified": True,
"acceptance": False, "workloadLaunched": False}, indent=2))
return 0
return launch(root, bundle, options.receipt_sha256, options.output.resolve())
except (OSError, KeyError, TypeError, ValueError, subprocess.SubprocessError) as error:
print(f"Late-slope launcher rejected the request: {error}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -1,97 +0,0 @@
#!/usr/bin/env python3
"""Publish and record a fresh immutable Linux ARM64 bundle without running the workload."""
import argparse
from datetime import datetime, timezone
import json
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
sys.dont_write_bytecode = True
from analyze import APP, RUNTIME, SETTINGS, digest, require
from bundle import CANONICAL_SOURCES, TOOLS, check_application, context_identity, image_identity, manifest, verify_bundle
HERE = Path(__file__).resolve().parent
REPO = HERE.parents[3]
PROJECT = REPO / "tests/Server" / APP / (APP + ".csproj")
SUPPORT = PROJECT.with_name("Sdk2LifecycleLateSlopeTests.cs")
MODULE_SOURCE = HERE.parent / "ArtifactModuleInfo.cs"
DESIGN = HERE.parent / "LATE_SLOPE.md"
def check_source(revision):
current = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=REPO, text=True).strip()
require(revision == current, "Publish the specified committed source revision")
require(not subprocess.check_output(["git", "status", "--porcelain"], cwd=REPO, text=True).strip(),
"The publication checkout must be clean")
require('"late-slope-result"' in SUPPORT.read_text(encoding="utf-8"), "The committed canonical test lacks late-slope mode")
sources = [*(REPO / name for name in CANONICAL_SOURCES), MODULE_SOURCE, DESIGN,
HERE / "prepare.py", HERE / "test_offline.py", *(HERE / name for name in TOOLS)]
return {path.relative_to(REPO).as_posix(): digest(path) for path in sources}
def managed_modules(build, root):
project = ET.Element("Project", Sdk="Microsoft.NET.Sdk")
settings = ET.SubElement(project, "PropertyGroup")
for key, value in {"OutputType": "Exe", "TargetFramework": "net10.0", "ImplicitUsings": "enable",
"EnableDefaultCompileItems": "false", "TreatWarningsAsErrors": "true"}.items():
ET.SubElement(settings, key).text = value
ET.SubElement(ET.SubElement(project, "ItemGroup"), "Compile", Include=str(MODULE_SOURCE))
path = build / "module-info.csproj"
ET.ElementTree(project).write(path, encoding="unicode")
with (root / "module-info-build.log").open("w", encoding="utf-8") as log:
subprocess.run(["dotnet", "build", str(path), "-c", "Release", "-m:2", "--nologo"],
stdout=log, stderr=subprocess.STDOUT, check=True, timeout=300)
command = ["dotnet", str(build / "bin/Release/net10.0/module-info.dll"), str(root), str(root / "app")]
return json.loads(subprocess.check_output(command, text=True, timeout=60))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--source-revision", required=True)
parser.add_argument("--image", required=True)
parser.add_argument("--docker-context", required=True)
options = parser.parse_args()
sources = check_source(options.source_revision)
context = context_identity(options.docker_context)
image = image_identity(options.image, context)
root = options.output.resolve()
root.mkdir(mode=0o700, parents=True, exist_ok=False)
with tempfile.TemporaryDirectory(prefix="opcua-late-slope-publish-") as scratch:
build = Path(scratch)
command = ["dotnet", "publish", str(PROJECT), "-c", "Release", "-r", "linux-arm64",
"--self-contained", "true", f"-p:RuntimeFrameworkVersion={RUNTIME}",
"-p:PublishSingleFile=false", "-p:PublishTrimmed=false", "-p:UseAppHost=true",
"--artifacts-path", str(build / "artifacts"), "-o", str(root / "app"), "-m:2", "--nologo"]
with (root / "publish.log").open("w", encoding="utf-8") as log:
subprocess.run(command, cwd=REPO, stdout=log, stderr=subprocess.STDOUT, check=True, timeout=900)
sdk, gateway = check_application(root / "app")
modules = managed_modules(build, root)
tools = root / "tools"
tools.mkdir()
for name in TOOLS:
shutil.copy2(HERE / name, tools / name)
require(check_source(options.source_revision) == sources, "Committed inputs changed during publication")
require(context_identity(options.docker_context) == context and image_identity(options.image, context) == image,
"The local Docker context or image changed during publication")
receipt = {**SETTINGS, "kind": "bundle", "sourceRevision": options.source_revision, "sourceFiles": sources, "dockerContext": context,
"image": options.image, "imageIdentity": image, "preparedUtc": datetime.now(timezone.utc).isoformat(),
"publishCommand": command, "sdkLibraries": sdk, "gatewayLibraries": gateway,
"managedModules": modules, "files": manifest(root)}
path = root / "receipt.json"
path.write_text(json.dumps(receipt, indent=2, allow_nan=False) + "\n", encoding="utf-8")
sha = digest(path)
verify_bundle(root, sha)
print(json.dumps({"bundle": str(root), "sourceRevision": options.source_revision,
"receiptSha256": sha, "acceptance": False, "workloadLaunched": False}, indent=2))
if __name__ == "__main__":
main()
@@ -1,383 +0,0 @@
#!/usr/bin/env python3
"""Synthetic evidence controls. No application, Docker, or workload execution."""
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
from uuid import UUID
import xml.etree.ElementTree as ET
sys.dont_write_bytecode = True
import analyze
import bundle
REVISION = "a" * 40
IMAGE = "sha256:" + "b" * 64
CONTEXT = {"name": "local-control", "endpoint": "unix:///tmp/control.sock"}
RUN_ID = "c" * 32
def canonical_rows(period=4, rejected=True):
yield {"kind": "start", "mode": analyze.MODE, "requestedSeconds": 7200, "minimumFullSeconds": 7200,
"runtime": analyze.RUNTIME, "sdk": analyze.SDK, "architecture": "Arm64", "processId": 123,
"startupSeconds": 1, "build": REVISION, "acceptance": False, "limits": analyze.BUDGETS.copy()}
cycles = 7200 // period
baseline = None
for cycle in range(1, cycles + 1):
elapsed = cycle * period - 1
value = dict.fromkeys(analyze.SAMPLE_FIELDS, 0)
value.update(Cycle=cycle, Seconds=elapsed, Variables=19, Folders=4, Conditions=1,
CachedCertificateStores=2, Descriptors=190, ManagedBytes=20_000_000 + elapsed * 32,
RssBytes=200_000_000 + elapsed * 128, GcCommittedBytes=48_000_000, JitMethods=76000,
JitIlBytes=11500000, ActiveTimers=12, Threads=24)
if cycle == 3:
baseline = value.copy()
if rejected and cycle == 4:
limit = baseline["RssBytes"] + analyze.BUDGETS["rssGrowthBytes"]
value["RssBytes"] = limit + 1
yield {"kind": "active", "cycle": cycle, "sessions": 2, "subscriptions": 1, "items": 3,
"browsePoints": 1, "historyPoints": 6}
yield {"kind": "network-stall-recovered", "cycle": cycle, "status": "BadRequestInterrupted", "seconds": 1}
yield {"kind": "resource-observation", "resources": value.copy()}
if rejected and cycle == 4:
yield {"kind": "budget-rejected", "cycle": cycle, "seconds": elapsed + 0.1,
"resources": value.copy(), "baseline": baseline,
"failure": (f"Shouldly.ShouldAssertException: {limit + 1}L\r\n should be less than or equal to"
f"\r\n{limit}L\r\n but was not\n at synthetic fixture")}
yield {"kind": "quiescent-budget-rejected" if rejected and cycle >= 4 else "quiescent",
"cycleSeconds": period - 1, "resources": value.copy()}
if cycle % 4 == 0:
yield {"kind": "restart", "cycles": cycle, "restarts": cycle // 4, "seconds": 1, "boundedRestartSeconds": 0.9}
yield {"kind": "late-slope-cycle", "cycle": cycle, "restarts": cycle // 4,
"elapsedSeconds": cycle * period, "cycleSeconds": period}
yield {"kind": "late-slope-result", "mode": analyze.MODE, "cycles": cycles, "restarts": cycles // 4,
"cycleSeconds": cycles * period, "shutdownSeconds": 2, "durationCompleted": True,
"shutdownVerified": True, "fatalFailure": False, "failureTypes": [], "coldBudgetRejected": rejected, "acceptance": False}
def write_json(path, value):
path.write_text(json.dumps(value) + "\n", encoding="utf-8")
def write_trx(path, names, failure=None):
root = ET.Element("TestRun", xmlns=analyze.TRX["t"])
results = ET.SubElement(root, "Results")
for name in sorted(names):
result = ET.SubElement(results, "UnitTestResult", testName=name, outcome="Failed" if failure else "Passed")
if failure:
output = ET.SubElement(result, "Output")
ET.SubElement(ET.SubElement(output, "ErrorInfo"), "Message").text = failure
total = len(names)
summary = ET.SubElement(root, "ResultSummary")
ET.SubElement(summary, "Counters", total=str(total), executed=str(total), passed=str(total - bool(failure)),
failed=str(int(bool(failure))), notExecuted="0", error="0", timeout="0", aborted="0")
ET.ElementTree(root).write(path, encoding="utf-8")
class RowControls(unittest.TestCase):
def test_fixed_windows_and_known_linear_slopes(self):
_, samples, completions, rejection, _ = analyze.validate_rows(canonical_rows(), REVISION)
report = analyze.describe(samples, completions)
self.assertEqual(rejection["cycle"], 4)
self.assertEqual(report["allQuiescent"]["sampleCount"], 1800)
self.assertEqual(report["allQuiescent"]["RssBytes"]["max"], rejection["resources"]["RssBytes"])
self.assertEqual([row["windowSeconds"] for row in report["windows"]], [[1800, 2400], [3600, 4200], [6600, 7200]])
self.assertEqual([row["sampleCount"] for row in report["windows"]], [150, 150, 150])
self.assertAlmostEqual(report["leastSquares"]["rss"], 128 * 3600)
self.assertAlmostEqual(report["leastSquares"]["retainedManaged"], 32 * 3600)
self.assertEqual(report["leastSquares"]["sampleCount"], 1350)
self.assertTrue(report["workCompletedBy1800Seconds"]["earlierSaturationWorkloadReached"])
def test_slopes_remain_descriptive_below_earlier_workload(self):
_, samples, completions, rejection, _ = analyze.validate_rows(canonical_rows(25, rejected=False), REVISION)
report = analyze.describe(samples, completions)
self.assertIsNone(rejection)
work = report["workCompletedBy1800Seconds"]
self.assertEqual((work["cycles"], work["restarts"]), (72, 18))
self.assertFalse(work["earlierSaturationWorkloadReached"])
self.assertIn("attribution withheld", work["interpretation"])
self.assertAlmostEqual(report["leastSquares"]["rss"], 128 * 3600)
def test_wrong_mode_duration_runtime_or_startup_are_rejected(self):
for field, value in (("mode", "full"), ("requestedSeconds", 90), ("runtime", "10.0.3"),
("architecture", "X64"), ("sdk", "2.0.0"), ("startupSeconds", 121), ("acceptance", True)):
with self.subTest(field=field):
rows = list(canonical_rows(25))
rows[0][field] = value
with self.assertRaises(ValueError):
analyze.validate_rows(rows, REVISION)
def test_wrong_final_state_never_yields_a_report(self):
for field, value in (("kind", "complete"), ("cycleSeconds", 7199.9), ("cycleSeconds", 7231),
("shutdownVerified", False), ("durationCompleted", False), ("fatalFailure", True),
("shutdownSeconds", 61), ("coldBudgetRejected", False), ("acceptance", True), ("restarts", 1)):
with self.subTest(field=field, value=value):
rows = list(canonical_rows(25))
rows[-1][field] = value
with self.assertRaises(ValueError):
analyze.validate_rows(rows, REVISION)
def test_duplicate_reordered_missing_or_malformed_samples(self):
def change(rows, position, field, value):
rows[position]["resources"][field] = value
edits = (
lambda rows: rows.insert(4, rows[3]),
lambda rows: rows.pop(3),
lambda rows: change(rows, 8, "Cycle", 1),
lambda rows: change(rows, 8, "Seconds", rows[3]["resources"]["Seconds"]),
lambda rows: change(rows, 3, "RssBytes", -1),
lambda rows: change(rows, 3, "ManagedBytes", float("inf")),
lambda rows: change(rows, 3, "PkiDescriptors", 1),
lambda rows: rows.append({"kind": "complete"}),
)
for edit in edits:
rows = list(canonical_rows(25))
edit(rows)
with self.assertRaises(ValueError):
analyze.validate_rows(rows, REVISION)
def test_first_rejection_cannot_disappear_or_change_baseline(self):
rows = list(canonical_rows(25))
first = next(i for i, row in enumerate(rows) if row["kind"] == "budget-rejected")
rows[first]["baseline"]["RssBytes"] -= 1
with self.assertRaisesRegex(ValueError, "baseline"):
analyze.validate_rows(rows, REVISION)
rows = list(canonical_rows(25))
later = next(row for row in rows if row["kind"] == "quiescent-budget-rejected" and row["resources"]["Cycle"] == 5)
later["kind"] = "quiescent"
with self.assertRaisesRegex(ValueError, "quiescent-budget-rejected"):
analyze.validate_rows(rows, REVISION)
rows = list(canonical_rows(25))
rows.pop(first)
with self.assertRaisesRegex(ValueError, "budget-rejected"):
analyze.validate_rows(rows, REVISION)
def test_bounded_restart_and_complete_cycle_durations(self):
for kind, field, value, expected in (
("restart", "boundedRestartSeconds", 25.1, "bounded restart duration"),
("restart", "seconds", 31, "combined restart duration"),
("late-slope-cycle", "cycleSeconds", 31, "complete cycle duration"),
):
with self.subTest(field=field):
rows = list(canonical_rows(25))
row = next(row for row in rows if row["kind"] == kind)
row[field] = value
with self.assertRaisesRegex(ValueError, expected):
analyze.validate_rows(rows, REVISION)
def test_missing_windows_edges_and_internal_or_between_window_gaps(self):
_, samples, completions, _, _ = analyze.validate_rows(canonical_rows(25), REVISION)
for begin, end in ((3600, 4200), (1800, 1880), (7110, 7200), (1900, 1980), (2900, 2980)):
with self.subTest(begin=begin, end=end):
incomplete = [sample for sample in samples if not begin <= sample["Seconds"] <= end]
with self.assertRaises(ValueError):
analyze.describe(incomplete, completions)
def test_jsonl_rejects_duplicate_keys_nonfinite_literals_and_truncation(self):
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / "samples.jsonl"
for content in ('{"kind":"start","kind":"complete"}\n', '{"seconds":NaN}\n', '{"kind":"start"}'):
path.write_text(content, encoding="utf-8")
with self.assertRaises(ValueError):
list(analyze.read_rows(path))
class ReceiptControls(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name).resolve()
self.run = self.root / "run"
self.bundle_receipt = self.root / "bundle.json"
(self.run / "results").mkdir(parents=True)
self.build_evidence()
def build_evidence(self, rejected=True):
self.bundle = {**analyze.SETTINGS, "kind": "bundle", "sourceRevision": REVISION, "image": IMAGE,
"dockerContext": CONTEXT, "files": {"app/libcoreclr.so": "d" * 64}}
write_json(self.bundle_receipt, self.bundle)
self.bundle_hash = analyze.digest(self.bundle_receipt)
self.provenance = {"kind": "launch", "protocol": analyze.PROTOCOL, "acceptance": False,
"sourceRevision": REVISION, "image": IMAGE, "dockerContext": CONTEXT, "runId": RUN_ID,
"bundleReceiptSha256": self.bundle_hash, "coreClrSha256": "d" * 64,
"containerTimeoutSeconds": 7440, "bundlePath": str(self.root / "bundle"), "outputPath": str(self.run)}
self.provenance["command"] = analyze.container_command(self.root / "bundle", self.run, self.bundle, RUN_ID)
write_json(self.run / "launch.json", self.provenance)
rows = list(canonical_rows(25, rejected))
(self.run / "resources.jsonl").write_text("".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8")
status = int(rejected)
for filename in ("test-exit-status.txt", "container-exit-status.txt"):
(self.run / filename).write_text(f"{status}\n", encoding="ascii")
(self.run / "target-runtime.txt").write_text(
f"targetPid=123\nactualExecutable=/app/{analyze.APP}\nmappedCoreClr=/app/libcoreclr.so\ncoreClrSha256={'d' * 64}\n",
encoding="ascii")
for filename in ("container.log", "controller-runtime.txt", "descriptor-controls.log", "test.log"):
(self.run / filename).write_text("Synthetic control fixture\n", encoding="utf-8")
write_trx(self.run / "results/descriptor-controls.trx", analyze.DESCRIPTOR_TESTS)
failure = next((row["failure"] for row in rows if row["kind"] == "budget-rejected"), None)
message = analyze.assertion_message(failure).replace("Exception:", "Exception :", 1) if failure else None
write_trx(self.run / "results/late-slope.trx", {analyze.TEST_NAME}, message)
actual = analyze.inspect_run(self.run, self.provenance, 7205)
self.receipt = {**self.provenance, "kind": "run", "diagnosticCompleted": True, "containerRemoved": True,
"wallSeconds": 7205, "launcherExitStatus": status, **{key: actual[key] for key in (
"testExitStatus", "containerExitStatus", "descriptorTests", "experimentTests", "diagnosticOutcome")}}
self.refresh_receipt()
def refresh_receipt(self):
self.receipt["evidenceSha256"] = analyze.evidence_hashes(self.run)
write_json(self.run / "run-receipt.json", self.receipt)
def report(self):
return analyze.analyze_run(self.run, self.bundle_receipt, self.bundle_hash)
def test_cold_failure_and_zero_exit_are_separate_diagnostic_results(self):
cold = self.report()
self.assertEqual(cold["testExitStatus"], 1)
self.assertFalse(cold["acceptance"])
self.assertEqual(cold["firstColdRejection"]["exceededFields"], ["RssBytes"])
self.build_evidence(rejected=False)
within_cap = self.report()
self.assertEqual(within_cap["testExitStatus"], 0)
self.assertFalse(within_cap["acceptance"])
self.assertIsNone(within_cap["firstColdRejection"])
def test_changed_hash_or_bad_run_receipt_rejected(self):
(self.run / "test.log").write_text("Changed evidence\n", encoding="utf-8")
with self.assertRaisesRegex(ValueError, "evidence hashes"):
self.report()
for key, value in (("launcherExitStatus", 0), ("containerRemoved", False), ("diagnosticCompleted", False),
("wallSeconds", 7441), ("bundleReceiptSha256", "f" * 64)):
with self.subTest(key=key):
self.build_evidence()
self.receipt[key] = value
self.refresh_receipt()
with self.assertRaises(ValueError):
self.report()
def test_rehashed_exit_runtime_and_trx_cannot_mask_incomplete_evidence(self):
for filename, contents, pattern in (
("test-exit-status.txt", "0\n", "test/container exit"),
("target-runtime.txt", "targetPid=122\n", "mapped CLR"),
):
with self.subTest(filename=filename):
self.build_evidence()
(self.run / filename).write_text(contents, encoding="ascii")
self.refresh_receipt()
with self.assertRaisesRegex(ValueError, pattern):
self.report()
self.build_evidence()
write_trx(self.run / "results/late-slope.trx", {analyze.TEST_NAME}, "Shouldly.ShouldAssertException: wrong assertion")
self.refresh_receipt()
with self.assertRaisesRegex(ValueError, "first cold-cap assertion"):
self.report()
write_trx(self.run / "results/descriptor-controls.trx", {"SkippedOrWrongControl"})
self.refresh_receipt()
with self.assertRaisesRegex(ValueError, "descriptor-controls.trx"):
self.report()
def test_recorded_command_must_use_the_same_local_isolation(self):
for isolation in ("network", "read-only rootfs"):
with self.subTest(isolation=isolation):
self.build_evidence()
invocation = self.provenance["command"].copy()
if isolation == "network":
invocation[invocation.index("--network") + 1] = "host"
else:
invocation.remove("--read-only")
self.provenance["command"] = self.receipt["command"] = invocation
write_json(self.run / "launch.json", self.provenance)
self.refresh_receipt()
with self.assertRaisesRegex(ValueError, "container invocation"):
self.report()
class BundleControls(unittest.TestCase):
def test_mapped_runtime_requires_the_mounted_inode_and_live_file(self):
script = Path(__file__).with_name("container.sh")
with tempfile.TemporaryDirectory() as temporary:
maps = Path(temporary) / "maps"
live = "1000-2000 r-xp 00000000 00:01 42 /app/libcoreclr.so"
for line, expected in ((live, 0), (live + " (deleted)", 1), (live.replace(" 42 ", " 43 "), 1),
(live + "\n" + live.replace("/app/", "/different/"), 1)):
with self.subTest(line=line):
maps.write_text(line + "\n", encoding="ascii")
result = subprocess.run(["bash", "-c", 'source "$1"\nverify_mapped_coreclr "$2" "$3"',
"map-control", str(script), str(maps), "42 /app/libcoreclr.so"],
capture_output=True, text=True, timeout=5)
self.assertEqual(result.returncode, expected, result.stderr)
def test_remote_override_fails_before_asking_docker(self):
with patch.dict(os.environ, {"DOCKER_HOST": "tcp://example.invalid:2375"}), patch("bundle.subprocess.check_output") as docker:
with self.assertRaisesRegex(ValueError, "override"):
bundle.context_identity("local-control")
docker.assert_not_called()
def test_bundle_file_set_runtime_and_package_graph(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary).resolve()
app = root / "app"
app.mkdir()
elf = b"\x7fELF\x02\x01" + b"\x00" * 12 + b"\xb7\x00"
(app / analyze.APP).write_bytes(elf)
(app / analyze.APP).chmod(0o700)
(app / "libcoreclr.so").write_bytes(elf)
runtime = app / f"{analyze.APP}.runtimeconfig.json"
write_json(runtime, {"runtimeOptions": {"tfm": "net10.0", "includedFrameworks": [
{"name": "Microsoft.NETCore.App", "version": analyze.RUNTIME}]}})
dependencies = app / f"{analyze.APP}.deps.json"
write_json(dependencies, {"runtimeTarget": {"name": ".NETCoreApp,Version=v10.0/linux-arm64"},
"libraries": {key: {} for key in bundle.SDK_PACKAGES | bundle.GATEWAY_PACKAGES}})
modules = []
for index, assembly in enumerate((analyze.APP, "Opc.Ua.Server", "System.Private.CoreLib",
"ZB.MOM.WW.HistorianGateway.Client", "ZB.MOM.WW.HistorianGateway.Contracts"), 1):
filename = f"{assembly}.dll"
(app / filename).write_text("Synthetic metadata fixture\n", encoding="utf-8")
modules.append({"file": f"app/{filename}", "assembly": assembly, "version": "1.0.0.0", "mvid": str(UUID(int=index))})
tools = root / "tools"
tools.mkdir()
for name in bundle.TOOLS:
(tools / name).write_text("Synthetic tool fixture\n", encoding="utf-8")
receipt = {**analyze.SETTINGS, "kind": "bundle", "sourceRevision": REVISION, "image": IMAGE,
"imageIdentity": {"id": IMAGE, "os": "linux", "architecture": "arm64"}, "dockerContext": CONTEXT,
"sdkLibraries": sorted(bundle.SDK_PACKAGES), "gatewayLibraries": sorted(bundle.GATEWAY_PACKAGES),
"managedModules": modules, "sourceFiles": {**dict.fromkeys(bundle.CANONICAL_SOURCES, "e" * 64), **{
f"scripts/validation/opcua-lifecycle-memory/late-slope/{name}": analyze.digest(tools / name)
for name in bundle.TOOLS}}, "files": bundle.manifest(root)}
path = root / "receipt.json"
write_json(path, receipt)
approved = analyze.digest(path)
bundle.verify_bundle(root, approved)
extra = app / "extra"
extra.touch()
with self.assertRaisesRegex(ValueError, "file set"):
bundle.verify_bundle(root, approved)
extra.unlink()
extra.symlink_to(app / "libcoreclr.so")
with self.assertRaisesRegex(ValueError, "artifact type"):
bundle.verify_bundle(root, approved)
extra.unlink()
write_json(runtime, {"runtimeOptions": {"tfm": "net10.0", "includedFrameworks": [
{"name": "Microsoft.NETCore.App", "version": "10.0.3"}]}})
receipt["files"] = bundle.manifest(root)
write_json(path, receipt)
with self.assertRaisesRegex(ValueError, "self-contained application runtime"):
bundle.verify_bundle(root, analyze.digest(path))
write_json(runtime, {"runtimeOptions": {"tfm": "net10.0", "includedFrameworks": [
{"name": "Microsoft.NETCore.App", "version": analyze.RUNTIME}]}})
write_json(dependencies, {"runtimeTarget": {"name": ".NETCoreApp,Version=v10.0/linux-arm64"}, "libraries": {}})
receipt["files"] = bundle.manifest(root)
write_json(path, receipt)
with self.assertRaisesRegex(ValueError, "dependency|graph"):
bundle.verify_bundle(root, analyze.digest(path))
if __name__ == "__main__":
unittest.main(verbosity=2)
@@ -1 +0,0 @@
__pycache__/
@@ -1,95 +0,0 @@
# Native empty-keychain control — negative retention result
The plain C control completed **200 private keychain create/delete/CFRelease
operations successfully**, with `Security::AppleDatabase` heap inventory staying
at **1 object / 768 shallow bytes** before, after the first operation, after 200,
and after a five-second idle. This **does not reproduce** the retention seen in
private-PFX and SDK configured-store loading. Empty native create/delete/release
alone is therefore insufficient to explain that result on this machine.
This does not rule out deletion/resource interactions for populated imported
keychains, identify the retaining owner, or assign an SDK, CLR or Apple defect.
Private-key/certificate materialization and its associated native interactions
remain distinct. No production mitigation, resource-budget decision, G4 pass or
#799 closure follows from this control.
## Reviewed execution and isolation
Astra reviewed the actual C/Python source before execution and afterward
independently verified all 207 JSONL rows, 200 ordered operations/status triples,
reference balance, default/search-list fingerprints, all four native heap
counts/bytes, FD/file counts, source/runner/binary hashes, cleanup and warnings.
Only **one actual native execution** ran, serialized after the coordinator's other
ownership control. Source is `PureAppleControl.c`; collector is `pure_apple.py`.
The child is a plain native C program linked only to Security.framework,
CoreFoundation.framework and libSystem. It does not load certificates/PFX, the
OPC UA SDK or CLR. `SecKeychainCreate` used a fresh random filename beneath its
private mode-0700 task root, a generated password and `promptUser=false`. A returned
keychain's path had to exactly match that generated private filename/backing file
before deletion was allowed. Each returned create reference received exactly
one `CFRelease`, including failure paths. The program made no default/search-list
setter or restore call. Read-only `SecKeychainCopyDefault`,
`SecKeychainCopySearchList` and `SecKeychainGetPath` produced hashed before/after
fingerprints; owned Copy results were released, borrowed array elements were not
released separately. Existing keychain paths/passwords were never logged.
Native checkpoints were before, first, 200 and five-second idle. Each parent
snapshot used `heap --noContent -s` and numeric lsof FDs, with Mach task resident
bytes reported by the child. It did not invoke GC or allocator-pressure APIs.
The original fixed 180-second checkpoint/child limit, 20-second heap and
five-second lsof limits, bounded final wait and terminate/kill reap remain in the
collector; those probe/reap bounds can extend total wall time beyond 180 seconds.
Actual child monotonic duration was **8.995 seconds**; parent wall time including
probes was **9.288 seconds**. The child exited zero and was reaped.
## Actual results
| Checkpoint | AppleDatabase count / shallow bytes | Numeric FDs | Owned keychain files / FDs | RSS bytes |
|---|---:|---:|---:|---:|
| Before | 1 / 768 | 3 | 0 / 0 | 7,372,800 |
| First | 1 / 768 | 3 | 0 / 0 | 8,028,160 |
| 200 | 1 / 768 | 3 | 0 / 0 | 8,699,904 |
| Five-second idle | 1 / 768 | 3 | 0 / 0 | 8,699,904 |
All **200 create/path/delete OSStatus triples were `(0, 0, 0)`**. Each create
returned an owned reference, its private path was verified, and all **200/200**
references were released exactly once. All read-only default/path/search-list
query statuses were zero. Before/after fingerprints were identical, with search
list count **2** at both observations. Four query-owned Copy references were
released across the two fingerprint collections. No list setter was invoked.
Default fingerprint:
`08c2c77dd8eaf8042b4020991ea06c6215cc391532869bbf6ced2cf1126c5e63`.
Search-list fingerprint:
`c05fae700986dc7a6a147a938ef2adc4a274173420e137929ed58ad986eb2a3c`.
Owned keychain files were already zero before harness cleanup. The fresh owned
fixture root was then removed. Native class count is a heap classification, not
an open-handle count or an exclusive attribution of the modest RSS change.
The first attempt stopped at **compilation**, before a native child or API call:
strict C11 headers did not declare `explicit_bzero`. Replacing only that
caller-owned buffer wipe with a volatile-byte wipe made the build succeed; empty
fixture creation was moved after successful compilation. The failed build is
retained separately. Both compile attempts emitted **five unsuppressed
deprecated-public-API warnings** for the intentionally exercised legacy keychain
APIs. This is not a zero-warning build and no warning suppression was added.
## Evidence and reproduction
Selected, home-path-redacted receipts are under
`evidence/2026-11-10-pure-apple/`; `verification.json` retains the unchanged local
raw receipt hashes. Actual raw output is
`/tmp/opcua2-pure-apple-20261110b`; the first compile-only failure is
`/tmp/opcua2-pure-apple-20261110a`. Raw native snapshots stay private/local.
```sh
python3 scripts/validation/opcua-lifecycle-memory/native-retention/pure_apple.py \
--output /tmp/opcua2-pure-apple-FRESH
```
The output must be fresh. Fixed counts are not configurable; all generated paths
are task-owned and no user/fleet PKI is used. This negative control refines the
[A/B directory-load findings](README.md), and does not approve another experiment,
a native workaround, a public upstream post or a long run.
@@ -1,123 +0,0 @@
// Diagnostic only: private SecKeychainCreate/Delete/CFRelease, with no certificate, PFX, SDK or CLR.
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
#include <CommonCrypto/CommonDigest.h>
#include <mach/mach.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define REPETITIONS 200
static char root[PATH_MAX];
static struct timespec started;
static unsigned created_refs, released_refs, query_releases;
static double elapsed(void) {
struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now);
return (double)(now.tv_sec-started.tv_sec)+(double)(now.tv_nsec-started.tv_nsec)/1e9;
}
static void die(const char *message) {
fprintf(stdout,"{\"kind\":\"failed\",\"message\":\"%s\",\"createdRefs\":%u,\"releasedRefs\":%u,\"elapsedSeconds\":%.6f}\n",message,created_refs,released_refs,elapsed());
fflush(stdout); exit(1);
}
static void hex(const unsigned char *bytes, size_t length, char *out) {
const char *alphabet="0123456789abcdef";
for(size_t i=0;i<length;i++){out[i*2]=alphabet[bytes[i]>>4];out[i*2+1]=alphabet[bytes[i]&15];}
out[length*2]=0;
}
static void wipe(void *memory, size_t length) {
volatile unsigned char *bytes=(volatile unsigned char *)memory;
while(length--)*bytes++=0;
}
static OSStatus keychain_path(SecKeychainRef keychain, char *path) {
UInt32 length=PATH_MAX-1;
OSStatus status=SecKeychainGetPath(keychain,&length,path);
if(status==errSecSuccess && length<PATH_MAX)path[length]=0;
return status;
}
struct fingerprint { char default_sha[65], search_sha[65]; CFIndex search_count; };
static struct fingerprint fingerprint(const char *stage) {
SecKeychainRef default_keychain=NULL;
CFArrayRef search=NULL;
struct fingerprint out; memset(&out,0,sizeof(out));
char path[PATH_MAX]; memset(path,0,sizeof(path));
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
OSStatus default_status=SecKeychainCopyDefault(&default_keychain);
OSStatus default_path_status=default_keychain?keychain_path(default_keychain,path):errSecInvalidKeychain;
if(default_status==errSecSuccess && default_path_status==errSecSuccess){CC_SHA256(path,(CC_LONG)strlen(path),digest);hex(digest,sizeof(digest),out.default_sha);}
if(default_keychain){CFRelease(default_keychain);query_releases++;default_keychain=NULL;}
OSStatus search_status=SecKeychainCopySearchList(&search);
OSStatus search_path_status=errSecSuccess;
CC_SHA256_CTX hash; CC_SHA256_Init(&hash);
if(search_status==errSecSuccess && search){
out.search_count=CFArrayGetCount(search);
uint64_t count=(uint64_t)out.search_count; CC_SHA256_Update(&hash,&count,sizeof(count));
for(CFIndex i=0;i<out.search_count;i++){
// CFArray values are borrowed. Releasing the owned array balances its element references.
SecKeychainRef borrowed=(SecKeychainRef)CFArrayGetValueAtIndex(search,i);
memset(path,0,sizeof(path)); OSStatus status=keychain_path(borrowed,path);
if(status!=errSecSuccess){search_path_status=status;break;}
uint32_t length=(uint32_t)strlen(path);CC_SHA256_Update(&hash,&length,sizeof(length));CC_SHA256_Update(&hash,path,length);
}
}
CC_SHA256_Final(digest,&hash);hex(digest,sizeof(digest),out.search_sha);
if(search){CFRelease(search);query_releases++;search=NULL;}
fprintf(stdout,"{\"kind\":\"fingerprint\",\"stage\":\"%s\",\"defaultStatus\":%d,\"defaultPathStatus\":%d,\"searchStatus\":%d,\"searchPathStatus\":%d,\"defaultSha256\":\"%s\",\"searchSha256\":\"%s\",\"searchCount\":%ld,\"ownedQueryReferencesReleased\":%u}\n",stage,(int)default_status,(int)default_path_status,(int)search_status,(int)search_path_status,out.default_sha,out.search_sha,(long)out.search_count,query_releases);fflush(stdout);
if(default_status || default_path_status || search_status || search_path_status)die("Read-only keychain fingerprint failed");
return out;
}
static void checkpoint(const char *stage, unsigned operations) {
struct mach_task_basic_info info; mach_msg_type_number_t count=MACH_TASK_BASIC_INFO_COUNT;
kern_return_t status=task_info(mach_task_self(),MACH_TASK_BASIC_INFO,(task_info_t)&info,&count);
if(status!=KERN_SUCCESS)die("task_info failed");
struct timespec wall;clock_gettime(CLOCK_REALTIME,&wall);struct tm utc;gmtime_r(&wall.tv_sec,&utc);
char stamp[40];strftime(stamp,sizeof(stamp),"%Y-%m-%dT%H:%M:%S",&utc);
fprintf(stdout,"{\"kind\":\"checkpoint\",\"mode\":\"pure-apple\",\"stage\":\"%s\",\"operations\":%u,\"processId\":%d,\"utc\":\"%s.%03ldZ\",\"elapsedSeconds\":%.6f,\"rssBytes\":%llu,\"createdRefs\":%u,\"releasedRefs\":%u,\"managedRuntimePresent\":false}\n",stage,operations,getpid(),stamp,wall.tv_nsec/1000000,elapsed(),(unsigned long long)info.resident_size,created_refs,released_refs);fflush(stdout);
char next[PATH_MAX];int n=snprintf(next,sizeof(next),"%s/continue-%s",root,stage);if(n<0 || (size_t)n>=sizeof(next))die("Checkpoint path overflow");
while(access(next,F_OK)!=0){if(elapsed()>180)die("Checkpoint deadline exceeded");struct timespec pause={0,25000000};nanosleep(&pause,NULL);}
}
static int absent(const char *path){struct stat state;if(lstat(path,&state)==0)return 0;return errno==ENOENT;}
static void operation(unsigned iteration) {
unsigned char random[48];if(SecRandomCopyBytes(kSecRandomDefault,sizeof(random),random)!=errSecSuccess)die("Secure random failed");
char suffix[33],password[65];hex(random,16,suffix);hex(random+16,32,password);wipe(random,sizeof(random));
char path[PATH_MAX],backing[PATH_MAX];int n=snprintf(path,sizeof(path),"%s/tmp/native-retention-%s.keychain",root,suffix);
if(n<0 || (size_t)n>=sizeof(path))die("Fixture path overflow");
n=snprintf(backing,sizeof(backing),"%s-db",path);if(n<0 || (size_t)n>=sizeof(backing))die("Fixture backing path overflow");
if(!absent(path)||!absent(backing))die("Generated private fixture path already exists");
SecKeychainRef keychain=NULL;
OSStatus create_status=SecKeychainCreate(path,(UInt32)strlen(password),password,false,NULL,&keychain);
wipe(password,sizeof(password));
OSStatus path_status=errSecInvalidKeychain,delete_status=INT32_MIN;
int verified=0,released=0;
if(keychain){
created_refs++;
char actual[PATH_MAX];memset(actual,0,sizeof(actual));path_status=keychain_path(keychain,actual);
// Only a returned reference whose path is exactly this generated private fixture can be deleted.
verified=path_status==errSecSuccess && (!strcmp(actual,path)||!strcmp(actual,backing));
if(verified)delete_status=SecKeychainDelete(keychain);
// Exactly one CFRelease for this returned owned reference, irrespective of delete status.
CFRelease(keychain);keychain=NULL;released_refs++;released=1;
}
fprintf(stdout,"{\"kind\":\"operation\",\"iteration\":%u,\"createStatus\":%d,\"pathStatus\":%d,\"deleteStatus\":%d,\"returnedRef\":%s,\"privatePathVerified\":%s,\"releasedExactlyOnce\":%s}\n",iteration,(int)create_status,(int)path_status,(int)delete_status,released?"true":"false",verified?"true":"false",released?"true":"false");fflush(stdout);
if(create_status || path_status || delete_status || !verified || !released)die("Private keychain lifecycle operation failed");
}
int main(int argc,char **argv) {
clock_gettime(CLOCK_MONOTONIC,&started);
if(argc!=2 || !realpath(argv[1],root))die("Expected existing private fixture root");
struct stat state;if(stat(root,&state) || !S_ISDIR(state.st_mode) || (state.st_mode&077))die("Fixture root must be mode0700");
struct fingerprint before=fingerprint("before");checkpoint("before",0);
for(unsigned i=1;i<=REPETITIONS;i++){operation(i);if(i==1)checkpoint("first",i);}
checkpoint("repeated",REPETITIONS);
struct timespec idle={5,0};while(nanosleep(&idle,&idle)!=0){if(errno!=EINTR)die("Idle delay failed");}
struct fingerprint after=fingerprint("after");
checkpoint("idle",REPETITIONS);
int unchanged=!strcmp(before.default_sha,after.default_sha)&&!strcmp(before.search_sha,after.search_sha)&&before.search_count==after.search_count;
if(!unchanged || created_refs!=REPETITIONS || released_refs!=REPETITIONS)die("Fingerprint or native ownership guard failed");
fprintf(stdout,"{\"kind\":\"complete\",\"operations\":200,\"createdRefs\":%u,\"releasedRefs\":%u,\"readOnlyDefaultAndSearchListUnchanged\":true,\"elapsedSeconds\":%.6f,\"fullSoakGate\":false}\n",created_refs,released_refs,elapsed());fflush(stdout);return 0;
}
@@ -1,219 +0,0 @@
# 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.
A separately reviewed [plain native Apple empty-keychain control](PURE_APPLE.md)
then executed 200 successful create/delete/CFRelease operations and kept native
AppleDatabase at 1 throughout. That negative result rules out empty native
create/delete/release alone as sufficient here; it does not identify the
private-key/certificate import retaining owner.
@@ -1,176 +0,0 @@
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;
}
}
}
@@ -1,31 +0,0 @@
<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c:40:21: warning: 'SecKeychainGetPath' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations]
40 | OSStatus status=SecKeychainGetPath(keychain,&length,path);
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:474:10: note: 'SecKeychainGetPath' has been explicitly marked deprecated here
474 | OSStatus SecKeychainGetPath(SecKeychainRef __nullable keychain, UInt32 *ioPathLength, char *pathName)
| ^
<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c:51:29: warning: 'SecKeychainCopyDefault' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations]
51 | OSStatus default_status=SecKeychainCopyDefault(&default_keychain);
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:384:10: note: 'SecKeychainCopyDefault' has been explicitly marked deprecated here
384 | OSStatus SecKeychainCopyDefault(SecKeychainRef * __nonnull CF_RETURNS_RETAINED keychain)
| ^
<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c:55:28: warning: 'SecKeychainCopySearchList' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations]
55 | OSStatus search_status=SecKeychainCopySearchList(&search);
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:404:10: note: 'SecKeychainCopySearchList' has been explicitly marked deprecated here
404 | OSStatus SecKeychainCopySearchList(CFArrayRef * __nonnull CF_RETURNS_RETAINED searchList)
| ^
<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c:94:28: warning: 'SecKeychainCreate' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations]
94 | OSStatus create_status=SecKeychainCreate(path,(UInt32)strlen(password),password,false,NULL,&keychain);
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:309:10: note: 'SecKeychainCreate' has been explicitly marked deprecated here
309 | OSStatus SecKeychainCreate(const char *pathName, UInt32 passwordLength, const void * __nullable password, Boolean promptUser, SecAccessRef __nullable initialAccess, SecKeychainRef * __nonnull CF_RETURNS_RETAINED keychain)
| ^
<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c:103:35: warning: 'SecKeychainDelete' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations]
103 | if(verified)delete_status=SecKeychainDelete(keychain);
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:319:10: note: 'SecKeychainDelete' has been explicitly marked deprecated here
319 | OSStatus SecKeychainDelete(SecKeychainRef __nullable keychainOrArray)
| ^
5 warnings generated.
@@ -1,39 +0,0 @@
{
"sourceSha256": "78354b154778fa61a7fe67fdef3fab6ea04ac5bdf2ae8190a2e7414781e1397d",
"runnerSourceSha256": "1b2148f1c20578529355379ab94b439931d7241608a5d5effce8fc86f4af7bc6",
"compileCommand": [
"clang",
"-std=c11",
"-O0",
"-g",
"-Wall",
"-Wextra",
"<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c",
"-framework",
"Security",
"-framework",
"CoreFoundation",
"-o",
"/private/tmp/opcua2-pure-apple-20261110b/PureAppleControl"
],
"compiler": "Apple clang version 21.0.0 (clang-2100.0.123.102)\nTarget: arm64-apple-darwin25.5.0\nThread model: posix\nInstalledDir: /Library/Developer/CommandLineTools/usr/bin\n",
"os": "ProductName:\t\tmacOS\nProductVersion:\t\t26.5.2\nBuildVersion:\t\t25F84\n",
"architecture": "arm64",
"repetitions": 200,
"idleSeconds": 5,
"stages": [
"before",
"first",
"repeated",
"idle"
],
"fullSoakGate": false,
"readOnlyKeychainQueries": [
"SecKeychainCopyDefault",
"SecKeychainCopySearchList",
"SecKeychainGetPath"
],
"doesNotCallAnyDefaultOrSearchListSetter": true,
"binarySha256": "6c87f4394c0e51bcf26f27187628d0a867feacf9b8c7a20fb94ab42baeb1d087",
"linkedLibraries": "/private/tmp/opcua2-pure-apple-20261110b/PureAppleControl:\n\t/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 61901.101.4)\n\t/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 4424.1.255)\n\t/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)\n"
}
@@ -1,129 +0,0 @@
{
"status": "passed",
"exitCode": 0,
"wallSecondsIncludingProbes": 9.287588333012536,
"samples": [
{
"kind": "checkpoint",
"mode": "pure-apple",
"stage": "before",
"operations": 0,
"processId": 93020,
"utc": "2026-11-10T21:47:09.322Z",
"elapsedSeconds": 0.004359,
"rssBytes": 7372800,
"createdRefs": 0,
"releasedRefs": 0,
"managedRuntimePresent": false,
"AppleDatabaseCount": 1,
"AppleDatabaseShallowBytes": 768,
"AppleDatabaseClassAbsent": false,
"databaseManagerClassPresent": true,
"fdCount": 3,
"ownedTemporaryFdCount": 0,
"ownedKeychainFdCount": 0,
"ownedKeychainFileCount": 0
},
{
"kind": "checkpoint",
"mode": "pure-apple",
"stage": "first",
"operations": 1,
"processId": 93020,
"utc": "2026-11-10T21:47:10.311Z",
"elapsedSeconds": 0.993379,
"rssBytes": 8028160,
"createdRefs": 1,
"releasedRefs": 1,
"managedRuntimePresent": false,
"AppleDatabaseCount": 1,
"AppleDatabaseShallowBytes": 768,
"AppleDatabaseClassAbsent": false,
"databaseManagerClassPresent": true,
"fdCount": 3,
"ownedTemporaryFdCount": 0,
"ownedKeychainFdCount": 0,
"ownedKeychainFileCount": 0
},
{
"kind": "checkpoint",
"mode": "pure-apple",
"stage": "repeated",
"operations": 200,
"processId": 93020,
"utc": "2026-11-10T21:47:11.859Z",
"elapsedSeconds": 2.541567,
"rssBytes": 8699904,
"createdRefs": 200,
"releasedRefs": 200,
"managedRuntimePresent": false,
"AppleDatabaseCount": 1,
"AppleDatabaseShallowBytes": 768,
"AppleDatabaseClassAbsent": false,
"databaseManagerClassPresent": true,
"fdCount": 3,
"ownedTemporaryFdCount": 0,
"ownedKeychainFdCount": 0,
"ownedKeychainFileCount": 0
},
{
"kind": "checkpoint",
"mode": "pure-apple",
"stage": "idle",
"operations": 200,
"processId": 93020,
"utc": "2026-11-10T21:47:17.582Z",
"elapsedSeconds": 8.264326,
"rssBytes": 8699904,
"createdRefs": 200,
"releasedRefs": 200,
"managedRuntimePresent": false,
"AppleDatabaseCount": 1,
"AppleDatabaseShallowBytes": 768,
"AppleDatabaseClassAbsent": false,
"databaseManagerClassPresent": true,
"fdCount": 3,
"ownedTemporaryFdCount": 0,
"ownedKeychainFdCount": 0,
"ownedKeychainFileCount": 0
}
],
"fingerprints": [
{
"kind": "fingerprint",
"stage": "before",
"defaultStatus": 0,
"defaultPathStatus": 0,
"searchStatus": 0,
"searchPathStatus": 0,
"defaultSha256": "08c2c77dd8eaf8042b4020991ea06c6215cc391532869bbf6ced2cf1126c5e63",
"searchSha256": "c05fae700986dc7a6a147a938ef2adc4a274173420e137929ed58ad986eb2a3c",
"searchCount": 2,
"ownedQueryReferencesReleased": 2
},
{
"kind": "fingerprint",
"stage": "after",
"defaultStatus": 0,
"defaultPathStatus": 0,
"searchStatus": 0,
"searchPathStatus": 0,
"defaultSha256": "08c2c77dd8eaf8042b4020991ea06c6215cc391532869bbf6ced2cf1126c5e63",
"searchSha256": "c05fae700986dc7a6a147a938ef2adc4a274173420e137929ed58ad986eb2a3c",
"searchCount": 2,
"ownedQueryReferencesReleased": 4
}
],
"complete": {
"kind": "complete",
"operations": 200,
"createdRefs": 200,
"releasedRefs": 200,
"readOnlyDefaultAndSearchListUnchanged": true,
"elapsedSeconds": 8.994713,
"fullSoakGate": false
},
"fullSoakGate": false,
"nativeInventoryIsNotOpenHandlesOrExclusiveRss": true,
"nativeDeleteOsStatusesAllZero": true
}
@@ -1,207 +0,0 @@
{"kind":"fingerprint","stage":"before","defaultStatus":0,"defaultPathStatus":0,"searchStatus":0,"searchPathStatus":0,"defaultSha256":"08c2c77dd8eaf8042b4020991ea06c6215cc391532869bbf6ced2cf1126c5e63","searchSha256":"c05fae700986dc7a6a147a938ef2adc4a274173420e137929ed58ad986eb2a3c","searchCount":2,"ownedQueryReferencesReleased":2}
{"kind":"checkpoint","mode":"pure-apple","stage":"before","operations":0,"processId":93020,"utc":"2026-11-10T21:47:09.322Z","elapsedSeconds":0.004359,"rssBytes":7372800,"createdRefs":0,"releasedRefs":0,"managedRuntimePresent":false}
{"kind":"operation","iteration":1,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"checkpoint","mode":"pure-apple","stage":"first","operations":1,"processId":93020,"utc":"2026-11-10T21:47:10.311Z","elapsedSeconds":0.993379,"rssBytes":8028160,"createdRefs":1,"releasedRefs":1,"managedRuntimePresent":false}
{"kind":"operation","iteration":2,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":3,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":4,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":5,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":6,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":7,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":8,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":9,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":10,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":11,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":12,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":13,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":14,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":15,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":16,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":17,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":18,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":19,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":20,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":21,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":22,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":23,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":24,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":25,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":26,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":27,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":28,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":29,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":30,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":31,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":32,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":33,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":34,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":35,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":36,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":37,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":38,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":39,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":40,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":41,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":42,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":43,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":44,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":45,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":46,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":47,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":48,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":49,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":50,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":51,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":52,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":53,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":54,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":55,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":56,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":57,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":58,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":59,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":60,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":61,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":62,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":63,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":64,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":65,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":66,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":67,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":68,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":69,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":70,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":71,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":72,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":73,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":74,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":75,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":76,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":77,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":78,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":79,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":80,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":81,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":82,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":83,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":84,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":85,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":86,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":87,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":88,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":89,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":90,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":91,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":92,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":93,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":94,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":95,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":96,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":97,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":98,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":99,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":100,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":101,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":102,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":103,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":104,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":105,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":106,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":107,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":108,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":109,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":110,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":111,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":112,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":113,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":114,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":115,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":116,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":117,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":118,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":119,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":120,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":121,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":122,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":123,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":124,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":125,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":126,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":127,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":128,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":129,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":130,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":131,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":132,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":133,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":134,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":135,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":136,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":137,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":138,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":139,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":140,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":141,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":142,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":143,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":144,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":145,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":146,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":147,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":148,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":149,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":150,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":151,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":152,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":153,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":154,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":155,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":156,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":157,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":158,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":159,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":160,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":161,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":162,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":163,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":164,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":165,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":166,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":167,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":168,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":169,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":170,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":171,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":172,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":173,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":174,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":175,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":176,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":177,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":178,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":179,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":180,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":181,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":182,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":183,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":184,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":185,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":186,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":187,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":188,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":189,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":190,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":191,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":192,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":193,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":194,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":195,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":196,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":197,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":198,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":199,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"operation","iteration":200,"createStatus":0,"pathStatus":0,"deleteStatus":0,"returnedRef":true,"privatePathVerified":true,"releasedExactlyOnce":true}
{"kind":"checkpoint","mode":"pure-apple","stage":"repeated","operations":200,"processId":93020,"utc":"2026-11-10T21:47:11.859Z","elapsedSeconds":2.541567,"rssBytes":8699904,"createdRefs":200,"releasedRefs":200,"managedRuntimePresent":false}
{"kind":"fingerprint","stage":"after","defaultStatus":0,"defaultPathStatus":0,"searchStatus":0,"searchPathStatus":0,"defaultSha256":"08c2c77dd8eaf8042b4020991ea06c6215cc391532869bbf6ced2cf1126c5e63","searchSha256":"c05fae700986dc7a6a147a938ef2adc4a274173420e137929ed58ad986eb2a3c","searchCount":2,"ownedQueryReferencesReleased":4}
{"kind":"checkpoint","mode":"pure-apple","stage":"idle","operations":200,"processId":93020,"utc":"2026-11-10T21:47:17.582Z","elapsedSeconds":8.264326,"rssBytes":8699904,"createdRefs":200,"releasedRefs":200,"managedRuntimePresent":false}
{"kind":"complete","operations":200,"createdRefs":200,"releasedRefs":200,"readOnlyDefaultAndSearchListUnchanged":true,"elapsedSeconds":8.994713,"fullSoakGate":false}
@@ -1,34 +0,0 @@
<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c:36:21: warning: 'SecKeychainGetPath' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations]
36 | OSStatus status=SecKeychainGetPath(keychain,&length,path);
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:474:10: note: 'SecKeychainGetPath' has been explicitly marked deprecated here
474 | OSStatus SecKeychainGetPath(SecKeychainRef __nullable keychain, UInt32 *ioPathLength, char *pathName)
| ^
<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c:47:29: warning: 'SecKeychainCopyDefault' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations]
47 | OSStatus default_status=SecKeychainCopyDefault(&default_keychain);
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:384:10: note: 'SecKeychainCopyDefault' has been explicitly marked deprecated here
384 | OSStatus SecKeychainCopyDefault(SecKeychainRef * __nonnull CF_RETURNS_RETAINED keychain)
| ^
<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c:51:28: warning: 'SecKeychainCopySearchList' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations]
51 | OSStatus search_status=SecKeychainCopySearchList(&search);
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:404:10: note: 'SecKeychainCopySearchList' has been explicitly marked deprecated here
404 | OSStatus SecKeychainCopySearchList(CFArrayRef * __nonnull CF_RETURNS_RETAINED searchList)
| ^
<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c:84:83: error: call to undeclared function 'explicit_bzero'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
84 | char suffix[33],password[65];hex(random,16,suffix);hex(random+16,32,password);explicit_bzero(random,sizeof(random));
| ^
<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c:90:28: warning: 'SecKeychainCreate' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations]
90 | OSStatus create_status=SecKeychainCreate(path,(UInt32)strlen(password),password,false,NULL,&keychain);
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:309:10: note: 'SecKeychainCreate' has been explicitly marked deprecated here
309 | OSStatus SecKeychainCreate(const char *pathName, UInt32 passwordLength, const void * __nullable password, Boolean promptUser, SecAccessRef __nullable initialAccess, SecKeychainRef * __nonnull CF_RETURNS_RETAINED keychain)
| ^
<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c:99:35: warning: 'SecKeychainDelete' is deprecated: first deprecated in macOS 10.10 - SecKeychain is deprecated [-Wdeprecated-declarations]
99 | if(verified)delete_status=SecKeychainDelete(keychain);
| ^
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Security.framework/Headers/SecKeychain.h:319:10: note: 'SecKeychainDelete' has been explicitly marked deprecated here
319 | OSStatus SecKeychainDelete(SecKeychainRef __nullable keychainOrArray)
| ^
5 warnings and 1 error generated.
@@ -1,5 +0,0 @@
{
"compileExitCode": 1,
"nativeProcessNeverStarted": true,
"emptyOwnedFixtureRootRemoved": true
}
@@ -1,37 +0,0 @@
{
"sourceSha256": "f3d5b393f4bfcb350b37d610a8b6602b480e573e632f697b311813df276be347",
"runnerSourceSha256": "22f0ee93b2998f1cb337aa5f763dcf578b2efe4616502ad095a309cc774c3c76",
"compileCommand": [
"clang",
"-std=c11",
"-O0",
"-g",
"-Wall",
"-Wextra",
"<user-home>/.codex/worktrees/opcua2-client-lifecycle/OtOpcUa/scripts/validation/opcua-lifecycle-memory/native-retention/PureAppleControl.c",
"-framework",
"Security",
"-framework",
"CoreFoundation",
"-o",
"/private/tmp/opcua2-pure-apple-20261110a/PureAppleControl"
],
"compiler": "Apple clang version 21.0.0 (clang-2100.0.123.102)\nTarget: arm64-apple-darwin25.5.0\nThread model: posix\nInstalledDir: /Library/Developer/CommandLineTools/usr/bin\n",
"os": "ProductName:\t\tmacOS\nProductVersion:\t\t26.5.2\nBuildVersion:\t\t25F84\n",
"architecture": "arm64",
"repetitions": 200,
"idleSeconds": 5,
"stages": [
"before",
"first",
"repeated",
"idle"
],
"fullSoakGate": false,
"readOnlyKeychainQueries": [
"SecKeychainCopyDefault",
"SecKeychainCopySearchList",
"SecKeychainGetPath"
],
"doesNotCallAnyDefaultOrSearchListSetter": true
}
@@ -1,21 +0,0 @@
{
"actualNativeExecutions": 1,
"earlierAttemptWasCompileOnly": true,
"rawActualOutput": "/tmp/opcua2-pure-apple-20261110b",
"rawCompileOnlyOutput": "/tmp/opcua2-pure-apple-20261110a",
"rawSha256": {
"actual-run-result.json": "70b7f3c57a5d14baf9cc593c16670da85adcb525e5f60bb419c8f0f01e206347",
"actual-run-provenance.json": "aa181c0213588aede6d20888ede49ef2a74bb409c075d90a6bad214cfbdac29f",
"actual-run-samples.jsonl": "4ebb9870e0e66d1a217ce489058a1658b0be71a5928115bbf347478bc1e6066a",
"actual-run-build.log": "786ee9f813146b634d814e1cdb8f35fe1d9ef88a9ea96265bc3ffdafe4c61b6e",
"actual-run-fixture-before-harness-cleanup.json": "eaaf36a5e7f7d1d3635e987d8f9a2600ce55f6482011ec9a7abf3e4dd759d9c4",
"actual-run-fixture-cleanup.json": "1785d6ee67b2f7e77037a469ca2bdb9b07c8892781ea830a985f6ef505f84d69",
"first-build-only-build.log": "ebaa2a34424167053a50f410ee04c6ed0988566c5c68e7c2473a55579baf1818",
"first-build-only-provenance.json": "7236bb169732e2fe03d78af2691be3d41632bf1e9a79902ae74aa0ed4c6a539b",
"first-build-only-fixture-cleanup.json": "e5551f93e5a2d898cf35b59f775ef8b6fecc06c673680cf8486019e73b44866f"
},
"redaction": "Selected text replaces only the local home prefix; raw receipts were not modified.",
"review": "Astra independently verified all207 rows,200 ordered operations/status triples, reference balance, fingerprints, all4 native counts/bytes, FD/file counts, hashes, cleanup and5 unsuppressed warnings.",
"retentionReproducedByEmptyNativeLifecycle": false,
"fullSoakGate": false
}
@@ -1,9 +0,0 @@
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
@@ -1,3 +0,0 @@
{
"allOwnedFixtureDirectoriesRemoved": true
}
@@ -1,34 +0,0 @@
{
"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"
}
}
@@ -1,6 +0,0 @@
{"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}
@@ -1,366 +0,0 @@
{
"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 @@
{"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"}}
@@ -1,54 +0,0 @@
{
"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"
}
]
}
@@ -1,6 +0,0 @@
{"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}
@@ -1,71 +0,0 @@
{
"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
}
}
@@ -1,135 +0,0 @@
#!/usr/bin/env python3
"""Bounded, attribution-only native Apple keychain lifecycle, without PFX/SDK/CLR."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import time
HERE = Path(__file__).resolve().parent
STAGES = ['before', 'first', 'repeated', 'idle']
def save(path, value):
path.write_text(json.dumps(value, indent=2) + '\n')
def complete_rows(path):
return [json.loads(line) for line in path.read_text().split('\n')[:-1]]
def snapshot(pid, task, output, stage):
heap = subprocess.run(['/usr/bin/heap', '--noContent', '-s', str(pid)], text=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, timeout=20, check=True)
(output / ('heap-' + stage + '.txt')).write_text(heap.stdout)
database = re.search(r'^\s*(\d+)\s+(\d+)\s+[\d.]+\s+Security::AppleDatabase\s', heap.stdout, re.MULTILINE)
manager = re.search(r'^\s*(\d+)\s+(\d+)\s+[\d.]+\s+Security::AppleDatabaseManager\s', heap.stdout, re.MULTILINE)
if database is None and manager is None:
raise RuntimeError('Native inventory could not classify the database or its manager; do not silently infer zero')
descriptors = subprocess.run(['/usr/sbin/lsof', '-a', '-p', str(pid), '-Fnf'], text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5, check=True)
(output / ('lsof-' + stage + '.txt')).write_text(descriptors.stdout)
total = temp = keychain = 0
in_descriptor = False
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/'):
path = Path(line[1:].removesuffix(' (deleted)')).resolve()
owned = path == task / 'tmp' or task / 'tmp' in path.parents
temp += int(owned)
keychain += int(owned and '.keychain' in path.name)
return {'AppleDatabaseCount': int(database[1]) if database else 0,
'AppleDatabaseShallowBytes': int(database[2]) if database else 0,
'AppleDatabaseClassAbsent': database is None,
'databaseManagerClassPresent': manager is not None, 'fdCount': total,
'ownedTemporaryFdCount': temp, 'ownedKeychainFdCount': keychain,
'ownedKeychainFileCount': sum(p.is_file() and '.keychain' in p.name for p in (task / 'tmp').rglob('*'))}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--output', required=True, type=Path, help='Fresh private output directory')
options = parser.parse_args()
if sys.platform != 'darwin':
parser.error('Native control requires macOS')
output = options.output.resolve()
output.mkdir(parents=True, exist_ok=False)
os.chmod(output, 0o700)
task = output / 'task'
binary = output / 'PureAppleControl'
command = ['clang', '-std=c11', '-O0', '-g', '-Wall', '-Wextra', str(HERE / 'PureAppleControl.c'),
'-framework', 'Security', '-framework', 'CoreFoundation', '-o', str(binary)]
source_hash = hashlib.sha256((HERE / 'PureAppleControl.c').read_bytes()).hexdigest()
provenance = {'sourceSha256': source_hash, 'runnerSourceSha256': hashlib.sha256((HERE / 'pure_apple.py').read_bytes()).hexdigest(), 'compileCommand': command,
'compiler': subprocess.check_output(['clang', '--version'], text=True),
'os': subprocess.check_output(['sw_vers'], text=True), 'architecture': os.uname().machine,
'repetitions': 200, 'idleSeconds': 5, 'stages': STAGES, 'fullSoakGate': False,
'readOnlyKeychainQueries': ['SecKeychainCopyDefault', 'SecKeychainCopySearchList', 'SecKeychainGetPath'],
'doesNotCallAnyDefaultOrSearchListSetter': True}
save(output / 'provenance.json', provenance)
with (output / 'build.log').open('w') as log:
subprocess.run(command, stdout=log, stderr=subprocess.STDOUT, timeout=60, check=True)
provenance['binarySha256'] = hashlib.sha256(binary.read_bytes()).hexdigest()
provenance['linkedLibraries'] = subprocess.check_output(['otool', '-L', str(binary)], text=True)
if 'libcoreclr' in provenance['linkedLibraries'] or 'Opc.Ua' in provenance['linkedLibraries']:
raise RuntimeError('Pure Apple control must not load SDK or CLR')
save(output / 'provenance.json', provenance)
task.mkdir(mode=0o700)
(task / 'tmp').mkdir(mode=0o700)
started = time.monotonic()
with (output / 'samples.jsonl').open('w') as log:
process = subprocess.Popen([str(binary), str(task)], stdout=log, stderr=subprocess.STDOUT,
env=dict(os.environ, TMPDIR=str(task / 'tmp')))
try:
deadline = time.monotonic() + 180
samples = []
for stage in STAGES:
while True:
rows = complete_rows(output / 'samples.jsonl')
if any(row['kind'] == 'failed' for row in rows):
raise RuntimeError('Native control failed; preserve raw status rows')
checkpoints = [row for row in rows if row.get('stage') == stage and row['kind'] == 'checkpoint']
if checkpoints:
samples.append({**checkpoints[0], **snapshot(process.pid, task, output, stage)})
(task / ('continue-' + stage)).touch()
break
if process.poll() is not None or time.monotonic() >= deadline:
raise RuntimeError('Native checkpoint exceeded bound: ' + stage)
time.sleep(.05)
status = process.wait(timeout=15)
rows = complete_rows(output / 'samples.jsonl')
operations = [r for r in rows if r['kind'] == 'operation']
fingerprints = [r for r in rows if r['kind'] == 'fingerprint']
complete = [r for r in rows if r['kind'] == 'complete']
if status or len(operations) != 200 or len(fingerprints) != 2 or len(complete) != 1:
raise RuntimeError('Incomplete native execution count')
if any(r['createStatus'] or r['deleteStatus'] or not r['privatePathVerified'] or not r['releasedExactlyOnce'] for r in operations):
raise RuntimeError('Native lifecycle status/ownership failed')
if any(samples[-1][k] for k in ['ownedKeychainFdCount', 'ownedKeychainFileCount']):
raise RuntimeError('Private native keychain files/descriptors retained')
result = {'status': 'passed', 'exitCode': status, 'wallSecondsIncludingProbes': time.monotonic() - started,
'samples': samples, 'fingerprints': fingerprints, 'complete': complete[0], 'fullSoakGate': False,
'nativeInventoryIsNotOpenHandlesOrExclusiveRss': True, 'nativeDeleteOsStatusesAllZero': True}
save(output / 'result.json', result)
finally:
if process.poll() is None:
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill(); process.wait(timeout=5)
save(output / 'fixture-before-harness-cleanup.json', {'remainingOwnedKeychainFiles': sum(p.is_file() and '.keychain' in p.name for p in (task / 'tmp').rglob('*'))})
shutil.rmtree(task)
save(output / 'fixture-cleanup.json', {'ownedFixtureRootRemoved': not task.exists()})
print(output)
if __name__ == '__main__':
main()
@@ -1,168 +0,0 @@
#!/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()
@@ -175,9 +175,6 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
private StandardServer? _server;
private readonly ITelemetryContext _telemetry = DefaultTelemetry.Create(_ => { });
private readonly List<IUserTokenAuthenticator> _identityAuthenticators = [];
private readonly object _lifetimeLock = new();
private readonly TaskCompletionSource _disposeCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously);
private int _disposeStarted;
/// <summary>Initializes a new instance of the OPC UA application host.</summary>
/// <param name="options">The host configuration options.</param>
@@ -205,21 +202,18 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task StartAsync(StandardServer server, CancellationToken cancellationToken)
{
OtOpcUaSdkServer sdkServer;
lock (_lifetimeLock)
if (server is not OtOpcUaSdkServer sdkServer)
throw new ArgumentException("The server must support application authentication before its listeners open.", nameof(server));
_server = server;
// ApplicationInstance receives the host's ITelemetryContext. DefaultTelemetry.Create wires
// the SDK's internal logging; an empty builder keeps
// the SDK's trace off our ILogger (the host keeps its own _logger) — sufficient for the bootstrap.
_application = new ApplicationInstance(_telemetry)
{
ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposeStarted) != 0, this);
sdkServer = server as OtOpcUaSdkServer
?? throw new ArgumentException("The server must support application authentication before its listeners open.", nameof(server));
_server = server;
// The empty telemetry builder keeps SDK tracing separate from the host's ILogger.
_application = new ApplicationInstance(_telemetry)
{
ApplicationName = _options.ApplicationName,
ApplicationType = ApplicationType.Server,
ConfigSectionName = "OtOpcUa",
};
}
ApplicationName = _options.ApplicationName,
ApplicationType = ApplicationType.Server,
ConfigSectionName = "OtOpcUa",
};
try
{
@@ -646,63 +640,16 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable
/// <summary>Disposes the application host and cleans up resources.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
public ValueTask DisposeAsync()
public async ValueTask DisposeAsync()
{
if (Interlocked.CompareExchange(ref _disposeStarted, 1, 0) == 0)
_ = CompleteDisposalAsync();
return new ValueTask(_disposeCompletion.Task);
}
private async Task CompleteDisposalAsync()
{
Exception? failure = null;
try
// Authenticators remain installed until the SDK closes every listener and disposes its registry.
if (_application is { } application)
{
ApplicationInstance? application;
StandardServer? server;
bool adopted;
lock (_lifetimeLock)
{
application = _application;
server = _server;
// SDK2 adopts the server only when ApplicationInstance.StartAsync is reached.
adopted = server is not null && ReferenceEquals(application?.Server, server);
_application = null;
}
try
{
if (application is not null)
{
try { await application.DisposeAsync().ConfigureAwait(false); }
catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: disposal threw"); }
}
}
finally
{
// Failed configuration/certificate work may precede the application's ownership handoff.
// An adopted server remains solely owned by ApplicationInstance.DisposeAsync.
if (!adopted)
{
try { if (server is not null) await server.DisposeAsync().ConfigureAwait(false); }
catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: unadopted server disposal threw"); }
}
}
}
catch (Exception error) { failure = error; }
finally
{
try
{
// Only the cleanup owner clears adapters after every listener and registry has closed.
_identityAuthenticators.Clear();
_server = null;
}
catch (Exception error) { failure = failure is null ? error : new AggregateException(failure, error); }
finally
{
if (failure is null) _disposeCompletion.TrySetResult();
else _disposeCompletion.TrySetException(failure);
}
_application = null;
try { await application.DisposeAsync().ConfigureAwait(false); }
catch (Exception ex) { _logger.LogWarning(ex, "OpcUaApplicationHost: disposal threw"); }
}
_identityAuthenticators.Clear();
_server = null;
}
}
@@ -122,15 +122,12 @@ public sealed class EventHistoryPagingOverTheWireTests
/// <summary>(time, id) order, [start, end), cap + truncation flag — the SQL backends' contract.</summary>
internal sealed class SqlLikeEventSource(List<HistoricalEvent> events) : IHistorianDataSource
{
private int _reads;
internal int ReadCount => Volatile.Read(ref _reads);
private readonly List<HistoricalEvent> _all =
events.OrderBy(e => e.EventTimeUtc).ThenBy(e => e.EventId, StringComparer.Ordinal).ToList();
public async Task<HistoricalEventsResult> ReadEventsAsync(
string? sourceName, DateTime startUtc, DateTime endUtc, int maxEvents, CancellationToken ct)
{
Interlocked.Increment(ref _reads);
await Task.Delay(1, ct).ConfigureAwait(false); // complete on a pool thread, like a real backend
var window = _all.Where(e => e.EventTimeUtc >= startUtc && e.EventTimeUtc < endUtc).ToList();
var cap = maxEvents <= 0 ? 1000 : maxEvents;
@@ -1,99 +0,0 @@
using Opc.Ua;
using Opc.Ua.Configuration;
using Opc.Ua.Server;
using Shouldly;
using Xunit;
using ClientSession = Opc.Ua.Client.ISession;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
public sealed partial class Sdk2LifecycleSoakTests
{
/// <summary>Concurrent host disposal awaits real encrypted-session shutdown before clearing ownership.</summary>
[Fact]
public async Task Concurrent_host_disposal_waits_for_the_actual_encrypted_session_to_close()
{
var ct = TestContext.Current.CancellationToken;
var clientRoot = Path.Combine(Path.GetTempPath(), $"otua-dispose-client-{Guid.NewGuid():N}");
ApplicationInstance? application = null;
ServerEventRig? rig = null;
ClientSession? client = null;
ISessionManager? sessionManager = null;
SessionEventHandler? closing = null;
using var release = new ManualResetEventSlim(false);
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var calls = new List<Task>();
var timedOut = 0;
var callbackCount = 0;
try
{
var telemetry = DefaultTelemetry.Create(_ => { });
var security = TestClientSecurity.Build(clientRoot);
security.ApplicationCertificate!.SubjectName = "CN=SDK2LifecycleSoak";
var config = new ApplicationConfiguration(telemetry)
{
ApplicationName = "SDK2LifecycleSoak", ApplicationUri = $"urn:SDK2LifecycleSoak:{Guid.NewGuid():N}",
ApplicationType = ApplicationType.Client, SecurityConfiguration = security,
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 15_000 },
TransportQuotas = new TransportQuotas { OperationTimeout = 5000 },
};
application = new ApplicationInstance(telemetry) { ApplicationConfiguration = config };
await config.ValidateAsync(ApplicationType.Client, ct);
await application.CheckApplicationInstanceCertificatesAsync(true, null, ct);
rig = await ServerEventRig.StartAsync("SDK2ConcurrentDisposal", encrypted: true, ct);
Configure(rig);
client = await ConnectAsync(rig, config, null, ct);
sessionManager = rig.Server.CurrentInstance.SessionManager;
sessionManager.GetSessions().Count.ShouldBe(1);
var sessionId = client.SessionId;
closing = (session, reason) =>
{
if (session.Id != sessionId || reason != SessionEventReason.Closing) return;
Interlocked.Increment(ref callbackCount);
entered.TrySetResult();
// SDK swallows event exceptions; keep an explicit failure flag for this bounded barrier.
if (!release.Wait(TimeSpan.FromSeconds(15))) Interlocked.Exchange(ref timedOut, 1);
};
sessionManager.SessionClosing += closing;
var host = RequiredField<OpcUaApplicationHost>(rig, "_host");
var manager = rig.Server.CertificateManager!;
calls.Add(Task.Run(async () => await host.DisposeAsync(), ct));
await entered.Task.WaitAsync(TimeSpan.FromSeconds(10), ct);
for (var i = 0; i < 3; i++) calls.Add(host.DisposeAsync().AsTask());
calls.ShouldAllBe(call => !call.IsCompleted, "every caller must await the actual shutdown owner");
host.Server.ShouldBeSameAs(rig.Server);
RequiredField<bool>(manager, "m_disposed").ShouldBeFalse();
release.Set();
await Task.WhenAll(calls).WaitAsync(TimeSpan.FromSeconds(20), ct);
Volatile.Read(ref timedOut).ShouldBe(0);
Volatile.Read(ref callbackCount).ShouldBe(1);
host.Server.ShouldBeNull();
host.ApplicationInstance.ShouldBeNull();
RequiredField<bool>(manager, "m_disposed").ShouldBeTrue();
CertificateCores(manager).Sum(CachedStores).ShouldBe(0);
}
finally
{
// Removing a handler takes the SDK's event lock: release before awaiting or unsubscribing.
release.Set();
try { await Task.WhenAll(calls).WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); }
finally
{
try { if (sessionManager is not null && closing is not null) sessionManager.SessionClosing -= closing; }
finally
{
try { client?.Dispose(); }
finally
{
try { if (rig is not null) await rig.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); }
finally
{
try { if (application is not null) await application.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); }
finally { if (Directory.Exists(clientRoot)) Directory.Delete(clientRoot, true); }
}
}
}
}
}
}
}
@@ -1,248 +0,0 @@
using System.Collections;
using System.Runtime.CompilerServices;
using System.Reflection;
using System.Text.Json;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
public sealed partial class Sdk2LifecycleSoakTests
{
public static bool CloseControlEnabled =>
Environment.GetEnvironmentVariable("OTOPCUA_SDK2_CLOSE_CONTROL") == "1";
[Fact(SkipUnless = nameof(CloseControlEnabled), Skip = "Opt in to the bounded SDK2 close/weak-root control.")]
public async Task Closed_tokens_are_rejected_and_ownership_is_observed_without_harness_roots()
{
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
deadline.CancelAfter(TimeSpan.FromSeconds(120));
var ct = deadline.Token;
// Positive and negative controls for the observer itself, independent of SDK objects.
var alive = new object();
var aliveReference = new WeakReference(alive);
var collectible = CollectibleReference();
var observer = await ObserveWeakAsync([new("retained-control", aliveReference), new("collectible-control", collectible)], ct);
observer.Last().Alive.ShouldBe([true, false]);
GC.KeepAlive(alive);
WriteCloseControl("observer-controls", observer);
// The helper returns no rig, application, config, Session, point or store objects.
var stoppedTargets = await Task.Run(() => RunCloseFixture(ct), ct).WaitAsync(TimeSpan.FromSeconds(120), ct);
var stopped = await ObserveWeakAsync(stoppedTargets, ct);
WriteCloseControl("after-normal-host-stop", stopped);
stopped.Last().Alive.ShouldAllBe(value => !value);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static WeakReference CollectibleReference() => new(new object());
private sealed record WeakTarget(string Kind, WeakReference Reference);
private sealed record WeakObservation(int Collection, string[] Kinds, bool[] Alive);
private static async Task<WeakObservation[]> ObserveWeakAsync(WeakTarget[] targets, CancellationToken ct)
{
var observations = new List<WeakObservation>();
// Fixed count/delay; no extension or retrieving/resurrecting targets during collection.
var delays = new[] { 50, 100, 250, 500, 1000 };
for (var i = 0; i < delays.Length; i++)
{
ct.ThrowIfCancellationRequested();
await Task.Run(() =>
{
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true, compacting: true);
GC.WaitForPendingFinalizers();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true, compacting: true);
}, ct).WaitAsync(TimeSpan.FromSeconds(5), ct);
await Task.Delay(delays[i], ct);
observations.Add(new(i + 1, targets.Select(t => t.Kind).ToArray(), targets.Select(t => t.Reference.IsAlive).ToArray()));
}
return observations.ToArray();
}
private static void WriteCloseControl(string phase, object observation) =>
TestContext.Current.TestOutputHelper!.WriteLine(JsonSerializer.Serialize(new { phase, observation, acceptance = false }));
[MethodImpl(MethodImplOptions.NoInlining)]
private static WeakTarget[] RunCloseFixture(CancellationToken ct) => ExerciseCloseOwnershipAsync(ct).GetAwaiter().GetResult();
[MethodImpl(MethodImplOptions.NoInlining)]
private static WeakTarget[] RunClosedSession(ServerEventRig rig, ApplicationConfiguration config, CancellationToken ct) =>
CloseAndDenyOldTokensAsync(rig, config, ct).GetAwaiter().GetResult();
[MethodImpl(MethodImplOptions.NoInlining)]
private static async Task<WeakTarget[]> ExerciseCloseOwnershipAsync(CancellationToken ct)
{
var clientRoot = Path.Combine(Path.GetTempPath(), $"otua-close-client-{Guid.NewGuid():N}");
var telemetry = DefaultTelemetry.Create(_ => { });
var security = TestClientSecurity.Build(clientRoot);
security.ApplicationCertificate!.SubjectName = "CN=SDK2LifecycleSoak";
var config = new ApplicationConfiguration(telemetry)
{
ApplicationName = "SDK2LifecycleSoak", ApplicationUri = $"urn:SDK2LifecycleSoak:{Guid.NewGuid():N}",
ApplicationType = ApplicationType.Client, SecurityConfiguration = security,
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 15_000 },
TransportQuotas = new TransportQuotas { OperationTimeout = 5000 },
};
ApplicationInstance? application = new(telemetry) { ApplicationConfiguration = config };
ServerEventRig? rig = null;
Exception? originalFailure = null;
try
{
await config.ValidateAsync(ApplicationType.Client, ct);
await application.CheckApplicationInstanceCertificatesAsync(true, null, ct);
rig = await ServerEventRig.StartAsync("SDK2CloseOwnership", encrypted: true, ct);
Configure(rig);
var closedTargets = RunClosedSession(rig, config, ct);
var closed = await ObserveWeakAsync(closedTargets, ct);
WriteCloseControl("closed-objects-same-host-live", closed);
closed.Last().Alive.ShouldAllBe(value => !value);
GC.KeepAlive(rig);
var clientManager = config.CertificateManager;
var serverManager = rig.Server.CertificateManager!;
var serverApplication = RequiredField<OpcUaApplicationHost>(rig, "_host").ApplicationInstance!;
var cores = CertificateCores(clientManager).Concat(CertificateCores(serverManager)).ToArray();
var stoppedTargets = new List<WeakTarget>
{
new("client-application", new(application)), new("client-config", new(config)),
new("client-manager", new(clientManager)), new("server-manager", new(serverManager)),
new("server-application", new(serverApplication)),
new("server-rig", new(rig)), new("server", new(rig.Server)),
new("node-manager", new(rig.Nm)), new("event-source", new(rig.Nm.HistorianDataSource!)),
};
foreach (var core in cores)
{
stoppedTargets.Add(new("certificate-validation-core", new(core)));
foreach (var store in RequiredField<IDictionary>(core, "m_stores").Values)
if (store is not null) stoppedTargets.Add(new("cached-store", new(store)));
}
await rig.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20), ct);
await application.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20), ct);
application = null;
RequiredField<bool>(clientManager, "m_disposed").ShouldBeTrue();
RequiredField<bool>(serverManager, "m_disposed").ShouldBeTrue();
cores.Sum(CachedStores).ShouldBe(0);
(await DescriptorCountsAsync(ct, clientRoot, RequiredField<string>(rig, "_pkiRoot"))).Pki.ShouldBe(0);
rig = null;
WriteCloseControl("sdk2-disposal-proofs", new { managersDisposed = 2, cachedStores = 0, pkiDescriptors = 0 });
return stoppedTargets.ToArray();
}
catch (Exception error)
{
originalFailure = error;
throw;
}
finally
{
var cleanupFailures = new List<Exception>();
try
{
try { if (rig is not null) await rig.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20)); }
catch (Exception error) { cleanupFailures.Add(error); }
}
finally
{
try
{
try { if (application is not null) await application.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20)); }
catch (Exception error) { cleanupFailures.Add(error); }
}
finally
{
try { if (Directory.Exists(clientRoot)) Directory.Delete(clientRoot, true); }
catch (Exception error) { cleanupFailures.Add(error); }
}
}
if (cleanupFailures.Count > 0)
{
if (originalFailure is not null) cleanupFailures.Insert(0, originalFailure);
throw new AggregateException("Close-control cleanup failed; original failure retained when present.", cleanupFailures);
}
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static async Task<WeakTarget[]> CloseAndDenyOldTokensAsync(ServerEventRig rig,
ApplicationConfiguration config, CancellationToken ct)
{
using var old = await ConnectAsync(rig, config, null, ct);
var authenticationProperty = typeof(ClientBase).GetProperty("AuthenticationToken", BindingFlags.Instance | BindingFlags.NonPublic)
?? throw new InvalidOperationException("Exact SDK2 authentication diagnostic is unavailable.");
authenticationProperty.PropertyType.ShouldBe(typeof(NodeId));
var oldAuthentication = authenticationProperty.GetValue(old).ShouldBeOfType<NodeId>();
oldAuthentication.IsNull.ShouldBeFalse();
var browse = await BrowseAsync(old, ct);
ByteString history = default;
for (var i = 0; i < 6; i++)
{
var point = (await PageAsync(old, default, false, ct)).ContinuationPoint;
point.IsEmpty.ShouldBeFalse();
if (i == 0) history = point;
}
browse.IsEmpty.ShouldBeFalse(); history.IsEmpty.ShouldBeFalse();
var serverSession = rig.Server.CurrentInstance.SessionManager.GetSessions().Single();
var points = serverSession.ContinuationPoints;
ContinuationCounts(serverSession).ShouldBe((1, 6));
var weak = new List<WeakTarget>
{
new("closed-client-session", new(old)), new("closed-server-session", new(serverSession)),
new("closed-point-owner", new(points)),
};
using (RequiredField<System.Threading.Lock>(points, "m_lock").EnterScope())
foreach (var field in new[] { "m_browse", "m_history" })
foreach (var point in RequiredField<IEnumerable>(points, field))
{
weak.Add(new(field, new(point)));
if (point is Opc.Ua.Server.ContinuationPoint browsePoint && browsePoint.Data is { } data)
weak.Add(new("browse-data", new(data)));
if (field == "m_history")
{
var value = point.GetType().GetField("Value")!.GetValue(point)!;
weak.Add(new("history-value", new(value)));
weak.Add(new("history-state", new(value.GetType().GetProperty("State")!.GetValue(value)!)));
}
}
await old.CloseAsync(ct).WaitAsync(TimeSpan.FromSeconds(10), ct);
rig.Server.CurrentInstance.SessionManager.GetSessions().ShouldBeEmpty();
ContinuationCounts(serverSession).ShouldBe((0, 0));
using var fresh = await ConnectAsync(rig, config, null, ct);
await OwnPointsRoundTripAsync(fresh, ct);
(await fresh.ReadValueAsync(Raw(fresh, Scalar), ct)).StatusCode.ShouldBe(StatusCodes.Good);
var requestHeader = new RequestHeader { AuthenticationToken = oldAuthentication };
var failure = await Should.ThrowAsync<ServiceResultException>(async () =>
await fresh.ReadAsync(requestHeader, 0, TimestampsToReturn.Neither,
[new ReadValueId { NodeId = Raw(fresh, Scalar), AttributeId = Attributes.Value }], ct));
failure.StatusCode.ShouldBe(StatusCodes.BadSessionIdInvalid);
requestHeader.AuthenticationToken.ShouldBe(oldAuthentication);
(await fresh.BrowseNextAsync(null, false, [browse], ct)).Results[0].StatusCode.ShouldBe(StatusCodes.BadContinuationPointInvalid);
var source = rig.Nm.HistorianDataSource.ShouldBeOfType<EventHistoryPagingOverTheWireTests.SqlLikeEventSource>();
var readsBefore = source.ReadCount;
(await PageAsync(fresh, history, false, ct)).StatusCode.ShouldBe(StatusCodes.BadContinuationPointInvalid);
source.ReadCount.ShouldBe(readsBefore, "invalid old tokens must not reach the backend");
(await fresh.ReadValueAsync(Raw(fresh, Scalar), ct)).StatusCode.ShouldBe(StatusCodes.Good);
await OwnPointsRoundTripAsync(fresh, ct);
await fresh.CloseAsync(ct).WaitAsync(TimeSpan.FromSeconds(10), ct);
rig.Server.CurrentInstance.SessionManager.GetSessions().ShouldBeEmpty();
WriteCloseControl("wire-contract", new { oldAuthStatus = failure.StatusCode.Code, oldBrowseRejected = true, oldHistoryRejected = true,
abandonedBrowse = 1, abandonedHistory = 6, freshPositiveRounds = 2 });
return weak.ToArray();
}
private static async Task OwnPointsRoundTripAsync(Opc.Ua.Client.ISession session, CancellationToken ct)
{
var browse = await BrowseAsync(session, ct);
browse.IsEmpty.ShouldBeFalse();
(await session.BrowseNextAsync(null, false, [browse], ct)).Results[0].StatusCode.ShouldBe(StatusCodes.Good);
var history = (await PageAsync(session, default, false, ct)).ContinuationPoint;
history.IsEmpty.ShouldBeFalse();
var resumed = await PageAsync(session, history, false, ct);
resumed.StatusCode.ShouldBe(StatusCodes.Good);
resumed.ContinuationPoint.IsEmpty.ShouldBeFalse();
(await PageAsync(session, resumed.ContinuationPoint, true, ct)).StatusCode.ShouldBe(StatusCodes.Good);
}
}
@@ -32,7 +32,7 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
/// No key, certificate, store path, token, or OPC UA payload is written to the JSONL evidence.
/// </summary>
[Trait("Category", "E2E")]
public sealed partial class Sdk2LifecycleSoakTests
public sealed class Sdk2LifecycleSoakTests
{
private const string Scalar = ServerEventRig.RawDeviceFolder + "/SoakScalar";
private const string ArrayTag = ServerEventRig.RawDeviceFolder + "/SoakArray";
@@ -1,277 +0,0 @@
using Microsoft.Extensions.Logging.Abstractions;
using Opc.Ua;
using Opc.Ua.Security.Certificates;
using Opc.Ua.Server;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests;
/// <summary>Verifies host ownership using the real sealed SDK2 server and public manager factories.</summary>
public sealed class OpcUaApplicationHostDisposalTests : IDisposable
{
private static CancellationToken Ct => TestContext.Current.CancellationToken;
private readonly string _pki = Path.Combine(Path.GetTempPath(), $"otopcua-sdk2-disposal-{Guid.NewGuid():N}");
/// <summary>Normal and repeated disposal complete one manager shutdown and terminal server cleanup.</summary>
[Fact]
public async Task Host_terminally_disposes_its_booted_server_once()
{
var factory = new RecordingSessionManagerFactory();
var (host, server) = await BootAsync(factory);
var manager = factory.Created.ShouldNotBeNull();
try
{
await host.DisposeAsync();
manager.ShutdownCalls.ShouldBe(1);
manager.DisposalCalls.ShouldBe(1);
await RequireTerminalServerAsync(server);
await host.DisposeAsync();
manager.ShutdownCalls.ShouldBe(1);
manager.DisposalCalls.ShouldBe(1);
await RequireTerminalServerAsync(server);
}
finally { await CleanupAsync(host, server); }
}
/// <summary>Manager disposal during Stop alone does not satisfy the terminal server assertion.</summary>
[Fact]
public async Task Stop_only_is_distinct_from_terminal_server_disposal()
{
var factory = new RecordingSessionManagerFactory();
var (host, server) = await BootAsync(factory);
var manager = factory.Created.ShouldNotBeNull();
try
{
await server.StopAsync(Ct);
manager.ShutdownCalls.ShouldBe(1);
manager.DisposalCalls.ShouldBe(1);
var probe = new NonCreatingNodeManagerFactory();
var stopped = await Should.ThrowAsync<InvalidOperationException>(() =>
server.NodeManagerLifecycle.AddAsync(probe, callerContext: null, Ct).AsTask());
stopped.GetType().ShouldBe(typeof(InvalidOperationException));
probe.CreateCalls.ShouldBe(0);
await host.DisposeAsync();
await RequireTerminalServerAsync(server);
manager.ShutdownCalls.ShouldBe(1);
manager.DisposalCalls.ShouldBe(1);
}
finally { await CleanupAsync(host, server); }
}
/// <summary>A manager shutdown failure is recorded by the SDK while terminal cleanup still completes.</summary>
[Fact]
public async Task Shutdown_stage_failure_still_terminally_disposes_the_server()
{
var factory = new RecordingSessionManagerFactory { ThrowAfterShutdown = true };
var (host, server) = await BootAsync(factory);
var manager = factory.Created.ShouldNotBeNull();
try
{
await host.DisposeAsync();
manager.ShutdownCalls.ShouldBe(1);
manager.DisposalCalls.ShouldBe(1);
ServiceResult.IsBad(server.ServerError).ShouldBeTrue();
server.ServerError.ToString().ShouldContain(RecordingSessionManager.ShutdownFailure);
await RequireTerminalServerAsync(server);
}
finally { await CleanupAsync(host, server); }
}
/// <summary>A factory failure after address-space initialization leaves the real server owned for cleanup.</summary>
[Fact]
public async Task Partial_startup_failure_does_not_strand_the_server()
{
var factory = new RecordingSessionManagerFactory { ThrowDuringCreate = true };
var host = NewHost(OpcUaHostStart.AllocateFreePort());
var server = new OtOpcUaSdkServer { SessionManagerFactory = factory };
try
{
var failure = await Should.ThrowAsync<ServiceResultException>(() => host.StartAsync(server, Ct));
failure.StatusCode.ShouldBe(StatusCodes.BadInternalError);
failure.InnerResult.ShouldNotBeNull().LocalizedText.Text.ShouldNotBeNull()
.ShouldContain(RecordingSessionManagerFactory.StartupFailure);
factory.CreateCalls.ShouldBe(1);
factory.Created.ShouldBeNull("the sentinel fails before constructing an unowned manager");
host.ApplicationInstance.ShouldBeNull();
host.Server.ShouldBeNull();
await RequireTerminalServerAsync(server);
await host.DisposeAsync();
await RequireTerminalServerAsync(server);
}
finally { await CleanupAsync(host, server); }
}
/// <summary>Configuration cancellation before ApplicationInstance adopts the server still releases its host owner.</summary>
[Fact]
public async Task Canceled_start_before_application_adoption_terminally_disposes_the_server()
{
var factory = new RecordingSessionManagerFactory();
var host = NewHost(OpcUaHostStart.AllocateFreePort());
var server = new OtOpcUaSdkServer { SessionManagerFactory = factory };
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
try
{
await Should.ThrowAsync<OperationCanceledException>(() => host.StartAsync(server, cancellation.Token));
factory.CreateCalls.ShouldBe(0);
factory.Created.ShouldBeNull();
host.ApplicationInstance.ShouldBeNull();
host.Server.ShouldBeNull();
await host.DisposeAsync();
await RequireTerminalServerAsync(server);
}
finally { await CleanupAsync(host, server); }
}
/// <summary>Every concurrent host disposer waits until the one actual manager shutdown is released.</summary>
[Fact]
public async Task Concurrent_disposers_share_completion_and_one_cleanup_owner()
{
var factory = new RecordingSessionManagerFactory();
var (host, server) = await BootAsync(factory);
var manager = factory.Created.ShouldNotBeNull();
manager.BlockShutdown = true;
var calls = new List<Task>();
try
{
calls.Add(Task.Run(async () => await host.DisposeAsync(), Ct));
await manager.ShutdownEntered.Task.WaitAsync(TimeSpan.FromSeconds(10), Ct);
for (var i = 0; i < 3; i++) calls.Add(host.DisposeAsync().AsTask());
calls.ShouldAllBe(task => !task.IsCompleted);
manager.ShutdownCalls.ShouldBe(1);
manager.DisposalCalls.ShouldBe(0);
manager.ReleaseShutdown.TrySetResult();
await Task.WhenAll(calls).WaitAsync(TimeSpan.FromSeconds(20), Ct);
manager.ShutdownCalls.ShouldBe(1);
manager.DisposalCalls.ShouldBe(1);
await RequireTerminalServerAsync(server);
}
finally
{
manager.ReleaseShutdown.TrySetResult();
try { await Task.WhenAll(calls).WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); }
finally { await CleanupAsync(host, server); }
}
}
/// <summary>A terminal host rejects a new real server before invoking its manager factory.</summary>
[Fact]
public async Task Disposed_host_rejects_start_without_taking_server_ownership()
{
var factory = new RecordingSessionManagerFactory();
var host = NewHost(OpcUaHostStart.AllocateFreePort());
var server = new OtOpcUaSdkServer { SessionManagerFactory = factory };
try
{
await host.DisposeAsync();
await Should.ThrowAsync<ObjectDisposedException>(() => host.StartAsync(server, Ct));
factory.CreateCalls.ShouldBe(0, "the rejected server still belongs to its caller");
host.ApplicationInstance.ShouldBeNull();
host.Server.ShouldBeNull();
}
finally { await CleanupAsync(host, server); }
}
private Task<(OpcUaApplicationHost Host, OtOpcUaSdkServer Server)> BootAsync(RecordingSessionManagerFactory factory) =>
OpcUaHostStart.StartAsync(NewHost, () => new OtOpcUaSdkServer { SessionManagerFactory = factory }, Ct);
private OpcUaApplicationHost NewHost(int port) => new(new OpcUaApplicationHostOptions
{
ApplicationName = "OtOpcUa.SDK2.Disposal", ApplicationUri = "urn:OtOpcUa.SDK2.Disposal",
PublicHostname = "localhost", OpcUaPort = port, PkiStoreRoot = _pki, AllowAnonymous = false,
}, NullLogger<OpcUaApplicationHost>.Instance);
private static async Task RequireTerminalServerAsync(OtOpcUaSdkServer server)
{
// The public lifecycle checks terminal disposal before considering or invoking this factory.
var probe = new NonCreatingNodeManagerFactory();
var disposed = await Should.ThrowAsync<ObjectDisposedException>(() =>
server.NodeManagerLifecycle.AddAsync(probe, callerContext: null, CancellationToken.None).AsTask());
disposed.GetType().ShouldBe(typeof(ObjectDisposedException));
probe.CreateCalls.ShouldBe(0);
}
private static async Task CleanupAsync(OpcUaApplicationHost host, OtOpcUaSdkServer server)
{
try { await host.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None); }
finally
{
// RED assertions still release the actual test-owned server; never dispose a manager manually.
await server.DisposeAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None);
}
}
private sealed class RecordingSessionManagerFactory : ISessionManagerFactory
{
internal const string StartupFailure = "controlled SDK2 partial startup";
private int _createCalls;
internal int CreateCalls => Volatile.Read(ref _createCalls);
internal RecordingSessionManager? Created { get; private set; }
internal bool ThrowDuringCreate { get; init; }
internal bool ThrowAfterShutdown { get; init; }
public ISessionManager Create(IServerInternal server, ApplicationConfiguration configuration,
TimeProvider timeProvider, Func<string, Certificate?> serverCertificateProvider)
{
Interlocked.Increment(ref _createCalls);
if (ThrowDuringCreate) throw new InvalidOperationException(StartupFailure);
return Created = new RecordingSessionManager(server, configuration, timeProvider)
{
ThrowAfterShutdown = ThrowAfterShutdown,
};
}
}
private sealed class RecordingSessionManager(IServerInternal server, ApplicationConfiguration configuration, TimeProvider timeProvider)
: SessionManager(server, configuration, timeProvider)
{
internal const string ShutdownFailure = "controlled SDK2 manager shutdown failure";
private int _shutdownCalls;
private int _disposalCalls;
internal int ShutdownCalls => Volatile.Read(ref _shutdownCalls);
internal int DisposalCalls => Volatile.Read(ref _disposalCalls);
internal bool ThrowAfterShutdown { get; init; }
internal bool BlockShutdown { get; set; }
internal TaskCompletionSource ShutdownEntered { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
internal TaskCompletionSource ReleaseShutdown { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
public override async ValueTask ShutdownAsync(CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref _shutdownCalls);
if (BlockShutdown)
{
ShutdownEntered.TrySetResult();
await ReleaseShutdown.Task.WaitAsync(TimeSpan.FromSeconds(20), CancellationToken.None);
}
await base.ShutdownAsync(cancellationToken);
if (ThrowAfterShutdown) throw new InvalidOperationException(ShutdownFailure);
}
protected override void Dispose(bool disposing)
{
if (disposing) Interlocked.Increment(ref _disposalCalls);
base.Dispose(disposing);
}
}
private sealed class NonCreatingNodeManagerFactory : INodeManagerFactory
{
private int _createCalls;
internal int CreateCalls => Volatile.Read(ref _createCalls);
public ArrayOf<string> NamespacesUris => [];
public INodeManager Create(IServerInternal server, ApplicationConfiguration configuration)
{
Interlocked.Increment(ref _createCalls);
throw new InvalidOperationException("The terminal lifecycle probe must not create a node manager.");
}
}
/// <summary>Removes only this test's private fixture after bounded server cleanup.</summary>
public void Dispose()
{
if (Directory.Exists(_pki)) Directory.Delete(_pki, true);
}
}