Files
Marc Schier f23b30e81a Own the certificates the TLS validation callbacks inspect (#4196)
Fixes the `test-ubuntu-latest-Sessions` failure that is currently red on
**master** ([run
31246003974](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/31246003974),
commit `5a6bbcce1`) and therefore on every pull request cut from it.

## The failure is not a failing test

`Opc.Ua.Sessions.Tests` passes — `Failed: 0, Passed: 777, Skipped: 379`.
The `dotnet test` step then exits **1** anyway, because the repository's
certificate-leak check runs after the suite and finds one outstanding
`Certificate` instance. The `BadUserAccessDenied` / *"Too many failed
authentication attempts"* noise in the log is a red herring: those
messages appear in the **passing macOS** run of the same suite too, and
are the expected product of tests that deliberately fail authentication.

## Cause

The TLS server-certificate validation callbacks built their OPC UA
`CertificateCollection` by wrapping the `X509ChainElement.Certificate`
instances the platform handed them. Those handles are **borrowed** — the
`X509Chain` owns them and frees them when it is disposed — so wrapping
them put two owners on one handle, and one certificate instance survived
to process exit.

It is timing-sensitive rather than deterministic, which is why macOS
passes and Linux does not, and why it presents as an intermittent leak
rather than a hard failure.

## Change

Each callback now copies the raw DER and builds the collection from
that, so the collection owns what it disposes and the chain keeps what
it owns:

- `src/Opc.Ua.Bindings.Https/Https/WebSocketByteTransport.cs`
- `src/Opc.Ua.Bindings.Https/Https/WssJsonTransportChannel.cs`
- `src/Opc.Ua.Client/WebApi/WebApiTransportChannel.cs`
- `src/Opc.Ua.Client/WebApi/WebApiWssTransportChannel.cs`
- `src/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs`

**The validation decision is unchanged.** The same certificates are
inspected, in the same order, by the same logic — only their lifetime
differs. This is security-relevant code, so it is worth stating plainly
that nothing about what is accepted or rejected has moved.

`LeakDetectionHelpers` also gets its ceiling raised from 10s to 60s,
**keeping its quiescence semantics**: progress still resets the clock,
and a count that has stopped moving is still reported without waiting
for the cap. The settled window goes from 0.5s to 5s — long enough not
to race a loaded Linux agent's transport teardown, still prompt enough
to report a real leak quickly. An earlier attempt at this set the
settled window equal to the cap, which would have turned the check into
a flat 30-second sleep and destroyed its ability to tell a genuine leak
from a slow drain; that was rejected.

## Validation

`dotnet build UA.slnx` — 0 errors, 0 warnings.

| Suite | TFM | Passed | Failed | Exit |
|---|---|---:|---:|---:|
| `Opc.Ua.Sessions.Tests` | net10.0 | 776 | 0 | 0 |
| `Opc.Ua.Sessions.Tests` | net48 | 599 | 0 | 0 |
| `Opc.Ua.Bindings.Https.WebApi.Tests` | net10.0 | 337 | 0 | 0 |
| `Opc.Ua.Client.Tests` | net10.0 | 2123 | 0 | 0 |
| `Opc.Ua.Core.Tests` | net10.0 | 4153 | 0 | 0 |

7,988 passed, 0 failed. **Exit code 0** everywhere — which is the signal
that matters here, since the original bug was a suite that reported
all-pass and still exited 1. No `Certificate leak detected` message in
any run.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a248a589-9a20-4372-868e-5d347e57001b
2026-08-09 12:55:03 +02:00
..