Files
Michał Pierzchała a904ef0d5d fix(fuzz): run parser cases in a worker process, not the runner's thread (#2053) (#2055)
The unit-lane corpus replay executed adversarial parser cases on worker
threads of the Vitest worker running the test file. A fault in a worker
thread ends its whole process, so a case that faulted killed the test
runner: `[vitest-pool]: Worker forks emitted error / Worker exited
unexpectedly`, with no test, file, or case named. Six of six Coverage
deaths before #1994's split were this one file out of ~1100, and the
uninstrumented second leg it created then lost the same file six more
times in three days.

Cases now run in a worker *process*. The two faults a case cannot report
about itself are both classified from outside it: a case that never
returns is a `hang` (unchanged), and one that ends the process it runs in
is a new `crash` failure carrying the exit code or signal and the tail of
the worker's stderr — the death certificate the lane used to lose. A
sixth self-check target seeds that kind, so a regression in reporting it
fails the harness self-check like every other kind.
2026-08-26 20:40:57 +02:00

36 lines
1.8 KiB
TypeScript

// Case-executing worker process for the parser fuzz lane (#1414).
//
// Cases run out of process for two reasons. A synchronous parser that never returns cannot be
// timed out from inside its own tick, so the budget is enforced from outside — the runner
// (scripts/fuzz/execute.ts) kills this process when a case stops answering, which is how a hang
// is attributed to an exact input. And a case that faults the process it runs in cannot be
// caught at all: only a separate process keeps that fault off the caller, whose thread may be
// the unit lane's test runner (#2053).
import { checkCase, type FuzzFailure } from './invariant.ts';
import { getFuzzTarget } from './registry.ts';
export type FuzzWorkerRequest = { kind: 'case'; input: string };
export type FuzzWorkerMessage = { kind: 'ready' } | { kind: 'result'; failure: FuzzFailure | null };
const send = process.send?.bind(process);
if (!send) throw new Error('scripts/fuzz/worker.ts must be run as a forked child process.');
const target = getFuzzTarget(process.argv[2] ?? '');
// The batch-steps parser warns on deprecated step shapes; a fuzz run would emit thousands of
// those lines and bury the failure report. Only JavaScript writes go through here: a fatal
// fault writes to the descriptor itself, which is why the runner keeps this process's stderr
// piped rather than discarded.
process.stderr.write = (() => true) as typeof process.stderr.write;
process.on('message', (request: FuzzWorkerRequest) => {
const failure = checkCase(target, request.input);
send({ kind: 'result', failure } satisfies FuzzWorkerMessage);
});
// Module loading (type stripping, parser imports) can outlast a per-case budget, so the runner
// only starts a case budget after this handshake.
send({ kind: 'ready' } satisfies FuzzWorkerMessage);