process.dlopen: check for exception after writing replaced exports - #34746
process.dlopen: check for exception after writing replaced exports#34746robobun wants to merge 1 commit into
Conversation
|
Reproduced on a debug+ASAN build with CI on 5f4c7c5: the diff is green. The only red is |
|
Updated 7:29 PM PT - Jul 19th, 2026
❌ @robobun, your commit 5f4c7c5 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 34746That installs a local version of the PR into your bun-34746 --bun |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/napi/dlopen-replace-exports-exception-check.test.ts`:
- Around line 11-18: Shorten the comment above the N-API initialization test to
no more than three lines, retaining only the non-obvious requirement that a
differing Init return value causes module.exports to be written and requires an
exception check, along with the relevant reason for directly exporting
napi_register_module_v1.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b157ea4c-85ab-4cc6-951b-47e3c79e159c
📒 Files selected for processing (2)
src/jsc/bindings/BunProcess.cpptest/napi/dlopen-replace-exports-exception-check.test.ts
When a napi_register_module_v1 entry point returns a value other than the
original exports object, Process_functionDlopen writes it back to
module.exports via JSObject::put, which opens a ThrowScope inside
putInlineSlow. The outer scope was destroyed without observing that
write's exception state, so on an assert-enabled build with the JSC
exception-check validator on, loading any addon that replaces its exports
aborted with:
This scope can throw a JS exception: putInlineSlow @ JSObject.cpp:836
But the exception was unchecked as of this scope: Process_functionDlopen
The sibling call site in Napi::executePendingNapiModule already has the
matching RETURN_IF_EXCEPTION; this one was missed.
428b0c7 to
5f4c7c5
Compare
|
Warning Review limit reached
Next review available in: 14 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 |
There was a problem hiding this comment.
LGTM — one-line RETURN_IF_EXCEPTION after JSObject::put, matching the parallel site in Napi::executePendingNapiModule.
What was reviewed
- Confirmed the sibling write at
napi.cpp:788already has the identical check, so this brings the dlsym path to parity. - Test compiles a minimal C addon against the in-repo headers (
src/runtime/napi/exists), skips when nocc/Windows, drains pipes concurrently, and asserts on a combined{stdout, stderr, exitCode}object. - Ruled out: stale
test/no-validate-exceptions.txtentries — those cover the N-API entry-point side handled separately (#32911), not this wrapper.
Extended reasoning...
Overview
Single-line change to src/jsc/bindings/BunProcess.cpp: adds RETURN_IF_EXCEPTION(scope, {}) immediately after strongModule->put(...) in Process_functionDlopen. When an N-API addon's napi_register_module_v1 returns a value different from the original exports, dlopen writes it back to module.exports via JSObject::put, which opens its own ThrowScope in putInlineSlow. Without the check, the outer scope's destructor trips verifyExceptionCheckNeedIsSatisfied under BUN_JSC_validateExceptionChecks=1. The parallel self-registration path in napi.cpp:786-790 already has exactly this check, so the change is bringing the two call sites to parity.
A new test (test/napi/dlopen-replace-exports-exception-check.test.ts) compiles a minimal C addon with the system cc against the in-repo N-API headers, exports napi_register_module_v1 directly (forcing the dlsym path rather than static-ctor self-registration), returns a fresh function as exports, and requires it in a subprocess with the exception validator enabled.
Security risks
None. The added line only observes and propagates an existing exception; no new inputs are parsed, no allocations, no privilege changes. The test compiles a local C file into a shared object in a temp dir and loads it in a child process — standard for the test/napi/ suite.
Level of scrutiny
Low. This is the canonical "exception check after every call that can enter JS" fix the review guide calls the most-blocked category. The change is one line, the mechanism is well-explained and independently verifiable against the sibling site, and the PR description shows a stash-on/stash-off repro proving the test fails for the right reason on main. On release builds (validator compiled out) behaviour is unchanged for non-throwing setters; if a user setter does throw, the exception now propagates to require() instead of being left pending — strictly more correct.
Other factors
- The test follows harness conventions:
tempDir,bunEnv/bunExe,skipIf(isWindows || !cc), concurrent pipe drain viaPromise.all, combined-object assertion so stderr appears in the failure diff without being asserted exactly (per the "never assert stderr is exactly empty" rule). - CodeRabbit's only inline comment (trim the explanatory comment to ≤3 lines) is marked resolved and the current diff reflects the trimmed 3-line version.
- Verifier agents examined whether
test/no-validate-exceptions.txtsuppression entries should be removed in this PR and concluded no — those entries are tied to the N-API entry-point work in #32911, which the PR description explicitly scopes out. - No prior claude[bot] reviews on this PR; no outstanding human reviewer comments.
Repro
With
BUN_JSC_validateExceptionChecks=1on an assert-enabled build, requiring any N-API addon whosenapi_register_module_v1returns a value other than the originalexportsobject aborts at module load:This is the shape of every "module exports a single function/class" addon (Node's own
js-native-api/4_object_factoryand5_function_factoryconformance tests included), and is why those two entries are intest/no-validate-exceptions.txt. #32911 fixes the N-API entry-point side of the validator failures and explicitly leaves this one, which is in theprocess.dlopenwrapper itself, for a separate change.Cause
When
napi_register_module_v1returns a replacement exports value,Process_functionDlopenwrites it back tomodule.exportsviaJSObject::put.putcan run a user-defined setter and opens its ownThrowScopeinsideputInlineSlow; the function then returned without observing that write's exception state, so the outer scope's destructor trippedverifyExceptionCheckNeedIsSatisfied. The parallel call site inNapi::executePendingNapiModule(the self-registration path) already has the matching check.Fix
Add the missing
RETURN_IF_EXCEPTION(scope, {})after theput, matchingexecutePendingNapiModule. If the setter throws, the exception now propagates to therequire()call instead of being left pending past an unchecked scope; on release builds (where the verifier is compiled out) the observable behaviour is unchanged for non-throwing setters.Verification
test/napi/dlopen-replace-exports-exception-check.test.tscompiles a minimal addon with the system C compiler against the in-repo N-API headers (no node-gyp) that exportsnapi_register_module_v1directly (no static-constructorNAPI_MODULEregistration, so thedlsympath inProcess_functionDlopenis taken) and returns a new function as its exports. It isrequire()d in a child process underBUN_JSC_validateExceptionChecks=1.On release builds the validator is compiled out, so the env var is a no-op and the test still passes.