Skip to content

Fix map and Set equality permits many-to-one matches - #7056

Merged
tim-smart merged 3 commits into
mainfrom
audit/repro-17f0b91a-equal-many-to-one
Aug 6, 2026
Merged

Fix map and Set equality permits many-to-one matches#7056
tim-smart merged 3 commits into
mainfrom
audit/repro-17f0b91a-equal-many-to-one

Conversation

@fubhy

@fubhy fubhy commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

Equal.equals returns true for same-sized Sets containing two distinct Equal-equivalent x values versus one x and one y, and likewise for Maps, because both left x values reuse the single right x; distinct collections are conflated.

Important

This PR includes focused regression tests and updates Map and Set comparison to consume each right-side match at most once.

Map and Set equality permits many-to-one matches

Module: effect/Equal
Audit ID: effect-c2ccacce11d583fb
Severity / confidence: medium / high

What happens

Equal.equals returns true for same-sized Sets containing two distinct Equal-equivalent x values versus one x and one y, and likewise for Maps, because both left x values reuse the single right x; distinct collections are conflated.

Why it happens

makeCompareMap and makeCompareSet independently scan the entire right iterable for each left item and stop at the first equivalent value, but never mark or remove a matched right item, so the size check does not prevent many-to-one reuse.

Expected behavior

Structural equality for same-sized Map and Set values must establish a one-to-one correspondence under key/value or element equivalence; no right-side entry may satisfy more than one left-side entry.

Relevant implementation

These links and excerpts are pinned to audit base 17f0b91a243ccfe4a38d27debdc983adf434e738.

View problematic code at packages/effect/src/Equal.ts:275-324
    } else if (self instanceof Map) {
      if (!(that instanceof Map) || self.size !== that.size) {
        return false
      }
      return compareMaps(self, that)
    } else if (self instanceof Set) {
      if (!(that instanceof Set) || self.size !== that.size) {
        return false
      }
      return compareSets(self, that)
    }
    return compareRecords(self as any, that as any)
  })
}

function withCache(self: object, that: object, f: (a: any, b: any) => boolean): boolean {
  // Check cache first
  let selfMap = equalityCache.get(self)
  if (!selfMap) {
    selfMap = new WeakMap()
    equalityCache.set(self, selfMap)
  } else if (selfMap.has(that)) {
    return selfMap.get(that)!
  }

  // Perform the comparison
  const result = f(self, that)

  // Cache the result bidirectionally
  selfMap.set(that, result)

  let thatMap = equalityCache.get(that)
  if (!thatMap) {
    thatMap = new WeakMap()
    equalityCache.set(that, thatMap)
  }
  thatMap.set(self, result)

  return result
}

const equalityCache = new WeakMap<object, WeakMap<object, boolean>>()

function compareArrays(self: Array<unknown>, that: Array<unknown>): boolean {
  for (let i = 0; i < self.length; i++) {
    if (!compareBoth(self[i], that[i])) {
      return false
    }
  }

View exact lines on GitHub

Excerpt truncated. Open the complete packages/effect/src/Equal.ts:275-399 range.

Reproduction

pnpm test --run packages/effect/test/Equal.test.ts -t "matches Map and Set entries one-to-one"

Observed failure: Focused contract assertion failed against 17f0b91, demonstrating: Map and Set equality permits many-to-one matches.

Implementation handoff

The initial reproduction tests on this branch are the regression specification for the implementation fix that should follow in this PR.

  1. Start with the pinned implementation excerpts and the Why it happens analysis above.
  2. Change the implementation so it satisfies the stated Expected behavior; do not weaken or remove the reproduction assertions.
  3. Run the focused reproduction command(s) and confirm the observed failures become passing tests:
pnpm test --run packages/effect/test/Equal.test.ts -t "matches Map and Set entries one-to-one"
  1. Run the affected package's existing tests, then the repository lint and type checks before requesting review.

Audit provenance

  • Audit base: 17f0b91a243ccfe4a38d27debdc983adf434e738
  • Reproduction base: 17f0b91a243ccfe4a38d27debdc983adf434e738
  • Findings: effect-c2ccacce11d583fb
  • Initial patch: focused reproduction tests; implementation fix added in this PR

Closes EFF-494

@fubhy fubhy added the audit Findings originating from the Effect runtime correctness audit label Aug 5, 2026
@changeset-bot

changeset-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f05514e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 30 packages
Name Type
effect Patch
@effect/ai-anthropic Patch
@effect/ai-openai Patch
@effect/ai-openai-compat Patch
@effect/ai-openrouter Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch
@effect/opentelemetry Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node Patch
@effect/platform-node-shared Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/vitest Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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

Important

This PR adds the correct regression test, but the implementation fix advertised in the title is not present. packages/effect/src/Equal.ts still uses makeCompareMap / makeCompareSet without marking matched right-side entries, so Equal.equals continues to return true for the cases the new test asserts should be false.

Reviewed changes

Reviewed the single-file diff adding a focused regression test for effect/Equal Map/Set many-to-one matching.

  • Added a Key helper class that compares by group and hashes to 0, producing Equal-equivalent but distinct values.
  • Added one test under Equal.equals > Map and Set mixed asserting that Set([Key("x"), Key("x")]) is not equal to Set([Key("x"), Key("y")]), and similarly for Map.

⚠️ Implementation fix is missing

The reproduction test fails as expected against the current implementation ([true, true] vs [false, false]), but the PR title says "Fix" and the body states the fix should be added to this branch. Before merging, packages/effect/src/Equal.ts needs to be updated so that makeCompareMap and makeCompareSet establish a one-to-one correspondence instead of allowing the same right-side entry to satisfy multiple left-side entries.

Technical details
# One-to-one matching in Map/Set equality

## Affected sites
- `packages/effect/src/Equal.ts:361``makeCompareMap` stops at the first matching `[key, value]` pair without marking it used.
- `packages/effect/src/Equal.ts:383``makeCompareSet` stops at the first matching value without marking it used.

## Required outcome
- After matching a right-side entry to a left-side entry, that right-side entry must not be reused for another left-side entry.
- `Equal.equals` must return `false` for the new test cases.

## Suggested approach
Collect `that` into an array and track consumed indices, or build a mutable copy of `that` and remove matched entries as you go. Apply the same change to both `makeCompareMap` and `makeCompareSet`, since both are exported and used by `Schema.ts` map/set equivalence builders.

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | Fix it ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

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

✅ No new issues found.

Reviewed changes

Reviewed the delta since the prior Pullfrog review, which added the implementation fix and changeset for the effect/Equal Map/Set many-to-one matching bug.

  • Added the one-to-one matching fix in packages/effect/src/Equal.ts. makeCompareMap and makeCompareSet now collect the right-side iterable into an array and splice out each matched entry so it cannot be reused by another left-side entry.
  • Added a patch changeset documenting the fix.
  • Confirmed the regression test passes. Equal.test.ts now asserts [false, false] for the many-to-one Map and Set cases.

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

Comment thread packages/effect/src/Equal.ts Outdated
@tim-smart
tim-smart enabled auto-merge (squash) August 6, 2026 00:24

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

✅ No new issues found.

Reviewed changes

Reviewed the delta since the prior Pullfrog review, which optimizes the one-to-one matching fix by replacing splice removal with a constant-time swap-and-pop.

  • Optimized matched-entry removal in packages/effect/src/Equal.ts. makeCompareMap and makeCompareSet now remove a consumed right-side entry by overwriting it with the last array entry and popping, avoiding the O(n) shift cost of splice while preserving one-to-one matching semantics.
  • Validated correctness and performance. Map/Set equality is order-independent, so swapping the matched entry does not affect results; overall complexity stays O(n²).

Pullfrog  | ⚠️ this action is pinned to a commit SHA, which freezes the cleanup step — switch to @v0 or keep the SHA fresh with Dependabot | View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

@tim-smart
tim-smart merged commit 781022a into main Aug 6, 2026
20 checks passed
@tim-smart
tim-smart deleted the audit/repro-17f0b91a-equal-many-to-one branch August 6, 2026 00:48
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
basic.ts 7.13 KB 7.06 KB +0.07 KB (+0.95%)
batching.ts 9.92 KB 9.86 KB +0.06 KB (+0.59%)
brand.ts 6.40 KB 6.34 KB +0.06 KB (+0.99%)
cache.ts 10.78 KB 10.71 KB +0.06 KB (+0.60%)
config.ts 20.79 KB 20.73 KB +0.06 KB (+0.29%)
differ.ts 20.37 KB 20.31 KB +0.06 KB (+0.29%)
http-client.ts 21.59 KB 21.53 KB +0.06 KB (+0.28%)
logger.ts 10.90 KB 10.84 KB +0.06 KB (+0.59%)
metric.ts 9.05 KB 8.98 KB +0.07 KB (+0.76%)
optic.ts 7.25 KB 7.18 KB +0.07 KB (+0.90%)
pubsub.ts 15.05 KB 14.99 KB +0.06 KB (+0.42%)
queue.ts 11.72 KB 11.66 KB +0.07 KB (+0.56%)
schedule.ts 10.89 KB 10.83 KB +0.06 KB (+0.56%)
schema-class.ts 19.33 KB 19.27 KB +0.06 KB (+0.30%)
schema-fromJsonSchemaDocument.ts 29.15 KB 29.09 KB +0.06 KB (+0.20%)
schema-representation-roundtrip.ts 25.47 KB 25.40 KB +0.06 KB (+0.24%)
schema-string-transformation.ts 13.48 KB 13.42 KB +0.06 KB (+0.43%)
schema-string.ts 11.01 KB 10.95 KB +0.06 KB (+0.54%)
schema-template-literal.ts 15.27 KB 15.21 KB +0.06 KB (+0.37%)
schema-toArbitraryLazy.ts 22.08 KB 22.05 KB +0.03 KB (+0.12%)
schema-toCodeDocument.ts 24.50 KB 24.45 KB +0.05 KB (+0.22%)
schema-toCodecJson.ts 19.34 KB 19.28 KB +0.06 KB (+0.31%)
schema-toEquivalence.ts 19.17 KB 19.11 KB +0.06 KB (+0.29%)
schema-toFormatter.ts 19.03 KB 18.97 KB +0.06 KB (+0.31%)
schema-toJsonSchemaDocument.ts 22.75 KB 22.69 KB +0.06 KB (+0.26%)
schema-toRepresentation.ts 19.66 KB 19.60 KB +0.06 KB (+0.30%)
schema.ts 18.57 KB 18.52 KB +0.06 KB (+0.30%)
stm.ts 12.69 KB 12.63 KB +0.06 KB (+0.51%)
stream.ts 9.86 KB 9.80 KB +0.06 KB (+0.62%)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

audit Findings originating from the Effect runtime correctness audit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants