Skip to content

bun:test: fix crash when constructing a mock function via Reflect.construct#31368

Closed
robobun wants to merge 1 commit into
mainfrom
farm/4e0236f3/fix-mock-construct
Closed

bun:test: fix crash when constructing a mock function via Reflect.construct#31368
robobun wants to merge 1 commit into
mainfrom
farm/4e0236f3/fix-mock-construct

Conversation

@robobun

@robobun robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes a crash found by fuzzing (Fuzzilli, fingerprint JSCJSValue.h(1043): ASSERTION FAILED: isCell()).

JSMockFunction registered jsMockFunctionCall as both its call and construct handler. A native [[Construct]] implementation must return an object, but the mock returns whatever the mock implementation produced (often undefined), so:

  • Reflect.construct(jest.fn(), []) hit ASSERT(isCell()) inside asObject() in debug builds (type confusion on the returned value in release builds).
  • new (jest.fn())() evaluated to undefined instead of an instance object (Jest returns an instance).
const { jest } = Bun.jest(import.meta.path);
Reflect.construct(jest.fn(), []); // ASSERTION FAILED: isCell()

This PR adds a dedicated construct handler that mirrors ordinary JS constructor semantics (and Jest's behavior for new mockFn()):

  • create this from new.target's prototype (falling back to a plain object),
  • invoke the mock with that this so calls/contexts are recorded against the instance,
  • return the implementation's result when it is an object, otherwise return the new instance.

How did you verify your code works?

  • The original fuzzer repro and the minimized repro no longer abort on a debug (ASAN + debug JSC) build.
  • Added mock() > new works and spyOn > constructing a spy works to test/js/bun/test/mock-fn.test.js; both fail before this change (new (jest.fn())() returns undefined, Reflect.construct asserts in debug) and pass after.
  • Existing mock/spy suites pass: mock-fn.test.js, spyMatchers.test.ts, mock-disposable.test.ts, mock/mock-module.test.ts, expect-toHaveReturnedWith.test.js, expect/toHaveReturnedWith.test.ts.

JSMockFunction used its call handler as its construct handler, so
constructing a mock returned whatever the implementation produced.
A native [[Construct]] must return an object, so
Reflect.construct(jest.fn(), []) failed an assertion in debug builds
and new (jest.fn())() evaluated to undefined.

Add a dedicated construct handler that follows ordinary constructor
semantics: create this from new.target's prototype, invoke the mock
with it, and return the implementation's result only when it is an
object, otherwise the new instance.
@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, we couldn't start this review because you've used your available PR reviews for now.

Your plan includes 5 reviews of capacity. Refill in 26 minutes and 3 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ecf71406-c521-48df-ac5b-be8ecb5fefcc

📥 Commits

Reviewing files that changed from the base of the PR and between 2fbfcb9 and 1b07a4c.

📒 Files selected for processing (2)
  • src/jsc/bindings/JSMockFunction.cpp
  • test/js/bun/test/mock-fn.test.js

Comment @coderabbitai help to get the list of available commands and usage tips.

@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:47 PM PT - May 24th, 2026

@robobun, your commit 1b07a4c has 1 failures in Build #57717 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 31368

That installs a local version of the PR into your bun-31368 executable, so you can run:

bun-31368 --bun

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix crash constructing jest.fn() via Reflect.construct #30212 - Also fixes the crash when constructing a mock function via Reflect.construct by adding a dedicated construct handler to JSMockFunction

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any issues — the construct handler mirrors the established callFrame->setThisValue() pattern from NapiClass.cpp and has solid test coverage — but since this is hand-written JSC [[Construct]] semantics with in-place CallFrame mutation, it's worth a quick human pass.

Extended reasoning...

Overview

This PR fixes a fuzzer-found crash where JSMockFunction registered jsMockFunctionCall as both its call and construct handler. Since [[Construct]] must return an object but the call handler can return undefined, Reflect.construct(jest.fn(), []) hit ASSERT(isCell()) in debug builds (and type-confused in release). The fix adds a dedicated jsMockFunctionConstruct (~25 lines) that synthesizes this from new.target.prototype, mutates the call frame's this-value, delegates to jsMockFunctionCall, and returns the result if it's an object or the new instance otherwise. Two new tests cover new, Reflect.construct, prototype inheritance, primitive-return discarding, and spyOn construction.

Security risks

None identified. This is test-runner-only code (bun:test mock functions), not reachable from production runtime paths. No auth, crypto, or untrusted-input parsing is involved. The get() on new.target.prototype can invoke user code, but that's spec-required behavior and is guarded with RETURN_IF_EXCEPTION.

Level of scrutiny

Medium. The logic is small and correct on inspection — it matches ordinary JS constructor semantics and Jest's behavior, exception scopes are placed correctly, and the callFrame->setThisValue() + delegate pattern is already used identically in src/jsc/bindings/NapiClass.cpp:62. However, this is native JSC binding code that manipulates a CallFrame in place and re-enters another host function with it; subtle invariants around construct frames are the kind of thing a maintainer familiar with Bun's JSC fork should sign off on rather than a bot.

Other factors

  • No CODEOWNERS for these files.
  • The bug-hunting system found no issues.
  • The simpler get(prototype) + constructEmptyObject approach here differs slightly from NapiClass's getFunctionRealm/createSubclassStructure path, but that's a reasonable simplification for mock functions and is covered by the new fn6.prototype = {marker: 3} test.
  • Test coverage is thorough and the PR description lists the existing suites that still pass.

@robobun

robobun commented May 24, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #30212, which fixes the same isCell() assertion with an equivalent construct handler for JSMockFunction and has already been through review. Closing this one; its additional spy-construction regression test has been added to #30212.

@robobun robobun closed this May 24, 2026
@robobun robobun deleted the farm/4e0236f3/fix-mock-construct branch May 24, 2026 21:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant