Skip to content

Stale StyleRuleKey hash after merge_style_rules causes missed dedup and nondeterministic minified output #1235

@7rulnik

Description

@7rulnik

My knowleged in rust is pretty minimal. So I used claude and codex to help write the patch. I've reviewed everything and reproduced it locally, but flagging it so you know.

What's wrong

In CssRuleList::minify, the dedup HashMap<StyleRuleKey, usize> has a broken Hash/Eq invariant: the hash is precomputed once at insert time, but PartialEq re-reads the live rule from the Vec. When merge_style_rules mutates a rule in place, its content changes, but the hash stored in the map doesn't. Future lookups end up in the wrong bucket and miss valid duplicates.

How it surfaced for us

We hit this as build non-determinism in rspack build using LigthningCSS minimzer. Byte-identical CSS input produced different minified output across fresh process invocations at ~1–2.5%. StyleRule::hash_key() uses ahash::AHasher::default() (random per-process seed), so whether the stale hash collides with a later rule's fresh hash varies between processes.

A constant seed would hide that variance but wouldn't fix the underlying invariant violation — there's also a fully deterministic missed-dedup case, shown below.

Repro

Open in playground.

.a { border-top-left-radius: 16px }
.a { border-top-right-radius: 16px }
.a { border-bottom-left-radius: 16px }
.a { border-bottom-right-radius: 16px }
.x { color: red }
.a { border-radius: 16px }

Expected:

.x{color:red}.a{border-radius:16px}

Actual:

.a{border-radius:16px}.x{color:red}.a{border-radius:16px}

The four longhands merge into rules[0] and collapse to a border-radius shorthand. The trailing .a { border-radius: 16px } should dedup against it but doesn't, because rules[0] is still indexed under the original border-top-left-radius hash bucket.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions