Skip to content

Fix stale-structure abort when a lazy Bun property builder throws mid-lookup - #37001

Open
robobun wants to merge 5 commits into
mainfrom
farm/f76ebd86/fix-stale-structure-getpropertyslot
Open

Fix stale-structure abort when a lazy Bun property builder throws mid-lookup#37001
robobun wants to merge 5 commits into
mainfrom
farm/f76ebd86/fix-stale-structure-getpropertyslot

Conversation

@robobun

@robobun robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

What

Fuzzilli kept hitting a flaky abort on debug builds (fingerprint StructureInlinesLight.h(56)):

ASSERTION FAILED: isCompilationThread() || Thread::mayBeGCThread() || object->structure() == this
JSValue JSC::Structure::storedPrototype(const JSObject *) const

The fuzzer sample tripped it while reading every property on the Bun object, which first-touches all of its lazy static-table properties.

Root cause

The inlined prototype-chain loop in JSObject::getPropertySlot (JSObject.h) reads the object's Structure* once, calls getOwnNonIndexPropertySlot, and on a miss uses that same pointer for structure->storedPrototype(object). A miss is not side-effect free for static-table objects: reifying a lazy PropertyCallback property transitions the object, and with our fork's throwing builders, setUpStaticFunctionSlot can return false after the transition happened (the builder threw, so the slot is reported as not found and the caller observes the exception). The prototype step then runs on the stale pre-transition structure. Release builds read the prototype off the old structure, which happens to be harmless for mono-proto objects, so only asserts builds crash.

Deterministic trigger: a first read of Bun.sql evaluates the sql internal modules, whose class SQLError extends Error reads Error.prototype at module scope. Hooking that with a proxy that first touches another lazy Bun property (transitioning the Bun object) and then throws reproduces the abort every time:

let phase = 0;
globalThis.Error = new Proxy(function () {}, {
  get(target, key, receiver) {
    if (key === "prototype" && phase === 0) {
      phase = 1;
      Bun.semver;
      throw "boom";
    }
    return Reflect.get(target, key, receiver);
  },
});
try { Bun.sql; } catch (e) {}

Later fuzzer samples reach the same assert with no hooks, by first-touching Bun.sql from the frame just above a stack overflow so the builder throws a RangeError. Tracing that variant in a debugger shows it takes a different route to the same line: the Bun object does not transition; instead the builder's debug-only exception report (fix 2 below) ran with the exception still pending and read process._fatalException, which reified the property but was reported as a miss because of the pending exception, so the prototype step asserted on the process object's stale structure. Plain code like this aborts an unfixed debug build every time:

let result = "not attempted";
function f() {
  try { f(); } catch {}
  if (result === "not attempted") {
    try { Bun.sql; result = "no throw"; } catch (e) { result = e.name; }
  }
}
f();
console.log(result); // RangeError with the fix

Fixes

  1. WebKit (JSObject::getPropertySlot: reload the structure before the prototype step WebKit#390, pinned here as its preview autobuild): reload the structure before the prototype step in JSObject::getPropertySlot. The megamorphic slow paths in JITOperations.cpp already do exactly this reload, with the comment "Reload it again since static-class-table can cause transition", and getNonIndexPropertySlot uses getPrototypeDirect() which re-reads it. This was the one caller left reusing the stale pointer.

  2. BunObject.cpp: fixing the assert surfaced a second bug in the same path. defaultBunSQLObject and constructBunSQLObject had a BUN_DEBUG-only reportUncaughtExceptionAtEventLoop call after requireId. Reporting consumes the pending exception, so RETURN_IF_EXCEPTION fell through and sqlValue.getObject() ran on an empty JSValue (null deref, caught by UBSan), and a builder returning empty with no pending exception would hit RELEASE_ASSERT_NOT_REACHED in setUpStaticFunctionSlot. Dropped the debug reporting so debug builds propagate the exception the same way release builds do.

Verification

  • The deterministic repro aborts on the current debug build and exits cleanly (caught: boom, exit 0) with both fixes.
  • The original fuzzer sample no longer aborts across repeated runs, including with BUN_JSC_collectContinuously=1.
  • Two regression tests added to test/js/bun/util/BunObject.test.ts. Both fail on an unfixed debug build (the spawned process exits 134 with this assertion) and pass with the fix. The Error proxy test transitions the Bun object inside the builder (phase: 1), so it needs the engine reload regardless of fix 2. The stack overflow test does not transition the Bun object; it covers fix 2 (its abort on main goes through the debug-only report, see above), takes about 0.6s under debug ASAN, and passed on every job of build 91796. The intermediate configuration is covered too: on a build with the patched WebKit but the BunObject.cpp blocks still present, both tests fail with UBSan's null member call at JSCJSValueCell.h:92 (the consumed-exception bug from fix 2), so neither fix alone makes them pass. Release builds never fired the assert, so the tests pass under USE_SYSTEM_BUN=1 by design; they guard the debug and ASAN lanes that the fuzzer runs.
  • SQL adapter tests (test/js/sql/adapter-env-var-precedence.test.ts), test/js/bun/util/BunObject.test.ts, test/js/bun/globals.test.js, and test/js/bun/namespace-prototype-pollution.test.ts pass against a local build of the patched WebKit.
  • CI on the current head (build 96720, rebased onto the current f0f60fd2 pin): no lane reported either regression test failing. The one red lane is debian 13 x64-asan, where four tests this change does not touch failed (three leak tests timing out and an elysia request cancellation test); the three that can be run locally pass on a release build of this head, and all four are reported separately as main breaks. Eight Windows test shards and the Windows baseline check never got agents (Azure throttling); everything else passed.

Landing sequence

The autobuild-preview-pr-390-* pin exists so CI on this branch can fetch the patched WebKit. It must not reach main: GitHub deletes the preview release once oven-sh/WebKit#390 merges or closes (scripts/build/download.ts documents exactly this failure mode), which would leave main commits that 404 on vendor fetch. So the order is: land oven-sh/WebKit#390 first, then swap WEBKIT_VERSION here to the merged sha (the durable autobuild-<sha> release, after its prebuilt assets exist for every platform and flavor), then merge this PR.


[decide:webkit] gate passed · iteration 14 · 3 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/util/BunObject.test.ts
bun test v1.4.0 (379f32246)

test/js/bun/util/BunObject.test.ts:
(pass) hasNonReifiedStatic [253.03ms]
52 |   const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
53 | 
54 |   // "phase: 1" proves the builder re-entered the Bun object mid-lookup; if the
55 |   // sql module stops reading Error.prototype at evaluation time, this test no
56 |   // longer exercises the code path and needs a new trigger.
57 |   expect({ stdout, stderr, exitCode }).toEqual({
                                            ^
error: expect(received).toEqual(expected)

  {
-   "exitCode": 0,
-   "stderr": "",
-   "stdout": 
- "caught: boom phase: 1
+   "exitCode": 1,
+   "stderr": 
+ "2 | globalThis.Error = new Proxy(function () {}, {
+ 3 |   get(target, key, receiver) {
+ 4 |     if (key === "prototype" && phase === 0) {
+ 5 |       phase = 1;
+ 6 |       Bun.semver;
+ 7 |       throw "boom";
+                 ^
+ error: boom
+       at get (/workspace/bun/[eval]:7:13)
+   
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (b7a043103)

test/js/bun/util/BunObject.test.ts:
(pass) hasNonReifiedStatic [42.61ms]
(pass) lazy property builder that transitions Bun and throws does not abort [11.02ms]
(pass) lazy property builder that throws from stack overflow does not abort [12.34ms]
(pass) require('bun') [0.16ms]
Module {
  $: [Function: BunShell2],
  Archive: [class Archive],
  ArrayBufferSink: [class ArrayBufferSink],
  CSRF: {
    generate: [Function: generate],
    verify: [Function: verify],
  },
  Cookie: [class Cookie],
  CookieMap: [class CookieMap],
  CryptoHasher: [class CryptoHasher],
  FFI: {
    viewSource: [Function: viewSource],
    dlopen: [Function: dlopen],
    callback: [Function: callback],
    linkSymbols: [Function: linkSymbols],
    toBuffer: [Function: toBuffer],
    toArrayBuffer: [Function: toArrayBuffer],
    closeCallback: [Function: closeCallback],
    cfunction: [Function: cfunction],
    CString: [class CString],
    ptr: [Function: ptr],
    read: {
      u8: [Function: u8],
      u16: [Function: u16],
      u32: [Function: u32],
      ptr: [Function: ptr],
      i8: [Function: i8],
      i16: [Function: i16],
      i32: [Function: i32
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/util/BunObject.test.ts
bun test v1.4.0 (379f32246)

test/js/bun/util/BunObject.test.ts:
(pass) hasNonReifiedStatic [318.47ms]
(pass) lazy property builder that transitions Bun and throws does not abort [495.98ms]
(pass) lazy property builder that throws from stack overflow does not abort [480.16ms]
(pass) require('bun') [9.62ms]
Module {
  $: [Function: BunShell2],
  Archive: [class Archive],
  ArrayBufferSink: [class ArrayBufferSink],
  CSRF: {
    generate: [Function: generate],
    verify: [Function: verify],
  },
  Cookie: [class Cookie],
  CookieMap: [class CookieMap],
  CryptoHasher: [class CryptoHasher],
  FFI: {
    viewSource: [Function: viewSource],
    dlopen: [Function: dlopen],
    callback: [Function: callback],
    linkSymbols: [Function: linkSymbols],
    toBuffer: [Function: toBuffer],
    toArrayBuffer: [Function: toArrayBuffer],
    closeCallback: [Function: closeCallback],
    cfunction: [Function: cfunction],
    CString: [class CString],
    ptr: [Function: ptr],
    read: {
      u8: [Function: u8],
 
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     379f32246a
  features     baseline

22 deps, 123 codegen, 1176 objects in 721ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1238] install /workspace/bun
bun install v1.4.0-canary.1 (b7a043103)

Checked 107 installs across 153 packages (no changes) [9.00ms]
[2/1238] gen ErrorCode+*.h
[3/1238] gen bindgenv2
[4/1238] install /workspace/bun/packages/bun-error
bun install v1.4.0-canary.1 (b7a043103)

Checked 1 install across 2 packages (no changes) [7.00ms]
[5/1238] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (b7a043103)

Checked 129 installs across 147 packages (no changes) [7.00ms]
[6/1238] fetch tinycc
[tinycc] up to date
[7/1237] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[8/1237] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[9/1237] gen .bind.ts → GeneratedBindings.cpp
[10/1237] fetch zlib
[zlib] up to date
[11/1237] gen ProcessBindingHTTPParser.lut.h
G
... (truncated)
diff hotspot
scripts/build/deps/webkit.ts       |  7 ++-
 src/jsc/bindings/BunObject.cpp     |  6 ---
 test/js/bun/util/BunObject.test.ts | 89 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 95 insertions(+), 7 deletions(-)

gate history · 8 passed · 2 rejected · iteration 14

evidence per changed file
file                                reads  edits  tests
scripts/build/deps/webkit.ts            5      6      0
src/jsc/bindings/BunObject.cpp          1      3      0
test/js/bun/util/BunObject.test.ts      4      6      0

@github-actions github-actions Bot added the claude label Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The WebKit pin now uses a PR 390 preview build. Bun SQL constructors no longer report exceptions through BUN_DEBUG. A subprocess regression test covers lazy Bun.sql evaluation and clean error handling.

Bun SQL exception handling

Layer / File(s) Summary
WebKit assertion fix
scripts/build/deps/webkit.ts
WEBKIT_VERSION now uses the autobuild-preview-pr-390-0c51423 preview build. Comments document the related assertion behavior.
Bun SQL exception handling and regression test
src/jsc/bindings/BunObject.cpp, test/js/bun/util/BunObject.test.ts
Bun SQL constructors no longer report exceptions through BUN_DEBUG blocks. The subprocess regression test verifies clean handling during lazy Bun.sql evaluation.

Possibly related PRs

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the primary stale-structure abort fix caused by a lazy Bun property builder.
Description check ✅ Passed The description clearly explains the root cause, fixes, verification results, regression tests, and WebKit landing sequence.

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

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix segfault when a lazy Bun.* getter throws during reification #33211 - Deletes the exact same #if BUN_DEBUG reportUncaughtExceptionAtEventLoop blocks in defaultBunSQLObject/constructBunSQLObject and adds a regression test to the same test/js/bun/util/BunObject.test.ts, for the same crash when a lazy Bun.* property builder throws.
  2. Fix Bun.inspect null deref with Proxy prototypes; stop reporting from inside the sql lazy-property builders #30245 - Rewrites the same two SQL lazy-property builders and removes the identical #if BUN_DEBUG blocks to stop a throwing lazy Bun initializer from causing a null-JSCell deref/abort.
  3. worker_threads: don't abort when terminate() interrupts a lazy property builder #33418 - Edits the same defaultBunSQLObject/constructBunSQLObject builders so an exception cannot escape into reifyStaticProperty — same functions and failure mode, triggered by worker.terminate() instead of a user throw.

🤖 Generated with Claude Code

@robobun

robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Those three are earlier passes at the same crash family, each working around it on the Bun side only: #33211 wraps the lazy builders so a throw is cleared and reified as undefined, #30245 rewrites the SQL builders (plus an unrelated Bun.inspect fix) to avoid the null JSCell deref, and #33418 covers the worker.terminate() interruption variant.

This PR fixes the engine-level gap those were steering around. JSObject::getPropertySlot kept using the structure it read before getOwnNonIndexPropertySlot, and a static-table reification that transitions the object and then misses (the builder threw) leaves that pointer stale, which is the assert the fuzzer keeps hitting. That fix is oven-sh/WebKit#390, pinned here as its preview autobuild. With it in place a throwing builder is a legal state: the exception propagates to the caller like any other failed property read, so no swallowing is needed. The only textual overlap with #33211 is the two deleted BUN_DEBUG lines in the SQL builders; the rest is disjoint.

Comment thread test/js/bun/util/BunObject.test.ts Outdated
@robobun

robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Good catch on the undrained stderr pipe, fixed in 2564ffa: the test now drains both pipes and asserts the combined { stdout, stderr, exitCode } object, so a regression surfaces the assertion text instead of just a wrong exit code.

On CI: the red builds so far are all the vendor fetch step 404ing on the pinned WebKit preview tag, which publishes when oven-sh/WebKit#390's autobuild finishes. The next push or retry after that lands should build normally.

@robobun

robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 6:05 PM PT - Aug 14th, 2026

@robobun, your commit 379f322 has 5 failures in Build #96720 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 37001

That installs a local version of the PR into your bun-37001 executable, so you can run:

bun-37001 --bun

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

Thanks for addressing the stderr-drain nit — the test now asserts the combined { stdout, stderr, exitCode } object. I didn't find any further issues in this revision, but this needs a human reviewer: the load-bearing fix is a WebKit engine change (oven-sh/WebKit#390) pinned here via a preview autobuild tag, and there are three overlapping open PRs (#33211, #30245, #33418) targeting the same crash family that need reconciling.

What was reviewed

  • defaultBunSQLObject / constructBunSQLObject: removing the BUN_DEBUG-only reportUncaughtExceptionAtEventLoop is correct — it consumed the pending exception so RETURN_IF_EXCEPTION fell through to a null-JSValue .getObject().
  • prebuiltDestDir / prebuiltUrl in webkit.ts handle the autobuild- prefixed tag correctly (no double-prefix, cache dir keyed on the full tag).
  • New test: subprocess pipes drained, phase: 1 assertion guards that the trigger path is still exercised.
Extended reasoning...

Overview

This PR fixes a debug-build assertion abort (ASSERT(object->structure() == this) in Structure::storedPrototype) that the fuzzer hits when a lazy Bun.* static-table property builder transitions the Bun object and then throws mid-getPropertySlot. Three files touched:

  • scripts/build/deps/webkit.ts: bumps WEBKIT_VERSION from a commit sha to autobuild-preview-pr-390-c3a156ea, a preview build of oven-sh/WebKit#390. That WebKit PR is the actual engine fix — it reloads the object's structure before the prototype step in JSObject::getPropertySlot.
  • src/jsc/bindings/BunObject.cpp: deletes two #if BUN_DEBUG blocks in defaultBunSQLObject / constructBunSQLObject that called reportUncaughtExceptionAtEventLoop after requireId. That call consumed the pending exception, so RETURN_IF_EXCEPTION fell through and sqlValue.getObject() ran on an empty JSValue.
  • test/js/bun/util/BunObject.test.ts: adds a spawned-subprocess regression test using a Proxy on globalThis.Error to make the Bun.sql builder re-enter the Bun object (via Bun.semver) and throw.

Security risks

None identified. The BunObject.cpp change removes debug-only diagnostic code; release builds are unchanged there. The test is hermetic (subprocess with bunEnv, no network). The WebKit change itself is out of scope for this diff.

Level of scrutiny

High, and beyond what I can sign off on:

  1. The core fix is a WebKit engine change in a separate repository. This diff only pins a preview autobuild tag; the actual JSObject::getPropertySlot modification isn't reviewable here. A JSC change to the property-lookup fast path affects every property read in the runtime and warrants maintainer review of the upstream PR.
  2. The pin is to a preview build of an unmerged WebKit PR, not a merged commit. A human should confirm this is the intended workflow (vs. waiting for oven-sh/WebKit#390 to merge and pinning the resulting sha).
  3. Three open PRs (#33211, #30245, #33418) attack the same crash family with Bun-side workarounds. The author's response explains why this engine-level fix supersedes them, but a maintainer needs to decide which to close.

Other factors

  • My prior review (the undrained-stderr nit) was addressed in 2564ffa; the test now drains both pipes and asserts the combined object.
  • I checked prebuiltUrl / prebuiltDestDir in webkit.ts for the new tag format: prebuiltUrl already handles the autobuild- prefix (no double-prefixing), and prebuiltDestDir uses the full tag when it starts with autobuild-, so the cache-dir key is correct.
  • The BunObject.cpp deletion is straightforward and matches the release-build code path exactly — no behavioral change outside BUN_DEBUG.
  • The PR description explicitly notes the test passes under USE_SYSTEM_BUN=1 by design (release builds never fired the assert). That's an acknowledged exception to the usual rule since the guarded failure is a debug-only assert.
  • CI was still building at the time of this review (initial runs 404'd on the not-yet-published preview tag).

@robobun

robobun commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

On the preview-pin question: the plan is the usual two-step. This PR pins the preview tag so CI exercises the exact JSC change; once oven-sh/WebKit#390 merges, the pin moves to the merge commit sha (either here before landing or as the routine WebKit upgrade that follows). The preview tag stays downloadable either way.

CI at 0a3a39f: 187 of 196 jobs passed so far. The only failures are the two debian x64-asan test lanes, both from tests unrelated to this diff: worker-transfer-terminate-stress.test.ts (a known intermittent terminate-during-transfer abort, documented in that test's own header and the subject of #33418) and AsyncLocalStorage-tracking.test.ts (pre-existing RSA keygen leak, also failing on main). Both are reported for main-side triage.

@robobun

robobun commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

Fuzzing hit the same assert through a different trigger, minimized to:

globalThis.Symbol = undefined;
try { Bun.sql } catch {}

Clobbering Symbol makes the BunSql module's setup throw (Symbol("resolve") evaluates with Symbol undefined) while the builder runs, and the Bun object's structure transitions before the exception unwinds, so the lookup aborts with the identical storedPrototype assert at StructureInlinesLight.h(56) on a main debug build (reproduced at 0ffabf6).

Verified against this branch with the pinned WebKit preview: the one-liner exits 0 with the TypeError caught. Unlike the Error.prototype hook in the regression test, this trigger does not depend on the sql module reading Error.prototype at evaluation time, so it could serve as a second case if that ever changes.

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

I reviewed this PR and found no bugs; the earlier stderr-drain nit was addressed. Because it pins the entire runtime to an unmerged WebKit preview build (oven-sh/WebKit#390) and overlaps with three earlier PRs on the same crash family, a human should sign off on the landing sequence.

What was reviewed:

  • BunObject.cpp: verified removing reportUncaughtExceptionAtEventLoop leaves RETURN_IF_EXCEPTION as the exception path — no other callers of scope between requireId and the check.
  • webkit.ts: the autobuild- prefix handling in prebuiltUrl/prebuiltDestDir already accommodates the new tag format.
  • Test: subprocess drains all pipes, asserts combined object, and phase: 1 guards the trigger staying live.
Extended reasoning...

Overview

Three files: scripts/build/deps/webkit.ts swaps the WebKit pin from commit sha ddea7131… to preview tag autobuild-preview-pr-390-01c983d8; src/jsc/bindings/BunObject.cpp deletes two #if BUN_DEBUG blocks that called reportUncaughtExceptionAtEventLoop (which consumed the pending exception, letting RETURN_IF_EXCEPTION fall through to a null-deref on sqlValue.getObject()); test/js/bun/util/BunObject.test.ts adds a spawned regression test using an Error.prototype proxy trap to transition the Bun object mid-lookup and throw.

Security risks

None identified. The C++ change deletes debug-only code and makes the debug exception path match release. The test is a spawned subprocess with fixed input. The WebKit pin points to a build produced by oven-sh's own release workflow.

Level of scrutiny

High. The load-bearing fix is in a separate repository (oven-sh/WebKit#390, a change to JSObject::getPropertySlot — one of the hottest paths in the engine) and cannot be reviewed from this diff. Pinning WEBKIT_VERSION to a preview of an unmerged PR is a dependency-bump decision the repo's landing-prs guidance calls out for maintainer attention, and the author explicitly describes a two-step plan (preview pin now, move to merge sha later) that a human should confirm. Per REVIEW.md, the test also passes under USE_SYSTEM_BUN=1 by the author's own account — the justification (guards debug/ASAN lanes only, since release never fired the assert) is reasonable but is a maintainer call.

Other factors

Three open PRs (#33211, #30245, #33418) target the same crash family with Bun-side workarounds; this PR supersedes them at the engine layer, so someone should decide which lands and close the rest. The BunObject.cpp deletion is small and clearly correct (reportUncaughtExceptionAtEventLoop clears the exception, so the following RETURN_IF_EXCEPTION becomes a no-op and control reaches .getObject() on an empty JSValue). The regression test now follows harness conventions after the earlier feedback. CI at 0a3a39f passed 187/196 with the two failures documented as pre-existing on main.

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

I reviewed this PR and didn't find any bugs (the earlier stderr-drain nit was addressed in b8619cc). Because the load-bearing fix is a WebKit version bump to an unmerged preview build of oven-sh/WebKit#390 — an engine-level change to JSObject::getPropertySlot that isn't visible in this diff — a maintainer sign-off on the JSC change and the preview-pin landing plan is still warranted.

What was reviewed:

  • BunObject.cpp: the deleted BUN_DEBUG blocks — confirmed reportUncaughtExceptionAtEventLoop consumes the pending exception, so removal restores correct RETURN_IF_EXCEPTION propagation and matches release-build behavior.
  • Regression test: subprocess drains both pipes and asserts the combined { stdout, stderr, exitCode }; phase: 1 guard makes the test self-invalidating if the sql module stops reading Error.prototype.
  • webkit.ts: the autobuild- tag prefix is already handled by prebuiltUrl/prebuiltDestDir, so the pin format is compatible with the existing download/cache-key logic.
Extended reasoning...

Overview

Three files touched: scripts/build/deps/webkit.ts bumps WEBKIT_VERSION from a merged commit sha to an unmerged preview tag autobuild-preview-pr-390-1394aa19; src/jsc/bindings/BunObject.cpp deletes two #if BUN_DEBUG blocks in defaultBunSQLObject/constructBunSQLObject that called reportUncaughtExceptionAtEventLoop before RETURN_IF_EXCEPTION; test/js/bun/util/BunObject.test.ts adds a spawned-subprocess regression test. The substantive fix — reloading the structure before the prototype step in JSObject::getPropertySlot — lives entirely in oven-sh/WebKit#390 and is not part of this diff.

Security risks

None identified. The deleted code was debug-only diagnostic reporting; the WebKit change is a correctness fix on the property-lookup slow path that affects debug-assert behavior for mono-proto objects. No auth, crypto, or untrusted-input handling is touched.

Level of scrutiny

High, because the real change is a JavaScriptCore engine modification to one of its hottest inline paths (JSObject::getPropertySlot). That change is only referenced here as a version pin — reviewing this PR alone cannot validate the JSC diff, its perf impact, or whether it interacts correctly with other prototype-chain callers. Per the repo's own guidance, dependency/vendoring bumps and WebKit upgrades warrant maintainer review. The Bun-side changes (6-line deletion, new test) are simple and would be approvable in isolation.

Other factors

  • The pin targets a preview autobuild of an unmerged WebKit PR. The author's stated plan is the usual two-step (land on preview, then re-pin to the merge sha), but that is a process decision a maintainer should confirm before this hits main.
  • Three overlapping open PRs (#33211, #30245, #33418) work around the same crash family on the Bun side; landing this one implies closing/rebasing those, which a maintainer should coordinate.
  • The bug-hunting system found no issues this run; my only prior comment (undrained stderr pipe) was fixed and the thread is resolved.
  • CI at 0a3a39f was mostly green with two ASAN-lane failures the author attributes to pre-existing flakes on main; the latest commit 80b355b is a CI retrigger for the now-published preview build.

@robobun
robobun force-pushed the farm/f76ebd86/fix-stale-structure-getpropertyslot branch from 80b355b to 9da6a46 Compare August 10, 2026 10:20

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/build/deps/webkit.ts`:
- Around line 6-11: Update WEBKIT_VERSION to the commit SHA produced when WebKit
PR `#390` merges, replacing the preview tag so local and prebuilt builds resolve
the same revision; do not add a process.versions assertion.
🪄 Autofix

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: 38d53589-06a1-401a-a579-55a9821c4fbb

📥 Commits

Reviewing files that changed from the base of the PR and between 80b355b and 9da6a46.

📒 Files selected for processing (1)
  • scripts/build/deps/webkit.ts

Comment thread scripts/build/deps/webkit.ts Outdated
Comment on lines +6 to +11
// Preview of oven-sh/WebKit#390 (rebased on 447082ab, the previous pin here):
// JSObject::getPropertySlot reloads the structure before the prototype step, so
// a static-table lazy property builder that transitions the object and then
// throws no longer trips ASSERT(object->structure() == this) in
// Structure::storedPrototype.
export const WEBKIT_VERSION = "autobuild-preview-pr-390-0c51423a";

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.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- dependency file ---'
cat -n scripts/build/deps/webkit.ts

printf '%s\n' '--- WebKit version usage ---'
rg -n -C 3 'WEBKIT_VERSION|process\.versions' --glob '*.{ts,tsx,js,jsx}' .

printf '%s\n' '--- repository guidance ---'
if [ -f REVIEW.md ]; then
  cat -n REVIEW.md
fi

Repository: oven-sh/bun

Length of output: 50368


🌐 Web query:

GitHub oven-sh/WebKit pull request 390 current status merged commit

💡 Result:

There is no publicly available information indicating that pull request #390 exists in the oven-sh/WebKit repository as of August 10, 2026. Review of the repository's activity, merged pull requests, and associated release tags shows recent pull requests such as #370 and others, but no PR #390 [1][2][3]. Results related to "PR 390" in other contexts refer to unrelated items, such as automotive engine parts, consumer products, or pull requests in different repositories [4][5][6][7][8][9][10][11].

Citations:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- dependency-version assertion ---'
sed -n '45,75p' scripts/build/depVersionsHeader.ts
sed -n '565,600p' test/js/node/process/process.test.js

printf '%s\n' '--- preview-tag handling ---'
sed -n '275,300p' scripts/build/download.ts
sed -n '1,50p' scripts/sync-webkit-source.ts

printf '%s\n' '--- upstream PR status ---'
curl -fsSL -H 'Accept: application/vnd.github+json' \
  https://api.github.com/repos/oven-sh/WebKit/pulls/390 |
  jq '{number,state,merged_at,merge_commit_sha,head:.head.sha,base:.base.ref,html_url}'

printf '%s\n' '--- preview release and tag ---'
curl -sS -o /tmp/webkit-release.json -w 'release_http=%{http_code}\n' \
  https://api.github.com/repos/oven-sh/WebKit/releases/tags/autobuild-preview-pr-390-0c51423a
if [ -s /tmp/webkit-release.json ]; then
  jq '{tag_name,name,draft,prerelease,published_at,target_commitish,html_url}' /tmp/webkit-release.json
fi

curl -sS -o /tmp/webkit-ref.json -w 'ref_http=%{http_code}\n' \
  https://api.github.com/repos/oven-sh/WebKit/git/ref/tags/autobuild-preview-pr-390-0c51423a
if [ -s /tmp/webkit-ref.json ]; then
  jq '{ref,object}' /tmp/webkit-ref.json
fi

Repository: oven-sh/bun

Length of output: 6565


🏁 Script executed:

#!/usr/bin/env bash
set -u

repo=https://github.com/oven-sh/WebKit.git
tag=autobuild-preview-pr-390-0c51423a

printf '%s\n' '--- remote preview tag ---'
git ls-remote "$repo" "refs/tags/$tag" "refs/tags/$tag^{}" 2>&1
printf 'tag_status=%s\n' "$?"

printf '%s\n' '--- remote PR refs ---'
git ls-remote "$repo" \
  "refs/pull/390/head" \
  "refs/pull/390/merge" 2>&1
printf 'pr_ref_status=%s\n' "$?"

printf '%s\n' '--- WebKit-specific version assertions and macros ---'
rg -n -i -C 3 'webkit|WEBKIT' \
  test/js/node/process/process.test.js \
  src \
  scripts/build/depVersionsHeader.ts \
  scripts/build/deps/webkit.ts \
  scripts/build/bun.ts \
  2>/dev/null | head -n 240

Repository: oven-sh/bun

Length of output: 17974


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- pull request page markers ---'
curl -L -sS -A 'Mozilla/5.0' https://github.com/oven-sh/WebKit/pull/390 |
  tr '\n' ' ' |
  grep -oE '.{0,100}(Open|Closed|Merged|pull request|PR `#390`).{0,160}' |
  head -n 20
printf 'page_status=%s\n' "$?"

printf '%s\n' '--- preview release asset status ---'
for asset in \
  bun-webkit-linux-amd64.tar.gz \
  bun-webkit-linux-amd64-debug.tar.gz \
  bun-webkit-macos-amd64.tar.gz; do
  url="https://github.com/oven-sh/WebKit/releases/download/autobuild-preview-pr-390-0c51423a/$asset"
  status="$(curl -L -sS -o /dev/null -w '%{http_code}' "$url")"
  printf '%s %s\n' "$status" "$asset"
done

printf '%s\n' '--- immutable commit existence ---'
for sha in 447082ab6897278727b44e1ba3c326ae6e1504c3 0c51423a042b9845a221bce1a6fb3a67a6b2afac eff835bf690f7a114669a4321c8a9a1323e5bbd2; do
  git ls-remote https://github.com/oven-sh/WebKit.git "$sha^{commit}" 2>&1
done

Repository: oven-sh/bun

Length of output: 4872


Replace the preview pin before merging. The preview tag resolves to the previous commit 447082ab..., while prebuilt assets use PR head 0c51423a...; local and prebuilt modes therefore use different revisions. After PR #390 merges, set WEBKIT_VERSION to its merged commit SHA. No matching hard-coded process.versions assertion exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/build/deps/webkit.ts` around lines 6 - 11, Update WEBKIT_VERSION to
the commit SHA produced when WebKit PR `#390` merges, replacing the preview tag so
local and prebuilt builds resolve the same revision; do not add a
process.versions assertion.

Sources: Coding guidelines, Learnings, MCP tools

@robobun

robobun commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

On the webkit.ts line comment: the preview tag is deliberate while oven-sh/WebKit#390 is still open, there is no merge sha to pin yet. Once that PR lands, the pin moves to the resulting commit on WebKit main, as described in the landing plan in the PR description. In the meantime the tag resolves identically in both modes: it is a release tag on the PR head (0c51423a, a rebase onto the current pin 447082ab), which is the exact revision the prebuilt archives were built from.

Comment thread scripts/build/deps/webkit.ts Outdated
Comment on lines +6 to +11
// Preview of oven-sh/WebKit#390 (rebased on 447082ab, the previous pin here):
// JSObject::getPropertySlot reloads the structure before the prototype step, so
// a static-table lazy property builder that transitions the object and then
// throws no longer trips ASSERT(object->structure() == this) in
// Structure::storedPrototype.
export const WEBKIT_VERSION = "autobuild-preview-pr-390-0c51423a";

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.

🔴 WEBKIT_VERSION is pinned to autobuild-preview-pr-390-0c51423a, an ephemeral preview tag for an unmerged oven-sh/WebKit PR. Per the repo's Dependencies & vendoring rule (.claude/docs/landing-prs.md:47, "Never merge a pin to an ephemeral artifact … swap to the merged upstream SHA … before merge"), and per scripts/build/download.ts's own error handling ("GitHub deletes the preview release when the PR merges or closes, so every build 404s at once"), this must be swapped to the merged oven-sh/WebKit#390 commit SHA before this PR lands — deferring to "the routine WebKit upgrade that follows" leaves a range of main commits that fail vendor-fetch on any cold cache or git bisect once the preview release is pruned.

Extended reasoning...

What the issue is. scripts/build/deps/webkit.ts:11 sets WEBKIT_VERSION = "autobuild-preview-pr-390-0c51423a". This is an autobuild-preview-pr-* tag — a preview build for an as-yet-unmerged oven-sh/WebKit pull request — not a merged upstream commit SHA. prebuiltUrl() in the same file (line ~85) constructs the download URL directly from this constant: https://github.com/oven-sh/WebKit/releases/download/autobuild-preview-pr-390-0c51423a/bun-webkit-<os>-<arch><suffix>.tar.gz, and prebuiltDestDir() keys the extraction cache on it.

Why this is a problem. The repo's own review guidance is explicit. .claude/docs/landing-prs.md:47 (Dependencies & vendoring), which REVIEW.md designates as the situational rule for dependency bumps, states verbatim:

Never merge a pin to an ephemeral artifact (preview tags, unmerged-PR builds) — swap to the merged upstream SHA and verify prebuilt artifacts exist for every platform × flavor before merge.

An autobuild-preview-pr-* tag is precisely the "preview tag / unmerged-PR build" the rule prohibits. The codebase itself documents the concrete failure mode — scripts/build/download.ts:277-295 has a dedicated error handler for exactly this case:

A missing prebuilt tarball is a bad pin, not a network blip. The autobuild-preview-pr-* WebKit tags are the sharp edge: GitHub deletes the preview release when the PR merges or closes, so every build 404s at once.

and the accompanying hint tells the user to "Set WEBKIT_VERSION in scripts/build/deps/webkit.ts to the merged main sha".

Step-by-step failure path.

  1. This PR merges to main with WEBKIT_VERSION = "autobuild-preview-pr-390-0c51423a".
  2. JSObject::getPropertySlot: reload the structure before the prototype step WebKit#390 later merges (or is closed / force-pushed). Per the download.ts comment, GitHub deletes the autobuild-preview-pr-390-0c51423a release.
  3. A developer does a fresh clone, or a CI runner starts with a cold cache, or someone runs git bisect and lands on any main commit in the range [this PR's merge, the follow-up WebKit bump).
  4. The build's vendor-fetch step calls prebuiltUrl(cfg) → GET .../releases/download/autobuild-preview-pr-390-0c51423a/bun-webkit-linux-amd64-debug.tar.gzHTTP 404.
  5. prebuiltDownloadError() throws BuildError: WebKit preview release is gone. The build fails before compilation begins. There is no fallback — the only remedy is manually editing WEBKIT_VERSION on that historical commit, which defeats bisect.

Why the timeline comment doesn't resolve this. The author's comment on 2026-08-06 says the pin will move to the merge commit SHA "either here before landing or as the routine WebKit upgrade that follows; the preview tag stays downloadable either way". The repo rule does not permit the second option: it requires the swap before merge, and the download.ts comment directly contradicts "stays downloadable either way" — preview releases are deleted when the PR merges or closes. Pinning the preview during CI iteration is fine; landing it on main is what the rule forbids.

Fix. Wait for oven-sh/WebKit#390 to merge, then before landing this PR update WEBKIT_VERSION to the merged 40-hex commit SHA on oven-sh/WebKit main (the durable autobuild-<sha> release), verify the prebuilt tarball exists for every platform × flavor combination prebuiltSuffix() produces, and drop the now-obsolete "Preview of oven-sh/WebKit#390" comment above the constant.

@robobun

robobun commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

The review comment on the webkit.ts pin is right, and it corrects my earlier reply: the preview tag does not stay downloadable after the WebKit PR merges. scripts/build/download.ts documents that GitHub deletes autobuild-preview-pr-* releases when the PR merges or closes, and the vendoring rule requires the swap to the merged sha to happen before this PR lands, not in a follow-up bump. I updated the landing sequence in the PR description to match: land oven-sh/WebKit#390 first, swap WEBKIT_VERSION here to the merged sha once its prebuilt assets exist for every platform and flavor, then merge this PR. Until that merge happens there is no durable sha to pin, so the preview tag stays only for CI on this branch.

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 54a79e0, which adds a second regression test. Recent fuzzer samples hit the same assert without touching any global: they recurse until the stack overflows and then first-touch Bun.sql from the frame just above the overflow, so the builder's module evaluation throws a RangeError after the Bun object has already transitioned. The minimal form (now in the PR description) aborts an unfixed debug build every run and exits 0 printing RangeError with the fix, in about 0.6s under debug ASAN. It does not depend on the sql module reading Error.prototype, so it stays valid if that ever changes. Checked both tests fail on an unfixed debug binary (exit 134 with this assertion) and pass with the pinned preview.

Comment thread test/js/bun/util/BunObject.test.ts
@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

On the stack overflow test comment: the second half of it was right, and checking it turned up something worth recording. I traced the script on an unfixed debug binary with breakpoints on structure transitions. The Bun object never transitions in that scenario. What aborts is the debug-only report that the builder used to make while the RangeError was still pending: Bun__handleUncaughtException reads process._fatalException, the static table reifies it (transition), setUpStaticFunctionSlot then reports a miss because of the pending exception, and the prototype step asserts on the process object's stale structure. So that test covers the BunObject.cpp change (it is the same lines that also consumed the exception), while the Error proxy test is the one that transitions the Bun object inside the builder and needs the engine reload on its own. f1ce747 rewrites the test comment to say exactly that, and the PR description and the WebKit PR now describe both routes.

On the exact stdout assertion, I kept it. The frame that runs the lookup is the one whose own call to f() just failed the stack check, and the builder cannot complete without entering JS functions that perform the same check, so it throws there on every configuration; the test passed on all 190 jobs of build 91796 (glibc, musl, darwin, windows, release and ASAN), and 8 of 8 local debug and 5 of 5 release runs print exactly RangeError. Accepting "no throw" would make the test pass in precisely the case where it stopped exercising anything, which is the failure mode the exact string is there to catch. The process-stdio precedent is different in kind: its getter is native and can legitimately finish in the remaining headroom.

…-lookup

Fuzzilli kept hitting ASSERT(object->structure() == this) in
JSC::Structure::storedPrototype on debug builds (fingerprint
StructureInlinesLight.h(56)). The inlined JSObject::getPropertySlot loop
caches the structure, and reifying a static-table lazy property can
transition the object inside getOwnNonIndexPropertySlot and still return
false when the PropertyCallback builder throws, so the prototype step ran
on the pre-transition structure. Trigger: a first read of Bun.sql whose
module evaluation touches another lazy Bun property and then throws.

oven-sh/WebKit#390 reloads the structure before the prototype step, the
same reload the megamorphic slow paths in JITOperations.cpp already do.
This bumps the WebKit pin to its preview build.

Fixing that surfaced a second bug in the same path: defaultBunSQLObject
and constructBunSQLObject had a BUN_DEBUG-only
reportUncaughtExceptionAtEventLoop call that consumed the pending
exception, so RETURN_IF_EXCEPTION fell through and getObject() ran on an
empty JSValue (null deref), and an empty builder result with no pending
exception would trip RELEASE_ASSERT_NOT_REACHED in
setUpStaticFunctionSlot. Drop the debug reporting; the exception now
propagates to the caller like in release builds.
@robobun
robobun force-pushed the farm/f76ebd86/fix-stale-structure-getpropertyslot branch from 3a8de41 to e6e9d7b Compare August 14, 2026 20:42
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.

1 participant