fix: prevent crash when throwing expect error with pending exception

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 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2025-11-24 08:07:01 +00:00
parent ddcec61f59
commit 743302c4fa
2 changed files with 23 additions and 2 deletions

View File

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

View File

@@ -1716,6 +1716,12 @@ template<bool isStrict, bool enableAsymmetricMatchers>
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<std::pair<JSC::JSValue, JSC::JSValue>, 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<EncodedJSValue> objVisited;
std::set<EncodedJSValue> subsetVisited;