Skip to content

wac plug output is not reproducible: plug components are emitted in nondeterministic order #211

Description

@techieyann

Version: wac-cli 0.10.1 (wac-cli-x86_64-unknown-linux-musl, sha256 250c11762916ba733c7d22b62487580f21270ec9dde4f13460ea69d300e25406; also reproduced with wac-cli-aarch64-apple-darwin)
Platforms: linux/amd64 and darwin/arm64 — both affected identically.

Summary

With two or more --plug arguments, repeated wac plug invocations on byte-identical inputs produce byte-different outputs. All outputs are valid and semantically equivalent — the plug subcomponents are embedded verbatim and only their emission order in the outer component section varies, along with the corresponding instantiate indices. With a single plug the output is stable.

This makes wac plug unusable in content-addressed or reproducible build pipelines, where a composed artifact's hash is its identity.

Reproduction

for i in $(seq 1 20); do
  wac plug socket.wasm --plug a.wasm --plug b.wasm -o out$i.wasm
done
sha256sum out*.wasm | awk '{print $1}' | sort | uniq -c
  • 2 plugs / 20 runs → 2 distinct digests (8 / 12)
  • 4 plugs / 30 runs → 16 distinct digests (4! = 24 permutations available)

All outputs are identical in size and all pass wasm-tools validate.

Command-line order is ignored

Running --plug a --plug b and --plug b --plug a 20 times each produces the same set of two digests in the same proportion. The output distribution is independent of argv order, so callers cannot work around this by ordering their arguments.

Localisation

Only the multi---plug path is affected:

  • wac compose with an explicit .wac source file is deterministic on the same inputs (20/20 and 21/21 identical at 2 and 4 dependencies).
  • Chaining single-plug wac plug invocations is deterministic (12/12).

This localises the issue to plug collection/iteration in wac plug rather than to composition or encoding generally.

wasm-tools objdump across 12 consecutive 4-plug runs shows the four plug subcomponents in 11 different orders, with the socket always emitted last. Each plug's payload is a verbatim byte-substring of the output in every run.

Likely cause

src/commands/plug.rs collects plugs into a std::collections::HashMap:

// Collect the plugs by their names
let mut plugs_by_name = std::collections::HashMap::<_, Vec<_>>::new();

which is then iterated directly:

for (name, plug_refs) in plugs_by_name {

Rust's HashMap has randomised iteration order, so the order in which plugs are registered and instantiated varies per process. crates/wac-graph/src/plug.rs takes an ordered Vec<PackageId> and appears unaffected.

Expected

Byte-identical output for byte-identical inputs — e.g. by preserving command-line order, or by iterating in a deterministic sorted order.

Contrast

The deprecated wasm-tools compose is deterministic on the same inputs (5/5 identical at both 2 and 4 dependencies). Since wac is the recommended replacement, reproducibility parity seems in scope.

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