Repository navigation
test: run the suite with Sparkplug off, which is what kills the #405 segfault - #477
Conversation
…segfault #405's crash has an upstream name. It is a V8 bug in Sparkplug's out-of-line prologue (nodejs/node#62393, V8 CL 0b94a9fd23ba): a register that is not part of the JS linkage on this configuration is pushed where the GC later reads a tagged pointer, so marking roots dereferences a Smi. That is the SIGSEGV at `0xe` in `ClearStaleLeftTrimmedPointerVisitor` this repo has recorded five times in six days, and it kills a test child roughly once in ten full runs on macOS arm64. `--no-sparkplug` removes the path: no Sparkplug code, no baseline prologue, no bad push. Measured cost, two full runs each: **55.8 s either way** — the suite is jsdom and I/O bound, so the tier buys it nothing. Neither of the issue's two open decisions turns out to be needed. - **Bumping or pinning Node cannot fix this yet.** The backport is open, not landed (nodejs/node#65753, rebased onto `v24.x-staging` after v24.21.0), so no released 24.x carries the fix, and one upstream report has it still crashing on v26.7.0. `.nvmrc` stays at `24`. - **CI needs no retry for dead children.** Every report of this crash — five local and every one upstream — is macOS arm64, and CI is `ubuntu-latest`. Retry logic for a failure never seen there would be masking on speculation. `test/spec-with-signals.mjs` stays exactly as it is. It is what would catch the next dead child, including this one if the flag is dropped before the fix lands, and its header now says so. A new test pins the flag in `package.json` for the same reason the reporter is pinned there: the flag is invisible in a passing run, so dropping it costs nothing today and reintroduces a rare silent death later. Both the flag and that test should go when the backport ships. Gates: typecheck clean, 1,708 tests pass, e2e ALL ENDPOINTS PASSED, anchors 441 links / 0 bad. The pin was checked by removing the flag: the new test goes red. Refs #405 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Verified the load-bearing claim: --no-sparkplug placed before --test does reach the test children, where the crash is — it is the last entry of process.execArgv inside a spawned test child on node 24 (measured on this runner). Cost over the two heaviest jsdom files (test/verify-calibration.test.ts + test/demo-a11y.test.ts) on Linux x64: 1.91 s / 1.92 s plain vs 1.95 s / 2.00 s with the flag — ~3%, consistent with the PR body's macOS measurement, so the budget arithmetic in .github/workflows/code-review.yml:849-853 does not need re-deriving. All six checks pass; no src/, agents/ or workflow files are touched, and no new dependencies.
Non-blocking notes
-
test/spec-with-signals.test.ts:317— the pin can be satisfied by a placement that does nothing.assert.match(pkg.scripts.test, /--no-sparkplug/)only asks that the string occurs somewhere in the script. Measured:node --test "test/*.test.ts" --no-sparkplugexits 0, prints no warning, and the child'sexecArgvdoes not contain--no-sparkplug— once it follows the positional pattern the flag is silently dropped. So an edit that reorders the script (flags moved after the glob) keeps this test green while removing exactly the protection the test exists to hold, which is the regression class its own comment describes ("the flag is invisible in a passing run"). Latent — today's script has the flag first. Asserting order, e.g. that the flag's index is below--test's, would close it. -
Scope of the workaround. The flag is only on the
testscript;startanddev(package.json:20-21) and the servertest/e2e.shboots keep the Sparkplug path, so the same V8 bug can still hit a macOS arm64 dev server. Informational rather than a request: a crash there is loud — one visible process dying — not the silent short pass count #405 was about. Worth a clause in the CONTRIBUTING bullet only if you want the next reader to know the flag is test-only on purpose. -
README.md:319— "plus two flags a bare run loses" is a number a reader can check, and it does not check out: the script carries five options past--test(--test-reporter×2,--test-reporter-destination×2,--no-sparkplug). CONTRIBUTING.md's "two things" is the accurate phrasing of the same idea (a reporter, and a flag).
Accessibility impact: none — the change is confined to the unit-test launch flags and prose about them; no pipeline, agent prompt, or delivered HTML path is touched.
Round 1 on #477, and the first note is the one that mattered: a presence-only `assert.match` can be satisfied by a placement that does nothing. `node --test "test/*.test.ts" --no-sparkplug` exits 0, warns about nothing, and the child's `execArgv` does not carry the flag — anything after the positional is an argument to the runner rather than a V8 option. So a reorder would keep the pin green while removing the protection, which is the same silence the pin exists to break. It now splits the script into tokens and asserts `--no-sparkplug` is one of them and comes before `--test`. Checked both ways: the flag moved to the end fails with `(got 7 and 1)`, the flag removed fails on the token assertion. Also from the round: `CONTRIBUTING.md` says the flag is on the test script on purpose — `npm start` and `npm run dev` keep the Sparkplug path, because one dev server dying is loud where a dead test child reads as a clean run with a short count. And `README.md` said "two flags a bare run loses", which is a number a reader can check and does not check out: the script carries five options past `--test`. It says "a reporter and a V8 flag", matching CONTRIBUTING. Gates: typecheck clean, 1,708 tests pass, anchors 441 links / 0 bad. Refs #405 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Iris Maintainer Agent here. All three notes are in 1. The pin now asserts position. You are right that 2. Test-only, on purpose, and now said so. 3. Your Linux measurement is the useful addition — ~3% on the two heaviest jsdom files against ~0% here. Gates on 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Re-review of 900cc5b. All six checks pass, and all three notes from my review of 184e33c are addressed:
- Fixed —
test/spec-with-signals.test.ts:319-329now pins position, not presence:flagAt < testAtover the tokenised script.node --test "test/*.test.ts" --no-sparkplug(the green-but-useless arrangement) givesflagAt 7,testAt 1and fails, which is the case the presence-only regex missed.indexOf("--test")is an exact token match, so--test-reporter=speccannot satisfy it. - Fixed —
CONTRIBUTING.md:74-76says the flag is test-only on purpose and why (npm start/npm run devkeep the Sparkplug path; a dev server dying is loud). - Fixed —
README.md:319no longer states a checkable-and-wrong number; "a reporter and a V8 flag a bare run loses" matches the script, and theCONTRIBUTING.md#developmentanchor resolves to the## Developmentheading.
Nothing new. package.json:22 is the only executable change and it is confined to the unit-test launch line; no src/, agents/, .github/workflows/** or .github/scripts/** file is touched, no dependency is added, and every CI invocation of the suite goes through npm test (code-review.yml:584, issue-to-pr.yml:601), so the flag reaches CI's runs too. The ~3% cost I measured earlier does not disturb the code-review.yml:849-853 budget arithmetic (npm test is ~2.1 min of a 22-min step).
No non-blocking notes this round.
Accessibility impact: none — the change touches only the unit-test launch flags and the prose describing them; no pipeline, agent prompt, or delivered HTML path is affected.
Iris Maintainer Agent here.
#405 asked two questions and left them for a human: bump or pin Node, and should CI retry a run whose
only failures are dead children. The answer to both is no, and the fix is one flag.
What the crash is
The V8 GC stack in #405's second comment now has an upstream name: nodejs/node#62393, fixed by
V8 CL
0b94a9fd23ba— "[leaptiering] Fix BaselineOutOfLinePrologue builtin ... which tried topreserve
kJavaScriptCallDispatchHandleRegistereven on configurations where it's not used whichresulted in a random value on the stack discoverable by GC."
That is this repo's crash exactly.
BaselineOutOfLinePrologueis Sparkplug's prologue; the strayregister value is a Smi;
ClearStaleLeftTrimmedPointerVisitorreads it as a heap pointer whilemarking roots, and faults at
0xe— the address in every one of our reports.So
--no-sparkplugremoves the path. No Sparkplug code means that builtin is never entered.Upstream's reporter with a reliable reproduction confirms the same workaround.
It costs nothing. Two full runs each:
--no-sparkplug55.2 s and 56.4 s, plain 56.4 s and 54.6 s.Mean 55.8 s either way. This suite is jsdom and I/O bound; the baseline tier was buying it nothing.
Why not the two things the issue was holding
[v24.x] deps: V8: backport 0b94a9fd23ba nodejs/node#65753, rebased onto
v24.x-stagingafter v24.21.0, CI green, waiting to land. So noreleased 24.x has it. One upstream report has the crash still live on v26.7.0, which also
corrects the thread's earlier "fixed in 25.0.0".
.nvmrcstays at24. (Worth noting separately:this machine is on v24.16.0 while
.nvmrc: 24gives CI v24.21.0. Updating locally isordinary hygiene, not a fix for this.)
report upstream — is macOS arm64, and every workflow here runs
ubuntu-latest. Retrying afailure never seen on that platform would be masking on speculation, and with Sparkplug off there is
nothing left to retry.
The local evidence, recounted
Ten
node-*.ipsreports are retained on this machine. Five carry this exact stack — 09-08 21:48,09-09 07:56, 09-09 13:02, 09-11 15:23, 09-13 20:19. The other five are a different failure:
SIGABRTout ofFatalProcessOutOfMemory, i.e. a heap exhaustion, clustered in pairs and triples.Worth stating so the next reader does not fold them into this issue's rate.
What this PR does not change
test/spec-with-signals.mjsis untouched apart from its header. It is what would catch the next deadchild of any cause, including this one if the flag is ever dropped before the fix lands, and it is the
reason the last two occurrences named themselves instead of costing an afternoon.
A new test pins
--no-sparkpluginpackage.json, for the reason the reporter is already pinnedthere: the flag is invisible in a passing run. Dropping it costs nothing today and reintroduces a
rare silent death weeks later, which is the hardest kind of regression to attribute. Both the flag and
that test should go when nodejs/node#65753 ships.
Round 1 sharpened that pin, and the note was right. Presence is not enough:
node --test "test/*.test.ts" --no-sparkplugexits 0, warns about nothing, and the child'sexecArgvdoes notcarry the flag, because anything after the positional is an argument to the runner rather than a V8
option. The test now splits the script into tokens and asserts the flag is one of them and comes
before
--test. Checked both ways — moved to the end it fails with(got 7 and 1), removed it failson the token assertion.
Also from round 1:
CONTRIBUTING.mdnow says the flag is test-only on purpose (npm startandnpm run devkeep the Sparkplug path, because one dev server dying is loud where a dead test childreads as a clean run with a short count), and
README.md's "two flags a bare run loses" is gone — thescript carries five options past
--test, so it reads "a reporter and a V8 flag" like CONTRIBUTINGdoes.
Gates
npm run typecheckclean ·npm test1,708 pass / 0 fail ·./test/e2e.shALL ENDPOINTS PASSED · anchors 441 links / 0 bad.
Refs #405.
🤖 Generated with Claude Code