Skip to content

fix(reference-implementation): stop the unit storage guard recursing on Node >=24.15.0 - #107

Merged
tnunamak merged 1 commit into
mainfrom
waspflow/fix-preload-guard-0912
Sep 12, 2026
Merged

tnunamak merged 1 commit into
mainfrom
waspflow/fix-preload-guard-0912

Conversation

@tnunamak

Copy link
Copy Markdown
Contributor

scripts/test-unit-preload.test.ts times out after 120s with no child output on Node >= 24.15.0, so the reference-implementation workflow cannot move off 24.14.x. Nothing blocks. The process spins.

driverRoots() in scripts/test-unit-preload.mjs resolves each denied driver lazily from inside the guard's own registerHooks resolve hook. Its only re-entrancy check was deniedDriverRoots.size > 0. That check can never latch during its own initialisation. The map is populated only after the require.resolve calls return. So every nested call sees an empty map and recurses again.

Node 24.15.0 runs require.resolve through module.registerHooks() (46cfad4138, nodejs/node#62028); before that it bypassed sync hooks. That is what closed the cycle. Instrumenting the hook with a depth counter showed mapsize 0 at every level. The same trace counted 302,332 resolutions of pg and better-sqlite3 in 12s. On 24.14.1 it never exceeds depth 1.

The fix adds a resolving latch set before the resolves, plus a rootsComputed flag that is separate from map size. It also primes the map before registerHooks runs, so the steady-state path never depends on the latch. Priming alone stops the hang. But without the latch, a later first call would still recurse. Both parts ship together.

Detection is unchanged. Six violation routes were tested: builtin import, computed getBuiltinModule name, pre-resolved file:// URL, createRequire CJS driver, swallowed denial, and real storage module. All six still fail the run on 24.14.1 and on 24.21.0, and no driver evaluates. Each was rerun with PDPP_TEST_UNIT_GUARD=0 to confirm it reaches storage and exits 0 when unguarded. That is what makes the denials real rather than incidental failures. The swallowed-denial route still reports pass 1 and still exits non-zero.

This was tested only against the memory-default profile, and only on one Linux machine. Node 24.15.0 and 24.19.0 were not retested after the fix.

Blocks #96, which cannot land until this does.

Assisted-by: AI

…on Node >=24.15.0

`driverRoots()` resolved each denied driver lazily from inside the guard's own
`registerHooks` resolve hook, using `deniedDriverRoots.size > 0` as its only
re-entrancy check. That check cannot latch during its own initialisation: the
map stays empty until the resolves finish.

Node 24.15.0 runs `require.resolve` through `module.registerHooks()`
(nodejs/node#62028, commit 46cfad4138); before that it bypassed sync hooks. So
those resolves began re-entering the hook, which called `driverRoots()` again
on a still-empty map, recursing without bound. The guard's own test timed out
after 120s with no child output; instrumenting the hook showed >300k nested
resolutions of `pg` and `better-sqlite3` in 12s.

Add an explicit `resolving` latch set before the resolves and a `rootsComputed`
flag separate from map size, and prime the map before `registerHooks` so the
steady-state path never depends on the latch.

What the guard detects is unchanged: all six violation routes (builtin import,
computed `getBuiltinModule` name, pre-resolved file URL, `createRequire` CJS
driver, swallowed denial, real storage module) still fire on 24.14.1 and
24.21.0, with no driver evaluating. Verified non-vacuous by running each route
with the guard off, where each reaches storage and exits 0.

Assisted-by: AI
Signed-off-by: Tim Nunamaker <tnunamak@gmail.com>
@tnunamak
tnunamak marked this pull request as ready for review September 12, 2026 20:27
@tnunamak
tnunamak merged commit c784936 into main Sep 12, 2026
16 checks passed
@tnunamak
tnunamak deleted the waspflow/fix-preload-guard-0912 branch September 12, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant