process.binding('uv').getErrorMap(): check for termination between puts - #39410
process.binding('uv').getErrorMap(): check for termination between puts#39410Jarred-Sumner wants to merge 2 commits into
Conversation
jsGetErrorMap built each [name, message] entry inside a void lambda that
dereferenced the result of constructEmptyArray without checking for an
exception (the check was left as a comment). constructEmptyArray returns
null once an exception is pending, and RETURN_IF_EXCEPTION also services
VM traps, so a worker.terminate() requested while a worker is inside
getErrorMap() turned into a null dereference that took down the whole
process ("Segmentation fault at address 0x4").
Build the map from a constexpr table in a plain loop instead, checking
the scope after every allocation, putDirectIndex and JSMap::set, so the
termination (or an out-of-memory error) propagates like any other
exception. The map contents and order are unchanged.
Regression coverage for the main-thread face of the terminate-on-ERR_* crash (tls.checkServerIdentity / Buffer.alloc(-1) under a vm timeout), fixed by #38457's ErrorCodeCache::createError change.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 59 minutes Limit details: You’ve used all 1 included review currently available under your plan. You completed 72 included PR reviews in the past 7 days; at that activity level, included reviews refill at 1 review per hour. 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 |
There was a problem hiding this comment.
LGTM — mechanical exception-check fix with regression tests.
What was reviewed
jsGetErrorMap: verifiedRETURN_IF_EXCEPTIONnow follows every fallible call (constructEmptyArray, bothputDirectIndex,map->set) under a properDECLARE_THROW_SCOPE;JSMap::create(vm, ...)takes no globalObject so no check needed there.- The lambda→
static constexprtable refactor preserves the exact entry set (sameBUN_UV_ERRNO_MAPexpansion) and avoids the per-entry code-bloat the old comment warned about. - Both new tests assert a positive stdout value + exit code (not absence-of-crash strings), use
await using, drain stdout before checking exitCode, and stderr is inherited so no pipe deadlock.
Extended reasoning...
Overview
The PR fixes a null-dereference crash in process.binding('uv').getErrorMap() (src/jsc/bindings/ProcessBindingUV.cpp) when a VM termination trap (worker.terminate() or node:vm timeout) is serviced mid-loop. The old code called constructEmptyArray inside a lambda with the RETURN_IF_EXCEPTION literally commented out, then dereferenced the result unconditionally. The fix declares a throw scope, converts the macro-expanded lambda calls into a static constexpr table + range-for, and checks the scope after each of the four fallible calls per iteration. Two subprocess regression tests are added: one for the getErrorMap/worker.terminate face and one for the node:vm-timeout/ERR_* face already fixed by #38457.
Security risks
None. This is purely adding missing exception-scope checks in a read-only error-table builder; no user input parsing, no auth, no I/O.
Level of scrutiny
Low-to-medium. The C++ change is 20 lines and is the textbook "add RETURN_IF_EXCEPTION after every call that can enter JS" pattern the repo's REVIEW.md explicitly requires. The refactor from ~85 macro-expanded lambda invocations to a constexpr table + loop is behavior-preserving (same BUN_UV_ERRNO_MAP X-macro drives both) and incidentally addresses the code-size concern the removed comment referenced. I checked that JSMap::create(vm, structure) does not take a globalObject and therefore needs no check, and that the final return needs no RELEASE_AND_RETURN because the last statement in the loop is already a scope check.
Other factors
- Tests follow harness conventions:
bunExe()/bunEnv,await usingon the subprocess, stdout asserted before exitCode,stderr: "inherit"(nothing to drain). The worker test asserts[1,1,1,1](all four workers exit with the terminated code) rather than grepping for absence of a crash string, so it can actually fail. The vm test's 120 × ≤6ms timeouts keep it well under a second. - The added
import { describe, expect, test } from "bun:test"in process-binding.test.ts makes the file's implicit globals explicit; harmless. - No prior human reviews or unresolved comments on the timeline (only a CodeRabbit rate-limit notice). The bug-hunting system found nothing.
What
process.binding("uv").getErrorMap()builds its Map withconstructEmptyArray/putDirectIndex/map->setcalls whoseRETURN_IF_EXCEPTIONs service VM traps, so a pendingworker.terminate()(ornode:vmtimeout) is delivered inside the loop and the next call runs on a null cell (ProcessBindingUV.cpp: member call on null pointerunder UBSan; SEGV on release). This is the last surviving face of the "termination lands on an ERR_* / error-table path" ledger row after #38457 fixedErrorCodeCache::createError; the fix hoists each fallible result into a local and checks the scope before using it (this is #37441's change, rebased) and adds a regression test for thenode:vm-timeout face of the same row that #38457 fixed.Repro (before)
Tests
test/js/node/process-binding.test.ts— "process.binding('uv').getErrorMap() survives worker.terminate() landing mid-call" (fails on the ASan canary and debug main);test/js/node/vm/vm.test.ts— "timeout landing while node validation errors are being constructed does not crash" (fails on the pre-#38457 canary; regression coverage for that half of the row).Supersedes #37441.