fix(test): fix assertion failure in expect.extend with non-JSFunction callables (#25099)

## Summary

- Fix debug assertion failure in `JSWrappingFunction` when
`expect.extend()` is called with objects containing non-`JSFunction`
callables
- The crash occurred because `jsCast<JSFunction*>` was used, which
asserts the value inherits from `JSFunction`, but callable class
constructors (like `Expect`) inherit from `InternalFunction` instead

## Changes

- Change `JSWrappingFunction` to store `JSObject*` instead of
`JSFunction*`
- Use `jsDynamicCast` instead of `jsCast` in `getWrappedFunction`
- Use `getObject()` instead of `jsCast` in `create()`

## Reproduction

```js
const jest = Bun.jest();
jest.expect.extend(jest);
```

Before fix (debug build):
```
ASSERTION FAILED: !from || from->JSCell::inherits(std::remove_pointer<To>::type::info())
JSCast.h(40) : To JSC::jsCast(From *) [To = JSC::JSFunction *, From = JSC::JSCell]
```

After fix: Properly throws `TypeError: expect.extend: 'jest' is not a
valid matcher`

## Test plan

- [x] Added regression test
`test/regression/issue/fuzzer-ENG-22942.test.ts`
- [x] Existing `expect-extend.test.js` tests pass (27 tests)
- [x] Build succeeds

Fixes ENG-22942

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
robobun
2025-11-26 13:34:02 -08:00
committed by GitHub
parent 44f2328111
commit 5965ff18ea
3 changed files with 45 additions and 5 deletions

View File

@@ -24,7 +24,7 @@ JS_EXPORT_PRIVATE JSWrappingFunction* JSWrappingFunction::create(
Zig::NativeFunctionPtr functionPointer,
JSC::JSValue wrappedFnValue)
{
JSC::JSFunction* wrappedFn = jsCast<JSC::JSFunction*>(wrappedFnValue.asCell());
JSC::JSObject* wrappedFn = wrappedFnValue.getObject();
ASSERT(wrappedFn != nullptr);
auto nameStr = symbolName->tag == BunStringTag::Empty ? WTF::emptyString() : symbolName->toWTFString();
@@ -75,9 +75,9 @@ extern "C" JSC::EncodedJSValue Bun__JSWrappingFunction__getWrappedFunction(
Zig::GlobalObject* globalObject)
{
JSC::JSValue thisValue = JSC::JSValue::decode(thisValueEncoded);
JSWrappingFunction* thisObject = jsCast<JSWrappingFunction*>(thisValue.asCell());
JSWrappingFunction* thisObject = jsDynamicCast<JSWrappingFunction*>(thisValue.asCell());
if (thisObject != nullptr) {
JSC::JSFunction* wrappedFn = thisObject->m_wrappedFn.get();
JSC::JSObject* wrappedFn = thisObject->m_wrappedFn.get();
return JSC::JSValue::encode(wrappedFn);
}
return {};

View File

@@ -59,7 +59,7 @@ public:
}
private:
JSWrappingFunction(JSC::VM& vm, JSC::NativeExecutable* native, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::JSFunction* wrappedFn)
JSWrappingFunction(JSC::VM& vm, JSC::NativeExecutable* native, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, JSC::JSObject* wrappedFn)
: Base(vm, native, globalObject, structure)
, m_wrappedFn(wrappedFn, JSC::WriteBarrierEarlyInit)
{
@@ -69,7 +69,7 @@ private:
DECLARE_VISIT_CHILDREN;
JSC::WriteBarrier<JSC::JSFunction> m_wrappedFn;
JSC::WriteBarrier<JSC::JSObject> m_wrappedFn;
};
}

View File

@@ -0,0 +1,40 @@
import { expect, test } from "bun:test";
// Regression test for ENG-22942: Crash when calling expect.extend with non-function values
// The crash occurred because JSWrappingFunction assumed all callable objects are JSFunction,
// but class constructors like Expect are callable but not JSFunction instances.
test("expect.extend with jest object should throw TypeError, not crash", () => {
const jest = Bun.jest(import.meta.path);
expect(() => {
jest.expect.extend(jest);
}).toThrow(TypeError);
});
test("expect.extend with object containing non-function values should throw", () => {
const jest = Bun.jest(import.meta.path);
expect(() => {
jest.expect.extend({
notAFunction: "string value",
});
}).toThrow("expect.extend: `notAFunction` is not a valid matcher");
});
test("expect.extend with valid matchers still works", () => {
const jest = Bun.jest(import.meta.path);
jest.expect.extend({
toBeEven(received: number) {
const pass = received % 2 === 0;
return {
message: () => `expected ${received} ${pass ? "not " : ""}to be even`,
pass,
};
},
});
jest.expect(4).toBeEven();
jest.expect(3).not.toBeEven();
});