From aff0d2c7757c577fecf3fc60d6c1886822e9ca7f Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 28 Aug 2026 04:54:44 +0000 Subject: [PATCH 1/3] Fix Bun.generateHeapSnapshot("v8") crashing on large heaps (WebKit#529) A heap with more than about 15M live cells aborted the process: the V8 snapshot builder stored one 96-byte Node per cell in a contiguous WTF::Vector, and vector growth past the ~2GiB single-allocation cap is a CRASH(). WebKit#529 shrinks the node record to 40 bytes, moves node storage to a SegmentedVector, and turns the remaining container limits into a recorded overflow instead of an abort. On the bun side, consume that overflow signal: Bun.generateHeapSnapshot and worker.getHeapSnapshot() now throw or reject a catchable error when a snapshot is too large to serialize, and the --heap-prof writer prints an error instead of silently writing nothing. Fixes #40686 --- scripts/build/deps/webkit.ts | 2 +- src/jsc/BunHeapProfiler.rs | 5 +++- src/jsc/bindings/BunObject.cpp | 8 ++++++ src/jsc/bindings/webcore/JSWorker.cpp | 13 +++++++++- test/js/bun/util/v8-heap-snapshot.test.ts | 31 ++++++++++++++++++++++- 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 0edda8cc6797..7732156c87fb 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "ceb9f90fb774fdb1ebf1275ae1aaf136ec66c754"; +export const WEBKIT_VERSION = "autobuild-preview-pr-529-d199c1f1"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/jsc/BunHeapProfiler.rs b/src/jsc/BunHeapProfiler.rs index 9bac149d314a..713e792ffece 100644 --- a/src/jsc/BunHeapProfiler.rs +++ b/src/jsc/BunHeapProfiler.rs @@ -33,7 +33,10 @@ pub(crate) fn generate_and_write_profile( }; if profile_string.is_empty() { - // No profile data generated + // The V8 builder returns an empty string when the snapshot was too + // large to serialize. Say so instead of silently writing nothing. + bun_core::pretty_errorln!("error: heap snapshot is too large to serialize"); + Output::flush(); return Ok(()); } diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index 4c8fce206b21..7bde9066b1be 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -815,6 +815,10 @@ JSC_DEFINE_HOST_FUNCTION(functionGenerateHeapSnapshot, (JSC::JSGlobalObject * gl JSC::BunV8HeapSnapshotBuilder builder(heapProfiler); auto bytes = builder.jsonBytes(); RETURN_IF_EXCEPTION(throwScope, {}); + if (builder.hasOverflowed()) { + throwException(globalObject, throwScope, createError(globalObject, "Heap snapshot is too large to serialize"_s)); + return {}; + } auto released = bytes.releaseBuffer(); auto span = released.leakSpan(); auto buffer = ArrayBuffer::createFromBytes(std::span { span.data(), span.size() }, createSharedTask([](void* p) { @@ -826,6 +830,10 @@ JSC_DEFINE_HOST_FUNCTION(functionGenerateHeapSnapshot, (JSC::JSGlobalObject * gl JSC::BunV8HeapSnapshotBuilder builder(heapProfiler); auto json = builder.json(); RETURN_IF_EXCEPTION(throwScope, {}); + if (json.isNull()) { + throwException(globalObject, throwScope, createError(globalObject, "Heap snapshot is too large to serialize"_s)); + return {}; + } return JSC::JSValue::encode(jsString(vm, json)); } diff --git a/src/jsc/bindings/webcore/JSWorker.cpp b/src/jsc/bindings/webcore/JSWorker.cpp index b94283911342..8e2d73d2b0e5 100644 --- a/src/jsc/bindings/webcore/JSWorker.cpp +++ b/src/jsc/bindings/webcore/JSWorker.cpp @@ -760,9 +760,20 @@ static inline JSC::EncodedJSValue jsWorkerPrototypeFunction_getHeapSnapshotBody( JSC::BunV8HeapSnapshotBuilder builder(heapProfiler); String snapshot = builder.json(); + // json() returns a null String when the snapshot was too large to + // serialize; reject instead of resolving with an empty string. ScriptExecutionContext::postTaskTo(parentId, parentLoopKind, [reqId, protectedProxy = WTF::move(protectedProxy), snapshot = snapshot.isolatedCopy()](ScriptExecutionContext& parentCtx) { - resolveCrossVMRequest(protectedProxy.get(), reqId, parentCtx, [&](VM& vm, JSGlobalObject*) -> JSValue { return jsString(vm, snapshot); }); + auto handle = protectedProxy->takeCrossVMRequest(reqId); + if (!handle || parentCtx.isJSExecutionForbidden()) + return; + auto& parentVM = parentCtx.vm(); + auto* parentGlobalObject = parentCtx.globalObject(); + if (snapshot.isNull()) { + handle->reject(parentVM, JSC::createError(parentGlobalObject, "Heap snapshot is too large to serialize"_s)); + return; + } + handle->resolve(parentGlobalObject, parentVM, jsString(parentVM, snapshot)); }); }); if (!accepted) { diff --git a/test/js/bun/util/v8-heap-snapshot.test.ts b/test/js/bun/util/v8-heap-snapshot.test.ts index 8141ce96d21a..98dfe268fa75 100644 --- a/test/js/bun/util/v8-heap-snapshot.test.ts +++ b/test/js/bun/util/v8-heap-snapshot.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { bunEnv, bunExe, tempDir } from "harness"; +import { bunEnv, bunExe, isASAN, isDebug, tempDir } from "harness"; import { join } from "node:path"; const fixture = join(import.meta.dir, "v8-heap-snapshot-fixture.ts"); @@ -89,6 +89,35 @@ test("v8.writeHeapSnapshot() with path", async () => { expect(await runFixture("write-path", { args: [path] })).toEqual({ returnedPath: path, ...structure }); }); +// #40686: the builder stored one 96-byte Node per heap cell in a contiguous +// WTF::Vector, whose single-allocation cap is ~2GiB. Past ~15M live cells the +// vector growth tripped that cap and aborted the process (SIGABRT) inside the +// marking walk. 16M small objects reproduce it. The walk visits every live +// cell, so the heap has to actually be this large; ~30s and ~4GB RSS in a +// release build, but 10-100x slower under debug/ASAN, hence the skip there. +test.skipIf(isDebug || isASAN)( + "v8 heap snapshot of a 16M-object heap does not crash", + async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `const items = Array.from({ length: 16_000_000 }, (_, i) => ({ i })); + const snapshot = Bun.generateHeapSnapshot("v8"); + console.log(items.length, snapshot.length > 100_000_000);`, + ], + env: bunEnv, + stdout: "pipe", + stderr: "ignore", + }); + + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout).toBe("16000000 true\n"); + expect(exitCode).toBe(0); + }, + 240_000, +); + test("v8 heap snapshot labels Web Streams internal edges", async () => { const edges = await runFixture("stream-edges"); // Every WriteBarrier member reported by analyzeHeap shows up as a named From b5532b6b8f4d211f8fb993dc58ec98b8cd192d3c Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 28 Aug 2026 07:06:33 +0000 Subject: [PATCH 2/3] test: pipe stderr in the large-heap snapshot test for crash diagnostics --- test/js/bun/util/v8-heap-snapshot.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/js/bun/util/v8-heap-snapshot.test.ts b/test/js/bun/util/v8-heap-snapshot.test.ts index 98dfe268fa75..276573fccec0 100644 --- a/test/js/bun/util/v8-heap-snapshot.test.ts +++ b/test/js/bun/util/v8-heap-snapshot.test.ts @@ -108,10 +108,11 @@ test.skipIf(isDebug || isASAN)( ], env: bunEnv, stdout: "pipe", - stderr: "ignore", + stderr: "pipe", }); - const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); expect(stdout).toBe("16000000 true\n"); expect(exitCode).toBe(0); }, From f373c287d33ec28d779889e4daeb9bbd523017b6 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 28 Aug 2026 07:17:45 +0000 Subject: [PATCH 3/3] heap-prof: scope the too-large message to the v8 snapshot path --- src/jsc/BunHeapProfiler.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/jsc/BunHeapProfiler.rs b/src/jsc/BunHeapProfiler.rs index 713e792ffece..bd4d4c9dc3c5 100644 --- a/src/jsc/BunHeapProfiler.rs +++ b/src/jsc/BunHeapProfiler.rs @@ -35,8 +35,11 @@ pub(crate) fn generate_and_write_profile( if profile_string.is_empty() { // The V8 builder returns an empty string when the snapshot was too // large to serialize. Say so instead of silently writing nothing. - bun_core::pretty_errorln!("error: heap snapshot is too large to serialize"); - Output::flush(); + // The text profiler reports its own errors inline, so leave it alone. + if !config.text_format { + bun_core::pretty_errorln!("error: heap snapshot is too large to serialize"); + Output::flush(); + } return Ok(()); }