Skip to content

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

Closed
robobun wants to merge 1 commit into
mainfrom
farm/60be592e/fix-foreachproperty-proxy-getprototype
Closed

Fix null deref in Bun.inspect when Proxy getPrototypeOf trap throws#29943
robobun wants to merge 1 commit into
mainfrom
farm/60be592e/fix-foreachproperty-proxy-getprototype

Conversation

@robobun

@robobun robobun commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes a null pointer dereference in JSC__JSValue__forEachPropertyImpl (used by Bun.inspect, console.log, and expect matcher error formatting) when walking the prototype chain of an object whose prototype is a Proxy and [[GetPrototypeOf]] throws.

Why

JSObject::getPrototype() can throw — either directly via a Proxy getPrototypeOf trap, or indirectly when there's a pending exception from a prior property access through the Proxy. When it throws, it returns an empty JSValue, and calling .getObject() on that empty value dereferences a null cell:

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

The fast-path branch a few lines above already handles this correctly by checking the return value and clearing the exception; the slow-path loop did not.

Repro

const obj = { a: 1 };
class Foo {}
Object.setPrototypeOf(obj, new Proxy(Foo.prototype, {
  getPrototypeOf() { throw new Error("trap!"); }
}));
console.log(obj); // crashed with null deref; now prints { a: 1 }

Found by Fuzzilli (fingerprint 72a7516faba947a1).

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

Handle the empty value and clear the exception, matching the existing
handling in the fast-path branch above.
@robobun

robobun commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:16 AM PT - Apr 29th, 2026

@robobun, your commit 6e5ad76 has 1 failures in Build #49174 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29943

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

bun-29943 --bun

@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

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: 9abd2f91-2126-4ced-9f8c-36f8d3c993fc

📥 Commits

Reviewing files that changed from the base of the PR and between 6c21a7e and 6e5ad76.

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

Walkthrough

The changes modify prototype-chain traversal in JSC__JSValue__forEachPropertyImpl to gracefully handle Proxy exceptions by capturing prototype values and clearing exceptions. A regression test is added to verify Bun.inspect handles Proxy-based prototypes without crashing.

Changes

Cohort / File(s) Summary
Core Implementation
src/bun.js/bindings/bindings.cpp
Modified JSC__JSValue__forEachPropertyImpl to treat prototype-chain traversal as exception-tolerant when dealing with Proxy objects. Now captures getPrototype(globalObject) as a JSValue, clears any resulting exceptions, and halts traversal if the prototype is falsy/absent, replacing the previous direct getPrototype(...).getObject() approach.
Test Coverage
test/js/bun/util/inspect.test.js
Added crash-regression test verifying that Bun.inspect successfully handles objects with Proxy-based prototypes that throw in getPrototypeOf traps, confirming the output reflects enumerable properties.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main fix: a null dereference in Bun.inspect triggered when a Proxy's getPrototypeOf trap throws.
Description check ✅ Passed The description covers both required template sections: 'What' explains the null dereference issue and affected functions, while 'Why' provides technical context and includes a code reproduction case.
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.


Review rate limit: 3/5 reviews remaining, refill in 15 minutes and 48 seconds.

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. Fix null deref in forEachProperty with Proxy in prototype chain #29816 - Fixes the same null deref in forEachPropertyImpl with Proxy prototype chain, same file and function
  2. fix(inspect): don't crash when a Proxy in the prototype chain throws #29642 - Fixes the same forEachPropertyImpl null deref plus extends the fix to napi_get_all_property_names
  3. inspect: handle throwing Proxy getPrototypeOf in forEachProperty #29814 - Fixes the same forEachPropertyImpl getPrototype null deref when Proxy getPrototypeOf trap throws
  4. inspect: clear exceptions when walking Proxy prototype chain #29845 - Fixes the same exception-clearing in forEachPropertyImpl prototype chain walking
  5. Fix null JSCell deref in Bun lazy property callbacks and forEachProperty #29671 - Fixes the same forEachPropertyImpl null deref plus an additional BunObject.cpp lazy property callback bug

🤖 Generated with Claude Code

@robobun

robobun commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #29642 / #29816 which already address this crash with more complete fixes (including the getPropertySlot exception-clearing and napi.cpp). Closing in favor of those.

@robobun robobun closed this Apr 29, 2026
@robobun robobun deleted the farm/60be592e/fix-foreachproperty-proxy-getprototype branch April 29, 2026 16:15

@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 that mirrors the existing fast-path handling in the same function, with a regression test.

Extended reasoning...

Overview

This PR fixes a null pointer dereference in JSC__JSValue__forEachPropertyImpl (bindings.cpp) when getPrototype() throws via a Proxy getPrototypeOf trap and returns an empty JSValue. The fix replaces a chained .getObject() call with an empty-value check and a CLEAR_IF_EXCEPTION, then adds a regression test in inspect.test.js. Total change is 4 lines of C++ and one test case.

Security risks

None. This is a defensive fix that prevents a crash in the property-enumeration path used by Bun.inspect / console.log. No new attack surface; it strictly hardens against hostile Proxy traps.

Level of scrutiny

Low. The change is mechanical and directly mirrors two existing patterns in the same function (lines ~5166 and ~5243) that already guard getPrototype() with if (JSValue proto = ...), and the CLEAR_IF_EXCEPTION matches the surrounding exception-swallowing semantics of this best-effort inspection routine (e.g. lines 5291, 5336–5338, 5372–5374). Setting iterating = nullptr cleanly terminates the enclosing for (; iterating && ...) loop.

Other factors

The only finding from the bug-hunting pass is a pre-existing instance of the same pattern in napi.cpp — explicitly flagged as out-of-scope follow-up, not a defect in this PR. The fix was found via Fuzzilli, has a clear repro, and ships with a test that exercises the exact crash path. No outstanding reviewer comments.

Comment on lines +5363 to +5366
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 (pre-existing, not introduced by this PR): the identical getPrototype(globalObject).getObject() pattern exists at src/bun.js/bindings/napi.cpp:1837 in the napi_key_include_prototypes filtering loop of napi_get_all_property_names. A Proxy whose getPrototypeOf/getOwnPropertyDescriptor trap throws during that per-key prototype re-walk will null-deref the same way — might be worth applying the same fix there in a follow-up.

Extended reasoning...

Summary

This PR correctly fixes a null deref in JSC__JSValue__forEachPropertyImpl where iterating->getPrototype(globalObject).getObject() crashes when getPrototype throws (Proxy getPrototypeOf trap). The same pattern exists, unfixed, at src/bun.js/bindings/napi.cpp:1837 inside napi_get_all_property_names. This is a pre-existing issue — the PR does not touch napi.cpp — but it's the exact bug class being addressed and is worth flagging for a follow-up.

The code path

In napi_get_all_property_names, when the caller passes napi_key_include_prototypes together with a descriptor-based filter (napi_key_writable / napi_key_enumerable / napi_key_configurable), the code first collects all keys via allPropertyKeys() and then, for each collected key, re-walks the prototype chain to find the owning descriptor:

JSObject* current_object = object;
while (!current_object->getOwnPropertyDescriptor(globalObject, key.toPropertyKey(globalObject), desc)) {
    JSObject* proto = current_object->getPrototype(globalObject).getObject();   // ← line 1837
    if (!proto) {
        break;
    }
    current_object = proto;
}

If getPrototype throws, it returns an empty JSValue. On JSVALUE64 an empty value is encoded as 0, so isCell() returns true, asCell() returns nullptr, and JSCell::getObject() dereferences m_type on a null pointer — exactly the crash this PR fixes in bindings.cpp. The if (!proto) break; on the next line never runs because the deref happens inside .getObject().

Why the existing guard doesn't help

There is a NAPI_RETURN_IF_EXCEPTION at line 1823 after allPropertyKeys(), but it only guards the initial property collection. The per-key filtering loop that follows performs its own prototype walk with no exception checks between getOwnPropertyDescriptor and getPrototype, nor after getPrototype itself. Two routes reach the crash:

  1. Throwing getOwnPropertyDescriptor trap — if a Proxy in the chain throws from its getOwnPropertyDescriptor trap, the call returns false with a pending exception, the while body executes, and getPrototype immediately returns empty because of the pending exception.
  2. Stateful getPrototypeOf trap — a trap that succeeds during allPropertyKeys() (so the line-1823 guard is passed) but throws on a later invocation during the per-key re-walk returns empty directly.

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) where obj's prototype chain contains new Proxy(target, { getOwnPropertyDescriptor() { throw new Error('boom'); } }).
  2. allPropertyKeys() collects keys (using ownKeys, not getOwnPropertyDescriptor), no exception is pending → passes the line-1823 check.
  3. Filtering loop iterates the first key; current_object->getOwnPropertyDescriptor(...) walks down to the Proxy, invokes the trap, which throws → returns false with a pending exception.
  4. Loop body runs: current_object->getPrototype(globalObject) sees the pending exception and returns an empty JSValue (encoded 0).
  5. .getObject()isCell() is !isNotCellMask(0) = true → asCell() = nullptrnullptr->m_typeSIGSEGV.

Impact and suggested fix

Any native addon that enumerates prototype-chain properties with a descriptor filter on user-controlled objects can be crashed by a hostile Proxy. The fix is the same as in this PR:

JSValue protoVal = current_object->getPrototype(globalObject);
if (scope.exception()) [[unlikely]] { ... }   // or CLEAR_IF_EXCEPTION + break
JSObject* proto = protoVal ? protoVal.getObject() : nullptr;
if (!proto) break;

(and ideally an exception check after getOwnPropertyDescriptor as well). Since this PR doesn't otherwise touch N-API, it's reasonable to land this fix as-is and address napi.cpp 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