test_runner: parent runner still crashes on child stdout bytes that mimic an event frame (survives the #64706 fix)
Les mainteneurs répondent en général sous 1 jour
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Accessibilité débutants
- 68/100
- Type d'issue
- Bug
- Clarté
- Clairement spécifiée
- Activité
- Active
- Stack technique
- javascript
- Domaine
- testing-qa
Piste de recherche
Start in lib/internal/test_runner/runner.js, tracing FileTest.parseMessage into #processRawBuffer, and run the deterministic poison.test.mjs reproduction with node --test. The fix is done when arbitrary child stdout, including bytes resembling a frame, no longer crashes the parent and subsequent test frames are still processed; verify the control and reproduction cases.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
- Version: v24.21.0 (this release includes the
>>> 0fix from #64706); also verified on v26.4.0 and v22.23.2 - Platform: macOS 26.6.2 (25G83), arm64
- Subsystem: test_runner
Current behavior
node --test crashes the parent runner (exit code 1; the failing component is the parent's stdout frame parser, not any test assertion) with:
Error: Unable to deserialize cloned data due to invalid or unsupported version.
at #processRawBuffer (node:internal/test_runner/runner:497:20)
at FileTest.parseMessage (node:internal/test_runner/runner:404:29)
at Socket.<anonymous> (node:internal/test_runner/runner:552:15)
How often: deterministic — 10/10 runs on every version below. Control (same file minus the stdout write): 10/10 green on every version.
| node | has #64706 >>> 0 fix |
repro | control |
|---|---|---|---|
| v24.21.0 | yes (verified in shipped runner.js source) |
10/10 crash | 10/10 pass |
| v26.4.0 | no (compiled-in source still has the signed read) | 10/10 crash | 10/10 pass |
| v22.23.2 | no (v22 backport tracked in #65934) | 10/10 crash | 10/10 pass |
Expected behavior
Anything a test (or code it imports) writes to stdout — text, banners, progress bars, QR codes, binary — should not be able to take down the parent runner. At worst it should degrade to test:stdout output or a per-file diagnostic, not abort the whole run with an error pointing at runner internals.
Steps to reproduce
Save as poison.test.mjs, then run node --test poison.test.mjs:
import { test } from 'node:test'
test('writes bytes that mimic a frame header', () => {
const poison = Buffer.from([
0xFF, 0x0F, // frame magic (v8 serdes header tag)
0x00, 0x00, 0x00, 0x08, // fake payload size = 8
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, // "ABCDEFGH" payload
])
process.stdout.write(poison)
})
test('subsequent test whose real frames must also be parsed', () => {
// trivial
})
Removing the process.stdout.write(poison) line makes it 10/10 green on all three versions.
Analysis
In lib/internal/test_runner/runner.js:
runTestFilespawns each test file withstdio: ['pipe', 'pipe', 'pipe']and pipes the child's raw stdout intoFileTest.parseMessage → #processRawBuffer.- The child's stdout therefore carries two unrelated producers on one pipe: the runner's own length-prefixed frames (
FF 0F | uint32 BE size | v8 payload) and arbitrary application output. There is no isolation or escape mechanism for user bytes. - The first loop in
#processRawBufferskips bytes up to the nextFF 0Fmagic and reports them astest:stdout. But after consuming a frame, the second loop continues without re-checking the magic — it trusts the next 4 bytes as a size and hands the slice straight toreadHeader()/readValue(), with no try/catch anywhere on this path (the escaping stack above is the proof). - #64706 hardened the size read itself (
>>> 0), which fixes the negative/garbage-size case. What remains — and what the repro above hits on v24.21.0 — is the rest of the path: any stdout bytes that begin withFF 0Fplus a plausible small size pass every guard,readHeader()throws on the payload, and the exception takes down the entire run. - Binary output containing
FF 0Fat a buffer head reaches exactly this state; plain text mostly takes the negative-size branch instead, which pre-#64706 silently drops buffered frames (see #65934) rather than crashing.
Real-world impact: one test's stdout can abort the whole node --test run with an internals-only error, which is hard for users to act on. We hit this as CI flakiness: a test printed a large terminal-QR block graphic and CI failed intermittently with the identical error; removing that output made the failures disappear.
Possible directions (not exhaustive): re-validate the FF 0F magic in the second loop before trusting the size; treat readHeader() / readValue() failure as lost/stray-output diagnostics for that file instead of a fatal exception; or an escaping mechanism so user stdout is never frame-parsed.
Related
- #64061 — signed size-read crash (same error/stack); fixed by #64706. This issue is about what that fix does not cover.
- #65934 — pending v22.x backport of #64706.
Secondary repro (plain text, intermittent)
The deterministic file above is a minimized stand-in. The organic trigger is chunk-boundary dependent — text landing in the same parse buffer as the end of a frame gets its bytes read as a size. This file matches our real-world case (QR-style block text) and crashes only intermittently:
import { test } from 'node:test'
const tick = () => new Promise((r) => setTimeout(r, 0))
test('passes some ordinary assertions', async () => { await tick() })
test('prints a large block of text like a QR code / ASCII banner', async () => {
for (let i = 0; i < 54; i++) {
console.log('█▀▄ '.repeat(25))
await tick() // let runner event frames flush between text writes
}
})
test('keeps running tests after the noisy one', async () => { await tick() })
- Langage dominant
- JavaScript
- Étoiles
- 122k
- Forks
- 38.4k
- Merge moyen
- 4 j 10 h
- PR mergées (30 j)
- 276
Préparer son environnement
- Aucun Dockerfile ni fichier Docker Compose
- Propose un modèle de pull request
- Lire le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de nodejs/node
-
doc
Difficulté 1/5 Moins d'une heure Accessibilité débutants 90/100
Les mainteneurs répondent en général sous 1 jour
-
doc
Difficulté 2/5 1-3 heures Accessibilité débutants 65/100
Les mainteneurs répondent en général sous 1 jour
-
build
Difficulté 1/5 Moins d'une heure Accessibilité débutants 88/100
nodejs/node#66076 · 2 commentaires ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 84/100
nodejs/node#65994 · 2 commentaires · 2 réactions ·
Les mainteneurs répondent en général sous 1 jour
-
feature request
Difficulté 2/5 1-3 heures Accessibilité débutants 68/100
nodejs/node#63841 · 1 commentaire ·
Les mainteneurs répondent en général sous 1 jour
Toutes les issues de nodejs/node
Issues similaires
-
bug
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
keyxmakerx/Chronicle#967 ·
Les mainteneurs répondent en général sous 1 jour
-
good first issue hacktoberfest
Difficulté 1/5 Moins d'une heure Accessibilité débutants 92/100
RogueAlg0/taken#386 · 3 commentaires ·
Les mainteneurs répondent en général sous 1 jour
-
external-issue to-triage
Difficulté 2/5 1-3 heures Accessibilité débutants 82/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 84/100
LearningCircuit/local-deep-research#7067 ·
Les mainteneurs répondent en général sous 1 jour