Skip to content

meld fuse silently truncates extended-const exprs beginning with global.get (drops trailing arithmetic) — corrupts global initializers + data/element offsets (PIE base+N) #338

Description

@avrabe

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).

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