Skip to content

fix(inspect): don't crash when a Proxy in the prototype chain throws#29642

Open
robobun wants to merge 3 commits into
mainfrom
farm/43e87898/inspect-proxy-proto-throw
Open

fix(inspect): don't crash when a Proxy in the prototype chain throws#29642
robobun wants to merge 3 commits into
mainfrom
farm/43e87898/inspect-proxy-proto-throw

Conversation

@robobun

@robobun robobun commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes crashes in Bun.inspect() / console.log() / expect matcher output and in napi_get_all_property_names when an object's prototype chain contains a Proxy whose traps throw or run JS that triggers GC.

Repro

const proto = {
  get foo() { throw new Error("nope"); }
};
const obj = Object.create(new Proxy(proto, {}));
Bun.inspect(obj); // segfault

Root cause

In JSC__JSValue__forEachPropertyImpl's slow-path prototype walk:

  1. When getPropertySlot goes through a ProxyObject with InternalMethodType::Get, JSC eagerly invokes [[Get]]. If the target getter throws, getPropertySlot returns false with the exception still pending, and we continued before CLEAR_IF_EXCEPTION.
  2. iterating->getPrototype(globalObject).getObject() dereferences a null JSCell* when getPrototype returns an empty JSValue{} (because of a leaked exception from (1), or because the getPrototypeOf trap itself throws).
  3. The raw JSObject* iterating is held across calls into JS (Proxy traps, iter callback) that can GC; if a trap detaches this link from the prototype chain it can be collected mid-iteration.

The same getPrototype().getObject() pattern also exists in napi_get_all_property_names.

Fix

  • Clear the exception before the early continue (matching forEachPropertyOrdered).
  • Guard the result of getPrototype() so a throwing getPrototypeOf trap stops iteration instead of crashing.
  • Hold the current prototype link as a JSValue under an EnsureStillAliveScope across the loop body.
  • Propagate Proxy-trap exceptions from getOwnPropertyDescriptor/getPrototype in napi_get_all_property_names as napi_pending_exception instead of dereferencing an empty JSValue.

Found by Fuzzilli (fingerprint d678cafe50a2ad6e). Supersedes #29071 #28991 #28919 #28918 #28882 #28854 #28530 #28325.

@robobun

robobun commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:36 AM PT - May 4th, 2026

@robobun, your commit 932c22b is building: #51099

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Prototype-chain traversal and property enumeration were hardened: prototypes are tracked as JSValue and preserved during enumeration, prototype hops are limited, Proxy Get and getPrototypeOf exceptions are cleared, null or mismatched prototypes stop traversal, and tests added to cover Proxy-related inspect regressions.

Changes

Cohort / File(s) Summary
Bindings: prototype & property enumeration
src/bun.js/bindings/bindings.cpp
Maintain current prototype as a JSValue (iteratingValue) and keep it alive with EnsureStillAliveScope; limit prototype hops (max ~5); record hasProperty from getPropertySlot(...); clear exceptions from Proxy Get and getPrototypeOf traps; stop traversal on falsy prototype or mismatched JSGlobalProxy.
N-API: safe prototype handling
src/bun.js/bindings/napi.cpp
Capture prototype into a JSValue, check for pending exceptions immediately after each getPrototype(globalObject) before converting to JSObject*, treat prototype pointer as nullable, and check for exceptions after getOwnPropertyDescriptor(...) in the direct-descriptor path.
Tests: Proxy-related inspect regressions
test/js/bun/util/inspect.test.js
Add regression tests ensuring Bun.inspect does not throw when prototype getters or getPrototypeOf traps on a Proxy throw, and that inspection prints expected output (e.g., {} or the object's own properties).
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: preventing crashes when Proxies in the prototype chain throw exceptions during Bun.inspect() and related operations.
Description check ✅ Passed The description covers both required template sections with comprehensive detail: it explains what the PR does (fixes crashes in Bun.inspect/console.log/napi functions), provides a clear reproducer, identifies root causes, details the implemented fixes, and mentions how verification was done (regression tests, Fuzzilli discovery).
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.


Comment @coderabbitai help to get the list of available commands and usage tips.

@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 — minimal, targeted crash fix that reorders an existing exception-clear and adds a null guard, matching the pattern already used in forEachPropertyOrdered.

Extended reasoning...

Overview

Two-file change: ~10 LOC in src/bun.js/bindings/bindings.cpp inside JSC__JSValue__forEachPropertyImpl, plus two regression tests in test/js/bun/util/inspect.test.js. The C++ change (1) moves CLEAR_IF_EXCEPTION(scope) to run before the early continue when getPropertySlot returns false, and (2) checks the result of iterating->getPrototype(globalObject) for empty/exception before calling .getObject() on it.

Security risks

None. This is the Bun.inspect / console.log property-enumeration path. The change swallows exceptions from Proxy traps during inspection, which is exactly what the surrounding code (and the sibling forEachPropertyOrdered at bindings.cpp:5472-5477) already does. No new data exposure, no auth/crypto, no user-controlled input handling beyond what already existed.

Level of scrutiny

Low. This is a fuzzer-found segfault fix with a clear root cause (pending exception leaks past a continue, then getPrototype returns empty JSValue{}, then .getObject() derefs null). The fix is the minimal correct reordering and mirrors the identical pattern already present a few hundred lines below in forEachPropertyOrdered. The added getPrototype guard is straightforward defensive null-checking.

Other factors

  • Two new regression tests cover both crash paths (throwing getter behind a Proxy prototype, and throwing getPrototypeOf trap).
  • Bug hunting system found no issues.
  • No design decisions or behavior changes — purely converts a crash into the existing "ignore exceptions during inspect" behavior.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix null deref in Bun.inspect when Proxy getPrototypeOf throws #29071 - Fixes the same null deref in forEachPropertyImpl when Proxy getPrototypeOf throws, with identical two-part fix (CLEAR_IF_EXCEPTION reorder + getPrototype guard)
  2. fix null deref in forEachProperty prototype walk #28991 - Fixes the same getPrototype().getObject() null deref in forEachPropertyImpl slow path
  3. Handle exception from getPrototype in forEachProperty #28918 - Handles exception from getPrototype in forEachPropertyImpl when Proxy trap throws
  4. Fix use-after-poison in forEachProperty prototype chain walk #28919 - Fixes use-after-poison in the same forEachPropertyImpl prototype chain walk
  5. Fix null deref in forEachProperty when Proxy prototype throws #28882 - Fixes null deref in forEachPropertyImpl when Proxy prototype throws
  6. Fix crash in forEachProperty when prototype chain contains a Proxy #28854 - Fixes crash in forEachPropertyImpl when prototype chain contains a Proxy
  7. fix: clear exception before continue in property enumeration slow path #28530 - Moves CLEAR_IF_EXCEPTION before continue in property enumeration slow path (fix Fix ?? operator  #1 of this PR)
  8. Fix null deref in forEachProperty when prototype is a Proxy #28325 - Fixes null deref in forEachPropertyImpl slow path by checking exceptions after getPrototype()
  9. getPrototype exception checks #24985 - Adds getPrototype exception checks addressing the same Bun.inspect crash

🤖 Generated with Claude Code

@robobun

robobun commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator Author

CI failures are unrelated pre-existing flakes, not caused by this change:

  • test/js/bun/net/socket.test.ts — "should not leak memory" heap count 4 vs ≤3 on Windows (GC timing; also failing in build #47551)
  • test/js/bun/webview/webview-chrome.test.ts — "Failed to spawn Chrome" on darwin-aarch64 (agent missing Chrome; also failing in build #47551)
  • test/js/bun/http/tls-keepalive.test.ts — memory growth 75.64MB vs <75MB on darwin-aarch64 (threshold barely exceeded)

This PR only touches JSC__JSValue__forEachPropertyImpl (used by Bun.inspect); inspect.test.js passes on all platforms including debian-13-x64-asan.

@alii

alii commented Apr 23, 2026

Copy link
Copy Markdown
Member

@robobun how many of these are duplicates? Which is the best one if they are dupes?

@robobun

robobun commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator Author

@alii — compared all 9. They all touch the same ~5 lines in JSC__JSValue__forEachPropertyImpl, but there are actually two bugs there:

  • A: getPropertySlot returns false with a pending exception → continue runs before CLEAR_IF_EXCEPTION → exception leaks to the next iteration
  • B: iterating->getPrototype(globalObject).getObject() derefs null when getPrototype returns empty (either because of a throwing getPrototypeOf trap, or because of the leaked exception from A)
PR A B Tests Notes
#29642 (this)
#29071 functionally identical to this one
#28530 partial — still crashes on direct getPrototypeOf throw
#28991
#28918
#28919 also restructures loop + EnsureStillAliveScope
#28882 also guards a similar pattern in napi.cpp
#28854 no tests
#28325
#24985 dylan-conway's; codebase-wide getPrototype exception sweep across 9 files; propagates instead of swallowing; 5 months stale

tl;dr: All 9 are dupes. Only #29642 and #29071 fix both bugs — they're byte-for-byte equivalent in the C++, so pick either (this one has the more minimal/direct regression tests in inspect.test.js). The other 7 fix only one of the two and leave a crash path reachable.

#24985 is the most principled if you want the broader getPrototype cleanup, but it needs a rebase and would still need fix A layered on top.

@alii

alii commented Apr 23, 2026

Copy link
Copy Markdown
Member

@robobun close all the others, bring the two changes into this PR

@robobun

robobun commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator Author

Comment thread src/jsc/bindings/napi.cpp
iterating = iterating->getPrototype(globalObject).getObject();
iteratingValue = iterating->getPrototype(globalObject);
// Ignore exceptions from Proxy "getPrototypeOf" trap.
CLEAR_IF_EXCEPTION(scope);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably not be clearing these exceptions, but looks like it was doing it originally

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — kept the swallow to match the existing fast-path handling a few lines up and forEachPropertyOrdered (so Bun.inspect/console.log stay best-effort rather than throwing mid-format). Switching this whole path to propagate is the scope of #24985; happy to rebase that on top of this if/when it's revived.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bun.js/bindings/bindings.cpp`:
- Line 5242: The slow-path prototype hop budget is off by one compared to the
fast path: update the conditional that checks prototypeCount in the slow-path
loop so it uses the same logic as the fast path (i.e., the same
comparison/operator used in the fast-path `prototypeCount++ < 5` check) to
ensure `forEachProperty*` visits the same number of prototype hops; locate the
`prototypeCount` check in the slow-path loop in bindings.cpp (the `if
(prototypeCount++ >= 5)` line) and change it to match the fast-path condition so
both paths allow the same five-prototype-hop traversal.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c84a1ad1-c643-4464-85ba-93160cceb85a

📥 Commits

Reviewing files that changed from the base of the PR and between 8da7cbc and 93d9a9c.

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

while (JSObject* iterating = iteratingValue.getObject()) {
if (iterating == globalObject->objectPrototype() || iterating == globalObject->functionPrototype() || (iterating->inherits<JSGlobalProxy>() && uncheckedDowncast<JSGlobalProxy>(iterating)->target() != globalObject))
break;
if (prototypeCount++ >= 5)

@coderabbitai coderabbitai Bot Apr 26, 2026

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.

⚠️ Potential issue | 🟡 Minor

Keep the slow-path prototype budget aligned with the fast path.

Line 5242 now stops after visiting the base object plus four prototype hops, while the fast path still reaches five hops (prototypeCount++ < 5 at Lines 5215-5222). That makes forEachProperty* shape-dependent again: properties that only exist on the fifth prototype hop are visible for fast-enumerable objects but skipped for slow-path ones.

♻️ Proposed fix
-            if (prototypeCount++ >= 5)
+            if (prototypeCount++ > 5)
                 break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun.js/bindings/bindings.cpp` at line 5242, The slow-path prototype hop
budget is off by one compared to the fast path: update the conditional that
checks prototypeCount in the slow-path loop so it uses the same logic as the
fast path (i.e., the same comparison/operator used in the fast-path
`prototypeCount++ < 5` check) to ensure `forEachProperty*` visits the same
number of prototype hops; locate the `prototypeCount` check in the slow-path
loop in bindings.cpp (the `if (prototypeCount++ >= 5)` line) and change it to
match the fast-path condition so both paths allow the same five-prototype-hop
traversal.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a false positive. The original condition was while (... && prototypeCount++ < 5), which runs the body for prototypeCount ∈ {0,1,2,3,4}. The refactored if (prototypeCount++ >= 5) break; is its exact negation (!(x < 5)x >= 5) and also runs the body for {0,1,2,3,4}. Changing to > 5 as suggested would run 6 iterations instead of 5, which would be the off-by-one.

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.

@robobun You're absolutely right — I apologize for the false positive. The logical equivalence is exact:

  • Original: prototypeCount++ < 5 → body runs for {0, 1, 2, 3, 4}
  • Refactored: prototypeCount++ >= 5 → negation of the above, break fires for {5, 6, …}, body runs for the same {0, 1, 2, 3, 4}

My suggested > 5 would have been the actual off-by-one, allowing a 6th iteration. The current code is correct.


✏️ Learnings added
Learnt from: robobun
Repo: oven-sh/bun PR: 29642
File: src/bun.js/bindings/bindings.cpp:5242-5242
Timestamp: 2026-04-26T02:43:45.164Z
Learning: In oven-sh/bun `src/bun.js/bindings/bindings.cpp` (`JSC__JSValue__forEachPropertyImpl`), the slow-path prototype-hop guard `if (prototypeCount++ >= 5) break;` is the exact logical negation of the original `while (... && prototypeCount++ < 5)` condition — both allow the loop body to execute for `prototypeCount ∈ {0,1,2,3,4}` (five hops). Do NOT flag this as an off-by-one or suggest changing it to `> 5`; that would introduce a genuine off-by-one (six iterations instead of five).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use JSC::WriteBarrier for heap-allocated references in V8 objects and implement visitChildren() for custom heap objects to support garbage collection

Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/AGENTS.md:0-0
Timestamp: 2025-11-24T18:37:47.899Z
Learning: Applies to src/bun.js/bindings/v8/**/<UNKNOWN> : <UNKNOWN>

Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use localToJSValue() to convert V8 handles to JSC values and perform JSC operations within V8 method implementations

Learnt from: robobun
Repo: oven-sh/bun PR: 28473
File: src/bun.js/bindings/BunCPUProfiler.cpp:31-35
Timestamp: 2026-03-23T16:54:42.592Z
Learning: In oven-sh/bun, Bun has a strict 1:1 JSC::VM to Zig::GlobalObject relationship for worker threads (each worker creates its own VM). ShadowRealm can create additional GlobalObjects on the same VM, but ShadowRealm cannot access `node:inspector`. Therefore, CPU profiler state stored per-Zig::GlobalObject (m_profilingStartTime, m_cpuSamplingInterval, m_isCPUProfilerRunning in ZigGlobalObject.h) is safe and correct — there is no multi-GlobalObject-per-VM scenario where inspector/profiler functions are reachable. Do not flag per-GlobalObject CPU profiler state as a cross-VM sharing risk in future reviews of src/bun.js/bindings/BunCPUProfiler.cpp or src/bun.js/bindings/ZigGlobalObject.h.

Learnt from: taylordotfish
Repo: oven-sh/bun PR: 23169
File: src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h:47-74
Timestamp: 2025-10-01T21:59:54.571Z
Learning: In the new bindings generator (bindgenv2) for `src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h`, the context-aware enumeration conversion overloads intentionally use stricter validation (requiring `value.isString()` without ToString coercion), diverging from Web IDL semantics. This is a design decision documented in comments.

Learnt from: dylan-conway
Repo: oven-sh/bun PR: 29581
File: src/bun.js/modules/NodeModuleModule.cpp:676-677
Timestamp: 2026-04-22T20:47:13.873Z
Learning: In oven-sh/bun `src/bun.js/modules/NodeModuleModule.cpp` (and similar PropertyCallback-based static-property initializers), returning an empty `JSValue` (`{}`) on allocation failure is intentional and correct. `reifyStaticProperty` (Lookup.h) never checks for exceptions between consecutive PropertyCallbacks; on the ESM path `generateNativeModule_NodeModule` already calls `topExceptionScope.tryClearException()` after the reification loop, so any exception thrown by the old `constructArray` path was silently cleared anyway. Do NOT suggest changing the `if (!array) [[unlikely]] return {};` guard to throw an OOM exception — that would reintroduce the ThrowScope bug the PR was designed to fix. The pattern matches `constructVersions` in `BunProcess.cpp` (`RETURN_IF_EXCEPTION(scope, {})` → empty value on OOM).

Learnt from: robobun
Repo: oven-sh/bun PR: 28763
File: src/bun.js/bindings/bindings.cpp:1067-1089
Timestamp: 2026-04-02T18:53:39.190Z
Learning: In `src/bun.js/bindings/bindings.cpp`, the `WTF::BitVector matchedIndices` used in the Set/Map O(n²) slow-path of `specialObjectsDequal` is sized to `set2->size()` / `map2->size()` at the start of the slow path and does NOT need a runtime bounds guard. `Bun__deepEquals` only reads properties of the elements inside the collections; it never calls `set2->add()` / `map2->set()`, so the collection sizes — and thus the bitvector capacity — are stable throughout the entire inner-loop iteration. Do not flag missing `ensureSize` guards in this loop as a correctness risk.

Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25429
File: src/bun.js/bindings/helpers.h:422-422
Timestamp: 2025-12-23T06:50:41.142Z
Learning: In Bun's C++ bindings (src/bun.js/bindings/helpers.h and similar files), when returning an empty JSC::Identifier and a VM is accessible, prefer using `vm.propertyNames->emptyIdentifier` over constructing with `JSC::Identifier(JSC::Identifier::EmptyIdentifierFlag::EmptyIdentifier)`. The cached identifier from the VM's property names table is more efficient and consistent with WebKit upgrade patterns.

Learnt from: robobun
Repo: oven-sh/bun PR: 29200
File: src/bun.js/bindings/JSMockFunction.cpp:985-998
Timestamp: 2026-04-11T22:50:18.935Z
Learning: In JavaScriptCore (JSC), for `InternalFunction` (C++ native) constructors, `callframe->thisValue()` returns `newTarget` (the constructor function itself), NOT a pre-created receiver. This is different from ordinary ECMAScript function constructors which pre-create a receiver via OrdinaryCreateFromConstructor. When implementing a native [[Construct]] handler for an InternalFunction in JSC (e.g. in oven-sh/bun's src/bun.js/bindings/JSMockFunction.cpp), to create a fallback object that correctly inherits from `newTarget.prototype`, use `InternalFunction::createSubclassStructure(globalObject, asObject(callframe->newTarget()), baseStructure)` followed by `JSC::constructEmptyObject(vm, structure)`. Do NOT use `callframe->thisValue()` as a receiver in this context.

Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-21T21:51:55.181Z
Learning: Applies to src/bun.js/bindings/*.cpp : C++ JavaScriptCore bindings should be placed in `src/bun.js/bindings/*.cpp`

Learnt from: robobun
Repo: oven-sh/bun PR: 28763
File: src/bun.js/bindings/bindings.cpp:1120-1129
Timestamp: 2026-04-01T22:37:16.953Z
Learning: In `src/bun.js/bindings/bindings.cpp`, the JSMap fast-path in `specialObjectsDequal` (around the Map deep-equality section) uses `map2->get()` with an `isUndefined()` guard. When `undefined` is returned (either because the key is absent or the value is genuinely `undefined`), the code conservatively falls to the O(n²) slow path. This is intentional and correct: the slow path handles both cases accurately (missing key → `found=false` → return false; present key with undefined value → `Bun__deepEquals(undefined, undefined)` → true). Do not flag this as a correctness bug in future reviews of this file — it is a minor pre-existing performance trade-off for the rare Map-with-undefined-values edge case.

Learnt from: robobun
Repo: oven-sh/bun PR: 28754
File: src/bun.js/web_worker.zig:449-453
Timestamp: 2026-04-01T19:29:31.324Z
Learning: In oven-sh/bun `src/bun.js/web_worker.zig`, the non-concurrent `unref()` call inside `notifyNeedTermination()` (via `setRefInternal(false)`) is a known pre-existing issue, flagged by the `// TODO(190n) delete` comment. The `onUnhandledRejection` handler intentionally bypasses `notifyNeedTermination()` and instead inlines the concurrent-safe sequence (`setRequestedTerminate()` + `parent_poll_ref.unrefConcurrently()` + `vm.eventLoop().wakeup()`). Do not flag the `notifyNeedTermination()` non-concurrent unref path as a new issue introduced by PRs that touch `onUnhandledRejection`.

Learnt from: robobun
Repo: oven-sh/bun PR: 29543
File: test/js/bun/util/stringWidth.test.ts:960-969
Timestamp: 2026-04-21T10:09:41.335Z
Learning: In `test/js/bun/util/stringWidth.test.ts` (oven-sh/bun PR `#29543`), the "rope with ANSI sequence straddling fibers falls back correctly" test intentionally does NOT assert `isRope(rope) === false` after calling `Bun.stringWidth`. The `expect(w).toBe(11)` assertion is the regression guard: a broken bail-out that sums raw per-fiber Latin-1 widths would yield 18, not 11. Pinning `isRope === false` would encode "must flatten" as a permanent contract and would break if a future refinement handles straddled ANSI sequences without resolving. Do NOT suggest adding a post-call `isRope(rope) === false` assertion to this test.

Learnt from: robobun
Repo: oven-sh/bun PR: 29256
File: test/regression/issue/29253.test.ts:23-34
Timestamp: 2026-04-13T11:41:44.588Z
Learning: In oven-sh/bun (PR `#29256`), `require('module').prototype` (the "disposable" prototype returned by `getModulePrototypeObject`) and the prototype of `new Module(...)` instances (`JSCommonJSModulePrototype`) are intentionally kept as two separate objects — prototype identity unification was reverted in commit 1bf6b8b because unifying them exposes the `_compile` CustomAccessor to prototype-level reads, which silently bails on the JSCommonJSModule cast and breaks `Module.prototype._compile` monkey-patching (Istanbul/nyc). Both prototypes now independently expose `load`, so `typeof m.load === 'function'` and `typeof require('module').prototype.load === 'function'` both hold, but `Object.getPrototypeOf(new Module(...)) !== Module.prototype`. Do NOT suggest or flag the absence of prototype identity (`Object.getPrototypeOf(m) === Module.prototype`) as a missing requirement; full unification is a separate, larger refactor that must first solve the `_compile` accessor issue.

Learnt from: robobun
Repo: oven-sh/bun PR: 29389
File: src/bun.js/bindings/V8HeapSnapshotBuilder.cpp:545-565
Timestamp: 2026-04-17T04:11:51.062Z
Learning: In oven-sh/bun PR `#29389`, `src/bun.js/bindings/V8HeapSnapshotBuilder.cpp` is an intentional near-verbatim copy of `vendor/WebKit/Source/JavaScriptCore/heap/BunV8HeapSnapshotBuilder.cpp`. The `addString` hash-collision issue (composite `hash + length` key without content verification in `m_stringsLookupTable`) is pre-existing upstream behavior, present identically in the shipping `JSC::BunV8HeapSnapshotBuilder`. The local copy exists only to land the crash fix (1024-char truncation + `RecordOverflow` StringBuilder policy) in an auditable, minimal diff; all other pre-existing issues (collision, potential race) are out of scope and belong in a follow-up PR against the WebKit fork. Do NOT re-flag the `addString` collision as a new issue in this file.

Learnt from: dylan-conway
Repo: oven-sh/bun PR: 29581
File: src/bun.js/modules/NodeModuleModule.cpp:663-681
Timestamp: 2026-04-22T20:47:16.874Z
Learning: In oven-sh/bun, do NOT suggest adding standalone regression tests that rely on the `BUN_JSC_validateExceptionChecks=1` environment variable to exercise JSC throw-scope / exception-scope validator paths (e.g., `reifyAllStaticProperties` PropertyCallback interactions). Per `CLAUDE.md`, all tests must pass with `USE_SYSTEM_BUN=1`, and `BUN_JSC_validateExceptionChecks` is a no-op on release/system Bun builds. The correct project mechanism for tracking this class of issue is: (1) the x64-asan CI shard, which sets the validator automatically, and (2) the `test/no-validate-exceptions.txt` opt-out list for tests that trigger pre-existing throw-scope assertion failures unrelated to the change under review. An in-source comment pointing at the specific exerciser (e.g., the tsgo/bun-types test) is sufficient documentation.

Learnt from: robobun
Repo: oven-sh/bun PR: 29359
File: test/js/node/test/parallel/test-event-emitter-method-names.js:31-33
Timestamp: 2026-04-16T01:35:53.279Z
Learning: In `test/js/node/test/parallel/test-event-emitter-method-names.js` (oven-sh/bun), the `return` statements inside the `for...of` loop over `Object.getOwnPropertyNames(E)` (which should be `continue`) are a known upstream Node.js bug present since at least v24.14.1. This file is synced verbatim from upstream and must not be modified to fix upstream bugs in sync PRs. Do NOT flag these `return` statements as a correctness issue in future reviews of this file or similar verbatim upstream sync PRs.

Learnt from: robobun
Repo: oven-sh/bun PR: 28763
File: test/regression/issue/28760.test.ts:34-47
Timestamp: 2026-04-02T18:53:26.727Z
Learning: In oven-sh/bun (`src/bun.js/bindings/bindings.cpp`), both `assert.deepEqual` and `assert.deepStrictEqual` route through the same `specialObjectsDequal` code path for `Map` (and `Set`) deep-equality comparisons. Therefore, for regression tests covering `Map`/`Set` deep-equality behavior, a single `deepStrictEqual` test is sufficient coverage — adding a `deepEqual` variant for the same scenario provides no additional code coverage.

Learnt from: robobun
Repo: oven-sh/bun PR: 29726
File: test/cli/install/bun-install.test.ts:8789-8837
Timestamp: 2026-04-26T00:01:53.408Z
Learning: In oven-sh/bun, file test/cli/install/bun-install.test.ts prefers a simple for-of loop for small parameterizations (e.g., ["hoisted","isolated"]) under the top-level describe.concurrent("bun-install") to avoid adding an extra describe.each nesting. Do not suggest switching these cases to describe.each() in this file.

Learnt from: robobun
Repo: oven-sh/bun PR: 27785
File: src/js/thirdparty/undici.js:839-841
Timestamp: 2026-03-04T10:41:55.568Z
Learning: In `src/js/thirdparty/undici.js`, the `SqliteCacheStore#prune()` method uses `this.size <= this.#maxCount` as its early-return guard (not `<`). This intentionally mirrors the upstream undici source at `vendor/node/deps/undici/src/lib/cache/sqlite-cache-store.js:353`. Do not suggest changing `<=` to `<` for upstream compatibility reasons.

Learnt from: robobun
Repo: oven-sh/bun PR: 28436
File: src/bun.js/bindings/webcore/SerializedScriptValue.cpp:6408-6414
Timestamp: 2026-03-22T16:46:16.777Z
Learning: In `src/bun.js/bindings/webcore/SerializedScriptValue.cpp`, the `resolvedTransferredStreams` vector in the stream-transfer resolution loop (around the `create()` function) intentionally stores the original `JSObject*` (via `stream.get()`) for `WritableStream` and `TransformStream` on the same-thread `structuredClone` path. Identity pass-through is correct here because cross-thread transfer is rejected earlier (WorkerPostMessage guard). Adding `setTransferred()` infrastructure for WritableStream/TransformStream is deferred follow-up work. Do not flag the original-object append for these two stream types as a bug in future reviews.

Learnt from: robobun
Repo: oven-sh/bun PR: 28436
File: src/bun.js/bindings/webcore/SerializedScriptValue.cpp:5249-5258
Timestamp: 2026-03-22T15:48:08.399Z
Learning: In `src/bun.js/bindings/webcore/SerializedScriptValue.cpp`, the `m_transferredStreams` field (`Vector<JSC::Strong<JSC::JSObject>>`) storing live JS stream wrappers (ReadableStream, WritableStream, TransformStream) for `Bun__ReadableStreamTransferTag` / `Bun__WritableStreamTransferTag` / `Bun__TransformStreamTransferTag` is intentionally safe: it is only populated and consumed on the same-thread structuredClone path. Cross-thread postMessage does not use this path, so there is no cross-realm or cross-thread object leak risk. Do not flag this as a cross-realm/cross-thread issue in future reviews.

Learnt from: robobun
Repo: oven-sh/bun PR: 29190
File: src/js_printer.zig:0-0
Timestamp: 2026-04-11T19:46:51.547Z
Learning: In oven-sh/bun (src/js_printer.zig), the ESM→CJS rewrite for exported variable declarations in printDeclStmt now uses a recursive helper printBundledExportsForBinding to emit Object.defineProperty getters on module.exports for all identifier leaves, including within nested object/array destructuring. Future changes to this area should preserve the recursion to avoid dropping nested exported names in --no-bundle/transform_only output.

Learnt from: robobun
Repo: oven-sh/bun PR: 29060
File: src/bun.js/web_worker.zig:500-517
Timestamp: 2026-04-09T00:41:01.359Z
Learning: In oven-sh/bun `src/bun.js/web_worker.zig` + `src/bun.js/VirtualMachine.zig` (PR `#29060`): when a Worker `error` event listener cancels propagation via `preventDefault()`, the exit-code rollback is handled entirely inside `VirtualMachine.uncaughtException` — it saves `prev_exit_code` before writing `exit_code = 1`, calls `onUnhandledRejection`, then restores `prev_exit_code` if `worker.error_event_prevented` is true. The `onUnhandledRejection` callback in `web_worker.zig` does NOT touch `exit_handler.exit_code` at all. The `.bun`-mode async-rejection path never sets `exit_code = 1`, so no restore is needed there.

Learnt from: cirospaciari
Repo: oven-sh/bun PR: 0
File: :0-0
Timestamp: 2026-03-30T19:21:21.507Z
Learning: In oven-sh/bun PR `#28614`, the cloneFrom/deref race on `ProxyEnvStorage` slot pointers is resolved via `ProxyEnvStorage.lock: bun.Mutex` (rare_data.zig line 122). `Bun__setEnvValue` (BunObject.zig) holds this mutex while modifying slot pointers; the worker snapshot path (web_worker.zig) holds the parent's lock across the entire `cloneFrom` + `cloneWithAllocator` + `syncInto` sequence. `cloneFrom`'s doc comment enforces "Caller must hold parent.lock" as a documented invariant. Do not re-flag this race in future reviews.

Learnt from: robobun
Repo: oven-sh/bun PR: 29152
File: test/no-validate-exceptions.txt:131-131
Timestamp: 2026-04-11T06:11:37.427Z
Learning: In oven-sh/bun, SIGILL crashes in ASAN CI for webcrypto tests (e.g., `test/js/bun/crypto/x25519-derive-bits.test.ts`) are caused by `BUN_JSC_validateExceptionChecks=1` triggering JSC throw-scope assertion failures → `WTFCrash()` → `ud2` → SIGILL. This env var is set by `scripts/runner.node.mjs` for ASAN builds unless the test is listed in `test/no-validate-exceptions.txt`. Adding the test under the `# normalizeCryptoAlgorithmParameters` section of that file is the correct and complete fix, matching the pattern used by all other webcrypto tests. The root cause is a pre-existing throw-scope issue in `SubtleCrypto.cpp` that affects all algorithms identically (ECDH, X25519, etc.) and is tracked as a separate effort. Do NOT flag additions to `test/no-validate-exceptions.txt` for webcrypto tests as insufficient when the SIGILL is of this type.

Learnt from: robobun
Repo: oven-sh/bun PR: 29451
File: test/js/bun/net/socket-retention.test.ts:47-53
Timestamp: 2026-04-18T18:14:06.150Z
Learning: In `test/js/bun/net/socket-retention.test.ts` (oven-sh/bun PR `#29451`), do NOT add a filtered empty-stderr assertion (even with ASAN-noise filtering) to subprocess crash-detection tests. For tests where the regression manifests as a crash (empty stdout + non-zero exit), the positive `expect(stdout.trim()).toBe(...)` and `expect(exitCode).toBe(0)` assertions are the correct and sufficient failure signal. Adding `expect(cleanKnownAsanNoise(stderr)).toBe("")` adds maintenance surface without improving diagnostic signal. The CLAUDE.md convention of asserting filtered empty-stderr applies to tests using `bunEnv` for behavioral correctness checks, not for crash-detection tests that use stdout/exitCode as their primary regression guard.

Learnt from: robobun
Repo: oven-sh/bun PR: 29538
File: test/js/bun/resolve/lower-using-bun-target.test.ts:80-82
Timestamp: 2026-04-21T09:47:22.507Z
Learning: In `test/js/bun/resolve/lower-using-bun-target.test.ts` (oven-sh/bun PR `#29538`), for runtime subprocess tests that spawn `bunExe()` with `bunEnv`, do NOT add `expect(stderr).toBe("")` assertions. The `WARNING: ASAN interferes with JSC signal handlers…` message is emitted to stderr on every JS-executing process launch in a debug ASAN build and is NOT suppressed by `bunEnv` / `BUN_DEBUG_QUIET_LOGS=1`. The correct regression guards are an exact-match on `stdout` and `expect(exitCode).toBe(0)`. Files that need to check stderr should filter out lines starting with "WARNING: ASAN interferes" using the repo's filter-based convention, but leaving stderr entirely unchecked is also acceptable when stdout + exitCode provide sufficient coverage.

Learnt from: robobun
Repo: oven-sh/bun PR: 28473
File: test/regression/issue/28472.test.ts:20-21
Timestamp: 2026-03-23T17:06:31.224Z
Learning: In `test/regression/issue/28472.test.ts` (oven-sh/bun PR `#28473`), the regression being tested is that `Profiler.stop` returns null/error in worker threads rather than a valid profile object. The test correctly asserts only that a non-null profile with a `nodes` array is returned (via `result.profile.nodes.length`). Do NOT suggest asserting on sample counts or timing fields — under ASAN or slow CI, short-lived profiling may collect zero real samples, making such assertions flaky. The empty-profile fallback (a root node with no sampled data) is correct behavior; the bug was returning null/error entirely.

Learnt from: robobun
Repo: oven-sh/bun PR: 28686
File: test/regression/issue/12306.test.ts:50-55
Timestamp: 2026-04-02T18:42:38.406Z
Learning: In `test/regression/issue/12306.test.ts` (oven-sh/bun PR `#28686`), the `done` Promise only rejects via the client `error` event — a genuine test failure. In that case, bun:test catches the unhandled rejection and reports the error, and the test process exits immediately, making the skipped `server.close()` harmless. Do not flag the missing try/finally around `await done` in this test as a resource-leak risk; the process-exit path is the correct and sufficient cleanup signal for this regression test.

Learnt from: robobun
Repo: oven-sh/bun PR: 29389
File: src/bun.js/bindings/V8HeapSnapshotBuilder.cpp:592-594
Timestamp: 2026-04-17T04:59:15.762Z
Learning: When reviewing C++ code that uses WebKit Template Framework (WTF)/JSC types, do not treat `if (str.isEmpty()) str = String();` as dead code or a no-op. In WTF, `WTF::String::isEmpty()` returns true for both null strings and empty non-null strings, while `String()` constructs a null string. This conditional therefore normalizes empty non-null strings into null strings and may change behavior for downstream callers that distinguish null vs empty—so it should not be flagged as redundant solely on that basis.

robobun added 3 commits May 4, 2026 10:35
When enumerating properties via forEachProperty, a Proxy in the
prototype chain eagerly invokes [[Get]] to fill the property slot. If
the underlying getter throws, getPropertySlot returns false with a
pending exception, and we would `continue` before clearing it. The
pending exception then caused the subsequent getPrototype() on the
Proxy to return an empty JSValue, and calling .getObject() on that is
a null-pointer member call.

Clear the exception before the early continue (matching the pattern
already used in forEachPropertyOrdered), and also guard the
getPrototype() result so a throwing getPrototypeOf trap stops
iteration instead of crashing.
…get_all_property_names

Hold the current link in the prototype chain as a JSValue under an
EnsureStillAliveScope so a Proxy trap that runs JS (and may GC) during
property enumeration can't free it out from under us.

Also guard the equivalent getPrototype().getObject() pattern in
napi_get_all_property_names, propagating Proxy trap exceptions as
napi_pending_exception.

Supersedes #29071 #28991 #28919 #28918 #28882 #28854 #28530 #28325.
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.

3 participants