Skip to content

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

Open
robobun wants to merge 2 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 2 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.


no test proof · iteration 20 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/napi/napi.test.ts

@robobun

robobun commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:27 AM PT - Jul 15th, 2026

@robobun, your commit 9540af6 has 2 failures in Build #73199 (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

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
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.
Title check ✅ Passed The title clearly summarizes the main change: Proxy trap exceptions now propagate from napi_get_all_property_names descriptor filtering.
Description check ✅ Passed The description covers the bug, fix, repro, and verification evidence, so it satisfies the template content despite using different headings.

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

@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 Outdated
@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
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.

@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.

@robobun
robobun force-pushed the farm/cac7b2a8/napi-get-all-property-names-exception-check branch from 13321a2 to d673000 Compare June 27, 2026 09:09
@robobun

robobun commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main at d673000604. The only conflict was my Windows CRLF test fix, which main picked up separately in #32722, so that commit was dropped. The diff is now just the napi_get_all_property_names fix and its test (4 files, +92).

Re-verified on the rebased build:

@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 — straightforward exception-propagation fix with a Node-comparison regression test.

Extended reasoning...

Overview

This PR adds NAPI_RETURN_IF_EXCEPTION(env) checks after each potentially-throwing JSC call in the descriptor-filter loop of napi_get_all_property_names (src/jsc/bindings/napi.cpp, ~20 net lines). The prototype walk is restructured from while(!getOwnPropertyDescriptor(...)) into an explicit while(true)/break body so the exception check runs regardless of the return value, and toPropertyKey is hoisted to run once per key. The remaining three files are test scaffolding: a native helper in standalone_tests.cpp, a JS fixture in module.js, and a checkSameOutput test in napi.test.ts that asserts Bun's output matches Node's byte-for-byte.

Security risks

None. The change only adds early-return paths on pending exceptions in an existing N-API function; no new inputs, no auth/crypto/permissions surface, no resource ownership changes.

Level of scrutiny

Low-to-moderate. The production change is mechanical — applying the established NAPI_RETURN_IF_EXCEPTION macro after get, toPropertyKey, getOwnPropertyDescriptor, getPrototype, and push, all of which can throw via Proxy traps. The loop restructuring is semantically equivalent to the original. This fixes a documented segfault (returning napi_ok with a pending exception, then crashing on the next call) and brings behavior in line with Node.

Other factors

  • CI is green on all lanes including x64-asan and Windows; the three reported failures were unrelated (artifact-download timeout, TLS test, OOM).
  • All inline review threads (CodeRabbit's and my prior nit about getPrototypeOf coverage) are resolved; the author gave a sound explanation for why a getPrototypeOf-throwing fixture can't be made engine-stable in a checkSameOutput test, and tightened the fixture comment accordingly.
  • The existing test_object N-API suite (non-throwing filter paths) continues to pass per the rebase verification.

@robobun

robobun commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator Author

CI on d6730006 (build 65420): 277 passed, 1 failed. The single failure is test/cli/install/bun-create.test.ts on darwin 26 aarch64, where a live GET github.com/dylan-conway/create-test returned error: An internal error occurred (HTTPError). That test hits GitHub over the network and is unrelated to this diff.

No napi failures on any lane. The napi_get_all_property_names regression test passed everywhere, including Windows and x64-asan.

The diff is green; the only red is network flake in an install test. This should be ready to merge.

@robobun
robobun force-pushed the farm/cac7b2a8/napi-get-all-property-names-exception-check branch from d673000 to 217657e Compare July 5, 2026 21:46
@robobun

robobun commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main at 217657eea4. The diff is unchanged: same 4 files, +92/-3.

Conflict resolved: test/napi/napi-app/standalone_tests.cpp. #33072 appended three new test functions (test_typedarray_info_byte_offset, test_dataview_info_byte_offset, test_create_arraybuffer_zeroed) at the same position as this PR's test_napi_get_all_property_names_throws, and both sides shared the trailing return ok(env); }. Kept all four functions, closing each one properly. napi.cpp and napi.test.ts auto-merged (#33072 only touched napi_create_arraybuffer, far from the napi_get_all_property_names filter loop).

Re-verified on the rebased build:

  • napi_get_all_property_names regression test passes
  • all three tests from Hardening round 11: input validation, bounds checks, lifetimes #33072 that landed in the conflicting region pass (napi_create_arraybuffer > returns zero-filled memory, napi_get_typedarray_info > reports the view's byte offset... x3)
  • test/napi/node-napi-tests/test/js-native-api/test_object/{test,test_exceptions,test_null}.js all exit 0

@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 fixture comment now matches what the test exercises, and the exception-check additions follow the established NAPI_RETURN_IF_EXCEPTION pattern exactly.

Extended reasoning...

Overview

This PR adds NAPI_RETURN_IF_EXCEPTION(env) after every JSC call that can throw inside the descriptor-filter loop of napi_get_all_property_names (src/jsc/bindings/napi.cpp): exportKeys->get, toPropertyKey, getOwnPropertyDescriptor (both branches), getPrototype, and filteredKeys->push. The prototype walk is restructured from while(!getOwnPropertyDescriptor(...)) into an explicit while(true) body so the exception check fires regardless of the boolean return, and toPropertyKey is hoisted so it is computed and checked once per key. Test coverage is added via a native fixture in standalone_tests.cpp, a JS driver in module.js, and a checkSameOutput assertion in napi.test.ts that compares Bun's status/exception output byte-for-byte against Node for both napi_key_own_only and napi_key_include_prototypes.

Security risks

None. No new allocations, ownership transfers, or user-controlled parsing are introduced; the change is purely additive exception propagation using the existing NAPI_RETURN_IF_EXCEPTION macro. The loop restructure is behavior-preserving apart from the new early returns on pending exceptions.

Level of scrutiny

Low-to-moderate. While napi.cpp is JSC-bindings C++, this specific change is the textbook fix CLAUDE.md prescribes ("Exception checks after every call that can enter JS") and mirrors dozens of identical macro applications elsewhere in the file. The refactor of the while-condition into an explicit body is trivially equivalent. No CODEOWNERS entry covers these files.

Other factors

My prior inline nit (the fixture comment naming getPrototypeOf when only getOwnPropertyDescriptor is exercised) was addressed in commit 217657e — the comment now accurately describes what the fixture does, and the author gave a sound engine-divergence justification for not adding a getPrototypeOf case to a checkSameOutput test. All CodeRabbit threads are resolved. CI has been green on three successive builds (64566, 65420, 68654 pending) across all lanes including Windows and x64-asan, with only unrelated network/OOM flakes. The existing test_object napi suite continues to pass. The duplicate-PR bot flagged #29642/#24985 as overlapping; the description explains this PR is the focused napi fix with a napi-level regression test and covers additional throw sites — which PR to land is a maintainer call, but this one is correct on its own.

@robobun

robobun commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

CI on the rebased 217657ee (build 68654): 284 passed, 2 failed. Both failures are infra, neither touches napi:

  • darwin 26 aarch64buildkite-agent artifact download timed out after 120s for step 'darwin-aarch64-build-bun'. The job never downloaded the binary, so it never ran a test.
  • ubuntu 25.04 x64-baselinetest/js/bun/util/v8-heap-snapshot.test.ts killed by SIGKILL (OOM).

Both are the same two flakes seen on build 64566 before the rebase, on different shards.

The napi_get_all_property_names regression test passed on every lane, including Windows and x64-asan, and appears in zero failure annotations. The three napi tests from #33072 that landed in the region I merged during the rebase also pass.

The diff is green. Ready to merge whenever a maintainer picks it up.

@robobun
robobun force-pushed the farm/cac7b2a8/napi-get-all-property-names-exception-check branch from 217657e to 14c028c Compare July 14, 2026 02:34
@robobun

robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main at 14c028c706. Diff unchanged: same 4 files, +92/-3.

Conflict resolved: test/napi/napi-app/module.js. #34067 appended threadsafe-function orphan/microtask tests at the same position as this PR's test_get_all_property_names_throwing_proxy. Kept both sides. napi.cpp, standalone_tests.cpp, and napi.test.ts auto-merged.

Re-verified on the rebased build:

@robobun

robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

CI on the rebased 14c028c7 (build 72671): 284 passed, 2 failed. Both failures are tracked main-branch breaks, unrelated to this diff:

The napi_get_all_property_names regression test passed on every lane including Windows and x64-asan, and appears in zero failure annotations.

The diff is green. This is ready to merge.

@robobun
robobun force-pushed the farm/cac7b2a8/napi-get-all-property-names-exception-check branch from 14c028c to 962aae2 Compare July 15, 2026 01:56
@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main at 962aae2d0d. Diff is now 4 files, +90/-3.

Conflicts resolved (~13 napi PRs landed since the last base):

Re-verified on the rebased build:

@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 — mechanical application of NAPI_RETURN_IF_EXCEPTION after each throwing JSC call in the descriptor-filter loop, matching the pattern used everywhere else in napi.cpp.

What was reviewed:

  • Verified each new check sits immediately after a call that can enter JS (get, toPropertyKey, getOwnPropertyDescriptor, getPrototype, push), and that the while(true) restructure preserves the original loop's exit conditions.
  • Confirmed toPropertyKey hoisting is behavior-preserving (keys from ownPropertyKeys/collectInheritedPropertyKeys are already property names) and only avoids redundant per-prototype-step conversion.
  • Test uses checkSameOutput against Node for both key modes; CI green on all lanes including x64-asan across four rebases. All prior inline threads resolved.
Extended reasoning...

Overview

This PR adds NAPI_RETURN_IF_EXCEPTION(env) after every JSC call that can throw inside napi_get_all_property_names's descriptor-filter loop in src/jsc/bindings/napi.cpp: exportKeys->get, key.toPropertyKey, getOwnPropertyDescriptor (both branches), getPrototype, and filteredKeys->push. The prototype walk is restructured from while(!getOwnPropertyDescriptor(...)) into an explicit while(true)/break body so the exception check runs regardless of the return value, and toPropertyKey is hoisted so it's computed once per key instead of once per prototype step. Test scaffolding adds a native helper in standalone_tests.cpp, a JS fixture in module.js, and a checkSameOutput test in napi.test.ts.

Security risks

None. This is exception-propagation hygiene in a napi property-enumeration path; no auth, crypto, filesystem, or network involvement. The fix strictly reduces attack surface by turning a segfault-on-next-call (release) / assertNoException() failure (debug) into the correct napi_pending_exception return.

Level of scrutiny

Medium-low. The change is a textbook instance of the CLAUDE.md rule "Exception checks after every call that can enter JS" — it inserts the file's own NAPI_RETURN_IF_EXCEPTION macro (which uses the napi_preamble_throw_scope__ established by NAPI_PREAMBLE at function entry) after each potentially-throwing call, exactly as dozens of neighboring napi functions already do. The only non-trivial part is the loop restructure, which I traced against the original: same entry (current_object = object), same two exit conditions (found → break, !proto → break), same desc population semantics including the pre-existing case where the chain is exhausted without a match (default-constructed PropertyDescriptor, unchanged from before).

Other factors

  • Test quality: checkSameOutput asserts Bun's output is byte-identical to Node's for both napi_key_own_only and napi_key_include_prototypes, plus targeted toContain diagnostics. The existing test_object node-napi suite continues to pass.
  • Review history: CodeRabbit and my own prior inline nit both suggested a getPrototypeOf-throwing case; robobun explained (correctly) that V8 and JSC walk the chain a different number of times, so no proxy shape produces identical output in a checkSameOutput test. The fixture comment was tightened accordingly. All threads resolved.
  • CI: Green on four separate builds across rebases (64566, 65420, 68654, 72671, plus 73114 in flight), including Windows and x64-asan; the only failures were unrelated infra flakes (artifact-download timeout, v8-heap-snapshot OOM, tracked worker-terminate assertion).
  • No CODEOWNERS for this path. Related PRs #29642 and #24985 are acknowledged in the description; this one is the focused napi fix with a napi regression test and covers throw sites those do not.

@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

CI on the rebased 962aae2d (build 73114): 281 passed, 3 failed. All failures are tracked main-branch breaks, unrelated to this diff:

Two darwin shards are still scheduled waiting on agents.

The napi_get_all_property_names regression test passed on every lane including Windows and x64-asan, and appears in zero failure annotations. The sibling napi_get_all_property_names test from #34126 (which shares the describe block after this rebase) also passes.

The diff is green. Ready to merge.

robobun added 2 commits July 15, 2026 06:36
…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 962aae2 to 9540af6 Compare July 15, 2026 06:41
@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main at 9540af6010. Diff unchanged: 4 files, +90/-3.

Conflict resolved: test/napi/napi-app/standalone_tests.cpp. #34147 appended three node_api_* experimental test functions and their registrations at the same spot as this PR's test_napi_get_all_property_names_throws; kept both sides.

src/jsc/bindings/napi.cpp auto-merged even though #34128 also touched the same filter loop (its isAccessorDescriptor() || desc.writable() change sits below this PR's exception checks; both coexist). module.js and napi.test.ts auto-merged.

Re-verified on the rebased build:

@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.

No issues found — the exception checks follow the file's established NAPI_RETURN_IF_EXCEPTION pattern and the loop restructure is behavior-preserving. Deferring because this is native JSC bindings and there are overlapping open PRs (#29642, #24985) a maintainer should reconcile.

What was reviewed:

  • Each new NAPI_RETURN_IF_EXCEPTION sits directly after a call that can enter JS (get, toPropertyKey, getOwnPropertyDescriptor, getPrototype, push); protoValue.getObject() is only reached after the check.
  • The while(true)/break rewrite preserves the non-throwing path; hoisting toPropertyKey is safe since key is loop-invariant in the prototype walk.
  • Test uses checkSameOutput (byte-for-byte Node comparison) for both key modes; the earlier getPrototypeOf-coverage nit was addressed and the fixture comment scoped accordingly.
Extended reasoning...

Overview

Adds NAPI_RETURN_IF_EXCEPTION(env) after every throwing JSC call in napi_get_all_property_names's descriptor-filter loop (src/jsc/bindings/napi.cpp), restructures the prototype walk from while(!getOwnPropertyDescriptor(...)) into an explicit body so the exception check runs regardless of the return value, and hoists toPropertyKey out of the inner loop. Test coverage is a new native helper in standalone_tests.cpp, a JS fixture in module.js, and a checkSameOutput assertion in napi.test.ts.

Security risks

None. This is exception-propagation hardening in a native API; no auth, crypto, permissions, or untrusted-input parsing beyond what already existed.

Level of scrutiny

Native C++ JSC bindings are a critical code path (memory safety, exception-scope discipline). The change itself is mechanical — the same macro appears ~100+ times in this file and is applied here exactly as elsewhere — but per guidelines, native bindings warrant a human sign-off even for mechanical changes. Additionally, two open PRs (#29642, #24985) touch the same loop; a maintainer should decide merge order rather than an automated approval.

Other factors

  • The bug-hunting system found no issues.
  • All prior review feedback (CodeRabbit's two suggestions, my earlier nit on getPrototypeOf coverage) is resolved; the fixture comment was tightened to describe only what it exercises.
  • CI has been green on the napi test across multiple rebases (builds 64566, 65420, 68654, 72671, 73114) including Windows and x64-asan; the existing test_object suite continues to pass.
  • I verified the restructured loop against the current tree: the non-throwing path is identical to the old while(!found) shape, and getObject() on protoValue is only called after the exception check so it never sees the throw sentinel.

@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

CI on the rebased 9540af60 (build 73199): 282 passed, 3 failed, 1 timed out. The failures are the identical set seen on build 73114, both tracked main-branch breaks:

The napi_get_all_property_names regression test passed on every lane including Windows and x64-asan. Ready to merge.

Jarred-Sumner pushed a commit that referenced this pull request Jul 18, 2026
…d String wrapper (#34479)

## What does this PR do?

`napi_get_all_property_names` with `napi_key_own_only` and
`napi_key_writable` / `napi_key_configurable` returned `napi_ok` with an
**empty** key array for Proxy objects and for `new String(...)`
character indices, where Node returns the keys. Follow-up to #34128,
which fixed the abort on the writable filter but left this
over-filtering behind.

### Repro

```js
// addon: napi_get_all_property_names(env, obj, napi_key_own_only, filter, napi_key_keep_numbers, &keys)
const proxy = new Proxy({ a: 1 }, {
  ownKeys: () => ["x", "y"],
  getOwnPropertyDescriptor: () => ({ value: 1, enumerable: true, configurable: true }),
});
apn(proxy, own_only, writable);              // bun: napi_ok, []   node: napi_ok, ["x","y"]
apn(new String("ab"), own_only, writable);   // bun: napi_ok, []   node: napi_ok, [0,1]
apn(new String("ab"), own_only, configurable); // bun: napi_ok, [] node: napi_ok, [0,1]
```

### Cause

Bun's descriptor-filter loop calls `getOwnPropertyDescriptor` per key
and checks `desc.writable()` / `desc.configurable()`. That is the
spec-visible descriptor, but V8's `KeyAccumulator` does not consult
those bits on two paths:

- `FilterProxyKeys` only applies `ONLY_ENUMERABLE`; `ONLY_WRITABLE` /
`ONLY_CONFIGURABLE` are never checked for Proxy keys (with or without
traps).
- `StringWrapperElementsAccessor::CollectElementIndicesImpl` adds every
character index unconditionally, without consulting the
`PropertyFilter`.

So V8/Node pass those keys through where Bun filtered them out.

### Fix

Track the owning object for each key and exempt it from the
writable/configurable predicate when the owner is a `ProxyObject`, or
when the owner is a `StringObject` / derived `StringObject` and the key
is a character index (`index < internalValue()->length()`).
`napi_key_enumerable` is still applied everywhere (V8 does filter
proxies by enumerable), and ordinary properties (plain objects, arrays,
frozen objects, a String wrapper's `length`, extra own properties on a
String wrapper) are still filtered by the real descriptor.

### Verification

New `test_get_all_property_names_proxy_and_string_wrapper` fixture in
`test/napi/napi-app/module.js`, compared against Node via
`checkSameOutput`. Fails on `main` (proxy/String cases return `[]`),
passes with this change. The existing `napi_get_all_property_names`
tests (own_only skip_strings/skip_symbols, accessor filtering,
cache-poisoning) continue to pass.

Note: #32263 touches the same loop to add exception propagation for
throwing Proxy traps; the two changes are complementary and will need a
small merge resolve whichever lands second.

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 0 · Platform-specific test(s) that do not
run on this machine. Deferring to CI, which covers all platforms:
test/napi/napi.test.ts

<!-- robobun:evidence:end -->
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