Skip to content

wac plug is non-deterministic: identical inputs produce a different artifact each run (same WIT, 33% of bytes differ) #216

Description

@avrabe

wac plug produces a different artifact on every invocation from identical
inputs. The components are semantically identical; only their serialization
order changes.

Reproduction

wac-cli 0.6.0, macOS arm64. Five component "plugs" into one socket:

wac plug cascade.wasm \
  --plug iekf.wasm --plug position.wasm --plug attitude.wasm \
  --plug rate.wasm --plug mixer.wasm -o out.wasm

Run twice, same inputs, same binary:

run 1: 3b15c2e365796e6d...   98335 bytes
run 2: 2f348129b91628a3...   98335 bytes

A third and fourth run give two more distinct hashes. Size is always identical.

It is ordering, not content

$ wasm-tools component wit out1.wasm > wit1
$ wasm-tools component wit out2.wasm > wit2
$ diff wit1 wit2      # no output — byte-identical

So the outputs are the same component, serialized differently.

Byte-level: 33.59% of bytes differ (33033 of 98335), across 1347 contiguous
runs, first at offset 0x5c37. At that offset:

run 1:  ... vehicle-state ... waypoint ...
run 2:  ... vehicle-state ... attitude-setpoint ...

Nested components are emitted in a different order each run. That is what
iteration over a HashMap/HashSet under Rust's randomly-seeded RandomState
looks like — I have not read enough of wac's internals to point at the exact
collection, but the signature seems clear enough to be worth reporting rather
than sitting on.

Why it matters to us

  1. Attestation. We cosign-sign artifacts and publish digests. For anything
    wac composed, "rebuild from these inputs and check you get our digest"
    cannot work — a rebuild from byte-identical inputs produces a different
    digest every time. That converts a reproducibility claim into a
    trust-the-builder claim.
  2. Bazel. We drive wac_plug through rules_wasm_component. Build systems
    assume a rule with identical inputs yields identical outputs; a
    non-deterministic action undermines cache correctness and makes any
    downstream digest unstable.
  3. Diffing releases. Comparing two releases' composed artifacts by digest is
    how a consumer sees "nothing changed". Right now every rebuild looks like a
    change.

Suggested fix

Sort deterministically wherever composition iterates over a keyed collection
before emitting — by name, or by insertion order via an IndexMap — so the
serialization is a pure function of the inputs.

Happy to test a patch against the reproduction above; it is a five-component
compose that takes under a second.

Environment

wac-cli 0.6.0
macOS 15 (arm64)
inputs: 6 wasm components, no_std, wasm32-unknown-unknown, 0 memory.grow

Activity

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

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