Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91bcd70ab1 |
+1
-1
@@ -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/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-31
@@ -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.
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"remainingOwnedKeychainFiles": 0
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"ownedFixtureRootRemoved": true
|
||||
}
|
||||
-39
@@ -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"
|
||||
}
|
||||
-129
@@ -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
|
||||
}
|
||||
-207
@@ -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}
|
||||
-34
@@ -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.
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"compileExitCode": 1,
|
||||
"nativeProcessNeverStarted": true,
|
||||
"emptyOwnedFixtureRootRemoved": true
|
||||
}
|
||||
-37
@@ -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
|
||||
}
|
||||
-21
@@ -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
|
||||
}
|
||||
-9
@@ -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
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"allOwnedFixtureDirectoriesRemoved": true
|
||||
}
|
||||
-34
@@ -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"
|
||||
}
|
||||
}
|
||||
-6
@@ -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}
|
||||
-366
@@ -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
@@ -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"}}
|
||||
-54
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
-6
@@ -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}
|
||||
-71
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
-3
@@ -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;
|
||||
|
||||
-99
@@ -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); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-248
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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";
|
||||
|
||||
-277
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user