Skip to content

fix(test): probe the native binding out of process - #2232

Merged
flora131 merged 1 commit into
mainfrom
fix/natives-probe-out-of-process
Aug 7, 2026
Merged

fix(test): probe the native binding out of process#2232
flora131 merged 1 commit into
mainfrom
fix/natives-probe-out-of-process

Conversation

@flora131

@flora131 flora131 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

The natives globalSetup I added in #2224 turned every Linux suites run red — including on main, and therefore on every PR branched from it.

Each run passes all ~5960 tests on both retry attempts and then dies with exit code 139 (SIGSEGV) during exit.

Attribution

main run sha suites (linux-x64)
08-06T00:33 ad2155163 success
08-06T21:21 65469006c failure
08-07T02:33 70d862d24 failure

ad2155163..65469006c contains exactly two commits, both mine:

Cause

function bindingLoads(): boolean {
	if (!existsSync(NATIVE_ENTRY)) return false;
	try {
		createRequire(import.meta.url)(NATIVE_ENTRY);   // ← in the orchestrator
		return true;
	} catch { return false; }
}

globalSetup runs in vitest's orchestrator — the process that owns the worker pool. That require dlopens the NAPI addon into a process which otherwise never touches it; workers load it on demand, in their own processes.

The addon carries #2205's Rust control plane and its Tokio runtime. On glibc Linux its destructors run at exit alongside pool teardown, and the process dies after the suite has already succeeded. Consistent with every observation: exit 139 after 5968 passed, on both attempts, Linux only.

Fix

Probe in a child process. Exit 0 means loadable; the orchestrator never loads the addon.

const probe = spawnSyncCollect([process.execPath, "-e", `require(${JSON.stringify(NATIVE_ENTRY)})`], {
	cwd: REPO_ROOT,
});
return probe.success;

This keeps every property the load-check was chosen for in review:

Cost is one short spawn, on a path that already spawns a Rust build when the binding is missing.

Verification

Three paths, locally:

warm (binding present)        2.9s, silent
foreign binding only          "present but not loadable here" → rebuilds
full unit suite               625 files / 5957 tests, exit 0

npm run check clean.

What I got wrong

I ran that suite locally many times while writing #2224 — always on macOS, which does not reproduce this. I chose the require() probe because the filename scan was correctly rejected in review, and never asked which process would be doing the loading.

I have not reproduced the segfault locally. The attribution rests on the exact commit boundary, the inert alternative, and a mechanism consistent with the platform split. The proof is this PR's own Linux suites job.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Greptile Summary

This change moves the native addon load check out of Vitest’s orchestrator and into a short-lived Node process, avoiding direct native-module loading in the process that manages test workers. Runtime checks confirmed that the real binding loads successfully, Vitest completes with the updated setup, and an intentionally non-loadable binding enters the rebuild-and-fail path rather than being accepted.

Confidence Score: 5/5

Safe to merge based on the focused native-binding and Vitest setup checks.

The exercised success path completed normally with the real native binding, and the exercised failure path correctly rejected a non-loadable binding and invoked the rebuild behavior.

Files Needing Attention: No files need additional attention; the reviewed change is limited to test/global-setup-natives.ts.

T-Rex T-Rex Logs

What T-Rex did

  • I ran the review-authored native-load isolation validation script and exercised the prior in-process and updated child-process Vitest flows with a real worker test.
  • The available-binding Vitest runs completed successfully and the direct child probe exited cleanly, while the isolated non-loadable fixture invoked a mocked rebuild and then failed with the rebuild exit code, proving that the updated probe accepts only a loadable binding.
  • I reviewed the executable validation script trex-artifacts/native-load-isolation-validation.mjs which runs the before/after Vitest flows and the isolated non-loadable fixture without modifying repository source.
  • I inspected the captured outputs in native-load-isolation-02-after.log and the before.log, which show the pass of both Vitest runs, the direct child probe loading, and the rebuild path for the non-loadable fixture.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "fix(test): probe the native binding out ..." | Re-trigger Greptile

The natives globalSetup I added in #2224 turned every Linux `suites` run
red, including on main. Each run passes all ~5960 tests on both retry
attempts and then dies with `exit code 139` -- SIGSEGV during exit.

main was green at ad21551 and red at the very next run, 6546900,
which is #2224. The only other commit in that range is #2225, which
changes timeout numbers, a contract test and docs, and cannot segfault.
The sole new executable code is test/global-setup-natives.ts.

Cause: `bindingLoads()` called `createRequire(...)(NATIVE_ENTRY)`, and
globalSetup runs in vitest's ORCHESTRATOR -- the process owning the
worker pool. That dlopened the NAPI addon into a process that otherwise
never touches it; workers load it on demand in their own processes. The
addon carries #2205's Rust control plane and its Tokio runtime, so on
glibc Linux its destructors run at exit alongside pool teardown and the
process dies after the suite has already succeeded.

The probe now runs in a child: `node -e "require(<entry>)"`, exit 0 means
loadable. That keeps every property the check was chosen for -- a real
load attempt rather than a filename scan, so it cannot drift from
napi-rs's ~700 lines of platform-arch-libc resolution, and a
foreign-platform binding still triggers a rebuild -- while the
orchestrator never loads the addon. It costs one short spawn on a path
that already spawns a Rust build when the binding is missing.

Verified locally on all three paths: warm (2.9s, silent), foreign binding
present (reports "present but not loadable here" and rebuilds), and full
unit suite 625 files / 5957 tests, exit 0.

I could not reproduce the segfault locally because I only ever ran this
suite on macOS, which is also why #2224 shipped with it. The proof is
this PR's own Linux `suites` job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@flora131
flora131 merged commit 5e1d5b7 into main Aug 7, 2026
18 checks passed
@flora131
flora131 deleted the fix/natives-probe-out-of-process branch August 14, 2026 01:16
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