From 4b23b02aec242c1b9892e7da60cf96c62f70dfdf Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 20 Jul 2026 02:19:39 +0000 Subject: [PATCH] bun:test: check exception after isArray() in expect.any(Array), toMatchObject, toHaveProperty JSC::isArray() declares a throw scope on its Proxy path (isArraySlowInline). Three call sites in bindings.cpp used it inside an if condition and then either entered another throw-scope-declaring call or returned with the outer ThrowScope still unsatisfied, which aborts under the JSC exception-scope validator: matchAsymmetricMatcherAndGetFlags: isArray then hasInstance Bun__deepMatch: two chained isArray calls then getPropertyNames JSC__JSValue__getIfPropertyExistsFromPath: isArray false then return Hoist each isArray() result out of the condition and RETURN_IF_EXCEPTION immediately after. --- src/jsc/bindings/bindings.cpp | 15 ++++++++++++--- test/js/bun/test/expect.test.js | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index d82aafa920be..a276c22764a5 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -390,7 +390,9 @@ AsymmetricMatcherResult matchAsymmetricMatcherAndGetFlags(JSGlobalObject* global } case AsymmetricMatcherConstructorType::Array: { - if (JSC::isArray(globalObject, otherProp)) { + bool otherIsArray = JSC::isArray(globalObject, otherProp); + RETURN_IF_EXCEPTION(throwScope, AsymmetricMatcherResult::FAIL); + if (otherIsArray) { return AsymmetricMatcherResult::PASS; } break; @@ -1648,8 +1650,13 @@ bool Bun__deepMatch( // - two "simple" arrays // similar to what is done in deepEquals (canPerformFastPropertyEnumerationForIterationBun) + bool objIsArray = isArray(globalObject, objValue); + RETURN_IF_EXCEPTION(throwScope, false); + bool subsetIsArray = isArray(globalObject, subsetValue); + RETURN_IF_EXCEPTION(throwScope, false); + // arrays should match exactly - if (isArray(globalObject, objValue) && isArray(globalObject, subsetValue)) { + if (objIsArray && subsetIsArray) { if (obj->getArrayLength() != subsetObj->getArrayLength()) { return false; } @@ -4369,7 +4376,9 @@ JSC::EncodedJSValue JSC__JSValue__getIfPropertyExistsFromPath(JSC::EncodedJSValu return JSValue::encode(currProp); } - if (isArray(globalObject, path)) { + bool pathIsArray = isArray(globalObject, path); + RETURN_IF_EXCEPTION(scope, {}); + if (pathIsArray) { // each item in array is property name, ignore dot/bracket notation JSValue currProp = value; auto* pathObject = path.toObject(globalObject); diff --git a/test/js/bun/test/expect.test.js b/test/js/bun/test/expect.test.js index 92a569d6ce08..a915a4e1f111 100644 --- a/test/js/bun/test/expect.test.js +++ b/test/js/bun/test/expect.test.js @@ -677,6 +677,38 @@ describe("expect()", () => { expect(p1).toStrictEqual(p2); } }); + + // JSC::isArray() declares a throw scope on the Proxy path. expect.any(Array), + // toMatchObject, and toHaveProperty called it without checking, which aborts + // under BUN_JSC_validateExceptionChecks=1 (a no-op on release builds). + test("expect.any(Array)/toMatchObject/toHaveProperty check the isArray() exception for Proxy values", async () => { + const { bunEnv, bunExe } = require("harness"); + const src = ` + const { expect } = require("bun:test"); + try { expect(new Proxy({}, {})).toEqual(expect.any(Array)); } catch {} + try { expect(new Proxy([], {})).toEqual(expect.any(Array)); } catch {} + try { expect(new Proxy([], {})).toMatchObject([]); } catch {} + try { expect([]).toMatchObject(new Proxy([], {})); } catch {} + try { expect(new Proxy([], {})).toMatchObject(new Proxy([], {})); } catch {} + try { expect({ a: 1 }).toHaveProperty(new Proxy(["a"], {})); } catch {} + try { expect({ a: 1 }).toHaveProperty(new Proxy(new Set(["a"]), {})); } catch {} + const rp = Proxy.revocable({}, {}); rp.revoke(); + try { expect(rp.proxy).toEqual(expect.any(Array)); } catch {} + console.log("ok"); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", src], + 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]); + expect({ stdout, stderr, exitCode, signalCode: proc.signalCode }).toMatchObject({ + stdout: "ok\n", + exitCode: 0, + signalCode: null, + }); + }); } test("deepEquals works with sets/maps/dates/strings", () => {