Skip to content

Fix assertion failure when spyOn is used on non-callable indexed property#29481

Closed
robobun wants to merge 1 commit into
mainfrom
farm/2e6b20d2/spyon-indexed-accessor-attribute
Closed

Fix assertion failure when spyOn is used on non-callable indexed property#29481
robobun wants to merge 1 commit into
mainfrom
farm/2e6b20d2/spyon-indexed-accessor-attribute

Conversation

@robobun

@robobun robobun commented Apr 19, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes a debug assertion in PropertySlot::setValue triggered after calling spyOn(obj, <numeric index>) on a non-callable (or missing) value.

const obj = {};
Bun.jest().spyOn(obj, 1);
obj[1]; // ASSERTION FAILED: ... !(attributes & PropertyAttribute::Accessor)

When spying on a non-callable value at a numeric index, spyOn stores the mock directly via putDirectIndex rather than wrapping it in a GetterSetter. However it was still passing PropertyAttribute::Accessor in the attributes. This left the indexed storage with a plain value carrying the Accessor attribute, so the next lookup on that index hit the PropertySlot::setValue assertion that forbids Accessor on plain values.

The fix masks out PropertyAttribute::Accessor for the putDirectIndex call.

Found by Fuzzilli. Fingerprint: PropertySlot.h(219)

How did you verify your code works?

Added a regression test in test/js/bun/test/mock-fn.test.js covering spyOn on missing and existing non-callable numeric indices, including subsequent reads and Object.getOwnPropertyDescriptor. The full mock-fn.test.js suite (73 tests) passes.

@robobun

robobun commented Apr 19, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:51 AM PT - May 6th, 2026

@robobun, your commit 22eaff9 has 4 failures in Build #52042 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29481

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

bun-29481 --bun

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7328adf8-6685-47eb-bf01-189507476fe4

📥 Commits

Reviewing files that changed from the base of the PR and between 55b62ef and 9c2d0bc.

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

Walkthrough

Modified JSMockFunction.cpp to clear the PropertyAttribute::Accessor bit when installing mocks on indexed properties via putDirectIndex. Added test case verifying spyOn behavior on numeric property keys for non-callable values, including proper restoration of original values.

Changes

Cohort / File(s) Summary
Mock Function Implementation
src/bun.js/bindings/JSMockFunction.cpp
Clear PropertyAttribute::Accessor bit when installing mocks on indexed properties via putDirectIndex instead of passing attributes unchanged.
Test Coverage
test/js/bun/test/mock-fn.test.js
Added test case for spyOn with numeric/indexed property keys on non-callable values, verifying spy installation, property descriptors, and mockRestore behavior.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main bug fix: preventing assertion failure when spyOn is used on non-callable indexed properties.
Description check ✅ Passed The description fully complies with the template, providing comprehensive details on what the PR does and how verification was performed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix spyOn crash with indexed properties #28540 - Fixes the same spyOn crash with indexed properties by stripping PropertyAttribute::Accessor in JSMock__jsSpyOn
  2. Fix PropertySlot assertion by stripping static hash table attribute bits #28335 - Fixes the same PropertySlot.h(219) assertion by stripping invalid attribute bits before putDirect/putDirectIndex in JSMockFunction.cpp

🤖 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.

LGTM — minimal one-line fix that correctly masks out PropertyAttribute::Accessor before calling putDirectIndex, with a solid regression test.

Extended reasoning...

Overview

The PR touches two files: (one-line change) and (regression test). It targets the non-callable indexed property branch of JSMock__jsSpyOn.

Security risks

None. This is purely in the test mock/spy infrastructure and involves no auth, crypto, or permissions concerns.

Level of scrutiny

Low. The bug is a clear invariant violation: JSC's PropertySlot::setValue asserts that plain values must not carry PropertyAttribute::Accessor. The existing code set that bit even when storing a plain mock value (not a GetterSetter) via putDirectIndex. The fix — attributes & ~PropertyAttribute::Accessor — is the minimal and correct remediation. The callable path already used putDirect without the Accessor bit; this makes the non-callable path consistent.

Other factors

No bugs were found by the bug hunting system. The regression test covers missing and existing non-callable indexed properties, Object.getOwnPropertyDescriptor, and mockRestore. The PR is Fuzzilli-found, well-described, and self-contained.

@robobun

robobun commented Apr 19, 2026

Copy link
Copy Markdown
Collaborator Author

CI summary for build #46435:

  • All builds and test suites passed on linux (x64/aarch64/musl/baseline/asan), alpine, debian, ubuntu, windows (x64/aarch64), and darwin x64.
  • darwin-13-aarch64-test-bun / darwin-14-aarch64-test-bun: Expired (0s) — jobs were never picked up by an agent, not test failures.
  • Format (autofix.ci): wants to reformat docs/runtime/bunfig.mdx, a file this PR does not touch, and hit a cherry-pick merge conflict. Same failure appears on other unrelated open PRs (e.g. Add Windows MSI installer built in CI #29475). clang-format passes on the one C++ file this PR modifies.

No failures are related to this change.

@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.

LGTM — minimal, correct fix masking out the Accessor bit for the indexed-property putDirectIndex path, with a regression test.

Extended reasoning...

Overview

This PR changes a single line in src/jsc/bindings/JSMockFunction.cpp inside JSMock__jsSpyOn, in the branch that handles spying on a non-callable value at a numeric index. The branch already stores the mock as a plain value via putDirectIndex (not wrapped in a GetterSetter), but it was passing attributes that had just had PropertyAttribute::Accessor OR'd in. The fix masks that bit back out (attributes & ~PropertyAttribute::Accessor) so the indexed storage doesn't carry an Accessor attribute on a plain value, which is what tripped the PropertySlot::setValue debug assertion. A regression test is added to test/js/bun/test/mock-fn.test.js.

Security risks

None. This is test-runner mock infrastructure (spyOn), not auth/crypto/network code. The change only narrows attribute bits passed to an internal JSC put; it cannot expose data or change permissions.

Level of scrutiny

Low. The diff is one expression change whose intent is fully explained by the surrounding code: the sibling else branch uses putDirectAccessor with a real GetterSetter (where Accessor is appropriate), while this branch stores a plain value (where Accessor is invalid). mock->spyAttributes is captured before attributes |= PropertyAttribute::Accessor, so the clearSpy() restore path is unaffected and the new mockRestore test confirms it.

Other factors

CI passed across all platforms (the only failures were unrelated darwin-aarch64 agent expirations and an autofix.ci formatting issue on an untouched docs file). No CODEOWNERS apply to these paths, no outstanding reviewer comments, and the bug-hunting system found nothing. The duplicate-PR bot flagged two earlier PRs targeting the same assertion, but that's a maintainer triage concern rather than a correctness concern for this change.

@robobun

robobun commented May 6, 2026

Copy link
Copy Markdown
Collaborator Author

CI summary for build #51953 (rebased commit 6d6cfe4):

None of the failures involve mock-fn.test.js, spyOn, or JSMockFunction.cpp:

Check Cause Also failing elsewhere
Lint JavaScript oxlint "Unsafe declaration merging" in src/js (not touched by this PR) Fails on PRs #30306, #30292, #30291, #30290
test/js/bun/test/parallel/test-http-should-emit-close-when-connection-is-aborted.ts Timeout on all 3 Windows targets Also fails on PR #30291
test/cli/hot/hot.test.ts Flaky should work with sourcemap generation on Windows (reloadCounter 38 vs 50) Listed in flaky annotation on this build too
test/js/web/fetch/fetch-http2-client.test.ts (debian-13-x64-asan) Unrelated ASSERTION FAILED: wasRemoved in AtomStringImpl.cpp(454) Pre-existing JSC/WTF issue

The added regression test (spyOn works with non-callable indexed properties) passes on all platforms.

@robobun robobun force-pushed the farm/2e6b20d2/spyon-indexed-accessor-attribute branch from 6d6cfe4 to 1ca98f8 Compare May 6, 2026 06:47
…erty

When spyOn(obj, 1) was called on an object where obj[1] was missing or
a non-callable value, the mock was stored via putDirectIndex with
PropertyAttribute::Accessor set, even though the stored value was a
plain JSMockFunction and not a GetterSetter. Any subsequent property
lookup on that index hit the PropertySlot::setValue assertion that
forbids the Accessor attribute on plain values.

Mask out the Accessor attribute when storing the mock at a numeric
index, since the indexed path intentionally does not wrap the mock in
a GetterSetter.
@robobun robobun force-pushed the farm/2e6b20d2/spyon-indexed-accessor-attribute branch from 1ca98f8 to 22eaff9 Compare May 6, 2026 06:55

@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.

LGTM — minimal, well-scoped fix that strips the invalid Accessor bit for the putDirectIndex path, with a regression test.

Extended reasoning...

Overview

This PR changes a single line in src/jsc/bindings/JSMockFunction.cpp (JSMock__jsSpyOn) and adds a regression test in test/js/bun/test/mock-fn.test.js. In the non-callable indexed-property branch, the code unconditionally OR'd PropertyAttribute::Accessor into attributes (intended for the putDirectAccessor + GetterSetter path), but then passed those same attributes to putDirectIndex, which stores the mock as a plain value. Storing a plain value with the Accessor attribute violates JSC's PropertySlot::setValue invariant and trips a debug assertion on the next read. The fix masks out PropertyAttribute::Accessor for the putDirectIndex call only.

Security risks

None. This is purely a correctness fix in test-runner mock infrastructure (bun:test spyOn). It does not touch auth, crypto, networking, parsing, or any user-input boundary. The change strictly removes an attribute bit that was incorrect in this branch and cannot widen capabilities.

Level of scrutiny

Low. The diff is one expression (attributesattributes & ~PropertyAttribute::Accessor), the rationale is mechanically verifiable from the surrounding code (the Accessor bit was added two lines above for the sibling putDirectAccessor branch and is meaningless/incorrect for a plain-value putDirectIndex), and mock->spyAttributes (used by clearSpy() for restoration) was captured before the bit was OR'd in, so restore behavior is unaffected. The added test exercises both the missing-index and existing-non-callable-index cases plus getOwnPropertyDescriptor and mockRestore.

Other factors

Earlier CI runs (#46435, #51953) passed on all platforms with only unrelated/flaky failures, and the new regression test passed everywhere. The most recent build (#52039) shows infrastructure-wide build-zig failures in scripts/build/ci.ts across every target, which are unrelated to this C++/test-only change. The duplicate-PR bot flagged #28540/#28335 as overlapping; that's a triage concern for maintainers but doesn't affect correctness here. No human reviewer comments are outstanding.

@robobun

robobun commented May 30, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #31619, which fixes the same assertion with the correct semantics.

This PR strips the Accessor attribute and stores the mock as a plain data property, so reading the spied index returns the mock object itself (descriptor {value, writable, enumerable, configurable}) and neither reads nor writes are tracked as spy calls.

The non-indexed spyOn path (the putDirectAccessor branch just below this code) installs the mock as a real getter/setter: reading obj.foo invokes the spy, records a call, and returns the mock's result; the descriptor has get/set. #31619 wraps the mock in a GetterSetter for the indexed case too, so spyOn(obj, 1) behaves identically to spyOn(obj, "foo") — consistent accessor semantics, and call tracking works.

@robobun robobun closed this May 30, 2026
@robobun robobun deleted the farm/2e6b20d2/spyon-indexed-accessor-attribute branch May 30, 2026 23:10
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