Ports Soul's uaf.js probe (scenario a: bare open/release cycles,
including a fresh open overlapping the previous runtime's close) into a
committed test. UV_THREADPOOL_SIZE and MALLOC_PERTURB_ must be set before
Node starts, so the test spawns a child `node` process running
test/support/release-uaf-probe.js (plain JS, not compiled, requiring
dist/ directly like the package's own compiled tests) once per pool size
(1, 2, 4) and asserts each exits 0.
Trimmed from the probe's original 300 cycles to 20: at pool size 1 with
the crypto.pbkdf2 saturation load, 300 cycles took over two minutes on
this workstation, and the ordering bug this test defends does not need
that many iterations to reach the race — 20 keeps the whole test under
about 25 seconds while still exercising many open/release cycles with a
fresh open racing a still-closing runtime.
Verified by hand: reverting release()'s order in
realtime-quic-native.ts (runtimeCloseFn before awaiting pumpLoop, the
pre-fix order) did not make this test fail on Windows — MALLOC_PERTURB_
is a glibc feature the Windows CRT ignores, so corruption here is silent
rather than a deterministic crash. The revert-and-confirm check that
actually demonstrates the bug runs on Yggdrasil's Linux container, where
glibc honors MALLOC_PERTURB_; see the verify report for that run's
output. The fix was restored immediately either way, confirmed by a clean
git diff before this commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each rule below fails under its own mutation of the code fixed in the previous
two commits; verified by hand and restored, per the fix-batch brief, not
committed as a mutation harness (the operator retired that pattern).
- realtime-quic-native.test.ts: release() refuses a call with no outstanding
reference (fix 2).
- realtime-quic-consumer.test.ts: a stream-kind/delivery mismatch faults only
its connection while a second, independent connection through the same
pump keeps working (fix 1 / M4); a peer-initiated shutdown rejects a
pending receive and cleans up exactly once, so a further dispose is a
no-op and a fresh receive rejects instead of hanging (fix 3); a slow
latest-only reader accumulates at most one pending frame per key, and an
equal generation is dropped outright (fix 6, M10); a real connected
transport's isVerifiedFor rejects a generation other than the one it
connected with, and the session manager disposes a real (not stubbed)
transport whose generation no longer matches a freshly verified route
(M3); the advertised certificate pin matches case-insensitively (M2).
- realtime-quic-session-manager.test.ts (new): eviction of a dead cached
session and redial on the next connect, single-flight sharing one dial
across concurrent callers, refusing a transport that lands after dispose()
(all fix 4); the race returning on first success without waiting out a
slow candidate (fix 5); the maxRacedCandidates cap (M6); the protocolIds
and authorityRuntimeId route filters (M7, M8); the session key including
authorityRuntimeId (M9). Trust verification is skipped via
mode: "local-development" over loopback endpoints, since these tests are
about the session manager's own binding/racing/caching rules, not route
trust (already pinned by realtime-quic-consumer.test.ts's trust
negatives).
Subtraction: `nativeBridgeAvailable` was defined twice, disagreeing on
whether to check the dependency library too; both test files now import one
definition from test/support/native-bridge.ts (the stricter of the two).
Gap, reported rather than papered over: the stream-shutdown pruning half of
fix 6 (streamKinds) has no direct test, because the map is private and
nothing here exposes it; the committed test only shows a second frame still
arrives after the pruned stream's shutdown, which does not by itself prove
the entry was removed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>