Summary
meld fuse silently truncates any wasm-2.0 extended-const expression that begins with
global.get: it reads only the leading global.get and drops the trailing arithmetic operators.
This corrupts both global initializers and data/element-segment offsets — exactly the
__memory_base + N / __table_base + N shape LLVM/emscripten emit for position-independent (PIE)
modules. The fused module still validates, so it's a silent miscompile.
Distinct from #152/LS-A-11, which fixed the const-first case (i32.const N; global.get; i32.add);
the global.get-first case was missed. Verified on meld v0.39.0 (9a40cff).
Root cause (source)
meld-core/src/segments.rs (parse_const_expr_with_value, data/elem offsets):
Operator::GlobalGet { global_index } => (ParsedConstExpr::GlobalGet(global_index), None),
meld-core/src/merger.rs:~3155 (convert_init_expr, global initializers) has the analogous
GlobalGet arm. Both return immediately with a bare GlobalGet, discarding any following
operators — unlike the I32Const/I64Const arms, which fold through to End via
fold_extended_const_*. So global.get $base; i32.const N; i32.add is read as just
global.get $base.
Reproduction (executed vs wasmtime)
Case 1 — global initializer g = base + 100 (base = 1000):
$ meld fuse ec_comp.wasm -o fused_ec.wasm
golden (unfused component) getg = 1100
fused (meld) getg = 1000 # BUG: +100 dropped; validates VALID
The input global is (global $g i32 global.get $base i32.const 100 i32.add); meld emits
(global (;1;) i32 global.get 0) — the i32.const 100 i32.add is gone.
Case 2 — data-segment offset base + 8 (base = 16 → data belongs at 24):
golden peek(24)=4022250974 peek(16)=0 # data at 24, correct
fused peek(24)=0 peek(16)=... # data landed at 16 — BUG: +8 dropped
Impact
Any component whose globals/segment offsets use global.get <imm-base> (+/-/*) const — the standard
PIE relocation form — is silently mislaid after fusion: global values wrong, data/element segments
written to the wrong linear-memory/table offset. No error, validates clean. Distinct from
#326/#328/#331 (memory strategy / DWARF / name sections).
Suggested fix
In both GlobalGet arms, continue folding the remaining operators (mirror the I32Const/I64Const
arms' fold_extended_const_* path) so global.get; const; add|sub|mul is preserved/evaluated,
rather than returning early on the leading global.get. Extend the LS-A-11 regression test with a
global.get-first case.
Reported by the pulseengine-challenge harness (research-agent lead; clean-room reverified by source
- structural inspection of the fused module + wasmtime execution differential).
Summary
meld fusesilently truncates any wasm-2.0 extended-const expression that begins withglobal.get: it reads only the leadingglobal.getand drops the trailing arithmetic operators.This corrupts both global initializers and data/element-segment offsets — exactly the
__memory_base + N/__table_base + Nshape LLVM/emscripten emit for position-independent (PIE)modules. The fused module still validates, so it's a silent miscompile.
Distinct from #152/LS-A-11, which fixed the const-first case (
i32.const N; global.get; i32.add);the
global.get-first case was missed. Verified on meld v0.39.0 (9a40cff).Root cause (source)
meld-core/src/segments.rs(parse_const_expr_with_value, data/elem offsets):meld-core/src/merger.rs:~3155(convert_init_expr, global initializers) has the analogousGlobalGetarm. Both return immediately with a bareGlobalGet, discarding any followingoperators — unlike the
I32Const/I64Constarms, which fold through toEndviafold_extended_const_*. Soglobal.get $base; i32.const N; i32.addis read as justglobal.get $base.Reproduction (executed vs wasmtime)
Case 1 — global initializer
g = base + 100(base = 1000):The input global is
(global $g i32 global.get $base i32.const 100 i32.add); meld emits(global (;1;) i32 global.get 0)— thei32.const 100 i32.addis gone.Case 2 — data-segment offset
base + 8(base = 16 → data belongs at 24):Impact
Any component whose globals/segment offsets use
global.get <imm-base> (+/-/*) const— the standardPIE relocation form — is silently mislaid after fusion: global values wrong, data/element segments
written to the wrong linear-memory/table offset. No error, validates clean. Distinct from
#326/#328/#331 (memory strategy / DWARF / name sections).
Suggested fix
In both
GlobalGetarms, continue folding the remaining operators (mirror theI32Const/I64Constarms'
fold_extended_const_*path) soglobal.get; const; add|sub|mulis preserved/evaluated,rather than returning early on the leading
global.get. Extend the LS-A-11 regression test with aglobal.get-first case.Reported by the pulseengine-challenge harness (research-agent lead; clean-room reverified by source