From 743302c4fae2cf04c1910c2dbc503f5b50e4feae Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Mon, 24 Nov 2025 08:07:01 +0000 Subject: [PATCH] fix: prevent crash when throwing expect error with pending exception MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a JavaScript exception (like stack overflow from recursive constructor calls) is caught but not fully cleared from JSC's internal state, subsequent calls to Bun.jest().expect() matchers would crash with an assertion failure in debug builds. The crash occurred because: 1. DECLARE_THROW_SCOPE in C++ asserts there's no pending exception 2. VM.throwError in Zig asserts no exception before throwing This fix adds early-return checks in: - deepEqualsWrapperImpl: returns false if exception pending - JSC__JSValue__isStrictEqual: returns false if exception pending - JSC__JSValue__jestDeepMatch: returns false if exception pending - VM.throwError: returns JSError if exception already pending The existing exception will propagate correctly through the CatchScope in the calling Zig code. Fixes ENG-21977 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/bun.js/bindings/VM.zig | 7 ++++++- src/bun.js/bindings/bindings.cpp | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/bun.js/bindings/VM.zig b/src/bun.js/bindings/VM.zig index cc29608cf0..4cd9d2747d 100644 --- a/src/bun.js/bindings/VM.zig +++ b/src/bun.js/bindings/VM.zig @@ -160,7 +160,12 @@ pub const VM = opaque { var scope: bun.jsc.ExceptionValidationScope = undefined; scope.init(global_object, @src()); defer scope.deinit(); - scope.assertNoException(); + // If there's already an exception pending (e.g., from a caught stack overflow + // in JavaScript that wasn't fully cleared), just return the error without + // asserting or throwing a new exception. The existing exception will propagate. + if (scope.hasExceptionOrFalseWhenAssertionsAreDisabled()) { + return error.JSError; + } JSC__VM__throwError(vm, global_object, value); scope.assertExceptionPresenceMatches(true); return error.JSError; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 945c31bda4..b5555588e0 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -1716,6 +1716,12 @@ template inline bool deepEqualsWrapperImpl(JSC::EncodedJSValue a, JSC::EncodedJSValue b, JSC::JSGlobalObject* global) { auto& vm = global->vm(); + // If there's already an exception pending (e.g., from a caught stack overflow), + // return false immediately to avoid triggering assertion failures when creating + // a ThrowScope. The caller (fromJSHostCallGeneric in Zig) has a CatchScope that + // will handle propagating the exception. + if (vm.exceptionForInspection()) + return false; auto scope = DECLARE_THROW_SCOPE(vm); Vector, 16> stack; MarkedArgumentBuffer args; @@ -2636,6 +2642,10 @@ size_t JSC__VM__heapSize(JSC::VM* arg0) bool JSC__JSValue__isStrictEqual(JSC::EncodedJSValue l, JSC::EncodedJSValue r, JSC::JSGlobalObject* globalObject) { auto& vm = globalObject->vm(); + // If there's already an exception pending, return false immediately to avoid + // triggering assertion failures when creating a ThrowScope. + if (vm.exceptionForInspection()) + return false; auto scope = DECLARE_THROW_SCOPE(vm); RELEASE_AND_RETURN(scope, JSC::JSValue::strictEqual(globalObject, JSC::JSValue::decode(l), JSC::JSValue::decode(r))); } @@ -2672,10 +2682,16 @@ bool JSC__JSValue__jestStrictDeepEquals(JSC::EncodedJSValue JSValue0, JSC::Encod bool JSC__JSValue__jestDeepMatch(JSC::EncodedJSValue JSValue0, JSC::EncodedJSValue JSValue1, JSC::JSGlobalObject* globalObject, bool replacePropsWithAsymmetricMatchers) { + auto& vm = globalObject->vm(); + // If there's already an exception pending, return false immediately to avoid + // triggering assertion failures when creating a ThrowScope. + if (vm.exceptionForInspection()) + return false; + JSValue obj = JSValue::decode(JSValue0); JSValue subset = JSValue::decode(JSValue1); - ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm()); + ThrowScope scope = DECLARE_THROW_SCOPE(vm); std::set objVisited; std::set subsetVisited;