test(napi): cover worker.terminate() with napi_async_work execute in flight - #37918
test(napi): cover worker.terminate() with napi_async_work execute in flight#37918robobun wants to merge 1 commit into
Conversation
…flight Regression test for the worker-teardown race fixed by #37075. A worker queues four napi_async_works whose execute callbacks sleep and then write a 16 MiB ArrayBuffer on the pool thread; the parent terminates it while they are in flight. Before #37075 (at 52bf09c, its parent) the subprocess died 3/3 under ASAN with a heap-use-after-free in the pool thread's completion post (EventLoop::vm_ref <- enqueue_task_concurrent <- napi_async_work::run, freed by WebWorker::shutdown). On main it passes 3/3. The addon uses only public node-api.
|
Warning Review limit reached
Next review available in: 2 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
Comment |
|
Updated 5:05 PM PT - Aug 12th, 2026
❌ @robobun, your commit 62f7ed6 has 1 failures in 🧪 To try this PR locally: bunx bun-pr 37918That installs a local version of the PR into your bun-37918 --bun |
There was a problem hiding this comment.
LGTM — test-only PR adding a regression test for the #37075 fix; follows established napi test patterns.
What was reviewed:
- The C addon uses public node-api only, matches the CHECK/free pattern of sibling
.caddons, and thebinding.gyptarget is a byte-for-byte copy of neighbors. - The test awaits the observable
postMessage("up")before terminating, wireserror/exitto reject, drains stdout/stderr/exited concurrently, and asserts a combined object — matching REVIEW.md subprocess conventions and neighboring tests in this file. - Checked that the fixed sleeps live in the pool-thread
executecallback (creating the race), not on the JS side waiting for a condition; iteration count branches onisASAN;needsInstall()picks up the new.csource.
Extended reasoning...
Overview
This PR adds a single regression test for a heap-use-after-free that #37075 already fixed (worker.terminate() while napi_async_work execute callbacks are in flight on the thread pool). It touches three files, all under test/napi/: a new ~110-line C addon using only public node-api, a new binding.gyp target copied verbatim from sibling targets, and one new it() block inside the existing napi_async_work describe in napi.test.ts. No production code changes.
Security risks
None. This is test-only code that compiles a small napi addon and spawns a Bun subprocess with bunExe()/bunEnv. No network access, no auth, no untrusted input parsing.
Level of scrutiny
Low-to-medium. Test-only, self-contained, and the PR description documents fail-before (3/3 ASAN heap-use-after-free at the pre-#37075 commit) and pass-after (3/3 on main). The main risks for a test PR of this shape are flakiness and CI cost, both of which I checked.
Other factors
- Test conventions: The test is placed in the existing
napi_async_workdescribe (not a new file), spawns viabunExe()+bunEnv, drains all pipes withPromise.all, and asserts{stdout, stderr, exitCode}as one object. The worker "up" handshake wiresmessage/error/exitso a premature exit rejects rather than hangs. Thestderr: ""assertion matches several neighboring tests in this file that also spawnbunExe() -eunderbunEnv. - Sleeps: The 300-450ms sleeps are inside the addon's thread-pool
executecallback — they are the in-flight work the test terminates around, not a JS-side wait for a condition. The JS side awaitspostMessage("up")beforew.terminate(). This is the correct shape for reproducing the race and is documented in the test comment. - Timing/CI cost: 3 (ASAN) or 5 iterations, each bounded by the longest execute (~450ms) plus worker startup; the 30s per-test timeout has headroom for debug+ASAN. The suite is
describe.concurrent, so this runs alongside siblings. - LSan disable: The comment justifies
detect_leaks=0(pre-existing eval-worker source-Blob leak unrelated to napi), matching REVIEW.md's rule that skipping a safety net needs a stated reason. - Addon: The
CHECKmacro leaks thecalloc'dwork_ton a mid-setup napi failure, but that's the established idiom across the sibling test addons and only reachable on programmer error in fixture code. - Build integration:
needsInstall()in this file scans*.csources, so the new addon triggers a rebuild on first run.
|
CI at 62f7ed6 (build 93517): The one non-flaky red is Ready for review. |
|
Looked into the LSan report behind the
So the |
Problem
worker.terminate()while anapi_async_workexecutecallback was still running on the thread pool freed the worker's VM under it.EventLoop::vm_ref(freed byWebWorker::shutdown).executealso kept writing anArrayBufferteardown had released.Fix
executesleeps, then memsets a JS-ownedArrayBufferon the pool thread.executeis asleep.Blobs at exit on main regardless of the addon, so the test asserts on the crash only.Background
test/napi/napi-appare built by node-gyp and driven fromnapi.test.ts.napi_async_workrunsexecuteon a thread pool, then posts to the owning event loop socompleteruns on the JS thread.worker_threadsworker owns a VM and event loop;terminate()frees both and finalizes heap objects such asArrayBufferstores.no test proof · iteration 4 · Platform-specific test-only change; deferring to CI.
Original description
Problem
worker.terminate()while anapi_async_workexecutecallback was still running on the thread pool freed the worker's VM under it; the pool thread's completion post then hit a heap-use-after-free (EventLoop::vm_ref<-enqueue_task_concurrent<-napi_async_work::run, freed byWebWorker::shutdown), and the addon'sexecutekept writing anArrayBufferstore that VM teardown had already released.Test
test/napi/napi.test.ts,napi_async_work > worker.terminate() with execute callbacks in flight ...: a subprocess repeatedly starts a worker that queues four works (300-450 msexecute, each memsetting a 16 MiBArrayBufferon the pool thread) and terminates it while they are in flight.test/napi/napi-app/test_async_work_worker_terminate.c, public node-api only (napi_get_arraybuffer_info+napi_refpin +napi_create/queue_async_work).bun bd(ASAN),heap-use-after-freeinEventLoop::vm_refas above.-escript that creates eval workers reports their sourceBlobs at exit with or without the addon, so the test asserts on the crash only (the napi-side state is freed at teardown; nothing napi-related shows up with LSan on).fail-before at 52bf09c
(3/3 runs)