threads pool worker isolate SIGSEGVs in V8 GC on large suites (vite 8 rolldown, node 24/26, macOS arm64)
Describe the bug
Running a large test suite (454 files, ~4.9k tests) through the Node API with pool: 'threads' and maxWorkers: 1 reliably crashes the process with SIGSEGV during V8 GC on the threads-pool worker isolate. The same file set runs clean under pool: 'forks' (both via the CLI and via the Node API).
Reproduction
Against https://github.com/novus-power/hive (public), on a fresh clone with pnpm install:
// repro.mjs, run with `node repro.mjs` from the repo root
import { createVitest } from "vitest/node";
import fs from "node:fs";
process.env.NODE_ENV = "test";
process.env.VITEST = "1";
const report = JSON.parse(fs.readFileSync("related.json", "utf8")); // testResults[].name of the 454 related files, see below
const files = report.testResults.map((t) => t.name.replace(process.cwd() + "/", ""));
const ctx = await createVitest("test", {
config: "vitest.config.ts",
pool: "threads",
maxWorkers: 1,
coverage: { enabled: false },
watch: false,
bail: 1,
});
const specs = await ctx.globTestSpecifications(files);
await ctx.runTestSpecifications(specs);
await ctx.close();related.json is just npx vitest related --run --reporter=json --outputFile=related.json core/graph/d1/src/*.ts (excluding test files) — the related set of that package's sources. We couldn't shrink it to a synthetic repro: a generated 420-file suite with real better-sqlite3 and PGlite usage per test does not crash; the real repo's module graph and test weight appear to be what pushes the worker isolate into the crashing GC window.
Expected behaviour
No SIGSEGV; or at worst the same behaviour forks pool exhibits (clean run).
Environment
- Vitest 4.1.10, vite 8.1.4 (rolldown)
- node 24.19.0 and node 26.8.1, both reproduce
- macOS 15 arm64 (also seen on GitHub Actions arm64 macOS runners)
Additional context
Crash signature from macOS DiagnosticReports, consistent across every occurrence (~15 crash reports on this machine alone, since these runs are part of our CI): faulting thread is the pool's WorkerThread, stack terminates in either
v8::internal::Isolate::Deinit→Heap::StartTearDown→CppHeap::StartDetachingIsolate→Heap::CollectGarbage→GlobalHandles::InvokeFirstPassWeakCallbacks→ SIGSEGV (worker teardown GC), orIncrementalMarkingJob::Task::RunInternal→MarkCompactCollector::StartMarking→MarkingWorklists::Local::~Local→ SIGSEGV (near-null deref, 0x89).
The crashing process hosts 18 rolldown-worker native threads (vite 8 transforms) alongside the worker isolate; only .node addons loaded are rolldown-binding.darwin-arm64.node and fsevents.node. This is why we originally hit it through StrykerJS — its vitest runner forces pool: 'threads' (https://github.com/stryker-mutator/stryker-js/issues/6223) — but the repro above has no Stryker involved.
Happy to run instrumented builds or test candidate fixes against the repro if that helps.
Source: vitest-dev/vitest