From 14ebf3ebdb54b88b40fffdcfe8374cbf6d135824 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 19:03:48 +0000 Subject: [PATCH 1/2] bun:jsc: propagate JSONParse exceptions from samplingProfilerStackTraces samplingProfilerStackTraces asserted no exception after JSON-parsing the profiler's stack traces, so an out-of-memory error thrown by JSONParse crashed the process with a release assert instead of reaching the caller. Assert before the parse, where nothing can have thrown, and release the throw scope on return, matching generateHeapSnapshotForDebugging. --- src/jsc/modules/BunJSCModule.h | 3 +-- test/js/bun/jsc/bun-jsc.test.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/jsc/modules/BunJSCModule.h b/src/jsc/modules/BunJSCModule.h index 253567844d19..86c4b5df9bcc 100644 --- a/src/jsc/modules/BunJSCModule.h +++ b/src/jsc/modules/BunJSCModule.h @@ -499,9 +499,8 @@ JSC_DEFINE_HOST_FUNCTION(functionSamplingProfilerStackTraces, createError(globalObject, "Sampling profiler was never started"_s))); WTF::String jsonString = vm.samplingProfiler()->stackTracesAsJSON()->toJSONString(); - JSC::EncodedJSValue result = JSC::JSValue::encode(JSONParse(globalObject, jsonString)); scope.releaseAssertNoException(); - return result; + RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSONParse(globalObject, jsonString))); } JSC_DECLARE_HOST_FUNCTION(functionGetRandomSeed); diff --git a/test/js/bun/jsc/bun-jsc.test.ts b/test/js/bun/jsc/bun-jsc.test.ts index 993aaf6ab4c4..a69cc18d2830 100644 --- a/test/js/bun/jsc/bun-jsc.test.ts +++ b/test/js/bun/jsc/bun-jsc.test.ts @@ -567,3 +567,32 @@ it("deserialize applies the same nesting depth limit to arrays as to objects", a const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect({ stdout, exitCode }).toEqual({ stdout: "rejected\n65\n", exitCode: 0 }); }); + +it("samplingProfilerStackTraces returns parsed traces and survives BUN_JSC_validateExceptionChecks", async () => { + // samplingProfilerStackTraces JSON-parses the profiler's stack traces, and + // JSONParse can throw (OOM on a large profile), so the enclosing throw scope + // must release before returning instead of asserting no exception after the + // parse. With validateExceptionChecks enabled the process aborts on unchecked + // scopes; on release builds the option is a no-op and this just exercises the + // stack-trace path. + const script = ` + const jsc = require("bun:jsc"); + jsc.startSamplingProfiler(); + let j = 0; + for (let i = 0; i < 999999; i++) j += i % 7; + const traces = jsc.samplingProfilerStackTraces(); + console.log("ok", typeof traces, Array.isArray(traces.traces)); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", script], + env: { ...bunEnv, BUN_JSC_validateExceptionChecks: "1" }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + const uncheckedScopes = stderr + .split("\n") + .map(line => line.trim()) + .filter(line => line.startsWith("This scope can throw") || line.startsWith("But the exception was unchecked")); + expect({ stdout, uncheckedScopes, exitCode }).toEqual({ stdout: "ok object true\n", uncheckedScopes: [], exitCode: 0 }); +}); From f0594955fec42b8f7aa892989efdfddbf81fb6dc Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 6 Aug 2026 19:49:31 +0000 Subject: [PATCH 2/2] test: cover the never-started throw branch of samplingProfilerStackTraces --- test/js/bun/jsc/bun-jsc.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/js/bun/jsc/bun-jsc.test.ts b/test/js/bun/jsc/bun-jsc.test.ts index a69cc18d2830..19c9efba73a2 100644 --- a/test/js/bun/jsc/bun-jsc.test.ts +++ b/test/js/bun/jsc/bun-jsc.test.ts @@ -577,6 +577,12 @@ it("samplingProfilerStackTraces returns parsed traces and survives BUN_JSC_valid // stack-trace path. const script = ` const jsc = require("bun:jsc"); + try { + jsc.samplingProfilerStackTraces(); + console.log("no-throw"); + } catch (e) { + console.log("threw", e.message); + } jsc.startSamplingProfiler(); let j = 0; for (let i = 0; i < 999999; i++) j += i % 7; @@ -594,5 +600,9 @@ it("samplingProfilerStackTraces returns parsed traces and survives BUN_JSC_valid .split("\n") .map(line => line.trim()) .filter(line => line.startsWith("This scope can throw") || line.startsWith("But the exception was unchecked")); - expect({ stdout, uncheckedScopes, exitCode }).toEqual({ stdout: "ok object true\n", uncheckedScopes: [], exitCode: 0 }); + expect({ stdout, uncheckedScopes, exitCode }).toEqual({ + stdout: "threw Sampling profiler was never started\nok object true\n", + uncheckedScopes: [], + exitCode: 0, + }); });