Skip to content

fix null deref in forEachProperty prototype walk#28991

Closed
robobun wants to merge 1 commit into
mainfrom
farm/094b242e/fix-foreach-proto-empty-jsvalue
Closed

fix null deref in forEachProperty prototype walk#28991
robobun wants to merge 1 commit into
mainfrom
farm/094b242e/fix-foreach-proto-empty-jsvalue

Conversation

@robobun

@robobun robobun commented Apr 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes a crash found by Fuzzilli (fingerprint 6b65dd404623d6cd).

The slow path of JSC__JSValue__forEachPropertyImpl called .getObject() directly on the return value of iterating->getPrototype():

iterating = iterating->getPrototype(globalObject).getObject();

If getPrototype() returns an empty JSValue (e.g. because a Proxy getPrototypeOf trap threw), getObject() crashes: an empty JSValue is all zeros, so isCell() returns true while asCell() returns nullptr, which UBSAN/ASAN catches as a member call on a null JSCell.

Use the same pattern the fast path (lines 5121–5131) already uses: check truthiness of the returned JSValue and clear any pending exception from the Proxy trap.

Repro

const v = Bun.jest().expect();
Object.setPrototypeOf(v, new Proxy(Object.getPrototypeOf(v), {}));
v.toBe(v);

Before the fix this segfaults while formatting v for the toBe error message. After, it throws a normal expect(...).toBe(...) failure.

Regression test added in test/js/bun/util/inspect.test.js.

@github-actions github-actions Bot added the claude label Apr 8, 2026
@robobun

robobun commented Apr 8, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 6:51 AM PT - Apr 8th, 2026

❌ Your commit 1e11be9f has 4 failures in Build #44433 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 28991

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

bun-28991 --bun

@github-actions

github-actions Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. ECONNREFUSED on localhost WebSocket Server in WSL2 #9110 - ECONNREFUSED on localhost WebSocket in WSL2 may be caused by c-ares failing to resolve "localhost" to the correct loopback address; this PR's special-casing of localhost to return both 127.0.0.1 and ::1 directly could fix it

If this is helpful, consider adding Fixes #9110 to the PR description to auto-close the issue on merge.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Prototype-chain iteration in the C++ bindings was made null-safe and clears pending exceptions when advancing to an object's prototype. A regression test was added that spawns a child Bun process, sets an object's prototype to a Proxy, calls a matcher and Bun.inspect, and verifies the child exits normally.

Changes

Cohort / File(s) Summary
Prototype Chain Iteration
src/bun.js/bindings/bindings.cpp
Refactored JSC__JSValue__forEachPropertyImpl to store getPrototype into a temporary JSValue proto, call CLEAR_IF_EXCEPTION(scope), and advance iterating only when proto is an object, making prototype traversal null-safe and preventing getPrototype exceptions from persisting across iterations.
Regression Test & Harness Import
test/js/bun/util/inspect.test.js
Extended harness imports (bunEnv, bunExe, writeFileSync). Added a regression test that writes a child script, spawns Bun via bunExe() with bunEnv, replaces an object's prototype with a Proxy, runs a matcher and Bun.inspect, and asserts the child exited without signal and stderr contains no crash indicators.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main fix: addressing a null dereference issue in the prototype chain iteration logic of forEachProperty.
Description check ✅ Passed The description includes the 'What does this PR do?' section with detailed explanation of the bug and fix, but lacks explicit 'How did you verify your code works?' section, though verification is mentioned through repro case and regression test.

✏️ 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.

@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 — minimal, correct null-check matching the existing fast-path pattern, with regression test.

Extended reasoning...

Overview

This PR fixes a null dereference crash in the slow path of JSC__JSValue__forEachPropertyImpl in src/bun.js/bindings/bindings.cpp. A two-file change: one C++ bug fix (3 lines changed) and one regression test added to test/js/bun/util/inspect.test.js.

Security risks

None. This is a crash fix that converts a segfault/UBSAN abort into a graceful exception. No auth, crypto, or permission paths are involved.

Level of scrutiny

Low — the fix is a direct, mechanical application of the pattern already used on the fast path (lines 5121–5131). The guard proto ? proto.getObject() : nullptr plus CLEAR_IF_EXCEPTION(scope) is identical in structure and intent to the surrounding code. The regression test reproduces the exact Fuzzilli-found crash.

Other factors

No prior reviews exist on this PR. The change is self-contained, well-described, and has a clear before/after explanation. The test adequately exercises the three call sites (Bun.inspect, String(), and the toBe error formatting path) where the crash was observed.

@robobun robobun force-pushed the farm/094b242e/fix-foreach-proto-empty-jsvalue branch 2 times, most recently from f21bd66 to 7965927 Compare April 8, 2026 08:55
Comment on lines 5364 to 5372
break;
if (iterating == globalObject)
break;
iterating = iterating->getPrototype(globalObject).getObject();
JSValue proto = iterating->getPrototype(globalObject);
CLEAR_IF_EXCEPTION(scope);
iterating = proto ? proto.getObject() : nullptr;
}
}

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.

🔴 The PR correctly fixes the slow-path loop (line 5368) and the fast-path loop (line 5257), but the fast-path setup block at lines 5166–5178 has an identical unguarded getPrototype() call. When an object has no own properties (outOfLineSize==0 && inlineSize==0), it sets fast=false and calls object->getPrototype(globalObject) — if a Proxy getPrototypeOf trap throws, the empty JSValue is returned, the if(proto) block is skipped, and the pending exception is never cleared. Execution falls through to the slow path where RETURN_IF_EXCEPTION at line 5276 fires immediately, silently skipping all prototype-chain enumeration. Fix: add CLEAR_IF_EXCEPTION(scope); after line 5176 (closing brace of the if(proto) block in the fast-path setup).

Extended reasoning...

The Bug

The PR addresses a null-dereference crash in JSC__JSValue__forEachPropertyImpl when a Proxy getPrototypeOf trap throws during prototype-chain walking. It correctly patches two sites: the slow-path loop at lines 5367–5369 (the primary crash site) and the fast-path loop at lines 5247–5257 (which already had CLEAR_IF_EXCEPTION). However, there is a third getPrototype call at line 5170 — the fast-path setup block — that still lacks the guard.

The Code Path

Lines 5166–5178 handle the case where the object has a fast structure but zero own properties (outOfLineSize==0 && inlineSize==0). In that case:

  1. fast is set to false (line 5168)
  2. object->getPrototype(globalObject) is called (line 5170)
  3. If the result is truthy, the prototype's structure is examined and fast may be set back to true
  4. If getPrototype throws (e.g., via a Proxy trap), it returns an empty JSValue; the if(proto) body is never entered — but no CLEAR_IF_EXCEPTION follows at line 5176

Why Existing Code Doesn't Catch It

After the fast-path setup block exits at line 5178, fast is false (set at 5168 and never restored), so the if(fast) block at line 5184 is entirely skipped. Execution continues to the slow path at line 5263. The very first RETURN_IF_EXCEPTION(scope, void()) in the slow path (line 5276) sees the stale exception from the Proxy trap and returns the function immediately — before any properties of the original object or its prototype chain are enumerated.

Concrete Proof

Suppose obj has a fast structure, zero own properties, and its prototype is a Proxy whose getPrototypeOf trap throws TypeError: boom:

  • Line 5167: structure->outOfLineSize()==0 && structure->inlineSize()==0 → true
  • Line 5168: fast = false
  • Line 5170: getPrototype(globalObject) → Proxy trap fires, throws, returns empty JSValue
  • Line 5170: if(proto) → false, block skipped, exception left pending in scope
  • Line 5178: exits fast-path setup; fast is still false
  • Line 5184: if(fast) → false, skipped entirely
  • Line 5263: enters slow path; iterating = prototypeObject.getObject() = obj
  • Line 5276: RETURN_IF_EXCEPTION(scope, void()) → fires on the stale Proxy exception → function returns, zero properties reported

This means any object with no own properties whose prototype is a throwing Proxy will appear completely empty to forEachProperty, rather than correctly reporting the error or walking the chain.

Impact

The consequence is silent data loss during property enumeration: properties that exist on the prototype chain are simply not reported, with no error surfaced to the caller. In the formatting context (Bun.inspect, String()), this produces silently truncated output rather than a crash — harder to detect than the original null-dereference but still incorrect behavior.

Fix

Add CLEAR_IF_EXCEPTION(scope); after line 5176 (the closing brace of if(proto)), exactly matching the pattern at lines 5247–5257 (fast-path loop) and lines 5367–5369 (PR fix for slow-path loop). A comment like // Ignore exceptions from Proxy getPrototype trap. would match the existing style.

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.

The fast-path setup block at lines 5166–5178 still has the unguarded getPrototype() call without a following CLEAR_IF_EXCEPTION. The PR only fixed the slow-path loop. Line 5170 calls object->getPrototype(globalObject) — if a Proxy trap throws, the empty JSValue is returned, if(proto) is skipped, and the pending exception stays in the scope. When fast remains false and execution falls through to the slow path, RETURN_IF_EXCEPTION fires immediately, silently skipping all enumeration.

Fix: add CLEAR_IF_EXCEPTION(scope); after line 5176 (closing brace of the if(proto) block):

            if (JSValue proto = object->getPrototype(globalObject)) {
                if ((structure = proto.structureOrNull())) {
                    prototypeObject = proto;
                    fast = canPerformFastPropertyEnumerationForIterationBun(structure);
                    prototypeCount = 1;
                }
            }
            CLEAR_IF_EXCEPTION(scope); // <-- add this

Comment on lines +5367 to +5369
JSValue proto = iterating->getPrototype(globalObject);
CLEAR_IF_EXCEPTION(scope);
iterating = proto ? proto.getObject() : nullptr;

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.

🟣 The same unsafe getPrototype().getObject() pattern this PR fixes in forEachPropertyImpl also exists verbatim at napi.cpp:1837 inside napi_get_all_property_names(). If a Proxy getPrototypeOf trap throws, getPrototype() returns an empty JSValue (all zeros), and calling .getObject() on it dereferences a null cell pointer — the \!proto check at line 1838 never executes because the crash happens inside .getObject() before the result is stored. This is a pre-existing bug of the exact same class, but it was not addressed by this PR.

Extended reasoning...

What the bug is and how it manifests

In napi_get_all_property_names() (napi.cpp:1836–1841), when key_mode == napi_key_include_prototypes and descriptor filtering is requested, the function walks the prototype chain of each key to find the owning object's property descriptor:

JSObject* proto = current_object->getPrototype(globalObject).getObject();
if (\!proto) {
    break;
}

This is the exact pattern this PR fixes in bindings.cpp. If getPrototype() encounters a Proxy whose getPrototypeOf trap throws, it returns an empty JSValue (all-zero bit pattern). Calling .getObject() on that empty value calls isObject(), which calls isCell() (returns true because all-zero encodes as a cell pointer) then asCell()->isObject() — but asCell() returns nullptr, so this is a null pointer dereference and crashes under ASAN/UBSAN.

The specific code path

napi.cpp:1836–1841 (inside the key_filter & filter_by_any_descriptor branch of napi_get_all_property_names): the while loop calls getPrototype(globalObject) and immediately chains .getObject() without first checking the returned JSValue for truthiness or clearing any pending exception.

Why existing guards don't help

NAPI_PREAMBLE declares a throw scope but the comment in that macro explicitly states: "You should not use this throw scope directly". There is no CLEAR_IF_EXCEPTION call in this loop. The if (\!proto) { break; } guard at line 1838 never executes because the dereference crash occurs inside .getObject() before the result is assigned to proto.

Impact

Any native addon calling napi_get_all_property_names(env, obj, napi_key_include_prototypes, napi_key_enumerable|...) on an object whose prototype chain contains a Proxy with a throwing getPrototypeOf trap will crash Bun. This is a hard segfault / UBSAN null-member-call, not a catchable JS error.

How to fix

Apply the same pattern this PR uses in bindings.cpp:

JSValue protoValue = current_object->getPrototype(globalObject);
CLEAR_IF_EXCEPTION(scope); // use a local throw scope, not the preamble scope
if (\!protoValue) break;
JSObject* proto = protoValue.getObject();
if (\!proto) break;
current_object = proto;

Step-by-step proof

  1. Native addon calls napi_get_all_property_names(env, obj, napi_key_include_prototypes, napi_key_enumerable, napi_key_numbers_to_strings, &result).
  2. obj has been constructed so that Object.getPrototypeOf(obj) returns a Proxy whose getPrototypeOf trap is () => { throw new Error('trap\!'); }.
  3. allPropertyKeys succeeds (it has its own prototype walk; the trap may or may not fire there).
  4. Because key_filter & napi_key_enumerable is truthy, execution enters the descriptor-filtering branch (line 1827).
  5. For each key, the while loop on line 1836 fires. current_object->getOwnPropertyDescriptor returns false (the key is inherited).
  6. Line 1837: current_object->getPrototype(globalObject) fires the Proxy trap, which throws. JSC returns an empty JSValue (all zeros) and sets a pending exception.
  7. .getObject() is immediately called on the empty JSValue: isObject()isCell() returns true (zero bits) → asCell() returns nullptrnullptr->isObject()null dereference, process crash.
  8. The if (\!proto) guard at line 1838 is never reached.

Comment thread test/js/bun/util/inspect.test.js Outdated
forEachPropertyImpl's slow path called .getObject() directly on the
return value of iterating->getPrototype(), which crashes when the
call returns an empty JSValue (isCell() returns true, asCell() is
null). Use the same pattern as the fast path: check truthiness and
clear pending exceptions from the Proxy getPrototypeOf trap.
@robobun robobun force-pushed the farm/094b242e/fix-foreach-proto-empty-jsvalue branch from 7965927 to 1e11be9 Compare April 8, 2026 09:31

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/js/bun/util/inspect.test.js`:
- Around line 767-771: Add an assertion to verify the child process exit code is
0: after the existing checks on result.signalCode and stderr, assert that
result.exitCode === 0 so the test verifies the process completed successfully
(the code path where Bun.inspect(v) runs and the thrown toBe error is caught).
Locate the assertions around the variable result in the test (the block with
result.stderr, result.signalCode) and add the exitCode check there to follow the
spawn-process coding guideline.
🪄 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: 2b24eda9-8bee-43a6-be8a-d2224eff4dc1

📥 Commits

Reviewing files that changed from the base of the PR and between 7965927ecd2e0cf9c2cbf471153c3f558c405873 and 1e11be9.

📒 Files selected for processing (2)
  • src/bun.js/bindings/bindings.cpp
  • test/js/bun/util/inspect.test.js

Comment on lines +767 to +771
const stderr = result.stderr?.toString() ?? "";
expect(result.signalCode).toBeUndefined();
expect(stderr).not.toContain("Segmentation fault");
expect(stderr).not.toContain("runtime error");
expect(stderr).not.toContain("panic");

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.

🧹 Nitpick | 🔵 Trivial

Add exitCode check per coding guidelines.

The test correctly checks signalCode and stderr for crash indicators, but per coding guidelines, exit codes should also be checked when spawning processes. Since the script catches the toBe error and Bun.inspect(v) should complete successfully, the process should exit with code 0.

Suggested fix
   const stderr = result.stderr?.toString() ?? "";
+  expect(result.exitCode).toBe(0);
   expect(result.signalCode).toBeUndefined();
   expect(stderr).not.toContain("Segmentation fault");
   expect(stderr).not.toContain("runtime error");
   expect(stderr).not.toContain("panic");

As per coding guidelines: "Always check exit codes and test error scenarios when spawning processes in tests."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const stderr = result.stderr?.toString() ?? "";
expect(result.signalCode).toBeUndefined();
expect(stderr).not.toContain("Segmentation fault");
expect(stderr).not.toContain("runtime error");
expect(stderr).not.toContain("panic");
const stderr = result.stderr?.toString() ?? "";
expect(result.signalCode).toBeUndefined();
expect(stderr).not.toContain("Segmentation fault");
expect(stderr).not.toContain("runtime error");
expect(stderr).not.toContain("panic");
expect(result.exitCode).toBe(0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/js/bun/util/inspect.test.js` around lines 767 - 771, Add an assertion to
verify the child process exit code is 0: after the existing checks on
result.signalCode and stderr, assert that result.exitCode === 0 so the test
verifies the process completed successfully (the code path where Bun.inspect(v)
runs and the thrown toBe error is caught). Locate the assertions around the
variable result in the test (the block with result.stderr, result.signalCode)
and add the exitCode check there to follow the spawn-process coding guideline.

@robobun

robobun commented Apr 8, 2026

Copy link
Copy Markdown
Collaborator Author

Gate is rejecting because it can't build the with-fix variants — all four build steps hit network failures fetching WebKit/tinycc (unable to verify the first certificate), and the release build output is truncated mid-cares-compilation before producing junit. The without-fix variant correctly shows 1 FAILED on baked bun (the regression test catches the crash as expected).

The C++ fix is a 3-line change matching the pattern already used in the fast-path loop (bindings.cpp:5247-5257), and the test was verified locally on the pre-rebase build. I don't see a code change that can make the gate's infrastructure successfully fetch dependencies.

Comment on lines +763 to +772
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const stderr = result.stderr?.toString() ?? "";
expect(result.signalCode).toBeUndefined();
expect(stderr).not.toContain("Segmentation fault");
expect(stderr).not.toContain("runtime error");
expect(stderr).not.toContain("panic");
});

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.

🟡 The regression test never asserts expect(result.exitCode).toBe(0), so a non-crash failure (e.g., Bun.inspect(v) throwing an uncaught JS exception and exiting with code 1) would leave signalCode undefined, none of the stderr substring checks would fire, and the test would silently pass despite the child process having failed. Adding expect(result.exitCode).toBe(0) would close this gap.

Extended reasoning...

What the bug is and how it manifests

The new regression test (lines 763–772) spawns a child process and makes four assertions: result.signalCode is undefined, and stderr does not contain "Segmentation fault", "runtime error", or "panic". There is no expect(result.exitCode).toBe(0) call. The child script ends with an unguarded Bun.inspect(v); that has no surrounding try/catch. If that call ever throws an uncaught JS exception (instead of crashing with a signal), the child process exits with code 1 and no signal — all four existing assertions pass, but the child process actually failed.

The specific code path that triggers it

The child script is:

import { expect } from "bun:test";
const v = expect();
Object.setPrototypeOf(v, new Proxy(Object.getPrototypeOf(v), {}));
try { v.toBe(v); } catch {}
Bun.inspect(v);

The try/catch around v.toBe(v) prevents the expected assertion error from propagating. But Bun.inspect(v) on the last line is unguarded. Any future regression that makes Bun.inspect throw a JS exception for proxy-prototype objects (rather than crashing with SIGSEGV) would cause the child to exit with code 1 silently.

Why existing assertions do not prevent it

  • result.signalCode is undefined on a normal exit-code-1 (no signal was raised).
  • The stderr substring checks only look for crash-related strings; a JS exception message like "TypeError: cannot read property of undefined" would not match any of the three patterns.
  • Together, all four assertions pass, but the test is a false green.

Addressing the refutation

The refutation correctly observes that for the specific regression this PR fixes (SIGSEGV from null deref in the slow path prototype walk), the signalCode check is the primary and sufficient guard. The fix either prevents the crash (exit 0) or the crash occurs with SIGSEGV. However, the test is intended to serve as a long-lived regression guard. If a future refactoring changes the failure mode from a crash to a JS exception, the signalCode check would no longer catch it. The exitCode assertion costs one line and makes the test robust to both crash and non-crash failure modes.

Impact

False-green tests: a future regression in Bun.inspect that produces a non-signal, non-zero exit would go completely undetected by this test.

How to fix

Add expect(result.exitCode).toBe(0); alongside the signalCode assertion:

expect(result.exitCode).toBe(0);
expect(result.signalCode).toBeUndefined();
expect(stderr).not.toContain("Segmentation fault");
expect(stderr).not.toContain("runtime error");
expect(stderr).not.toContain("panic");

Step-by-step proof of the false-green scenario

  1. A future refactoring causes Bun.inspect(v) to throw TypeError: Cannot read properties of null for objects with Proxy prototypes, instead of crashing with SIGSEGV.
  2. The child process exits with code 1. signalCode is undefined (no signal was raised).
  3. The error is printed to stderr as something like: TypeError: Cannot read properties of null (reading 'foo').
  4. result.signalCode is undefined — assertion passes.
  5. stderr does not contain "Segmentation fault" — assertion passes.
  6. stderr does not contain "runtime error" — assertion passes.
  7. stderr does not contain "panic" — assertion passes.
  8. Test result: PASS — but the child process failed with exit code 1. The regression goes undetected.

Comment on lines +752 to +758
writeFileSync(
script,
`import { expect } from "bun:test";
const v = expect();
Object.setPrototypeOf(v, new Proxy(Object.getPrototypeOf(v), {}));
try { v.toBe(v); } catch {}
Bun.inspect(v);

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.

🟡 The new regression test uses tmpdirSync() and writeFileSync() to create a temporary script file, violating root CLAUDE.md line 117 which explicitly prohibits tmpdirSync. Since the spawned script is a short single file, test/CLAUDE.md's guideline of preferring -e over tempDir for single-file tests also applies — using cmd: [bunExe(), '-e', scriptContent] would eliminate the tmpdirSync, writeFileSync, and fs import entirely.

Extended reasoning...

What the bug is

Root CLAUDE.md line 117 states: "Use tempDir from "harness" to create a temporary directory. Do not use tmpdirSync or fs.mkdtempSync to create temporary directories." The new regression test at line 752 calls tmpdirSync() directly, which is the pattern explicitly prohibited by this rule.

The specific code path

The test at test/js/bun/util/inspect.test.js:752-758 does:

const dir = tmpdirSync();
const script = join(dir, "repro.js");
writeFileSync(script, `import { expect } from "bun:test"; ...`);
const result = Bun.spawnSync({ cmd: [bunExe(), script], ... });

Why existing code does not prevent it

While tmpdirSync was already imported and used elsewhere in this file (lines 107, 422), that does not justify adding a new violation — each new usage still breaks the stated convention. The rule is clear and unconditional.

The additional test/CLAUDE.md guideline

test/CLAUDE.md lines 62–63 state: "For single-file tests, prefer -e over tempDir." The spawned script is a short 5-line single file with a static import statement. Bun's -e flag supports static imports (confirmed by other tests in the repo using --input-type=module or inline ES module syntax). This makes the current approach more complex than necessary.

Step-by-step proof

  1. The test calls const dir = tmpdirSync() — banned by root CLAUDE.md line 117.
  2. The test writes a 5-line script to a temp file via writeFileSync — unnecessary if using -e.
  3. The same test could be written as:
const scriptContent = `import { expect } from "bun:test";
const v = expect();
Object.setPrototypeOf(v, new Proxy(Object.getPrototypeOf(v), {}));
try { v.toBe(v); } catch {}
Bun.inspect(v);`;
const result = Bun.spawnSync({ cmd: [bunExe(), "--input-type=module", "-e", scriptContent], env: bunEnv, ... });

This eliminates: tmpdirSync, writeFileSync, import { writeFileSync } from "fs", and import { join } from "path".

Impact

The test is functionally correct and tests the right crash scenario. This is purely a convention/style violation that adds unnecessary file I/O, temp directory management, and imports to a test that could be a clean inline invocation.

@robobun

robobun commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #29642, which combines the getPropertySlot exception-clear reorder, the getPrototype() null guard, the EnsureStillAliveScope for the prototype-walk object, and the equivalent guard in napi_get_all_property_names.

@robobun robobun closed this Apr 23, 2026
@robobun robobun deleted the farm/094b242e/fix-foreach-proto-empty-jsvalue branch April 23, 2026 21:40
robobun added a commit that referenced this pull request Apr 26, 2026
…get_all_property_names

Hold the current link in the prototype chain as a JSValue under an
EnsureStillAliveScope so a Proxy trap that runs JS (and may GC) during
property enumeration can't free it out from under us.

Also guard the equivalent getPrototype().getObject() pattern in
napi_get_all_property_names, propagating Proxy trap exceptions as
napi_pending_exception.

Supersedes #29071 #28991 #28919 #28918 #28882 #28854 #28530 #28325.
Jarred-Sumner pushed a commit that referenced this pull request May 4, 2026
…get_all_property_names

Hold the current link in the prototype chain as a JSValue under an
EnsureStillAliveScope so a Proxy trap that runs JS (and may GC) during
property enumeration can't free it out from under us.

Also guard the equivalent getPrototype().getObject() pattern in
napi_get_all_property_names, propagating Proxy trap exceptions as
napi_pending_exception.

Supersedes #29071 #28991 #28919 #28918 #28882 #28854 #28530 #28325.
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