Repository navigation
Conversation
Walkthrough
ChangesNapiBoundaryScope implementation and validation
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 7:49 PM PT - Jul 14th, 2026
❌ @robobun, your commit d839d6a has 2 failures in
🧪 To try this PR locally: bunx bun-pr 32911That installs a local version of the PR into your bun-32911 --bun |
There was a problem hiding this comment.
Thanks — both earlier points are addressed. No further issues from me, but the NapiBoundaryScope / ExceptionScope subclassing and the preamble macro changes touch every N-API entry point, so I'd like a human familiar with JSC's exception-scope machinery to sign off.
Extended reasoning...
Overview
This PR changes src/jsc/bindings/napi.cpp to introduce NapiBoundaryScope, a custom JSC::ExceptionScope subclass, and rewrites the NAPI_PREAMBLE / NAPI_PREAMBLE_NO_THROW_SCOPE macros to use it instead of DECLARE_THROW_SCOPE. These macros run at the entry of essentially every napi_* C ABI function. It also adds test/napi/napi-exception-check.test.ts, which compiles a minimal addon with the system C compiler and loads it under BUN_JSC_validateExceptionChecks=1.
Since my last review, commit 0718bfd addressed the remaining nit (stderr is now carried in the final assertion's received object so a regression's abort report shows in the failure diff). The earlier macOS -undefined dynamic_lookup fix is also in place. Both of my prior inline comments are resolved in the current diff.
Security risks
None identified. The change is confined to debug-build exception-scope verification bookkeeping; in release builds ENABLE(EXCEPTION_SCOPE_VERIFICATION) is off and NapiBoundaryScope collapses to the trivial base ExceptionScope. No new inputs are parsed and no trust boundaries change.
Level of scrutiny
High. While the rationale is well-argued and mirrors JSC's own TopExceptionScope precedent, this is a hand-rolled ExceptionScope subclass whose constructor/destructor interact with VM::m_needExceptionCheck, and the NAPI_PREAMBLE_NO_THROW_SCOPE macro changes shape from a do..while(0) block to a variable-declaring statement sequence across ~19 call sites. Getting the scope nesting or lifetime subtly wrong here could mask real exception-handling bugs across the entire N-API surface. This warrants review from someone with JSC internals knowledge rather than bot approval.
Other factors
- No CODEOWNERS entry covers
src/jsc/bindings/napi.cpp. - The bug-hunting system found no issues this round.
- The new test exercises the fix on debug builds and is a no-op pass on release builds, which is reasonable.
napi_preamble_throw_scope__keeps its name despite no longer being aThrowScope; harmless but worth noting if a reviewer wants to rename it.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@src/jsc/bindings/napi.cpp`:
- Around line 82-87: The new explanatory comments around the N-API exception
scope are too long and exceed the repo’s 3-line comment limit. Trim the prose in
the block near the C ABI boundary in napi.cpp, keeping only the durable
invariant about reading vm.exception() on entry/exit and not simulating throws,
and apply the same shortening to the matching comment block later in the file.
In `@test/napi/napi-exception-check.test.ts`:
- Around line 35-76: The standalone validator-only N-API test is not reliable
because BUN_JSC_validateExceptionChecks=1 is a no-op on release/system Bun, so
it can miss the regression under USE_SYSTEM_BUN=1. Move this addon-load
assertion into the existing N-API coverage in the same test suite and have the
exception-check behavior exercised by the validator-enabled CI shard instead of
relying on a separate test. Keep the coverage anchored around the existing addon
load path in napi-exception-check.test.ts and the load.js/addon.node scenario,
but make sure the test still fails in the system Bun path when the boundary bug
is present.
- Around line 11-17: Shorten the rationale comment in the N-API exception check
test so it fits the 3-line comment limit while preserving the validator
invariant. Trim the explanatory block near the module init example in
napi-exception-check.test.ts, keeping only the key point about
napi_create_function and napi_set_named_property exception scopes and the
BUN_JSC_validateExceptionChecks=1 abort behavior. Use the existing test
description context to condense wording without changing the meaning.
🪄 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: 11fd28a6-5f1c-4972-a407-3028cd21829d
📒 Files selected for processing (2)
src/jsc/bindings/napi.cpptest/napi/napi-exception-check.test.ts
There was a problem hiding this comment.
Both earlier comments are addressed and I didn't find anything new, but this introduces a custom JSC::ExceptionScope subclass and rewires the preamble macros used by every N-API entry point — worth a human with JSC familiarity signing off.
Extended reasoning...
Overview
The PR replaces the DECLARE_THROW_SCOPE in NAPI_PREAMBLE with a new NapiBoundaryScope (a hand-rolled JSC::ExceptionScope subclass) and adds the same boundary scope to NAPI_PREAMBLE_NO_THROW_SCOPE, which previously declared no scope at all. A new test compiles a minimal addon with the system C compiler and loads it under BUN_JSC_validateExceptionChecks=1. Both of my earlier inline comments (macOS -undefined dynamic_lookup, and carrying stderr into the final assertion's received object) have been applied and resolved.
Security risks
None identified. This is debug-build exception-scope bookkeeping; in release builds ENABLE(EXCEPTION_SCOPE_VERIFICATION) is off and NapiBoundaryScope collapses to the trivial base. No new trust boundaries, parsing, or external input handling.
Level of scrutiny
High. NAPI_PREAMBLE / NAPI_PREAMBLE_NO_THROW_SCOPE are expanded at the top of essentially every napi_* entry point, and napi_preamble_throw_scope__ is consumed by NAPI_RETURN_IF_EXCEPTION, NAPI_RETURN_IF_VM_EXCEPTION, and NAPI_RETURN_SUCCESS (assertNoException()). Subclassing JSC::ExceptionScope directly and relying on specific constructor/destructor semantics (reading exception() to clear m_needExceptionCheck, deliberately not simulating a throw toward the native caller) is the kind of JSC-internals reasoning a human reviewer should validate — including the ~19 NAPI_PREAMBLE_NO_THROW_SCOPE callsites that now gain an outer scope and changed from a do {} while(0) statement to a bare declaration.
Other factors
The design rationale is thorough and cites JSC's own DECLARE_TOP_EXCEPTION_SCOPE precedent for the same boundary problem, the regression test is targeted and platform-aware, and the broader napi test suites were reportedly re-run. Nothing looks wrong to me; it's just not a mechanical change I'm comfortable approving without a human look at the JSC-side assumptions.
There was a problem hiding this comment.
No bugs found on this revision — my earlier two points are addressed. Leaving this for a human reviewer since it subclasses JSC::ExceptionScope and changes the exception-scope semantics at every N-API entry point, which is the kind of JSC-internals decision someone familiar with WebKit's exception discipline should sign off on.
Extended reasoning...
Overview
This PR replaces the DECLARE_THROW_SCOPE in NAPI_PREAMBLE / NAPI_PREAMBLE_NO_THROW_SCOPE with a new NapiBoundaryScope class that subclasses JSC::ExceptionScope. The new scope reads vm.exception() on entry and exit (clearing the validator's pending-check bit) but never simulates a throw toward its caller, so back-to-back napi_* calls from addon C code no longer trip JSC's exception-check validator. NAPI_PREAMBLE_NO_THROW_SCOPE is also unwrapped from a do { } while (0) block into open statements so the boundary scope outlives any inner ThrowScope. A new test compiles a minimal addon with the system C compiler and loads it under BUN_JSC_validateExceptionChecks=1.
Security risks
None identified. This is debug-build exception-scope bookkeeping; in release builds ENABLE(EXCEPTION_SCOPE_VERIFICATION) is compiled out and NapiBoundaryScope collapses to the trivial base ExceptionScope, so there is no codegen change there. No auth, crypto, permissions, or untrusted-input parsing is touched.
Level of scrutiny
High. The macro changed here runs at the top of every N-API entry point, and the fix works by subclassing a JSC-internal type and relying on specific behavior of ExceptionScope's constructor/destructor and the m_needExceptionCheck machinery. The reasoning in the PR description is detailed and matches my reading of JSC's ThrowScope/CatchScope/TopExceptionScope, and the downstream macros (RETURN_IF_EXCEPTION, assertNoException) only use ExceptionScope members so the type change is compatible. But this is exactly the kind of subtle runtime/VM-boundary design decision that a maintainer with JSC context should confirm rather than a bot.
Other factors
- My two earlier inline comments (macOS
-undefined dynamic_lookuplink flag; carryingstderrinto the failure diff) were both addressed in e4e5a58 and 0718bfd. - I checked all
NAPI_PREAMBLE_NO_THROW_SCOPEcall sites: each is a standalone statement at function-body scope, so dropping thedo/while(0)wrapper doesn't change control flow anywhere. - There is an unresolved CodeRabbit comment about the test being a no-op under
USE_SYSTEM_BUN=1/ release builds; the PR description already explains this is by design (the validator is compiled out in release), but the author may want to respond or resolve it. - No bugs surfaced in this review pass.
There was a problem hiding this comment.
All my earlier feedback is addressed and I found nothing further in feee1a6, but introducing a custom JSC::ExceptionScope subclass at the N-API boundary and swapping the scope type used by every napi_* preamble is the kind of JSC-internals design decision that should get a human look.
Extended reasoning...
Overview
This PR touches two files: src/jsc/bindings/napi.cpp and a new test test/napi/napi-exception-check.test.ts. The substantive change introduces NapiBoundaryScope, a minimal JSC::ExceptionScope subclass that acknowledges pending exception checks on entry/exit without simulating a throw toward its (native C addon) caller, and uses it in NAPI_PREAMBLE / NAPI_PREAMBLE_NO_THROW_SCOPE in place of the previous ThrowScope. It also adds RETURN_IF_EXCEPTION checks between back-to-back NapiClass::create calls in Napi::defineProperty. The new test compiles two tiny addons with cc and loads them under BUN_JSC_validateExceptionChecks=1.
Security risks
None identified. The change is to debug-build exception-scope bookkeeping; in release builds ENABLE(EXCEPTION_SCOPE_VERIFICATION) is compiled out and NapiBoundaryScope collapses to the trivial base ExceptionScope, so there is no release-observable behavior change. The added RETURN_IF_EXCEPTION calls in defineProperty are strictly safer (early-return on a real pending exception rather than proceeding). No auth, crypto, permissions, or untrusted-input parsing is involved.
Level of scrutiny
Medium-high. While release behavior is unchanged, NAPI_PREAMBLE is expanded at the top of every napi_* entry point, and NapiBoundaryScope is a new bespoke participant in JSC's ExceptionScope/ThrowScope/CatchScope hierarchy. The PR description's reasoning (mirroring JSC's own DECLARE_TOP_EXCEPTION_SCOPE choice at the C API boundary, and why TopExceptionScope itself doesn't fit) is sound and I verified the downstream macro uses (RETURN_IF_EXCEPTION, .assertNoException()) are all base-ExceptionScope methods, so the type swap is compatible. But validating that this is the right design — versus, say, a CatchScope or some other JSC idiom — is a judgment call for someone who owns the JSC bindings.
Other factors
I left four rounds of feedback on this PR (macOS link flags, stderr-in-failure-diff, test.concurrent, async Bun.spawn), all of which the author addressed; CodeRabbit's comments are likewise all resolved. The bug-hunting pass on feee1a6 found nothing. The NAPI_PREAMBLE_NO_THROW_SCOPE macro changed shape from a do { } while(0) statement to a variable declaration, but that's intentional (the boundary scope must outlive any inner ThrowScope) and it's only ever used at function-body start. CI build #66255 is in progress for the head commit.
|
CI note for reviewers: the red lanes on this PR's builds are not related to the diff. On build 73079 (head d839d6a, after the rebase), 283 jobs passed and 3 failed:
Both tests added in this PR, Earlier builds of this PR were red only on the same categories: a buildkite artifact-download timeout that ran zero tests, This change compiles to a no-op in release builds, which is what every CI test lane runs, so it cannot affect those failures. The napi suites (including Node's upstream |
…line Each N-API entry point declared a ThrowScope in its preamble. On destruction a ThrowScope simulates a throw toward its caller, but the caller is addon C code, so the next napi_* call's scope constructor found the unsatisfied check and aborted under BUN_JSC_validateExceptionChecks=1 (any assert-enabled build). The canonical two-line module Init (napi_create_function followed by napi_set_named_property) was enough to trigger it, which meant no native addon could load at all under exception-scope validation. Replace the preamble's ThrowScope with a boundary scope that acknowledges pending checks on entry and exit and never simulates a throw of its own. Validation inside each entry point is unchanged, and a real pending exception is still reported as napi_pending_exception.
…acOS The napi_* symbols are resolved from the host process at dlopen time. GNU ld permits undefined symbols in shared objects by default, but macOS ld64 errors on them, so the compile step failed there. Pass the same flag node-gyp does on Darwin, and stop requiring an empty stderr from the compiler so a benign ld64 warning cannot fail the test.
If the child regresses and aborts, the useful diagnostic is the Unchecked JS exception report on stderr. Including stderr in the compared object (without asserting its contents) surfaces that report in the failure diff instead of only showing an empty stdout and exit code 134.
…roperty An accessor property descriptor with both a getter and a setter creates two NapiClass objects back to back. Each NapiClass::finishCreation opens and closes a throw scope, so JSC's exception-check discipline requires reading the exception state between them; without it, any addon defining such a property (every node-addon-api ObjectWrap accessor) aborts under BUN_JSC_validateExceptionChecks even with the preamble boundary scope in place. Also check after the property name lookup, which can genuinely throw.
feee1a6 to
d839d6a
Compare
There was a problem hiding this comment.
All four of my earlier comments are addressed and I didn't find anything further, but I'm leaving this for a human reviewer: introducing a custom JSC::ExceptionScope subclass and swapping the scope type in all three NAPI_PREAMBLE* macros is a design-level change to how the N-API boundary interacts with JSC's exception-check machinery, and a maintainer should sign off on that choice.
Checked: NAPI_PREAMBLE_NO_THROW_SCOPE losing its do..while wrapper is safe — every call site is a plain top-of-function statement. RETURN_IF_EXCEPTION / assertNoException() on the boundary scope resolve via the ExceptionScope base. The defineProperty accessor branch now checks after each NapiClass::create; the method branch was examined and doesn't need one (single create, existing check follows).
Extended reasoning...
Overview
The PR touches two files: src/jsc/bindings/napi.cpp (introduces NapiBoundaryScope, a JSC::ExceptionScope subclass; rewrites the three NAPI_PREAMBLE* macros to declare it instead of a ThrowScope; adds RETURN_IF_EXCEPTION between the getter/setter NapiClass::create calls in Napi::defineProperty) and a new test file test/napi/napi-exception-check.test.ts that compiles two minimal C addons with the system cc and loads them under BUN_JSC_validateExceptionChecks=1.
Security risks
None identified. The change is to debug-build exception-scope bookkeeping; in release builds ENABLE(EXCEPTION_SCOPE_VERIFICATION) is off and NapiBoundaryScope collapses to the same trivial ExceptionScope wrapper that ThrowScope does, so there is no release codegen or behavior change. No new user input parsing, no auth/crypto/permissions surface.
Level of scrutiny
High. The NAPI_PREAMBLE* macros gate essentially every napi_* entry point, and the fix works by subclassing a JSC internal (ExceptionScope) with custom constructor/destructor semantics that deliberately deviate from ThrowScope (read vm.exception() on entry/exit, never simulateThrow()). The PR description argues this mirrors JSC's own C-API boundary (DECLARE_TOP_EXCEPTION_SCOPE) and explains why TopExceptionScope itself isn't usable. That reasoning looks sound to me, but it is exactly the kind of JSC-internals design call a maintainer familiar with the exception-scope discipline should confirm — including whether assertNoException() on a non-throw scope in NAPI_RETURN_SUCCESS is the intended contract, and whether removing simulateThrow() toward the caller has any downside for the (non-addon) internal callers of these entry points.
Other factors
I left four inline comments over the PR's lifetime (macOS -undefined dynamic_lookup, stderr in the failure diff, test.concurrent, async spawn for the compile step) — all addressed. CodeRabbit's comment-length nits were addressed; its "must fail under USE_SYSTEM_BUN" objection was withdrawn after the author explained the validator is compiled out of release builds. The bug hunter raised and refuted whether the method branch of defineProperty needs the same check (it doesn't; only one NapiClass::create there). I additionally verified that dropping the do..while(0) from NAPI_PREAMBLE_NO_THROW_SCOPE doesn't break any call site (all ~18 uses are plain statements at function-body top level), and that the macros referencing napi_preamble_throw_scope__ (NAPI_RETURN_IF_EXCEPTION, NAPI_RETURN_SUCCESS, etc.) still compile against a NapiBoundaryScope since those members live on the ExceptionScope base. The PR was also rebased over two upstream refactors of the same code, which is another reason a human familiar with those refactors should take a look.
Audited every `[ ASAN ]` entry in `test/expectations.txt` and every entry in `test/no-validate-exceptions.txt` against a release-asan build at 9dc6c37. Each test was run under four configs (bare ASAN / +validateExceptionChecks / +LeakSanitizer / full CI), and every removal candidate was re-verified 3x. ### `test/expectations.txt` (11 `[ ASAN ]` entries removed) No test in this file reproduces an AddressSanitizer heap error anymore. Removed entries: | Test | Was | Now | |---|---|---| | `worker_threads/worker_threads.test.ts` | CRASH (bad free) | bare clean; flaky LSAN leak 1/4 → stays in `no-validate-leaksan.txt` | | `worker_threads/worker_destruction.test.ts` | CRASH (bad free) | bare clean; test-body timeout under `BUN_DESTRUCT_VM_ON_EXIT` → stays in `no-validate-leaksan.txt` | | `node/watch/fs.watch.test.ts` | CRASH (bad free) | bare clean; test-body timeout under `BUN_DESTRUCT_VM_ON_EXIT` → added to `no-validate-leaksan.txt` | | `test-worker-unref-from-message-during-exit.js` | CRASH (use-after-poison) | stable clean 3/3 under full CI config | | `test-fs-watch.js` | CRASH (use-after-poison) | stable clean 5/5 | | `test-fs-watch-recursive-watch-file.js` | CRASH (use-after-poison) | stable clean 5/5 | | `test-fs-promises-watch.js` | CRASH (use-after-poison) | stable clean 5/5 | | `cli/test/parallel.test.ts` | TIMEOUT | stable clean 3/3, ~22s under full config | | `cli/test/isolation.test.ts` | TIMEOUT | stable clean 3/3, ~6s under full config | | `bun/io/bun-write-leak.test.ts` | LEAK | stable clean 3/3, ~2s | | `test-net-error-twice.js` | SKIP (slow write) | stable clean 3/3, ~0.5s | Left in place: the two `transfer-terminate` entries added earlier today (#34686, known ~1/4000 flake); the next-pages / next-auth / napi / inspect / tls-sql / spawn / type-export entries (still fail under at least one config or could not be verified locally); the four `[ LEAK ]` entries that hit their 5s test-body timeout under ASAN. ### `test/no-validate-exceptions.txt` (63 lines removed) 60 entries now pass 3x under `BUN_JSC_validateExceptionChecks=1` on a release-asan build, plus 2 entries for files that no longer exist (`cli/install/bun-repl.test.ts` removed in fa3a30f, `node/test/system-ca/test-native-root-certs.test.mjs`), plus one orphaned section header. The `vendor/elysia/*` entries are left as-is (repo is cloned in CI via `test/vendor.json`, not present locally). ### `test/no-validate-leaksan.txt` Added `test/js/node/watch/fs.watch.test.ts` (test-body timeout under `BUN_DESTRUCT_VM_ON_EXIT`, no sanitizer report). Removed `test-fs-watch.js`, `test-fs-watch-recursive-watch-file.js`, and `test-fs-promises-watch.js` (stable clean 5/5 under LSAN). ### Remaining unchecked-exception sites in Bun code The still-failing entries in `no-validate-exceptions.txt` cluster around these throw → unchecked pairs in `src/jsc/`: | Throw | Unchecked at | Repro | |---|---|---| | `NapiClass.cpp:120` finishCreation | `JSObject::defineOwnNonIndexProperty` | 33× napi tests, `require-cache.test.ts` | | `napi_create_function` napi.cpp:954 | `napi_set_named_property` :598 etc. | addon Init() pattern (#32911) | | `defaultBunSQLObject` BunObject.cpp:319 | itself | `BunObject.test.ts`, `import-meta.test.js`, `resolve.test.ts` | | `JSObject::putInlineSlow` | `Process_functionDlopen` BunProcess.cpp:397 | napi `4_object_factory`, `5_function_factory` | | `jsString` | `jsFunctionWrap` NodeModuleModule.cpp:220 | `node-module-module.test.js` | | `jsSubstring` | `jsFunctionNodeModuleModuleConstructor` NodeModuleModule.cpp:172 | `module-resolve-filename-paths.test.js` | | `isArraySlowInline` | `determineSpecificType` ErrorCode.cpp:348 | `isArray-proxy-crash.test.ts` | | `importModuleInner` NodeVM.cpp:303 | `moduleLoaderImportModuleInner` NodeVM.cpp:1688 | `test-vm-module-referrer-realm.mjs` | | `JSGenericTypedArrayView::create` | `jsPublicKeyObjectPrototype_export` :40 | `node-crypto.test.js` | | `convertDictionaryToJS` JSURLPatternInit.cpp:148 | `convertURLPatternInputToJS` JSURLPatternResult.cpp:89 | `urlpattern.test.ts` | | `normalizeCryptoAlgorithmParameters` SubtleCrypto.cpp:135 | `JSDOMPromiseDeferred::reject` :194/:159 | webcrypto tests | | `evaluateWithScopeExtension` JSInjectedScriptHost.cpp:120 | `...PrototypeFunctionEvaluateWithScopeExtension` :275 | `inspect.test.ts` (WebKit) | | `JSOrderedHashTable::getImpl` | `executeBoundCall` Interpreter.cpp:1223 | next-pages tests (WebKit) |
|
Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-15, it conflicts with main, and its last CI run failed. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main. |
Repro
The canonical two-line
Initthat every N-API tutorial and the node-addon-api template emit is enough:On an assert-enabled build with
BUN_JSC_validateExceptionChecks=1,require()ing that addon aborts at module load:This means no native addon can load at all under exception-scope validation. Bun's CI already runs the validator on the linux x64 ASAN test shard (
scripts/runner.node.mjssetsBUN_JSC_validateExceptionChecks=1for every test file not listed intest/no-validate-exceptions.txt), and the entire N-API section of that list (node-napi-tests/,uv.test.ts,napi-finalizer-delete-ref.test.ts, ...) is a casualty of this: those tests cannot currently run with the validator on. Release builds compileENABLE(EXCEPTION_SCOPE_VERIFICATION)out, so they do not abort, but the underlying discipline violation is real: a pending exception raised insideInit(for example by a getter onexports) can be dropped or surface at an unrelated later point.Cause
Two instances of the same mistake: calling two scope-opening JSC operations on the N-API path with nothing observing the exception state in between.
NAPI_PREAMBLEdeclared aJSC::ThrowScope. AThrowScopedestructor always callssimulateThrow()toward its caller unless the caller is LLInt/JIT, which setsVM::m_needExceptionCheck. JSC's discipline then requires the caller to observeexception()before the next scope is constructed. But the caller of a N-API entry point is the addon's C code, which cannot participate in that discipline, so the very nextnapi_*call's scope constructor finds the unsatisfied check andRELEASE_ASSERTs. Any two back-to-backnapi_*calls from native code trigger it; moduleInitis just the most universal instance.Napi::definePropertycreates twoNapiClassobjects back to back for a property descriptor with both a getter and a setter (NapiClass::createfor the getter, then again for the setter). EachNapiClass::finishCreationopens and closes its ownThrowScope, so the second one's constructor aborts on the first one's simulated throw:That is the shape of every node-addon-api
ObjectWrapaccessor, and it is what was aborting Node's upstreamjs-native-api/6_object_wrapconformance test even with fix 1 applied.Fix
For 1, replace the preamble's
ThrowScopewith aNapiBoundaryScope, a minimalJSC::ExceptionScopesubclass for the C ABI boundary. It readsvm.exception()on entry (acknowledging the simulated throw left by a siblingnapi_*call) and on exit (acknowledging inner scopes'), and never simulates a throw toward its native caller.NAPI_PREAMBLE_NO_THROW_SCOPEnow declares the same boundary scope, covering entry points such asnapi_get_and_clear_last_exceptionthat construct an innerTopExceptionScope(whose constructor verifies) and would otherwise hit the identical abort.This does not weaken validation inside Bun's own code: every
DECLARE_THROW_SCOPEinside a N-API function body still verifies and simulates normally against its own nesting,NAPI_RETURN_SUCCESSstillRELEASE_ASSERTs that no real exception is pending, and a real pending exception is still reported to the addon asnapi_pending_exception. JSC's own C API makes the same choice at its boundary (DECLARE_TOP_EXCEPTION_SCOPEinJSObjectRef.cpp);TopExceptionScopeitself is not usable here because it verifies the pending-check bit in its constructor. In release builds the class collapses to the trivial non-verificationExceptionScope, so there is no behavior or codegen change.For 2, add the missing
RETURN_IF_EXCEPTIONchecks inNapi::defineProperty: after the property-name lookup (which can genuinely throw viatoPropertyKey) and after eachNapiClasscreation in the accessor branch.Verification
test/napi/napi-exception-check.test.tscompiles two addons with the system C compiler against the in-repo N-API headers (no node-gyp, so it is a separate file fromnapi.test.tsand does not depend on that suite's toolchain setup) andrequire()s each in a child process withBUN_JSC_validateExceptionChecks=1:Init(fix 1), andnapi_define_classwith a getter+setter property (fix 2).Without the
src/changes both abort (SIGABRT, exit 134) with theUnchecked JS exceptionreports above; with them, both load and run. With only fix 1 applied, the second test still aborted with thefinishCreationpair, so each test is pinned to its own fix. On release builds the validator is compiled out, so the option is a no-op and the tests still pass.Also ran, with
BUN_JSC_validateExceptionChecks=1against the fixed debug build:test/napi/napi-finalizer-delete-ref.test.tsand Node's upstreamjs-native-api/2_function_arguments,3_callbacks, and6_object_wrapsuites (6_object_wrapaborted before fix 2), plustest/napi/napi.test.tsand thenapi_get_value_string_utf8/napi_define_class/napi_wrapgroups without the validator.Not addressed here
Some
node-napi-testsentries still abort under the validator on a separate, pre-existing unchecked scope in the module-load path, before anynapi_*call runs (for examplejs-native-api/4_object_factory):That is in the
require/process.dlopenpath, not in the N-API entry points, so it is left for a separate change, and the N-API entries intest/no-validate-exceptions.txtare intentionally not removed yet.Rebase note
This branch was rebased over two upstream refactors of the same code and the fix was re-applied into their new shapes:
NAPI_PREAMBLEon main now ends withNAPI_RETURN_IF_EXCEPTION(also checks the env-stashednapi_throw*exception); the rebased preamble keeps that behavior and declares aNapiBoundaryScope. The new third preamble macro,NAPI_PREAMBLE_NO_PENDING_CHECK, received the same boundary-scope conversion since it has the identicalThrowScopepattern.Napi::definePropertywas rewritten on main (now returnsnapi_statusand uses aPropertyDescriptor). The upstream rewrite already checks aftertoPropertyKey, so that part of the original change is subsumed; the remaining back-to-backNapiClass::createcalls in the accessor branch still lacked a check between them, which is re-applied with the new return value.After the rebase, both tests still fail on main's
napi.cpp(with the samenapi_create_function/napi_set_named_propertyandfinishCreation/finishCreationpairs at updated line numbers) and pass with the fix.no test proof · iteration 4 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/napi/napi-exception-check.test.ts