Skip to content

Fix null deref in Bun.inspect when Proxy getPrototypeOf trap throws#30517

Closed
robobun wants to merge 1 commit into
mainfrom
farm/b8a40f4a/fix-inspect-proxy-getprototypeof
Closed

Fix null deref in Bun.inspect when Proxy getPrototypeOf trap throws#30517
robobun wants to merge 1 commit into
mainfrom
farm/b8a40f4a/fix-inspect-proxy-getprototypeof

Conversation

@robobun

@robobun robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Fuzzer found a crash (fingerprint b01f1338ddbd3ba7) where Bun.inspect() / console.log() dereferences null when the inspected object's prototype chain contains a Proxy whose getPrototypeOf trap throws.

Repro

const obj = {};
Object.setPrototypeOf(obj, new Proxy({}, {
  getPrototypeOf() { throw new Error("nope"); }
}));
Bun.inspect(obj);

Root cause

In JSC__JSValue__forEachPropertyImpl, when walking the prototype chain:

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

getPrototype() can throw (via a Proxy's getPrototypeOf trap) and return an empty JSValue. An empty JSValue passes isCell() but asCell() returns nullptr, so .getObject() calls a method on null.

Fix

Clear the exception and treat an empty result as end of the prototype chain, matching how the fast path in the same function already handles this case.

When walking the prototype chain in forEachPropertyImpl, getPrototype()
can throw (e.g. via a Proxy getPrototypeOf trap) and return an empty
JSValue. Calling .getObject() on an empty JSValue dereferences null.
@robobun

robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:55 PM PT - May 11th, 2026

@robobun, your commit 8824fb6 has 1 failures in Build #53460 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30517

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

bun-30517 --bun

@coderabbitai

coderabbitai Bot commented May 11, 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: 37dd0403-e72c-4252-8785-46e569f3cdf3

📥 Commits

Reviewing files that changed from the base of the PR and between 37bfbed and 8824fb6.

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

Walkthrough

This PR hardens exception handling during prototype chain traversal in JSValue property enumeration. The core fix in bindings.cpp explicitly clears exceptions after getPrototype() calls to prevent crashes when proxy traps throw. New test cases verify that Bun.inspect() no longer crashes when prototype-related proxy traps throw.

Changes

Prototype Walking Exception Handling

Layer / File(s) Summary
Exception Handling in Prototype Walking
src/jsc/bindings/bindings.cpp
Fast-path prototype iteration now stores getPrototype() result in a temporary JSValue, clears exceptions after the call, and conditionally advances the iteration pointer instead of directly chaining the call.
Crash Test Cases for Prototype Traps
test/js/bun/util/inspect.test.js
Added crash testing fixtures using Object.setPrototypeOf() and Object.create() with nested Proxy objects configured to throw from getPrototypeOf() traps.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: addressing a null dereference in Bun.inspect that occurs when a Proxy's getPrototypeOf trap throws.
Description check ✅ Passed The description exceeds the template by providing comprehensive context: fuzzer findings, detailed repro case, root cause analysis, and fix explanation. Both required template sections are implicitly addressed.
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.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. getPrototype exception checks #24985 - Original human-authored fix for the same getPrototype exception checks in Bun.inspect
  2. fix(inspect): don't crash when a Proxy in the prototype chain throws #29642 - Fixes the same forEachPropertyImpl crash plus additional napi and GC safety fixes
  3. Fix null JSCell deref in Bun lazy property callbacks and forEachProperty #29671 - Fixes the same forEachPropertyImpl null deref alongside a Bun object lazy property callback crash
  4. inspect: handle throwing Proxy getPrototypeOf in forEachProperty #29814 - Exact duplicate: handles throwing Proxy getPrototypeOf in forEachProperty
  5. inspect: clear exceptions when walking Proxy prototype chain #29845 - Exact duplicate: clears exceptions when walking Proxy prototype chain
  6. fix(inspect): handle Proxy trap exceptions when walking prototype chain #30099 - Exact duplicate: handles Proxy trap exceptions when walking prototype chain
  7. Fix null deref in Bun.inspect when Proxy prototype throws #30200 - Exact duplicate: fixes null deref in Bun.inspect when Proxy prototype throws
  8. Fix null JSCell derefs in Bun object lazy init and Bun.inspect with Proxy prototypes #30245 - Fixes the same forEachPropertyImpl null deref plus a separate Bun object PropertyCallback crash
  9. Fix null deref in forEachProperty when Proxy getPrototype throws #30457 - Exact duplicate: fixes null deref in forEachProperty when Proxy getPrototype throws

🤖 Generated with Claude Code

@robobun

robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #24985 / #29642 (and several others). Closing in favor of the earlier PRs.

@robobun robobun closed this May 11, 2026
@robobun robobun deleted the farm/b8a40f4a/fix-inspect-proxy-getprototypeof branch May 11, 2026 20:54

@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 null-check + exception-clear matching the fast path's existing handling; the napi.cpp note is pre-existing and non-blocking.

Extended reasoning...

Overview

This PR fixes a fuzzer-found null dereference in JSC__JSValue__forEachPropertyImpl (src/jsc/bindings/bindings.cpp). When walking the prototype chain during Bun.inspect(), iterating->getPrototype(globalObject) can throw via a Proxy getPrototypeOf trap and return an empty JSValue; calling .getObject() on that segfaults. The fix stores the result, clears the exception with CLEAR_IF_EXCEPTION(scope), and null-checks before .getObject(). Two regression cases are added to the existing crash-testing fixture in test/js/bun/util/inspect.test.js.

Security risks

None. This is a defensive null check in the property-enumeration path used for object inspection/formatting. It swallows an exception in a context where the surrounding code already swallows exceptions from getters and other proxy traps, so behavior is consistent. No auth, crypto, permissions, or untrusted input parsing is involved.

Level of scrutiny

Low. The change is 4 lines, mechanical, and mirrors the identical handling already present in the fast path of the same function (lines ~5336–5337: CLEAR_IF_EXCEPTION(scope) after a getPrototype call). The loop already terminates when iterating becomes nullptr, so treating a thrown trap as end-of-chain is the natural semantics here. The affected surface is Bun.inspect/console.log formatting, where best-effort output is expected.

Other factors

The only finding from the bug-hunting pass is a pre-existing instance of the same pattern in napi.cpp (napi_get_all_property_names), which this PR does not touch — flagged as a non-blocking follow-up suggestion, not a defect in this change. No CODEOWNERS cover this path. Regression tests are added to the existing crash fixture, which wraps Bun.inspect in try/catch and only fails on a process crash, so they exercise exactly the segfault case.

Comment on lines +5447 to +5450
JSValue proto = iterating->getPrototype(globalObject);
// Ignore exceptions from Proxy "getPrototypeOf" trap.
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.

🟣 Heads-up: the same unsafe pattern exists at src/jsc/bindings/napi.cpp:1837 in napi_get_all_property_namescurrent_object->getPrototype(globalObject).getObject() with no exception check, so a Proxy getPrototypeOf trap that throws will null-deref there too. This is pre-existing and not touched by this PR, but since a grep for getPrototype(globalObject).getObject() finds it, you may want to apply the same CLEAR_IF_EXCEPTION + empty-check fix there in this PR or a follow-up. Non-blocking.

Extended reasoning...

What the bug is

This PR correctly fixes a null dereference in JSC__JSValue__forEachPropertyImpl (bindings.cpp:5447) where iterating->getPrototype(globalObject).getObject() crashes when a Proxy's getPrototypeOf trap throws. However, the identical pattern remains at src/jsc/bindings/napi.cpp:1837 inside napi_get_all_property_names:

while (!current_object->getOwnPropertyDescriptor(globalObject, key.toPropertyKey(globalObject), desc)) {
    JSObject* proto = current_object->getPrototype(globalObject).getObject();  // <-- same unsafe pattern
    if (!proto) {
        break;
    }
    current_object = proto;
}

There is no RETURN_IF_EXCEPTION / CLEAR_IF_EXCEPTION between the getPrototype() call and .getObject(), so the exact crash class fixed by this PR still exists in the napi path.

Code path that triggers it

A native addon calls napi_get_all_property_names with:

  • key_mode = napi_key_include_prototypes (so the inner while-loop walks the prototype chain), and
  • key_filter containing any of napi_key_enumerable | napi_key_writable | napi_key_configurable (so the descriptor-filtering block at line 1827 is entered),

on an object whose prototype chain contains a Proxy with a throwing getPrototypeOf trap.

Why existing code doesn't prevent it

There's a NAPI_RETURN_IF_EXCEPTION(env) at line 1823 after the initial allPropertyKeys() walk, so a trivially-always-throwing trap would be caught there. But a stateful trap — e.g., one that returns normally for the first N calls (during allPropertyKeys's prototype walk) and throws on a subsequent call (during the per-key descriptor-filtering walk at line 1837) — passes the line-1823 check and reaches line 1837. At that point getPrototype() throws and returns an empty JSValue; per the PR description's own analysis, an empty JSValue passes isCell() but asCell() is nullptr, so .getObject() invokes JSCell::getObject() on null → segfault.

Step-by-step proof

  1. JS sets up: let n = 0; const p = new Proxy({}, { getPrototypeOf() { if (n++ > 3) throw new Error('nope'); return null; } }); const obj = Object.create(p); obj.x = 1;
  2. Native addon calls napi_get_all_property_names(env, obj, napi_key_include_prototypes, napi_key_enumerable, napi_key_numbers_to_strings, &result).
  3. Line 1818-ish: allPropertyKeys() walks the chain, calling the trap a few times; n increments but stays under the threshold; no exception → line 1823 passes.
  4. Line 1827: key_filter & filter_by_any_descriptor is true → enter filtering loop.
  5. For key "x", line 1836: getOwnPropertyDescriptor on obj succeeds, loop body skipped. But for any key contributed by the prototype (or if the descriptor lookup misses on obj), the while-loop at 1836 runs and line 1837 calls current_object->getPrototype(globalObject).
  6. The trap now throws (n > 3). getPrototype() returns JSValue() (empty).
  7. JSValue().getObject()isCell() is true for empty (bits == 0 has no number/undefined/null tag) → asCell() returns nullptrnullptr->getObject() reads type() through a null this → crash.

Impact

Process crash (SIGSEGV) reachable from any native addon that uses napi_get_all_property_names with prototype inclusion + descriptor filtering on user-controlled objects. Lower exposure than the Bun.inspect path since it requires a native addon, but it's the same crash class.

How to fix

Apply the same pattern as this PR:

JSValue protoVal = current_object->getPrototype(globalObject);
CLEAR_IF_EXCEPTION(scope);  // or NAPI_RETURN_IF_EXCEPTION(env) if propagating is preferred
JSObject* proto = protoVal ? protoVal.getObject() : nullptr;
if (!proto) break;

Severity

Pre-existing — this PR does not touch napi.cpp, add callers to it, or otherwise interact with this code path. Mentioning it only because the PR's stated goal is fixing exactly this crash class and a grep for getPrototype(globalObject).getObject() finds this remaining instance. Non-blocking; fine to address in a follow-up.

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