-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Fix null deref in Bun.inspect when Proxy prototype trap throws #30225
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 all commits
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 |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { describe, expect, it } from "bun:test"; | ||
| import { normalizeBunSnapshot, tmpdirSync } from "harness"; | ||
| import { bunEnv, bunExe, normalizeBunSnapshot, tmpdirSync } from "harness"; | ||
| import { join } from "path"; | ||
| import util from "util"; | ||
| it("prototype", () => { | ||
|
|
@@ -465,6 +465,49 @@ describe("crash testing", () => { | |
| } | ||
| }); | ||
| } | ||
|
|
||
| it.concurrent.each([ | ||
| [ | ||
| "throwing get trap", | ||
| ` | ||
| const o = {}; | ||
| Object.setPrototypeOf(o, new Proxy({ foo: 1 }, { | ||
| get(t, k) { if (typeof k === "symbol") return undefined; throw new Error("nope"); }, | ||
| })); | ||
| Bun.inspect(o); | ||
| `, | ||
| ], | ||
| [ | ||
| "throwing getPrototypeOf trap", | ||
| ` | ||
| const o = {}; | ||
| Object.setPrototypeOf(o, new Proxy({ foo: 1 }, { | ||
| getPrototypeOf() { throw new Error("nope"); }, | ||
| })); | ||
| Bun.inspect(o); | ||
| `, | ||
| ], | ||
| [ | ||
| "getter throws after side-effect from prior getter", | ||
| ` | ||
| const { expect } = Bun.jest(""); | ||
| const e = expect(1); | ||
| Object.setPrototypeOf(e, new Proxy(Object.getPrototypeOf(e), {})); | ||
| Bun.inspect(e); | ||
| `, | ||
| ], | ||
| ])("Proxy prototype with %s doesn't crash", async (_, code) => { | ||
| await using proc = Bun.spawn({ | ||
| cmd: [bunExe(), "-e", code + '\nconsole.log("OK");'], | ||
| env: bunEnv, | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
| expect(stderr).toBe(""); | ||
| expect(stdout).toBe("OK\n"); | ||
| expect(exitCode).toBe(0); | ||
|
Comment on lines
+506
to
+509
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. Filter known ASAN startup noise before asserting empty stderr. With Suggested patch- expect(stderr).toBe("");
+ const stderrLines = stderr
+ .split("\n")
+ .filter(line => !line.startsWith("WARNING: ASAN interferes"))
+ .filter(Boolean);
+ expect(stderrLines).toEqual([]);Based on learnings: In oven-sh/bun test files that spawn subprocesses using 🤖 Prompt for AI Agents |
||
| }); | ||
| }); | ||
|
|
||
| it("possibly formatted emojis log", () => { | ||
|
|
||
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 here): the same
getPrototype(globalObject).getObject()pattern without an exception check still exists atsrc/bun.js/bindings/napi.cpp:1837innapi_get_all_property_names. A throwing ProxygetPrototypeOf/getOwnPropertyDescriptortrap on the prototype chain will cause the same null deref there when called withnapi_key_include_prototypes+ a descriptor filter — might be worth a follow-up since it's the identical bug class.Extended reasoning...
What the bug is
This PR fixes a null deref in
JSC__JSValue__forEachPropertyImplwhereiterating->getPrototype(globalObject).getObject()was called without checking for a pending exception. The exact same vulnerable pattern still exists innapi_get_all_property_namesatsrc/bun.js/bindings/napi.cpp:1837:There is no exception check between
getPrototype(globalObject)and.getObject(), and the precedinggetOwnPropertyDescriptor(also a Proxy trap) is similarly unchecked.Code path that triggers it
A native addon calls
napi_get_all_property_nameswith:key_mode = napi_key_include_prototypeskey_filtercontaining any ofnapi_key_enumerable | napi_key_writable | napi_key_configurable(so the descriptor-filtering branch at line 1827 runs)Proxywith a throwinggetPrototypeOftrap, or a throwinggetOwnPropertyDescriptortrap.The earlier
allPropertyKeyscall (line 1818) goes through the ProxyownKeys/getPrototypeOftraps and is guarded byNAPI_RETURN_IF_EXCEPTIONat line 1823. However, the inner loop at line 1836 invokesgetOwnPropertyDescriptor— a different trap thatallPropertyKeysdoes not exercise — so a Proxy whoseownKeys/getPrototypeOfsucceed but whosegetOwnPropertyDescriptorthrows will sail past line 1823 and blow up here.Why the existing guard doesn't help
When a Proxy trap throws (or there is already a pending exception),
JSObject::getPrototype/ProxyObject::getPrototypereturns an emptyJSValue. On JSVALUE64, an emptyJSValueis encoded as0, which passes theisCell()check (0 & NotCellMask == 0).getObject()therefore evaluatesasCell()->isObject()on a nullJSCell*and segfaults before reaching theif (!proto) break;on line 1838 — that null check never gets a chance to run.Step-by-step proof
objwhose prototype isnew Proxy(target, { getOwnPropertyDescriptor() { throw new Error('nope'); } })andtargethas a keyfoo.napi_get_all_property_names(env, obj, napi_key_include_prototypes, napi_key_enumerable, napi_key_numbers_to_strings, &result).allPropertyKeyswalks the chain viaownKeys/getPrototypeOf(no throw), returns['foo'].NAPI_RETURN_IF_EXCEPTIONpasses.'foo'.object->getOwnPropertyDescriptoronobjitself returns false (own miss), enters loop body.obj->getPrototype(globalObject)returns the Proxy;.getObject()returns the Proxy object;current_object = proxy.proxy->getOwnPropertyDescriptor(...)invokes the trap, which throws. Returns false → loop body again with a pending exception.proxy->getPrototype(globalObject)runsProxyObject::getPrototype, which begins withRETURN_IF_EXCEPTION(scope, { })→ returns emptyJSValue..getObject()on empty →asCell()isnullptr→nullptr->isObject()→ SIGSEGV.(The simpler variant — a Proxy whose
getPrototypeOfthrows only on the second call — also reaches step 7 directly.)Impact
Same as the bug this PR fixes: a hard crash (segfault) of the Bun process, but gated behind a native addon calling this specific N-API with prototype-walking + descriptor filtering on adversarial input. Less reachable than the
Bun.inspectcase, but still the same crash class.Fix
Mirror what this PR does in
bindings.cpp:and add an exception check after
getOwnPropertyDescriptorin the loop condition. This is pre-existing and untouched by this PR — flagging only because it's the identical bug class and a natural follow-up.