[GH-ISSUE #5395] [Bug] WASM debug-name collisions hide an exported function signature change #3265

Closed
opened 2026-10-31 00:38:34 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 20, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5395

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

node scripts/rea.mjs inspect-wasm-artifact MODULE --format json assigns both distinct exports first and second the first function's index and signature when internal debug names decode to the same WAT identifier. Comparing the resulting Evidence with node scripts/rea.mjs compare-wasm-interfaces INPUT --format json then reports an actual exported parameter change as unchanged with complete coverage.

This was independently reproduced through the public CLI with valid modules and WABT 1.0.42. The colliding internal names are $"a b" and $a_b; the export names remain distinct.

Reproduction

From the source checkout with locked dependencies installed and the runtime built, use WABT 1.0.42 on PATH. The verifier ran the following fixture/CLI replay with isolated HOME, XDG directories, and temporary state. The program also runs a distinct-name control.

export REA_WABT_BIN_DIRECTORY="$(dirname "$(command -v wasm2wat)")"
export verification_dir="$(mktemp -d)"
python3 - <<'PY'
import json
import os
from pathlib import Path
import subprocess

root = Path(os.environ["verification_dir"])
wabt = Path(os.environ["REA_WABT_BIN_DIRECTORY"])
cli = Path("scripts/rea.mjs").resolve()

def run(label, command):
    result = subprocess.run(command, text=True, capture_output=True, check=True)
    (root / (label + ".stdout")).write_text(result.stdout)
    (root / (label + ".stderr")).write_text(result.stderr)
    return result.stdout

for prefix in ("", "control-"):
    evidence = {}
    for side, parameter in (("left", "i64"), ("right", "f32")):
        name = prefix + side
        first = '$"a b"' if not prefix else '$a'
        source = (
            f'(module\n (func {first} (param i32))\n'
            f' (func $a_b (param {parameter}))\n'
            f' (export "first" (func {first}))\n'
            ' (export "second" (func $a_b))\n)\n'
        )
        wat = root / (name + ".wat")
        wasm = root / (name + ".wasm")
        wat.write_text(source)
        run(name + "-assemble", [str(wabt / "wat2wasm"), "--enable-all",
            "--debug-names", str(wat), "-o", str(wasm)])
        run(name + "-validate", [str(wabt / "wasm-validate"),
            "--enable-all", str(wasm)])
        run(name + "-objdump", [str(wabt / "wasm-objdump"), "-x", str(wasm)])
        run(name + "-decoded", [str(wabt / "wasm2wat"), "--enable-all", str(wasm)])
        evidence[side] = json.loads(run(name + "-inspect", ["node", str(cli),
            "inspect-wasm-artifact", str(wasm), "--format", "json"]))
        print(name, json.dumps(evidence[side]["normalized_result"]["interface"]))
    comparison = root / (prefix + "comparison.json")
    comparison.write_text(json.dumps({**evidence, "scope": "exports"}))
    result = json.loads(run(prefix + "comparison", ["node", str(cli),
        "compare-wasm-interfaces", str(comparison), "--format", "json"]))
    report = result["normalized_result"]
    print(prefix + "comparison", report["summary"], report["coverage"])
PY

Assembly, validation, inspection, and comparison exited 0 for both pairs. The collision pair reports changed: 0, unchanged: 2, unknown: 0; the control reports changed: 1, unchanged: 1, unknown: 0. The verifier also replayed the draft's exact reproduction program successfully with the same distinction.

Expected behavior

docs/wasm-artifacts.md (Compare core-module interfaces) promises producer-owned typed interface facts and classification of unique exports by their signatures. AGENTS.md (Architecture and Cleanup) requires unresolved identity and information lost during normalization to remain explicit unknowns.

If original identity can be recovered reliably, second should resolve to index 1 with i64 on the left and f32 on the right, and compare as changed. Otherwise, affected entries should retain null unresolved facts and non-null issues, producing unknown comparisons with partial coverage. Both exports may conservatively be unknown because both decoded references are ambiguous. The distinct-name control must retain accurate facts.

Impact and scope

A valid debug-name collision corrupts typed inspection Evidence and hides a real declared interface change while claiming complete comparison coverage. Independently assessed P2: bounded to the reproduced function-export fixtures, with a demonstrated manual workaround using wasm-objdump -x export/function/type indices. Other export kinds and frequency in real-world modules were not tested. No runtime-compatibility or security-boundary impact was established; no module was instantiated or executed.

REA version

6.7.0; source commit 074768901f3cdc8205b61773fb1faf6f6c7b3ca8 (prebuilt runtime).

Runtime and platform

Node.js v26.12.0; macOS (Darwin), arm64; caller-supplied WABT 1.0.42. wat2wasm, wasm-validate, wasm-objdump, and wasm2wat each reported 1.0.42.

Target and artifact context

Synthetic valid core WebAssembly modules with debug names, two functions, and distinct exports first and second. The collision fixtures are 80 bytes each. The only changed declared type is the second function's parameter, i64 to f32.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The replay saves objdump, decoded WAT, inspection Evidence, and comparison results. Observed ground truth from wasm-objdump -x:

func[0] sig=0 <a b>
func[1] sig=1 <a_b>
export second -> function 1
signature 1: i64 parameter on left; f32 parameter on right

The last two lines summarize objdump's export/type rows. wasm2wat --enable-all prints both function definitions and both export references as $a_b, while preserving the two different function signatures.

Both inspection results nevertheless contain interface.state: "complete", issues: [], and first/second entries with index: 0, parameter i32, and issue: null. Comparison returns:

{"added":0,"removed":0,"changed":0,"unchanged":2,"unknown":0}

Coverage is complete-within-inputs. With distinct internal names $a and $a_b, second correctly resolves to index 1 and the respective i64/f32 signature, and comparison detects the change.

Typed outcome or recovery expected

Accurate original indices/signatures and one changed export, or explicit ambiguity for affected exports with unknown results and partial coverage. Incorrect signatures must not be reported confidently unchanged.

Analysis or suspected cause

Source-derived reasoning: src/wasm/WabtInterface.ts:285-304 resolves a named export reference with space.findIndex((d) => d.label === reference) and emits the first matching definition's index/signature without checking uniqueness. The public replay reaches this path through src/wasm/WabtArtifactProvider.ts and src/wasm/WabtWat.ts. Comparison consumes the incorrect typed facts; this is one inspection defect propagated into comparison, not a separate comparison issue.

The documented duplicate-export-name rule does not directly cover this trigger: exported names are unique, while decoded internal identifiers collide.

Additional context

Prepared with AI assistance and independently reproduced by a separate verifier. Upstream main was fetched before submission; no commits after the tested baseline touched the WASM inspection/comparison paths. Searches covered open and closed issues and PRs; the existing interface-comparison feature and debug-module-name preamble fix describe different behavior.

Regression range: NOT_ENOUGH_INFO; no known-good release or first-bad revision was established.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5395 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior `node scripts/rea.mjs inspect-wasm-artifact MODULE --format json` assigns both distinct exports `first` and `second` the first function's index and signature when internal debug names decode to the same WAT identifier. Comparing the resulting Evidence with `node scripts/rea.mjs compare-wasm-interfaces INPUT --format json` then reports an actual exported parameter change as unchanged with complete coverage. This was independently reproduced through the public CLI with valid modules and WABT 1.0.42. The colliding internal names are `$"a b"` and `$a_b`; the export names remain distinct. ### Reproduction From the source checkout with locked dependencies installed and the runtime built, use WABT 1.0.42 on PATH. The verifier ran the following fixture/CLI replay with isolated HOME, XDG directories, and temporary state. The program also runs a distinct-name control. ```sh export REA_WABT_BIN_DIRECTORY="$(dirname "$(command -v wasm2wat)")" export verification_dir="$(mktemp -d)" python3 - <<'PY' import json import os from pathlib import Path import subprocess root = Path(os.environ["verification_dir"]) wabt = Path(os.environ["REA_WABT_BIN_DIRECTORY"]) cli = Path("scripts/rea.mjs").resolve() def run(label, command): result = subprocess.run(command, text=True, capture_output=True, check=True) (root / (label + ".stdout")).write_text(result.stdout) (root / (label + ".stderr")).write_text(result.stderr) return result.stdout for prefix in ("", "control-"): evidence = {} for side, parameter in (("left", "i64"), ("right", "f32")): name = prefix + side first = '$"a b"' if not prefix else '$a' source = ( f'(module\n (func {first} (param i32))\n' f' (func $a_b (param {parameter}))\n' f' (export "first" (func {first}))\n' ' (export "second" (func $a_b))\n)\n' ) wat = root / (name + ".wat") wasm = root / (name + ".wasm") wat.write_text(source) run(name + "-assemble", [str(wabt / "wat2wasm"), "--enable-all", "--debug-names", str(wat), "-o", str(wasm)]) run(name + "-validate", [str(wabt / "wasm-validate"), "--enable-all", str(wasm)]) run(name + "-objdump", [str(wabt / "wasm-objdump"), "-x", str(wasm)]) run(name + "-decoded", [str(wabt / "wasm2wat"), "--enable-all", str(wasm)]) evidence[side] = json.loads(run(name + "-inspect", ["node", str(cli), "inspect-wasm-artifact", str(wasm), "--format", "json"])) print(name, json.dumps(evidence[side]["normalized_result"]["interface"])) comparison = root / (prefix + "comparison.json") comparison.write_text(json.dumps({**evidence, "scope": "exports"})) result = json.loads(run(prefix + "comparison", ["node", str(cli), "compare-wasm-interfaces", str(comparison), "--format", "json"])) report = result["normalized_result"] print(prefix + "comparison", report["summary"], report["coverage"]) PY ``` Assembly, validation, inspection, and comparison exited 0 for both pairs. The collision pair reports `changed: 0, unchanged: 2, unknown: 0`; the control reports `changed: 1, unchanged: 1, unknown: 0`. The verifier also replayed the draft's exact reproduction program successfully with the same distinction. ### Expected behavior `docs/wasm-artifacts.md` (Compare core-module interfaces) promises producer-owned typed interface facts and classification of unique exports by their signatures. `AGENTS.md` (Architecture and Cleanup) requires unresolved identity and information lost during normalization to remain explicit unknowns. If original identity can be recovered reliably, `second` should resolve to index 1 with `i64` on the left and `f32` on the right, and compare as changed. Otherwise, affected entries should retain null unresolved facts and non-null issues, producing unknown comparisons with partial coverage. Both exports may conservatively be unknown because both decoded references are ambiguous. The distinct-name control must retain accurate facts. ### Impact and scope A valid debug-name collision corrupts typed inspection Evidence and hides a real declared interface change while claiming complete comparison coverage. Independently assessed P2: bounded to the reproduced function-export fixtures, with a demonstrated manual workaround using `wasm-objdump -x` export/function/type indices. Other export kinds and frequency in real-world modules were not tested. No runtime-compatibility or security-boundary impact was established; no module was instantiated or executed. ### REA version 6.7.0; source commit `074768901f3cdc8205b61773fb1faf6f6c7b3ca8` (prebuilt runtime). ### Runtime and platform Node.js v26.12.0; macOS (Darwin), arm64; caller-supplied WABT 1.0.42. `wat2wasm`, `wasm-validate`, `wasm-objdump`, and `wasm2wat` each reported 1.0.42. ### Target and artifact context Synthetic valid core WebAssembly modules with debug names, two functions, and distinct exports `first` and `second`. The collision fixtures are 80 bytes each. The only changed declared type is the second function's parameter, `i64` to `f32`. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The replay saves objdump, decoded WAT, inspection Evidence, and comparison results. Observed ground truth from `wasm-objdump -x`: ```text func[0] sig=0 <a b> func[1] sig=1 <a_b> export second -> function 1 signature 1: i64 parameter on left; f32 parameter on right ``` The last two lines summarize objdump's export/type rows. `wasm2wat --enable-all` prints both function definitions and both export references as `$a_b`, while preserving the two different function signatures. Both inspection results nevertheless contain `interface.state: "complete"`, `issues: []`, and `first`/`second` entries with `index: 0`, parameter `i32`, and `issue: null`. Comparison returns: ```json {"added":0,"removed":0,"changed":0,"unchanged":2,"unknown":0} ``` Coverage is `complete-within-inputs`. With distinct internal names `$a` and `$a_b`, `second` correctly resolves to index 1 and the respective `i64`/`f32` signature, and comparison detects the change. ### Typed outcome or recovery expected Accurate original indices/signatures and one changed export, or explicit ambiguity for affected exports with unknown results and partial coverage. Incorrect signatures must not be reported confidently unchanged. ### Analysis or suspected cause Source-derived reasoning: `src/wasm/WabtInterface.ts:285-304` resolves a named export reference with `space.findIndex((d) => d.label === reference)` and emits the first matching definition's index/signature without checking uniqueness. The public replay reaches this path through `src/wasm/WabtArtifactProvider.ts` and `src/wasm/WabtWat.ts`. Comparison consumes the incorrect typed facts; this is one inspection defect propagated into comparison, not a separate comparison issue. The documented duplicate-export-name rule does not directly cover this trigger: exported names are unique, while decoded internal identifiers collide. ### Additional context Prepared with AI assistance and independently reproduced by a separate verifier. Upstream main was fetched before submission; no commits after the tested baseline touched the WASM inspection/comparison paths. Searches covered open and closed issues and PRs; the existing interface-comparison feature and debug-module-name preamble fix describe different behavior. Regression range: NOT_ENOUGH_INFO; no known-good release or first-bad revision was established.
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Duplicate decoded labels are now indexed as null per index space and record an ambiguity issue instead of resolving with first-match findIndex. Named exports of the colliding identifier therefore have null index/signature and a non-null issue. Numeric and unique references retain accurate identity.

Implementation: WabtInterface.ts, WabtInterface.ts.

The relevant regression checks in WabtInterface.test.ts, WasmInterfaceComparisonService.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380726591 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Duplicate decoded labels are now indexed as null per index space and record an ambiguity issue instead of resolving with first-match findIndex. Named exports of the colliding identifier therefore have null index/signature and a non-null issue. Numeric and unique references retain accurate identity. Implementation: [WabtInterface.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/wasm/WabtInterface.ts#L124), [WabtInterface.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/wasm/WabtInterface.ts#L299). The relevant regression checks in [WabtInterface.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/wasm/WabtInterface.test.ts), [WasmInterfaceComparisonService.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/application/wasm/WasmInterfaceComparisonService.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3265
No description provided.