From f4b6396eac9a5d33fedd7dc1f45a854dee4ca653 Mon Sep 17 00:00:00 2001 From: SUZUKI Sosuke Date: Sat, 25 Oct 2025 13:36:33 +0900 Subject: [PATCH] Fix unhandled exception in JSC__JSPromise__wrap when resolving promise (#23961) ### What does this PR do? Previously, `JSC__JSPromise__wrap` would call `JSC::JSPromise::resolvedPromise(globalObject, result)` without checking if an exception was thrown during promise resolution. This could happen in certain edge cases, such as when the result value is a thenable that triggers stack overflow, or when the promise resolution mechanism itself encounters an error. When such exceptions occurred, they would escape back to the Zig code, causing the CatchScope assertion to fail with "ASSERTION FAILED: Unexpected exception observed on thread" instead of being properly handled. This PR adds an exception check immediately after calling `JSC::JSPromise::resolvedPromise()` and before the `RELEASE_AND_RETURN` macro. If an exception is detected, the function now clears it and returns a rejected promise with the exception value, ensuring consistent error handling behavior. This matches the pattern already used earlier in the function for the initial function call exception handling. ### How did you verify your code works? new and existing tests --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/bindings/bindings.cpp | 9 ++++++++- test/js/web/fetch/response.test.ts | 16 +++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index db24591323..a3153d67e1 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3397,7 +3397,14 @@ JSC::EncodedJSValue JSC__JSPromise__wrap(JSC::JSGlobalObject* globalObject, void RELEASE_AND_RETURN(scope, JSValue::encode(JSC::JSPromise::rejectedPromise(globalObject, err))); } - RELEASE_AND_RETURN(scope, JSValue::encode(JSC::JSPromise::resolvedPromise(globalObject, result))); + JSValue resolved = JSC::JSPromise::resolvedPromise(globalObject, result); + if (scope.exception()) [[unlikely]] { + auto* exception = scope.exception(); + scope.clearException(); + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::JSPromise::rejectedPromise(globalObject, exception->value()))); + } + + RELEASE_AND_RETURN(scope, JSValue::encode(resolved)); } [[ZIG_EXPORT(check_slow)]] void JSC__JSPromise__reject(JSC::JSPromise* arg0, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue JSValue2) diff --git a/test/js/web/fetch/response.test.ts b/test/js/web/fetch/response.test.ts index c5a249f6d1..cdbdfd42a9 100644 --- a/test/js/web/fetch/response.test.ts +++ b/test/js/web/fetch/response.test.ts @@ -49,7 +49,7 @@ describe("2-arg form", () => { test("print size", () => { expect(normalizeBunSnapshot(Bun.inspect(new Response(Bun.file(import.meta.filename)))), import.meta.dir) .toMatchInlineSnapshot(` - "Response (3.82 KB) { + "Response (4.15 KB) { ok: true, url: "", status: 200, @@ -109,3 +109,17 @@ test("new Response(123, { method: 456 }) does not throw", () => { // @ts-expect-error expect(() => new Response("123", { method: 456 })).not.toThrow(); }); + +test("handle stack overflow", () => { + function f0(a1, a2) { + const v4 = new Response(); + // @ts-ignore + const v5 = v4.text(a2, a2, v4, f0, f0); + a1(a1); // Recursive call causes stack overflow + return v5; + } + expect(() => { + // @ts-ignore + f0(f0); + }).toThrow("Maximum call stack size exceeded."); +});