fix(bun:test): return an object when constructing a mock function#31372
fix(bun:test): return an object when constructing a mock function#31372robobun wants to merge 1 commit into
Conversation
Mock functions registered jsMockFunctionCall as both their call and construct callbacks, so `new mockFn()` / `Reflect.construct(mockFn, [])` could return a non-object (e.g. undefined), violating the [[Construct]] contract and failing the isCell() assertion in JSC::Interpreter::executeConstruct. Add a dedicated construct callback that creates `this` from newTarget.prototype, runs the mock body with it, and returns the body's result only when it is an object, matching ordinary constructor semantics.
|
Updated 6:36 PM PT - May 24th, 2026
❌ @robobun, your commit 1252053 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 31372That installs a local version of the PR into your bun-31372 --bun |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThis PR adds explicit constructor support to Bun's mock functions. The implementation refactors shared call logic into a helper, wires a new construct handler into ChangesMock Function Constructor Support
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
I didn't find any issues — the construct callback correctly mirrors ordinary [[Construct]] semantics and the refactor is clean — but since this is native JSC bindings code fixing a type-confusion path, it's worth a human glance.
Extended reasoning...
Overview
This PR splits jsMockFunctionCall into a shared jsMockFunctionCallImpl (parameterized on thisValue) and adds a dedicated jsMockFunctionConstruct callback for JSMockFunction. Previously the call handler was reused for construct, which could return primitives — violating the [[Construct]] invariant and triggering an isCell() assertion (debug) / type confusion via asObject() on a primitive encoding (release). The new construct path creates this from newTarget.prototype, runs the mock body, and returns the body's result only if it is an object — standard ordinary-constructor semantics. Tests cover new/Reflect.construct for empty mocks, primitive-returning impls, object-returning impls, mockReturnValue, and spyOn over a missing property.
Security risks
The bug being fixed is itself the security-relevant part: in release builds the old code fabricated a JSObject* from a non-cell encoding. The fix removes that hazard. The new code's asObject(callframe->newTarget()) is safe because newTarget is always an object in a native construct callback, and exception checks bracket the .prototype lookup and the body call. I don't see new risks introduced.
Level of scrutiny
Medium-high. The diff is small (~35 lines of new C++ plus a mechanical refactor) and follows the well-known JSC pattern for native constructors, but it lives in src/jsc/bindings and changes how every jest.fn()/mock()/spyOn() behaves under new. Given it's native code on a memory-safety-adjacent path, a human reviewer familiar with Bun's JSC bindings should sign off.
Other factors
No CODEOWNERS cover this file. Test coverage for the new behavior is solid and the existing mock test suites reportedly pass. The refactor of the call path is behavior-preserving (only thisValue sourcing moved to the wrapper). No outstanding reviewer comments.
|
Closing as a duplicate of #30212, which fixes the same The entry point from this fuzzer report — constructing a spy created over a missing property, e.g. |
What does this PR do?
Fixes a Fuzzilli-found crash (assertion
isCell()inJSCJSValue.h:1043, reached fromJSC::Interpreter::executeConstruct→asObject).JSMockFunction(the object behindjest.fn()/mock()/spyOn()) registeredjsMockFunctionCallas both its call and construct callbacks. That callback returns whatever the mock's implementation produces —undefinedwhen there is no implementation, or any primitive frommockReturnValue/a primitive-returning implementation. A native[[Construct]]callback must return an object, so:Reflect.construct(mockFn, [])hit theisCell()assertion in debug builds (and in release buildsasObject()fabricates a bogusJSObject*from a primitive encoding — type confusion).new mockFn()silently evaluated toundefined, which ordinary JS constructor semantics never allow.Minimal repro (crashes a debug build before this change):
Fix
Give
JSMockFunctiona dedicated construct callback that follows ordinary constructor semantics:thisfromnewTarget.prototype(falling back to a plain object).this.this.This matches how Jest mock functions behave, since they are ordinary JS functions there (
new jest.fn()()yields the new instance, and an object-returning implementation passes through).How did you verify your code works?
test/js/bun/test/mock-fn.test.jscoveringnew/Reflect.constructonjest.fn()(no implementation, primitive-returning, object-returning,mockReturnValue) and on a spy over a missing property (the fuzzer's exact path). They fail on the previous build and pass with this change.bun bd test test/js/bun/test/mock-fn.test.js(74 pass), plusmock-disposable,mock-module*,spyMatchers,expect-toHaveReturnedWith, anddescribesuites all pass.