-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Fix null deref in Bun.inspect when Proxy prototype traps throw #30030
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
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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
getPrototype(globalObject).getObject()null-deref pattern fixed here also exists atsrc/bun.js/bindings/napi.cpp:1837insidenapi_get_all_property_names. A native addon calling that API withnapi_key_include_prototypes+ an enumerable/writable/configurable filter on an object whose prototype chain has a Proxy with a throwinggetPrototypeOf(orgetOwnPropertyDescriptor) trap will crash the same way. Might be worth applying the same guard there as a follow-up.Extended reasoning...
What the bug is
This PR correctly fixes a null dereference in
JSC__JSValue__forEachPropertyImplwhereiterating->getPrototype(globalObject).getObject()crashes when a ProxygetPrototypeOftrap throws. The identical vulnerable pattern exists, untouched, atsrc/bun.js/bindings/napi.cpp:1837insidenapi_get_all_property_names:There is no exception check between
getPrototype()and.getObject(), and none aftergetOwnPropertyDescriptor()either (lines 1829–1861 contain zeroRETURN_IF_EXCEPTION/CLEAR_IF_EXCEPTION).Why it crashes (same mechanism as this PR)
When a Proxy
getPrototypeOftrap throws,JSObject::getPrototype()returns an emptyJSValue(encoded bits = 0). On JSVALUE64 an empty value reportsisCell() == truewithasCell() == nullptr, soJSValue::getObject()evaluatesisCell() && asCell()->isObject()and dereferences a nullJSCell*in->isObject(). Theif (!proto) break;at line 1838 cannot help — the segfault happens inside.getObject()beforeprotois ever assigned.Code path that triggers it
napi_get_all_property_names(env, obj, napi_key_include_prototypes, napi_key_enumerable /* or writable/configurable */, ...).obj's prototype chain contains aProxywhose handler has a throwinggetPrototypeOftrap (or a throwinggetOwnPropertyDescriptortrap, which makes thewhilecondition returnfalsewith a pending exception and thengetPrototyperuns under that exception).allPropertyKeysat line 1818 succeeds (it usesownKeys, notgetPrototypeOf/getOwnPropertyDescriptor), soNAPI_RETURN_IF_EXCEPTIONat 1823 passes.key_filter & (enumerable|writable|configurable)is set, the inner loop runs.object,getOwnPropertyDescriptorreturnsfalseand the body executescurrent_object->getPrototype(globalObject).getPrototype()returns emptyJSValue→.getObject()dereferences null → segfault.Step-by-step proof
allPropertyKeyswalks own keys ofobj(none) then the proxy'sownKeys→["x"]. No throw, line 1823 passes."x":object(=obj) has no own"x"→getOwnPropertyDescriptorreturnsfalse→ enter loop body.current_objectisobj;obj->getPrototype(globalObject)returns the Proxy (ordinary [[GetPrototypeOf]] onobj, no throw).proto= Proxy,current_object= Proxy.whiletest:getOwnPropertyDescriptoron the Proxy for"x"returns the descriptor → loop exits. Fine so far.getOwnPropertyDescriptor() { throw new Error("gOPD"); }: now the secondwhiletest throws and returnsfalse, body runs again, andcurrent_object->getPrototype(globalObject)invokes the Proxy'sgetPrototypeOftrap → throws → emptyJSValue→.getObject()→ null deref.obj → plain → Proxy), the second body iteration callsgetPrototypedirectly on the Proxy and crashes without needing the gOPD trap.Either way the vulnerable line is reached with a throwing trap and no guard.
Why existing code doesn't prevent it
The only exception check in this function is
NAPI_RETURN_IF_EXCEPTION(env)at line 1823, which runs before the descriptor-filtering loop. Inside the loop (1829–1861) there are no exception checks at all, so neither the throwinggetOwnPropertyDescriptornor the throwinggetPrototypeis caught.Impact
Segfault of the Bun process when a native addon enumerates properties (with prototype walk + descriptor filter) on user-controlled objects whose prototype chain contains a hostile/broken Proxy. Lower exposure than the
Bun.inspectcase since it requires a native addon using this specific N-API call, but it's the same crash class this PR is hardening against.Suggested fix (follow-up, not blocking)
Mirror this PR's fix in
napi.cpp: aftergetOwnPropertyDescriptorand aftergetPrototype, checkscope.exception()(or split outJSValue proto = current_object->getPrototype(globalObject); if (scope.exception() || !proto.isObject()) break;). Returningnapi_pending_exceptionviaNAPI_RETURN_IF_EXCEPTIONinside the loop would also be reasonable.This is pre-existing —
napi.cppis not touched by this PR and no new callers are added. Mentioning only because the PR is specifically about hardening this exact pattern.