Skip to content

napi: propagate Proxy trap exceptions from napi_get_all_property_names descriptor filter#32263

Open
robobun wants to merge 3 commits into
mainfrom
farm/cac7b2a8/napi-get-all-property-names-exception-check
Open

napi: propagate Proxy trap exceptions from napi_get_all_property_names descriptor filter#32263
robobun wants to merge 3 commits into
mainfrom
farm/cac7b2a8/napi-get-all-property-names-exception-check

Conversation

@robobun

@robobun robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

napi_get_all_property_names with a descriptor-based key_filter (napi_key_enumerable / napi_key_writable / napi_key_configurable) iterates the collected keys and calls getOwnPropertyDescriptor / getPrototype on the target to filter by attribute. When the target is a Proxy whose getOwnPropertyDescriptor or getPrototypeOf trap throws, the loop kept iterating with a pending exception and then hit NAPI_RETURN_SUCCESS, which calls assertNoException().

Repro

const proxy = new Proxy({}, {
  ownKeys() { return ['a', 'b']; },
  getOwnPropertyDescriptor() { throw new Error('trap threw'); },
});
// native addon:
napi_get_all_property_names(env, proxy, napi_key_own_only,
                            napi_key_enumerable, napi_key_keep_numbers, &out);

Node.js returns napi_pending_exception. Bun returned napi_ok with the exception still pending (release) and then segfaulted at address 0x5 on the next call with napi_key_include_prototypes:

napi_get_all_property_names(mode=1) status -> 0
napi_is_exception_pending -> true
...
panic(main thread): Segmentation fault at address 0x5

Fix

Add NAPI_RETURN_IF_EXCEPTION(env) after every call in the filter loop that can throw: exportKeys->get, toPropertyKey, getOwnPropertyDescriptor, getPrototype, and filteredKeys->push. The prototype walk is restructured from a while(!getOwnPropertyDescriptor(...)) condition into an explicit body so the exception check runs regardless of the return value, and toPropertyKey is hoisted out of the loop so it is computed (and checked) once per key.

Related: #29642 also touches this loop as part of a broader Bun.inspect fix; this PR is the focused napi change with a napi regression test and covers the additional throw sites that PR does not (get / toPropertyKey / push, and the case where getOwnPropertyDescriptor throws after returning true from the while condition).

Test

test/napi/napi.test.tsnapi_get_all_property_names > returns napi_pending_exception when a Proxy trap throws during descriptor filtering compares Bun against Node for both napi_key_own_only and napi_key_include_prototypes. The existing test/napi/node-napi-tests/test/js-native-api/test_object/ suite (which exercises the non-throwing filter paths) continues to pass.

@robobun

robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 8:11 PM PT - Jun 24th, 2026

@robobun, your commit 13321a2 has 2 failures in Build #64566 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32263

That installs a local version of the PR into your bun-32263 executable, so you can run:

bun-32263 --bun

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fcdc5d4c-a7ab-40d4-ab22-9171724b506c

📥 Commits

Reviewing files that changed from the base of the PR and between b528f53 and e715a66.

📒 Files selected for processing (2)
  • test/napi/napi-app/module.js
  • test/napi/napi-app/standalone_tests.cpp

Walkthrough

napi_get_all_property_names gains exception-safety guards in its descriptor-filtering loop: a single propertyKey is computed per key, the prototype-chain traversal is rewritten as a while(true)/break loop, and NAPI_RETURN_IF_EXCEPTION checks are added after each JSC call and before writing to the output array. A new native test function and JS/TS test cases verify the napi_pending_exception behavior when a Proxy trap throws.

Changes

Proxy trap exception handling in napi_get_all_property_names

Layer / File(s) Summary
Exception guards in descriptor-filtering loop
src/jsc/bindings/napi.cpp
Computes propertyKey once per key, refactors prototype-chain traversal to while(true)/break, and inserts NAPI_RETURN_IF_EXCEPTION after key conversion, after descriptor/prototype retrieval, and before pushing to the output array.
Native test function and registration
test/napi/napi-app/standalone_tests.cpp
Adds test_napi_get_all_property_names_throws, which invokes napi_get_all_property_names with a caller-supplied mode, then prints the returned status and any pending exception message. Registers the function via REGISTER_FUNCTION.
JS module test and TypeScript assertions
test/napi/napi-app/module.js, test/napi/napi.test.ts
JS test builds a Proxy whose getOwnPropertyDescriptor trap throws and calls the native test for both own-only and include-prototypes modes. TypeScript suite asserts napi_pending_exception (status 10) and the expected exception message substring for each mode.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: propagating Proxy trap exceptions from the napi_get_all_property_names descriptor filter logic.
Description check ✅ Passed The description is comprehensive, including what the PR does, how it was verified, detailed reproduction case, and the fix strategy with test coverage information.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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/napi-app/module.js`:
- Around line 962-985: The test_get_all_property_names_throwing_proxy function
currently only exercises the path where getOwnPropertyDescriptor throws, but the
PR fix adds exception checks after both getOwnPropertyDescriptor and
getPrototypeOf calls. Add a second Proxy test case named throwingPrototype that
has a working ownKeys and getOwnPropertyDescriptor but throws in the
getPrototypeOf trap to verify the prototype chain walking path is also fixed.
Add two test calls for this new throwingPrototype proxy (one with key filter
mode 1 for napi_key_own_only and one with mode 0 for
napi_key_include_prototypes) to ensure both property name collection modes
properly handle getPrototypeOf exceptions.

In `@test/napi/napi.test.ts`:
- Around line 931-938: In the test "returns napi_pending_exception when a Proxy
trap throws during descriptor filtering" within the describe block
"napi_get_all_property_names", verify whether the corresponding module.js test
fixture has been expanded to include a getPrototypeOf throwing case. If it has
been added, include an additional expect assertion to verify the exception
message for the getPrototypeOf trap throwing scenario. Additionally, replace the
individual expect calls checking for status lines with a more robust assertion
that filters the output for all status lines and verifies the exact count
matches the number of test cases being run, to catch unexpected output changes
and ensure the test remains maintainable as more cases are added.
🪄 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: 6591cd5a-da10-47e3-a289-ee6fa9fb7cc8

📥 Commits

Reviewing files that changed from the base of the PR and between e0acad3 and b528f53.

📒 Files selected for processing (4)
  • src/jsc/bindings/napi.cpp
  • test/napi/napi-app/module.js
  • test/napi/napi-app/standalone_tests.cpp
  • test/napi/napi.test.ts

Comment thread test/napi/napi-app/module.js
Comment thread test/napi/napi.test.ts
@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. fix(inspect): don't crash when a Proxy in the prototype chain throws #29642 - Also adds NAPI_RETURN_IF_EXCEPTION checks in the same napi_get_all_property_names descriptor filter loop (after getOwnPropertyDescriptor, getPrototype, and toPropertyKey), plus fixes forEachProperty in bindings.cpp
  2. getPrototype exception checks #24985 - Adds NAPI_RETURN_IF_EXCEPTION after getPrototype in the same prototype-climbing while loop inside napi_get_all_property_names, as part of a broader getPrototype exception-check sweep

🤖 Generated with Claude Code

Comment thread test/napi/napi-app/module.js

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — thanks for clarifying the fixture comment; the NAPI_RETURN_IF_EXCEPTION insertions and loop restructuring look correct.

Extended reasoning...

Overview

This PR fixes napi_get_all_property_names to propagate exceptions thrown by Proxy traps (getOwnPropertyDescriptor / getPrototypeOf) during the descriptor-filter loop, instead of continuing with a pending exception and hitting assertNoException() (segfault in release). The core change in src/jsc/bindings/napi.cpp (~15 lines) inserts NAPI_RETURN_IF_EXCEPTION(env) after each potentially-throwing JSC call (exportKeys->get, toPropertyKey, getOwnPropertyDescriptor, getPrototype, filteredKeys->push), hoists toPropertyKey out of the inner loop, and restructures the prototype walk from while(!getOwnPropertyDescriptor(...)) into an explicit while(true)/break body so the exception check runs regardless of the return value. The remaining three files add a native test helper, a JS fixture, and a checkSameOutput regression test comparing Bun against Node for both napi_key_own_only and napi_key_include_prototypes.

Security risks

None. This is a defensive correctness fix in the N-API bindings — it adds early-return-on-exception checks, which can only narrow behavior toward Node compatibility. No auth, crypto, permissions, filesystem, or network surface is touched.

Level of scrutiny

Low-to-medium. The NAPI_RETURN_IF_EXCEPTION macro is the established idiom throughout napi.cpp for exactly this purpose, and each insertion follows a call that is documented to invoke user-observable traps. The loop rewrite is a mechanical equivalence transform (while(!X){Y}while(true){x=X; check; if(x)break; Y; check;}). The non-throwing filter paths remain covered by the existing test/napi/node-napi-tests/test/js-native-api/test_object/ suite per the PR description.

Other factors

  • The bug-hunting system found no issues.
  • All inline review threads are resolved: CodeRabbit withdrew both suggestions, and my earlier nit about getPrototypeOf coverage was addressed — the author explained convincingly that a checkSameOutput test for that branch is infeasible because V8 and JSC walk the prototype chain a different number of times, and pushed d5a8dcb to scope the fixture comment to what it actually exercises.
  • The duplicate-PR bot noted overlap with #29642 / #24985; the PR description already explains this is the focused napi-only fix with a regression test and covers additional throw sites those PRs do not. That's a merge-ordering concern for maintainers, not a correctness concern for this diff.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the comment clarification addresses my earlier nit, and the exception guards follow the established pattern in this file.

Extended reasoning...

Overview

This PR adds NAPI_RETURN_IF_EXCEPTION(env) guards after each potentially-throwing JSC call inside the descriptor-filter loop of napi_get_all_property_names in src/jsc/bindings/napi.cpp (~20 lines net), restructures the prototype-walk while condition into an explicit body so the exception check runs unconditionally, and hoists toPropertyKey out of the inner loop. It adds a native test helper in standalone_tests.cpp, a JS fixture in module.js, and a checkSameOutput test in napi.test.ts that asserts byte-identical output against Node.js for both napi_key_own_only and napi_key_include_prototypes.

Security risks

None. This is defensive exception-propagation in a property-enumeration path; no auth, crypto, permissions, or untrusted-input parsing is involved. The change strictly narrows behavior (early-return on pending exception) rather than adding new code paths.

Level of scrutiny

Low-to-moderate. While napi.cpp is core runtime code, the change is mechanical: it inserts the same NAPI_RETURN_IF_EXCEPTION macro already used at line 1832 (immediately above this loop) after each call that can invoke a Proxy trap. The while(true)/break restructuring is a transparent semantic equivalent of the original while(!getOwnPropertyDescriptor(...)) condition. The fix replaces a segfault / assertNoException() violation with the Node-matching napi_pending_exception return.

Other factors

  • The bug hunting system found no issues.
  • My earlier nit (test coverage for the getPrototype check) was addressed: the author explained why a checkSameOutput test cannot deterministically reach that line across V8/JSC, and updated the fixture comment in commit d5a8dcb to accurately describe only what it exercises. Both CodeRabbit threads were similarly resolved and withdrawn.
  • The PR description explicitly addresses overlap with #29642 and #24985, noting this is the focused napi-only fix with additional throw-site coverage and a dedicated regression test.
  • No CODEOWNERS entry covers these paths.

robobun added 2 commits June 25, 2026 01:05
…s descriptor filter

When napi_get_all_property_names is called with napi_key_enumerable,
napi_key_writable, or napi_key_configurable, it iterates the collected
keys and calls getOwnPropertyDescriptor/getPrototype on the target to
filter by descriptor attributes. If the target is a Proxy whose
getOwnPropertyDescriptor or getPrototypeOf trap throws, the loop kept
iterating with a pending exception and then NAPI_RETURN_SUCCESS would
assert (debug) or return napi_ok with the exception still pending
(release). In release builds the next iteration would dereference an
empty JSValue from exportKeys->get and segfault.

Add NAPI_RETURN_IF_EXCEPTION after every call in the filter loop that
can run user JS (get, toPropertyKey, getOwnPropertyDescriptor,
getPrototype, push) and restructure the prototype walk so the
exception check runs regardless of getOwnPropertyDescriptor's return
value.
The fixture only exercises getOwnPropertyDescriptor throwing; V8 and JSC
walk the prototype chain a different number of times so a getPrototypeOf
case cannot be expressed as a checkSameOutput test.
@robobun robobun force-pushed the farm/cac7b2a8/napi-get-all-property-names-exception-check branch from e715a66 to 2d35d72 Compare June 25, 2026 01:14

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — comment clarification addresses my earlier note; the exception checks are mechanical and follow the existing NAPI_RETURN_IF_EXCEPTION pattern.

Extended reasoning...

Overview

This PR adds exception-propagation guards to the descriptor-filter loop in napi_get_all_property_names (src/jsc/bindings/napi.cpp). It inserts NAPI_RETURN_IF_EXCEPTION(env) after each JSC call that can throw (exportKeys->get, toPropertyKey, getOwnPropertyDescriptor, getPrototype, filteredKeys->push), restructures the prototype-walk while(!getOwnPropertyDescriptor(...)) into an explicit while(true)/break body so the exception check runs regardless of the boolean result, and hoists toPropertyKey to once per key. A native test helper, JS fixture, and checkSameOutput test are added covering both napi_key_own_only and napi_key_include_prototypes with a Proxy whose getOwnPropertyDescriptor trap throws.

Security risks

None. This is pure exception-safety plumbing in the NAPI binding layer — it changes a crash / napi_ok-with-pending-exception path into the correct napi_pending_exception return. No new data flow, no auth/crypto/permission surface.

Level of scrutiny

Low–medium. The production change is ~30 lines, entirely mechanical, and applies the same NAPI_RETURN_IF_EXCEPTION macro used throughout napi.cpp. The restructured loop is semantically identical on the non-throwing path (protoValue.getObject() returns nullptr for non-objects including null, preserving the original break). The existing test_object Node-API suite covers the non-throwing filter paths, and the new checkSameOutput test pins behavior to Node for the throwing path.

Other factors

  • No bugs found by the bug-hunting system.
  • All prior review threads (CodeRabbit ×2, my earlier nit on getPrototypeOf coverage) are resolved; the author explained why a cross-engine checkSameOutput test for the inner getPrototype check is not feasible and pushed 2d35d72 to align the fixture comment with what the test actually exercises.
  • No CODEOWNERS entry for src/jsc/bindings/napi.cpp.
  • The PR description notes overlap with #29642 / #24985 and explains why this is the focused, more complete change with a regression test.

The native helper writes via printf, which emits CRLF line endings on
Windows. Splitting on a bare LF left trailing CR on each line and
failed the toEqual check. checkSameOutput already verifies Bun and
Node produce identical bytes; this only fixes the supplementary
per-line assertion. From #32629.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — focused exception-propagation fix following the established NAPI_RETURN_IF_EXCEPTION pattern, with a Node-comparison regression test.

Extended reasoning...

Overview

This PR adds NAPI_RETURN_IF_EXCEPTION(env) checks after each JSC call that can throw inside the descriptor-filter loop of napi_get_all_property_names (src/jsc/bindings/napi.cpp:1838-1881): exportKeys->get, toPropertyKey, getOwnPropertyDescriptor, getPrototype, and filteredKeys->push. The prototype walk is restructured from while(!getOwnPropertyDescriptor(...)) into an explicit while(true) body so the exception check runs regardless of the boolean return. toPropertyKey is hoisted to compute once per key. A new native test helper plus JS/TS fixtures verify status napi_pending_exception (10) for both napi_key_own_only and napi_key_include_prototypes against Node via checkSameOutput. An incidental split(/\r?\n/) fix is included for the adjacent napi_is_arraybuffer test on Windows.

Security risks

None. This is purely additive exception-check plumbing in an existing N-API entry point; no new inputs, no auth/crypto/permissions surface, no data exposure paths. The only behavioral change is returning napi_pending_exception instead of napi_ok (and avoiding a segfault) when a Proxy trap throws — strictly a correctness/safety improvement.

Level of scrutiny

Low-to-moderate. The change is mechanical: every added line is the established NAPI_RETURN_IF_EXCEPTION macro applied immediately after a call that can run JS (Proxy traps). The restructured loop is semantically equivalent to the original modulo the new exception checks. No CODEOWNERS paths are touched. The regression test is byte-for-byte compared against Node, so divergent behavior would be caught in CI.

Other factors

I previously left a nit about getPrototypeOf coverage; the author explained convincingly that V8 and JSC walk the prototype chain a different number of times, making a checkSameOutput-style test for that branch inherently engine-divergent, and adjusted the fixture comment to accurately describe what it covers. CodeRabbit raised and withdrew the same point. All inline threads are resolved, the bug-hunting system found nothing, and the existing test_object N-API suite continues to exercise the non-throwing paths.

@robobun

robobun commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator Author

CI status for 13321a2 (build 64566): 277 jobs passed, 3 failed, all unrelated to this diff.

  • darwin: 26 aarch64 - test-bun: buildkite-agent artifact download timed out after 120s (never reached tests)
  • darwin: 14 aarch64 - test-bun: test/js/node/test/parallel/test-tls-client-destroy-soon.js byte-count assertion (TLS, unrelated)
  • ubuntu: 25.04 x64 - test-bun: test/js/bun/util/v8-heap-snapshot.test.ts killed by SIGKILL (OOM, unrelated)

The new napi_get_all_property_names test passed on every lane including Windows and x64-asan. The napi_is_arraybuffer Windows CRLF fix (13321a2) resolved the only napi-related failure seen in the previous build. The existing test/napi/node-napi-tests/test/js-native-api/test_object/ suite passes. Ready for review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant