mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
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:
@@ -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 {};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
40
test/regression/issue/fuzzer-ENG-22942.test.ts
Normal file
40
test/regression/issue/fuzzer-ENG-22942.test.ts
Normal 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();
|
||||
});
|
||||
Reference in New Issue
Block a user