-
Notifications
You must be signed in to change notification settings - Fork 5k
Fix null deref in Bun.inspect when Proxy prototype throws #30200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
984985c
8d5e31f
671b57b
bc4e2fc
216d8b3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -772,3 +772,36 @@ it("CustomEvent", () => { | |
| }" | ||
| `); | ||
| }); | ||
|
|
||
| describe("Proxy in prototype chain", () => { | ||
| it("does not crash when a getter throws through a Proxy prototype", () => { | ||
| const proto = {}; | ||
| Object.defineProperty(proto, "thrower", { | ||
| get() { | ||
| throw new Error("getter threw"); | ||
| }, | ||
| enumerable: true, | ||
| configurable: true, | ||
| }); | ||
| proto.foo = function () {}; | ||
| proto.bar = function () {}; | ||
|
|
||
| const obj = Object.create(proto); | ||
| Object.setPrototypeOf(obj, new Proxy(proto, {})); | ||
| expect(Bun.inspect(obj)).toContain("foo"); | ||
| }); | ||
|
Comment on lines
+777
to
+792
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | 💤 Low value Optional: Simplify test setup for clarity. Line 789 creates ♻️ Suggested simplification- const obj = Object.create(proto);
- Object.setPrototypeOf(obj, new Proxy(proto, {}));
+ const obj = Object.create(new Proxy(proto, {}));or - const obj = Object.create(proto);
+ const obj = {};
Object.setPrototypeOf(obj, new Proxy(proto, {}));🤖 Prompt for AI Agents |
||
|
|
||
| it("does not crash when a Proxy getPrototypeOf trap throws", () => { | ||
| const proto = { foo: 1, bar: 2 }; | ||
| const obj = {}; | ||
| Object.setPrototypeOf( | ||
| obj, | ||
| new Proxy(proto, { | ||
| getPrototypeOf() { | ||
| throw new Error("trap threw"); | ||
| }, | ||
| }), | ||
| ); | ||
| expect(Bun.inspect(obj)).toContain("foo"); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
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 same unguarded
getPrototype(globalObject).getObject()pattern still exists innapi_get_all_property_namesatsrc/bun.js/bindings/napi.cpp:1836-1842. A native addon enumerating withnapi_key_include_prototypes+ a writable/enumerable/configurable filter on an object whose prototype chain has a Proxy with a throwinggetOwnPropertyDescriptor/getPrototypeOftrap can hit the identical null deref — might be worth applying the same empty-value + exception guard there in a follow-up.Extended reasoning...
What the bug is
This PR fixes a null deref in
forEachPropertywheregetPrototype()on a Proxy returns an emptyJSValue(notjsNull()) when the trap throws, and.getObject()on an empty value passesisCell()(0 & NotCellMask == 0) and then dereferences a nullJSCell*. The exact same pattern exists, untouched and unguarded, innapi_get_all_property_namesatsrc/bun.js/bindings/napi.cpp:1836-1842:There is no
RETURN_IF_EXCEPTION/CLEAR_IF_EXCEPTIONbetweengetOwnPropertyDescriptor/getPrototypeand.getObject(), and no check that the prototype value is non-empty before calling.getObject()on it.How it manifests
A native addon calls:
on a JS object whose prototype chain contains a Proxy with hostile traps. Once the per-key filter loop is entered (line 1833), each iteration calls
getOwnPropertyDescriptorand thengetPrototypeoncurrent_object. If either of those throws whilecurrent_objectis a Proxy,getPrototype()returns an emptyJSValueandempty.getObject()segfaults readingm_typefrom a nullJSCell*— the same crash mechanism described in this PR's description.Why existing code doesn't prevent it
There is a
NAPI_RETURN_IF_EXCEPTIONat line 1823 afterallPropertyKeys, which catches a trivially throwinggetPrototypeOftrap (sinceallPropertyKeyswalks the chain once). However:getOwnPropertyDescriptortrap that throws makes thewhilecondition returnfalsewith a pending exception; the very next line then callsgetPrototype()on the Proxy, andProxyObject::performGetPrototypebails out early with{}because an exception is already pending.In both cases the empty value reaches
.getObject()unchecked.Step-by-step proof
allPropertyKeys(line 1818) walks the chain via[[OwnPropertyKeys]]+[[GetPrototypeOf]]; neither trap is overridden to throw here, so it succeeds and returns["foo"].NAPI_RETURN_IF_EXCEPTIONat 1823 passes.key_filter & filter_by_any_descriptoris true (napi_key_writable), so we enter the per-key loop with key"foo".key_mode == napi_key_include_prototypes, socurrent_object = objand we evaluate thewhilecondition:obj->getOwnPropertyDescriptor(globalObject, "foo", desc)→objhas no own"foo", returnsfalse, no throw.obj->getPrototype(globalObject)returns the Proxy (no trap, target's proto),.getObject()→proxy.current_object = proxy.whilecondition again:proxy->getOwnPropertyDescriptor(globalObject, "foo", desc)invokes thegetOwnPropertyDescriptortrap, which throws.getOwnPropertyDescriptorreturnsfalsewith the exception left pending inscope.proxy->getPrototype(globalObject)→ProxyObject::performGetPrototypesees the pending exception (or re-enters JS with it pending) and returnsJSValue()(empty).JSValue().getObject():isCell()checks(0 & NotCellMask) == 0→ true;asCell()returnsnullptr;JSCell::getObject()readsthis->m_type→ null deref / segfault.(An equivalent path exists with a stateful
getPrototypeOftrap that returnsnullduringallPropertyKeysand throws inside the per-key loop.)Impact
Process crash (SIGSEGV) reachable from a native addon that calls
napi_get_all_property_nameswithnapi_key_include_prototypesand any descriptor-filter bit on an attacker-/user-controlled object. Narrower than theBun.inspectcase (requires a native addon and a specific flag combination) but the same crash class.How to fix
Apply the same guard this PR adds at
bindings.cpp:5448-5451:Scope
This is pre-existing — the PR does not touch
napi.cpp, add callers, or otherwise interact withnapi_get_all_property_names. Flagging it only because it's the identical pattern being fixed here and is a natural follow-up.