-
Notifications
You must be signed in to change notification settings - Fork 5k
Fix null deref in forEachProperty when Proxy prototype getter throws #30541
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
|
|
||
| describe("Bun.inspect with Proxy in prototype chain", () => { | ||
| test("does not crash when a getter reached through a Proxy prototype throws", () => { | ||
| // The Expect prototype has getters (.rejects / .resolves) that mutate state and can | ||
| // throw when invoked in sequence. Going through a Proxy forces the slow property | ||
| // iteration path which invokes those getters. | ||
| const e = expect(1); | ||
| const proto = Object.getPrototypeOf(e); | ||
| Object.setPrototypeOf(e, new Proxy(proto, {})); | ||
| const out = Bun.inspect(e); | ||
| expect(out).toContain("toBe"); | ||
| }); | ||
|
|
||
| test("does not crash when a Proxy getPrototypeOf trap throws during iteration", () => { | ||
| class Foo { | ||
| bar() {} | ||
| } | ||
| const obj = new Foo(); | ||
| const proto = Object.getPrototypeOf(obj); | ||
| Object.setPrototypeOf( | ||
| obj, | ||
| new Proxy(proto, { | ||
| getPrototypeOf() { | ||
| throw new Error("nope"); | ||
| }, | ||
| }), | ||
| ); | ||
| const out = Bun.inspect(obj); | ||
| expect(out).toContain("bar"); | ||
| }); | ||
|
|
||
| test("does not crash when a Proxy get trap throws during iteration", () => { | ||
| class Foo { | ||
| bar() {} | ||
| baz() {} | ||
| } | ||
| const obj = new Foo(); | ||
| const proto = Object.getPrototypeOf(obj); | ||
| let count = 0; | ||
| Object.setPrototypeOf( | ||
| obj, | ||
| new Proxy(proto, { | ||
| get(t, k, r) { | ||
| if (++count > 3) throw new Error("get fail"); | ||
| return Reflect.get(t, k, r); | ||
|
Check warning on line 46 in test/js/bun/util/inspect-proxy-prototype.test.ts
|
||
| }, | ||
| }), | ||
| ); | ||
| expect(() => Bun.inspect(obj)).not.toThrow(); | ||
| }); | ||
| }); | ||
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.
🟡 This test never actually triggers the throw: the
gettrap is invoked exactly 3 times duringBun.inspect(obj)(once forSymbol(nodejs.util.inspect.custom), then once each forbarandbaz), so++count > 3is never true and the test passes even without the bindings.cpp fix. Lower the threshold to e.g.> 1so the trap actually throws insidegetPropertySlotand exercises theCLEAR_IF_EXCEPTION-before-continuepath. (Nit — tests 1 and 2 already cover the actual fixes, this one is just currently vacuous.)Extended reasoning...
What the test claims vs. what it does
The third test is titled "does not crash when a Proxy get trap throws during iteration" and asserts
expect(() => Bun.inspect(obj)).not.toThrow(). For that to be a meaningful regression test, thegettrap must actually throw at some point duringforEachPropertyImpl's slow-path iteration so that the newCLEAR_IF_EXCEPTION(scope)beforecontinue(bindings.cpp:5372–5376) is exercised. As written, the trap never throws.Step-by-step trace of the get-trap invocations
Running this scenario on bun and counting trap invocations gives exactly 3:
Symbol(nodejs.util.inspect.custom)lookup — BeforeforEachPropertyruns,Tag.getAdvanced(ConsoleObject.zig:1246) callsfastGet(.inspectCustom), which walksobj → proxyand invokesProxyObject::getOwnPropertySlotwithInternalMethodType::Get, calling the trap once.count = 1,1 > 3is false.bar— In the slow path,getOwnPropertyNameson the proxy (noownKeystrap) returns[constructor, bar, baz];constructoris filtered at bindings.cpp:5362. Forbar,object->getPropertySlot(globalObject, property, slot)walksobj → proxyand invokesperformGet, calling the trap.count = 2,2 > 3is false. The slot is a plain value slot, soslot.getValue()later returns the cached value with no second trap call.baz— Same as above.count = 3,3 > 3is false.proxy->getPrototype()(nogetPrototypeOftrap) returnsObject.prototype, the loop terminates.The condition
if (++count > 3) throwfirst fires on the 4th invocation, which never happens. No exception is ever raised insideforEachPropertyImpl, so neither the newCLEAR_IF_EXCEPTIONat 5374 nor thegetPrototypeguard at 5448–5451 is reached via this test.Why this matters
This was verified empirically on a bun build without this PR's fix: the test body runs to completion with
count == 3, no throw, no crash — so the test would pass even on the broken code it's meant to guard against. It currently provides no regression coverage for the throwing-get-trap path described in its title.Why this is only a nit
The first test (the
expect(1)+ Proxy-prototype repro from the fuzzer) does exercise the throwing-getter path that crashed pre-fix and covers bindings.cpp:5372–5376, and the second test covers thegetPrototypeguard at 5448–5451. So overall regression coverage for the fix exists; this third test is supplementary and just ineffective as written.Fix
Lower the threshold so the trap throws during the
getPropertySlotloop:With
> 1, theinspectCustomlookup succeeds (count=1), thenbarthrows insidegetPropertySlot(count=2) — exercising exactly the code path this PR fixed.> 2would also work (throws onbaz).