Skip to content

bun:test: treat a hole or a missing array element as undefined for asymmetric matchers - #34647

Closed
robobun wants to merge 5 commits into
mainfrom
farm/3a0a1ed2/fix-hole-asymmetric-matcher-crash
Closed

robobun wants to merge 5 commits into
mainfrom
farm/3a0a1ed2/fix-hole-asymmetric-matcher-crash

Conversation

@robobun

@robobun robobun commented Jul 18, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Each of these lines kills bun test with panic(main thread): Segmentation fault at address 0x5 (ASAN: SEGV on unknown address 0x000000000005): expect([, 1]).toEqual([expect.any(Date), 1]) and expect([expect.any(Number)]).not.toEqual([]).
  • Bun__deepEquals reads elements with getIndexWithoutAccessors (src/jsc/bindings/bindings.cpp:682). It returns the empty JSValue for a hole and for an index past the end of the shorter array. The array loop (:993) passes it to matchAsymmetricMatcherAndGetFlags as otherProp.
  • isCell() is true for the empty value, so isString(), isObject() and hasInstance() read a null cell. expect.anything() does not crash, but it accepts the empty value.

Fix

  • matchAsymmetricMatcherAndGetFlags (bindings.cpp:344) replaces an empty otherProp with jsUndefined() first.
  • Correct because non-strict toEqual already reads a hole or a missing trailing element as undefined. Jest does the same: its non-strict equals calls the matcher with undefined for a key that the other side does not have.
  • One place covers all four call sites, which already check the matcher side.
  • Verified: test/js/bun/test/expect.test.js, two new tests. Stock bun exits 139 on both. Also ran jest-extended.test.js and both expect-extend* files.

Background

  • An asymmetric matcher is the object that expect.any(Number) returns. toEqual gives it the value on the other side and uses its verdict.
  • The empty JSValue is the "no value" encoding in JavaScriptCore, not undefined. isCell() is true for it and asCell() is null, so code must test isEmpty() first.
  • toStrictEqual is not affected. It returns on a hole or a length mismatch before it reaches a matcher.
Notes

Shapes that crash on stock bun 1.4.3-canary.1 (Segmentation fault at address 0x5, exit 139). All pass with this change:

// hole aligned with a matcher, either side, top level or nested
expect([, 1]).toEqual([expect.any(Date), 1]);
expect([expect.any(Date), 1]).not.toEqual([, 1]);
expect({ x: [, 1] }).not.toEqual({ x: [expect.any(Date), 1] });
expect(Array(2)).not.toEqual([expect.any(Date), expect.any(Number)]);
// no hole: the array that holds the matcher is longer than the other side
expect([expect.any(Number)]).not.toEqual([]);
expect([1, expect.any(Number)]).not.toEqual([1]);
expect([]).not.toBeOneOf([[expect.any(Number)]]);
expect([[expect.any(Number)]]).not.toContainEqual([]);
expect(new Map([[expect.any(Number), 2]])).not.toContainEqual([]);
// an accessor at the index (getIndexWithoutAccessors skips it)
const a = [0];
Object.defineProperty(a, 0, { get: () => "s", enumerable: true });
expect(a).not.toEqual([expect.any(Number)]);

The same crash with expect.any(String), expect.any(Foo), expect.stringContaining, expect.stringMatching and expect.objectContaining.

Wrong verdict, no crash, on stock bun: expect([, 1]).toEqual([expect.anything(), 1]) and expect([expect.anything()]).toEqual([]) pass. isUndefinedOrNull() is false for the empty value. Both are a mismatch now.

Shapes that were already a clean result on stock bun: the matcher in the shorter array (expect([]).not.toEqual([expect.any(Number)]), the second loop at bindings.cpp:1019 returns false), toStrictEqual, toMatchObject (it compares array lengths first), toHaveBeenCalledWith with more or fewer arguments.

The accessor test uses a getter whose value the matcher rejects. The assertion holds whether toEqual skips the accessor or calls it, so the test pins the crash and not that choice.

Jest agrees with the new verdicts. Its non-strict equals (packages/expect-utils/src/jasmineUtils.ts) compares array lengths only for toStrictEqual. For a key that holds a matcher on one side and is missing on the other side, it calls the matcher with undefined. So expect([expect.not.stringContaining("a")]).toEqual([]) passes in Jest, and it passes with this change.

The other direction is not changed here. With the matcher in the longer expected array, the second loop (bindings.cpp:1019) returns a mismatch and does not call the matcher. The verdict differs from Jest only for a matcher that accepts undefined, for example expect([]).toEqual([expect.not.stringContaining("a")]). The same holds for a missing object key. That is a separate change.


[human-review] gate passed · iteration 6 · 2 files touched

fails on main (without fix)
ASAN without fix: BUILD FAILED (no junit output)
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/bun/test/expect.test.js
bun test v1.4.3 (6a92015fc)

test/js/bun/test/expect.test.js:
(pass) expect() > () [325.85ms]
(pass) expect() > toBe() > expect(0).toBe(0) == true [2.60ms]
(pass) expect() > toBe() > expect(0).toBe(0) == true [0.48ms]
(pass) expect() > toBe() > expect(0).toBe(0) == true [0.29ms]
(pass) expect() > toBe() > expect(-0).toBe(-0) == true [0.32ms]
(pass) expect() > toBe() > expect(1).toBe(1) == true [0.24ms]
(pass) expect() > toBe() > expect(1).toBe(1) == true [0.23ms]
(pass) expect() > toBe() > expect(NaN).toBe(NaN) == true [0.27ms]
(pass) expect() > toBe() > expect(Infinity).toBe(Infinity) == true [0.24ms]
(pass) expect() > toBe() > expect({}).toBe({}) == true [0.23ms]
(pass) expect() > toBe() > expect(Symbol(a)).toBe(Symbol(a)) == true [0.27ms]
(pass) expect() > toBe() > expect(0).toBe(false) == false [2.51ms]
(pass) expect() > toBe() > expect(0).toBe("") == false [0.53ms]
(pass) expect() > toBe() > expect(0).toBe(-0) == false [0.31ms]
(pass) expect() > toBe() > expect(0).toBe(-0) == false [0.36ms]
(pass) ex
... (truncated)

release without fix: BUILD FAILED (no junit output)
bun test v1.4.3-canary.1 (6a92015fc)

test/js/bun/test/expect.test.js:
(pass) expect() > () [0.93ms]
(pass) expect() > toBe() > expect(0).toBe(0) == true [0.04ms]
(pass) expect() > toBe() > expect(0).toBe(0) == true
(pass) expect() > toBe() > expect(0).toBe(0) == true
(pass) expect() > toBe() > expect(-0).toBe(-0) == true
(pass) expect() > toBe() > expect(1).toBe(1) == true
(pass) expect() > toBe() > expect(1).toBe(1) == true
(pass) expect() > toBe() > expect(NaN).toBe(NaN) == true
(pass) expect() > toBe() > expect(Infinity).toBe(Infinity) == true
(pass) expect() > toBe() > expect({}).toBe({}) == true
(pass) expect() > toBe() > expect(Symbol(a)).toBe(Symbol(a)) == true
(pass) expect() > toBe() > expect(0).toBe(false) == false [0.02ms]
(pass) expect() > toBe() > expect(0).toBe("") == false
(pass) expect() > toBe() > expect(0).toBe(-0) == false
(pass) expect() > toBe() > expect(0).toBe(-0) == false
(pass) expect() > toBe() > expect(1).toBe(2) == false
(pass) expect() > toBe() > expect(1).toBe(true) == false
(pass) expect() > toBe() > expect(1).toBe("1") == false
(pass) expect() > toBe() > expect(Infinity).toBe(-Infinity) == false
(pass) expect() > toBe() > expect("foo
... (truncated)
passes on PR (with fix)
ASAN with fix: 2 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/bun/test/expect.test.js
bun test v1.4.3 (6a92015fc)

test/js/bun/test/expect.test.js:
(pass) expect() > () [325.09ms]
(pass) expect() > toBe() > expect(0).toBe(0) == true [2.64ms]
(pass) expect() > toBe() > expect(0).toBe(0) == true [0.47ms]
(pass) expect() > toBe() > expect(0).toBe(0) == true [0.25ms]
(pass) expect() > toBe() > expect(-0).toBe(-0) == true [0.25ms]
(pass) expect() > toBe() > expect(1).toBe(1) == true [0.27ms]
(pass) expect() > toBe() > expect(1).toBe(1) == true [0.23ms]
(pass) expect() > toBe() > expect(NaN).toBe(NaN) == true [0.25ms]
(pass) expect() > toBe() > expect(Infinity).toBe(Infinity) == true [0.25ms]
(pass) expect() > toBe() > expect({}).toBe({}) == true [0.26ms]
(pass) expect() > toBe() > expect(Symbol(a)).toBe(Symbol(a)) == true [0.26ms]
(pass) expect() > toBe() > expect(0).toBe(false) == false [1.79ms]
(pass) expect() > toBe() > expect(0).toBe("") == false [0.49ms]
(pass) expect() > toBe() > expect(0).toBe(-0) == false [0.31ms]
(pass) expect() > toBe() > expect(0).toBe(-0) == false [0.30ms]
(pass) ex
... (truncated)

release with fix: 2 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     e0c9310714
  features     baseline

23 deps, 131 codegen, 1172 objects in 841ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1244] install /workspace/bun
bun install v1.4.3-canary.1 (6a92015fc)

Checked 22 installs across 61 packages (no changes) [13.00ms]
[2/1244] gen ErrorCode+*.h
[3/1244] install /workspace/bun/packages/bun-error
bun install v1.4.3-canary.1 (6a92015fc)

Checked 1 install across 2 packages (no changes) [1.00ms]
[4/1244] install /workspace/bun/src/node-fallbacks
bun install v1.4.3-canary.1 (6a92015fc)

Checked 111 installs across 104 packages (no changes) [6.00ms]
[5/1244] gen bindgenv2
[6/1244] gen node-fallbacks/react-refresh.js
Bundled 1 module in 6ms

  react-refresh.js  4.81 KB  (entry point)

[7/1244] fetch tinycc
[tinycc] up to date
[8/1243] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[9/1216] fetch zlib
[zlib] up to date
[10/1216] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[11/1216] gen .bind.ts → Gen
... (truncated)
diff hotspot
src/jsc/bindings/bindings.cpp   |  5 +++++
 test/js/bun/test/expect.test.js | 50 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 55 insertions(+)

gate history · 2 passed · 0 rejected · iteration 6

evidence per changed file
file                             reads  edits  tests
src/jsc/bindings/bindings.cpp        7      3     18
test/js/bun/test/expect.test.js      3      2     18

…ric matchers

expect([,1]).toEqual([expect.any(Date), 1]) segfaulted at 0x5: the hole
arrives in matchAsymmetricMatcherAndGetFlags as the empty JSValue, and
isString()/isObject()/hasInstance() dereference asCell() == nullptr.
Normalizing empty to jsUndefined() at the top of the function makes every
matcher path safe and also fixes expect.anything() incorrectly accepting a
hole (Jest treats holes as undefined, which anything() rejects).
@robobun

robobun commented Jul 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: CI is green on current main (build #114787). Ready for review.

Reproduced on stock bun 1.4.3-canary.1 with USE_SYSTEM_BUN=1 bun test test/js/bun/test/expect.test.js -t "asymmetric matcher": Segmentation fault at address 0x5, exit 139. This holds for the hole shapes (expect([, 1]).toEqual([expect.any(Date), 1])) and for the length-mismatch shapes that need no hole (expect([expect.any(Number)]).not.toEqual([]), toBeOneOf, toContainEqual, Map entries).

With the debug build both new tests pass. Full expect.test.js: 418 pass, 2 todo, 0 fail.

@robobun

robobun commented Jul 18, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 11:13 AM PT - Sep 12th, 2026

✅ @robobun, your commit e0c9310714227af09327590e9d386fcd6b2eaf5c passed in Build #114787! 🎉


🧪   To try this PR locally:

bunx bun-pr 34647

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

bun-34647 --bun

@coderabbitai

coderabbitai Bot commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 19f4c6cd-81a3-49ea-a92e-e87a124c6524

📥 Commits

Reviewing files that changed from the base of the PR and between a7d3016 and e0c9310.

📒 Files selected for processing (1)
  • src/jsc/bindings/bindings.cpp

Included review availability: Your plan provides up to 10 included reviews per hour; 4 remain after this review.


Walkthrough

The change normalizes empty array properties to undefined before asymmetric matcher evaluation. New tests cover holes, missing elements, nested arrays, deleted elements, accessors, containment matchers, and toEqual.

Changes

Asymmetric matcher array-hole handling

Layer / File(s) Summary
Normalize array holes and validate matcher behavior
src/jsc/bindings/bindings.cpp, test/js/bun/test/expect.test.js
Empty array properties are normalized to undefined during asymmetric matcher evaluation. Tests cover nested, deleted, generated, missing, and accessor-backed elements, plus toEqual equivalence.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to e0c93

The change is covered by passing targeted and full expect tests, with no unresolved merge-blocking concerns.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary change: treating array holes and missing elements as undefined for asymmetric matchers.
Description check ✅ Passed The description clearly explains the problem, fix, rationale, affected behavior, and verification results. It does not use the template headings exactly, but it provides the required information and i…

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

@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-placed crash fix with thorough test coverage.

What was reviewed:

  • Verified all four call sites of matchAsymmetricMatcher guard matcherProp as non-empty, so only otherProp needs normalizing — the fix is at the right layer and protects every caller.
  • Checked that readFlagsAndProcessPromise cannot re-introduce an empty value after normalization.
  • Test covers every crashing matcher branch (any, stringContaining, stringMatching, objectContaining, closeTo), both hole directions, nested arrays, Array(n)/delete, and the expect.anything() semantic fix.
Extended reasoning...

Overview

This PR fixes a segfault in bun:test when a sparse array element (hole) is compared against an asymmetric matcher via toEqual. The fix is a 4-line normalization at the top of matchAsymmetricMatcherAndGetFlags in src/jsc/bindings/bindings.cpp that converts an empty JSValue to jsUndefined() before any matcher branch touches it. A 26-line test block is added to test/js/bun/test/expect.test.js covering the full variant matrix.

Security risks

None. This is a defensive null-guard on an internal comparison path in the test runner. No user-controlled input reaches new allocations, syscalls, or external boundaries.

Level of scrutiny

Low-to-medium. The mechanism is well-explained (empty JSValue → isCell() true → asCell() null deref), the fix is a single defensive branch at the function entry, and the semantics (hole ≡ undefined) match both Jest and the existing toEqual array-hole handling elsewhere in Bun__deepEquals. The change cannot regress non-hole inputs since isEmpty() is false for every real JSValue.

Other factors

  • Right layer: REVIEW.md asks for fixes at the layer owning the invariant. All four callers of matchAsymmetricMatcher(AndGetFlags) already guard the matcher side with !isEmpty() / non-null cell checks, but pass the other side unguarded — normalizing inside the callee protects all of them at once (including the Bun__deepMatch path used by toMatchObject/objectContaining).
  • Test quality: Follows repo guidance — added to the existing expect.test.js file inside the deepEquals with asymmetric matchers describe block, covers every matcher branch that dereferences otherProp, both directions (hole on received vs expected), all three hole-production forms (literal, Array(n), delete), a nested case, the expect.anything() semantic bug, and a positive sanity assertion. PR description confirms USE_SYSTEM_BUN=1 reproduces the crash and the debug build passes.
  • No collateral risk: otherProp is a by-value parameter; reassigning it has no effect on callers. readFlagsAndProcessPromise round-trips the (now-defined) value through encode/decode, which is safe for jsUndefined().

@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 — clean segfault fix, guard placed at the shared helper.

What was reviewed:

  • Traced all four matchAsymmetricMatcher call sites; matcherProp is always cell-guarded, only otherProp can arrive empty — normalizing at function entry covers every path.
  • Walked each matcher branch (anything, any, stringContaining/Matching, objectContaining, arrayContaining, closeTo) with jsUndefined() — all resolve to FAIL as Jest specifies; no branch relied on the empty sentinel.
  • Test covers both hole directions, all crashing matcher types, the anything() false-positive, and alternate hole producers (Array(n), delete).
Extended reasoning...

Overview

The PR fixes a segfault in bun:test's deep-equality path when a sparse-array hole is compared against an asymmetric matcher (expect.any(...), expect.stringContaining, etc.). Bun__deepEquals reads holes via getIndexWithoutAccessors, which returns the empty JSValue; that empty value flowed into matchAsymmetricMatcherAndGetFlags where isCell() reports true for empty and subsequent asCell()-based type queries dereference null. The fix is a 3-line normalization at the top of matchAsymmetricMatcherAndGetFlags (bindings.cpp:313) turning empty into jsUndefined(), plus a comprehensive regression test in expect.test.js.

Security risks

None. This is test-runner deep-equality code; no untrusted input, network, or filesystem surface. The change strictly hardens against a null-cell dereference.

Level of scrutiny

Low-to-medium. The fix is a defensive one-liner in a shared helper. I verified it's placed at the right layer per REVIEW.md guidance: matchAsymmetricMatcher has four call sites (bindings.cpp:673, 683, 1685, 1699), and normalizing inside the callee protects all of them without duplicating guards. matcherProp cannot be empty at these sites (each guards isCell() && !isEmpty() or equivalent before the call), so only otherProp needed the check. I walked every downstream branch with jsUndefined() substituted — isUndefinedOrNull() → FAIL for anything() (fixing the false-PASS), isCell()/isString()/isObject()/isNumber() all false so each matcher returns FAIL, and hasInstance(globalObject, jsUndefined()) is well-defined. Nothing depended on the empty sentinel semantically.

Other factors

The test is thorough: it exercises the hole on both received and expected sides (both call-site orderings), every matcher type that crashed plus closeTo, the anything() correctness bug (not just the crash), nested arrays, and all three hole-production methods ([,x], Array(n), delete). It includes a positive sanity assertion that holes still deep-equal undefined. PR description confirms USE_SYSTEM_BUN=1 reproduces the crash and the debug build passes; full expect.test.js remains green. CI failures listed by robobun are unrelated pre-existing flakes. No prior human review comments to address.

An index past the end of the shorter array, and an accessor at the index,
come back from getIndexWithoutAccessors as the empty JSValue, the same as a
hole. expect([expect.any(Number)]).not.toEqual([]) took the same path into
matchAsymmetricMatcherAndGetFlags and crashed at 0x5. The existing
normalization covers it. Add tests for the length-mismatch shapes (toEqual,
toBeOneOf, toContainEqual, Map entries) and make the comment name every
source of the empty value.
Comment thread src/jsc/bindings/bindings.cpp Outdated
@robobun robobun changed the title bun:test: treat array holes as undefined when matched against asymmetric matchers bun:test: treat a hole or a missing array element as undefined for asymmetric matchers Sep 12, 2026

@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 — the empty→jsUndefined() normalization at the top of matchAsymmetricMatcherAndGetFlags is the right layer for this and covers all four matchAsymmetricMatcher call sites.

What was reviewed:

  • Traced the empty JSValue from getIndexWithoutAccessors through the non-strict array loop at bindings.cpp:993 into the recursive Bun__deepEquals → matchAsymmetricMatcher path; the guard sits before every otherProp.isCell()/asCell()/hasInstance use.
  • Checked that matcherProp itself is already isCell()-guarded at every call site (829/840/2250/2264), so only otherProp needed the empty check.
  • Test coverage hits the full variant matrix (every matcher type, both sides, nested, Array(n)/delete, off-end index, toBeOneOf/toContainEqual/Map, accessor index) plus the expect.anything() wrong-verdict case; all in-process, no timing or network.
Extended reasoning...

Overview

This PR fixes a segfault in bun:test's deep-equality path when an asymmetric matcher (expect.any(), expect.stringContaining(), etc.) is aligned against an array hole or an index past the end of the shorter array. The fix is a 4-line guard at the top of matchAsymmetricMatcherAndGetFlags in src/jsc/bindings/bindings.cpp that normalizes an empty JSValue otherProp to jsUndefined(). Two new it() blocks in test/js/bun/test/expect.test.js (~50 lines) cover the variant matrix.

Security risks

None. This is test-runner assertion logic; no untrusted-input parsing, no auth/crypto/permission surface. The change turns a null-cell dereference (crash) into a well-defined undefined comparison.

Level of scrutiny

Low-to-moderate. The C++ change is 4 lines with an obvious mechanism: JSC's empty JSValue reports isCell() as true with a null cell, and downstream code (otherProp.asCell()->type(), constructorObject->hasInstance(globalObject, otherProp), otherProp.isString()) dereferences it. Normalizing to jsUndefined() at function entry is consistent with how the non-strict array loop already treats holes (bindings.cpp:1009 short-circuits when the other side is undefined, and the second loop at :1019 treats empty as undefined). I verified the four call sites of matchAsymmetricMatcher all guard the matcher-side argument with isCell() before calling, so only otherProp can arrive empty — the single-sided guard is sufficient. The one-line comment names the provenance of the empty value, which is load-bearing per REVIEW.md.

Other factors

Test coverage is thorough per REVIEW.md's "cover the variant matrix" rule: every built-in asymmetric matcher, hole on both received and expected sides, nested arrays, alternate hole producers (Array(n), delete), matcher past end of shorter array, entry via toBeOneOf/toContainEqual/Map, an accessor-defined index, plus sanity checks. Tests are pure in-process assertions (no spawning, timing, or network), placed in the existing describe("expect()") block. The PR description states stock bun exits 139 on these, satisfying the fails-under-USE_SYSTEM_BUN=1 requirement. No CODEOWNERS entry covers these paths. Since the last review, commits a7d3016 and e0c9310 added the off-end-index test block and shortened the C++ comment; the bug-hunt ran to dry_streak with no findings.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

#42536 covers this crash as part of the fix for #42529 (a matcher at a key or index the other side lacks). It carries the crash-shape assertions from this PR. If #42536 lands first, this PR can close.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #42536.

#42536 fixes the same crash: an asymmetric matcher that receives the empty value for an array hole, or for an index past the end of the shorter array. It fixes it at the source. The non-strict array loops in Bun__deepEquals now read such an element as undefined before they recurse. It also runs the matcher at an object key that the other side lacks (#42529).

I ran all 28 assertions from the two test blocks of this PR against a debug (ASAN) build of #42536 at e287ff9. All of them pass. I also ran 95 more shapes, each in its own process. They cover toStrictEqual, toBeOneOf, toContainEqual, Map and Set members, the mock call and return matchers, toMatchObject, toHaveProperty, expect.arrayContaining, expect.objectContaining, a Proxy of an array, sparse arrays, and Bun.deepEquals. None of them crash. On 1.4.3-canary.1+b99371011 (same bindings.cpp as main), 63 of the 123 shapes end with Segmentation fault at address 0x5. Three more (expect.anything() at a hole or past the end) give the wrong verdict.

The guard in matchAsymmetricMatcherAndGetFlags from this PR is not necessary on top of #42536. The strict array loop returns before it recurses with a hole, and each property path returns false on an empty value. No caller can give the empty value to the matcher. The new describe block in #42536 asserts the crash shapes from this PR: a hole, a shorter array, toBeOneOf, toContainEqual, a Map, and an accessor at an index.

@robobun robobun closed this Sep 13, 2026
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.

2 participants