process: don't hand an empty JSValue to reifyStaticProperty on builder exception - #34879
process: don't hand an empty JSValue to reifyStaticProperty on builder exception#34879robobun wants to merge 1 commit into
Conversation
Six lazy PropertyCallback builders (constructVersions,
constructProcessReleaseObject, constructProcessReportObject,
constructProcessConfigObject, Process_stubEmptySet, constructFeatures)
declared a TopExceptionScope but used RETURN_IF_EXCEPTION(scope, {}) to
bail on failure. reifyStaticProperty stores the callback's result via
putDirect with no exception check, so an empty JSValue there trips
ASSERT(value) in putDirectInternal, and the exception escapes
unchecked.
Replace each RETURN_IF_EXCEPTION with a clearAndReportLazyPropertyException
helper that clears and reports the exception and returns jsUndefined(),
matching the existing pattern in Process_stubEmptyArray. Migrate the nine
other in-file callers of that inline pattern to the same helper.
|
Warning Review limit reached
Next review available in: 5 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 (2)
Comment |
|
Found 2 issues this PR may fix:
🤖 Generated with Claude Code |
|
Closing: this was already fixed at the WebKit layer in oven-sh/WebKit#282 and oven-sh/WebKit#306, which landed in bun's pinned WebKit via #34373.
The local |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
LGTM — mechanical extraction of an existing clear-and-report pattern into a file-local helper, applied to six RETURN_IF_EXCEPTION(scope, {}) sites and nine identical inline copies. The one inline finding is pre-existing and unrelated to this change.
Extended reasoning...
Overview
Two files touched: src/jsc/bindings/BunProcess.cpp gains a small static helper clearAndReportLazyPropertyException and 15 call sites are rewritten — six previously used RETURN_IF_EXCEPTION(scope, {}) (which would hand an empty JSValue to reifyStaticProperty's unchecked putDirect), and nine were already-correct inline copies of the exact clear-and-report pattern now deduplicated into the helper. test/js/node/process/process.test.js gains a scope-discipline guard that reifies every touched lazy property under BUN_JSC_validateExceptionChecks=1.
Security risks
None. No user-controlled input, no new API surface, no auth/crypto/permissions. The change only affects the OOM / termination-trap error path inside lazy process.* property builders.
Level of scrutiny
Low-to-medium. The helper is a byte-for-byte extraction of the pattern that already existed at nine sites in this file (e.g. constructEnv, Process_stubEmptyArray, constructStdin), so the nine migrations are pure dedup with no behavior change. The six RETURN_IF_EXCEPTION conversions change behavior only on the exception path (OOM in constructEmptyObject/constructEmptyArray/JSSet::create, or a termination trap landing in a sub-instruction window), where returning jsUndefined() and reporting is strictly better than returning {} into putDirectInternal's ASSERT(value). I verified each migrated site passes the same globalObject it previously used and that the double-read of scope.exception() (guard + helper) is harmless.
Other factors
- REVIEW.md's "never clearException()" concern doesn't apply: this is
tryClearException()followed byreportUncaughtExceptionAtEventLoop, the established pattern for lazy-property builders whose caller performs no exception check, and it was already in use at nine sites in this file before this PR. - The PR description honestly explains why no fail-before test is possible (no allocation-failure injection knob; the builders don't enter user JS). The added test is a reasonable
validateExceptionChecksguard and follows the file's existing subprocess conventions. - The one inline finding (duplicate
excludeEnvkey that should besignalat BunProcess.cpp:2528-2529) is pre-existing, sits in unchanged context lines, and is unrelated to this PR's exception-handling change — noted as a drive-by opportunity, not a blocker.
| report->putDirect(vm, JSC::Identifier::fromString(vm, "excludeEnv"_s), JSC::jsBoolean(false), 0); | ||
| report->putDirect(vm, JSC::Identifier::fromString(vm, "excludeEnv"_s), JSC::jsString(vm, String("SIGUSR2"_s)), 0); |
There was a problem hiding this comment.
🟣 Pre-existing (not introduced by this PR), but visible in the touched hunk: lines 2528-2529 both putDirect the key "excludeEnv" — the second, whose value is "SIGUSR2", was almost certainly meant to be "signal". As-is, process.report.signal is undefined and process.report.excludeEnv is the string "SIGUSR2" instead of boolean false. Worth fixing while you're in this function, but shouldn't block the PR.
Extended reasoning...
What the bug is
In constructProcessReportObject (BunProcess.cpp:2528-2529), two consecutive putDirect calls use the identical key "excludeEnv":
report->putDirect(vm, JSC::Identifier::fromString(vm, "excludeEnv"_s), JSC::jsBoolean(false), 0);
report->putDirect(vm, JSC::Identifier::fromString(vm, "excludeEnv"_s), JSC::jsString(vm, String("SIGUSR2"_s)), 0);The second call overwrites the first. Node's process.report exposes a signal property that defaults to 'SIGUSR2' and an excludeEnv boolean that defaults to false, so the second line's key was clearly meant to be "signal" — this is a copy-paste slip.
Step-by-step
- User evaluates
process.report→ the lazyPropertyCallbackconstructProcessReportObjectruns. - Line 2528 sets
report.excludeEnv = false. - Line 2529 sets
report.excludeEnv = "SIGUSR2", overwriting step 2. - No line ever sets
report.signal. - Observable result:
process.report.excludeEnv === "SIGUSR2"(string, wrong type) andprocess.report.signal === undefined(missing).
Why nothing prevents it
putDirect on an existing own property silently replaces the slot; it doesn't warn or throw on duplicate keys. There is no test asserting the type of process.report.excludeEnv or the presence of process.report.signal — the existing process.report test just calls JSON.stringify(process.report.getReport()), and this PR's new test only checks typeof process.report === "object".
Impact
Node-compat divergence on process.report: code that reads process.report.signal (e.g., to know which signal triggers report generation) gets undefined, and code that branches on the boolean process.report.excludeEnv sees a truthy string instead of false. Low practical impact since process.report is largely a stub in Bun, but it's a straightforward correctness bug.
Fix
Change the second key to "signal":
report->putDirect(vm, JSC::Identifier::fromString(vm, "excludeEnv"_s), JSC::jsBoolean(false), 0);
report->putDirect(vm, JSC::Identifier::fromString(vm, "signal"_s), JSC::jsString(vm, String("SIGUSR2"_s)), 0);Relation to this PR
These lines are unchanged context in the diff — git blame attributes them to a commit predating this PR. This PR only replaced the RETURN_IF_EXCEPTION(scope, {}) on the line immediately below with the new clearAndReportLazyPropertyException helper. The bug is unrelated to the exception-handling change; it's flagged only because it sits inside the touched hunk and is trivial to fix while here.
Problem
Six lazy
PropertyCallbackbuilders inBunProcess.cpp(constructVersions,constructProcessReleaseObject,constructProcessReportObject, the tail ofconstructProcessConfigObject,Process_stubEmptySet,constructFeatures) declare aTopExceptionScopebut bail withRETURN_IF_EXCEPTION(scope, {}).reifyStaticPropertystores aPropertyCallback's result viathisObj.putDirect(vm, name, result, attrs)with no exception check (Lookup.h), so that{}lands inputDirectInternal:which crashes the debug build, and in release stores an empty value the next reader dereferences. The exception itself also escapes
reifyStaticPropertyunchecked.RETURN_IF_EXCEPTIONadditionally handles VM traps, so a workerterminate()that arms the termination trap between the scope declaration and the check is enough for the macro to convert it into a pendingTerminationExceptionand take thereturn {}branch. In practice these six builders do not enter JS, so the window is very narrow; the remaining trigger is OOM insideconstructEmptyObject/constructEmptyArray/JSSet::create.Fix
Replace each
RETURN_IF_EXCEPTION(scope, {})with the same clear-and-report patternProcess_stubEmptyArrayandconstructProcessConfigObjectalready use for this exact reason (the latter's comment: "Lazy property builder: exceptions must not propagate into reifyStaticProperty, which performs no exception check"). Extract that pattern into a small file-local helper and migrate the nine existing inline copies of it in the same file:jsUndefined()is a valid value forputDirect, the exception is reported instead of silently propagating past the caller, and because the check isscope.exception()rather thanRETURN_IF_EXCEPTIONit no longer handles the termination trap mid-builder, so aterminate()landing in that window is observed at the next real safepoint instead of producing an empty property value.Swapping the scope type to
DECLARE_THROW_SCOPE(the literal inverse of ab84aa2) would not help: it still returns{}on exception, andreifyStaticPropertystill has no check.Why no fail-before test
None of the six builders run any JS that user code can cause to throw; the only reachable exception is OOM (or the sub-instruction termination race above). There is no allocation-failure injection knob, so the defect cannot be reproduced deterministically without instrumenting
src/. The new test inprocess.test.jsreifies every touched lazy property underBUN_JSC_validateExceptionChecks=1as a scope-discipline guard and asserts the expected types, andBUN_JSC_validateExceptionChecks=1 BUN_JSC_dumpSimulatedThrows=1over all of them plusprocess.report.getReport()/process.stdin/process.stdout/process.stderr/process.nextTickis clean.