Skip to content

fix(338): preserve global.get extended-const exprs — silent miscompile of PIE base+N offsets - #346

Merged
avrabe merged 1 commit into
mainfrom
feat/338-const-expr-globalget
Jul 15, 2026
Merged

fix(338): preserve global.get extended-const exprs — silent miscompile of PIE base+N offsets#346
avrabe merged 1 commit into
mainfrom
feat/338-const-expr-globalget

Conversation

@avrabe

@avrabe avrabe commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

meld fuse silently truncated any wasm-2.0 extended-const expression containing a global.get — both global.get $base; i32.const N; i32.add and the operand-swapped i32.const N; global.get $base; i32.add became just global.get $base, dropping the arithmetic. This corrupted global initializers and data/element-segment offsets — the position-independent __memory_base + N / __table_base + N shape. The module still validated → silent miscompile (SR-51). #152/LS-A-11 fixed only the const-first-no-global.get fold; both global.get cases were missed.

Fix

The value is runtime-dependent, so it can't fold to a constant — meld now PRESERVES the full extended-const operator sequence and re-emits it with global indices remapped. New ParsedConstExpr::ExtendedGlobalGet + ExtConstOp + read_extended_const_global_get; the fold_extended_const_{i32,i64} arms return ExtConstFold::{Value,Extended} and route an embedded global.get into the preserve-and-remap path (instead of erroring → un-remapped raw-bytes fallback). Wired at both sites: segments.rs (data/elem offsets) + merger.rs convert_init_expr (global inits). Operand order preserved (sub/mul non-commutative).

Load-bearing ordering fix (found by the Mythos pass): the imported-global index-map population was hoisted to run BEFORE the defined-globals loop that calls convert_init_expr — previously the remap silently missed (unwrap_or kept the local index), so a multi-module fuse that shifts __memory_base off import-index 0 read the wrong base.

Tests (executed vs wasmtime; 737 green; clippy + fmt clean)

  • global-init base+1001100 and multi-module 100+base1100 (base-shift fixture; pre-fix read __stack_pointer=5100).
  • data-offset base+N and N+base land at base+N (pre-fix truncated to base+0 / hard-error).
  • bare global.get + const-first fold unchanged.

Falsification

If the preserve-and-remap regressed, test_338_multimodule_const_first_embedded_globalget_remaps (asserts the fused global reads the correct base, not the un-remapped 5100) fails. If truncation returned, test_338_global_initializer_extended_const_preserved (==1100) fails.

Refs #338, #152, SR-51, SYS-1, LS-A-11.

🤖 Generated with Claude Code

meld fuse silently TRUNCATED any wasm-2.0 extended-const expr containing a
`global.get` — `global.get $base; i32.const N; i32.add` (and the operand-swapped
`i32.const N; global.get $base; i32.add`) became just `global.get $base`,
dropping the arithmetic. This corrupted global initializers and data/element
offsets — the PIE `__memory_base + N` / `__table_base + N` shape. The module
still validated → silent miscompile (SR-51). #152/LS-A-11 fixed only the
const-first-no-globalget fold; both global.get cases were missed.

Fix: preserve the FULL extended-const operator sequence (the value is
runtime-dependent, so it cannot fold to a constant) and re-emit it with global
indices remapped. New `ParsedConstExpr::ExtendedGlobalGet` + `ExtConstOp` +
`read_extended_const_global_get`; the `fold_extended_const_{i32,i64}` arms now
return `ExtConstFold::{Value,Extended}` and route an embedded `global.get` into
the preserve-and-remap path instead of erroring (which had fallen back to
UN-remapped raw bytes). Wired at both sites: segments.rs
(parse_const_expr_with_value, data/elem offsets) + merger.rs (convert_init_expr,
global inits). Operand order preserved (sub/mul non-commutative).

Load-bearing ordering fix (Mythos): the imported-global index-map population was
hoisted to run BEFORE the defined-globals loop that calls convert_init_expr —
previously the remap silently missed (unwrap_or kept the local index), so a
multi-module fuse that shifts __memory_base off import-index 0 read the wrong
base.

Tests (executed vs wasmtime): global-init `base+100`->1100 and `100+base`->1100
(multi-module base-shift; pre-fix read __stack_pointer=5100); data-offset
`base+N` and `N+base` land at base+N (pre-fix truncated to base+0 / hard-error);
bare global.get + const-first fold unchanged. 737 lib+integration green; clippy
+ fmt clean; rivet validate PASS.

Refs #338, #152, SR-51, SYS-1, LS-A-11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@avrabe avrabe added the mythos-pass-done Mythos delta-pass completed on Tier-5 file changes; findings (or NO FINDINGS) attached to PR label Jul 15, 2026
@avrabe

avrabe commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Mythos discover pass — done (two passes; one incomplete-fix caught + fixed)

Pass 1 on the initial global.get-first fix: the fix itself was sound across all angles (op whitelist with no silent fall-through, correct remap keying, operand order preserved, value=None safely errors on the rebasing path, valid re-encode). But it flagged a MEDIUM incomplete fix: the operand-swapped sibling i32.const N; global.get $base; i32.add (const-FIRST with an embedded global.get) was STILL a silent miscompile in global initializers — the const-first fold path errored on the embedded global.get and the merger fell back to ConstExpr::raw(<un-remapped bytes>), so in multi-module fusion where __memory_base's merged import index shifts off 0 it read the wrong base. The new test masked it (single-module keeps base at index 0).

Fix (this PR): routed the embedded-global.get case from the fold_extended_const_{i32,i64} arms into the same preserve-and-remap path (ExtConstFold::Extended), and — the actual root cause — hoisted the imported-global index-map population before convert_init_expr so the remap no longer silently misses. Added a multi-module base-shift test (__memory_base at a non-zero merged index; pre-fix read __stack_pointer=5100, post-fix reads 1100) + a data-offset N+base test (pre-fix hard-errored).

Pass 2 / re-verification: full suite re-run on the rebased branch — 36 test binaries, 0 failures; clippy + fmt clean; rivet validate PASS. Both operand orders, both sites (global inits + data/elem offsets), single- and multi-module, all covered. mythos-pass-done.

@github-actions

Copy link
Copy Markdown

Mythos delta-pass required

This PR modifies one or more Tier-5 source files (per
scripts/mythos/rank.md):

meld-core/src/merger.rs
meld-core/src/segments.rs

Before merge, run the Mythos discover protocol on the
modified Tier-5 files:

  1. Follow scripts/mythos/discover.md
    — one fresh agent session per touched Tier-5 file.
  2. For each finding, the agent must produce both a Kani
    harness and a failing PoC test (per the protocol's
    "if you cannot produce both, do not report" rule).
  3. Attach a comment on this PR with either the findings
    (formatted per discover.md's output schema) or
    NO FINDINGS.
  4. Add the mythos-pass-done label to this PR.

Why this gate exists: LS-A-10
(CABI alignment padding in async-lift retptr writeback) was
found by the v0.8.0 pre-release Mythos pass — but it had
lived in the callback emitter since #128, across six
releases. A PR-time gate would have caught it at review
time instead of at the release boundary.

The gate check on this PR will pass once the label is
applied.

@github-actions

Copy link
Copy Markdown

LS-N verification gate

59/59 approved LS entries verified

count
Passed (≥1 test, all green) 59
Failed (≥1 test failure) 0
Missing (no ls_*_NN_* test found) 0

Approved loss-scenarios.yaml entries are expected to have a
regression test named ls_<letter>_<num>_* (e.g. LS-A-11
ls_a_11_*). The gate runs each prefix via cargo test --lib --no-fail-fast and aggregates pass/fail/missing.

Failed LS entries

(none)

Missing regression tests

(none)

Updated automatically by tools/post_verification_comment.py.
Source of truth: safety/stpa/loss-scenarios.yaml.

@github-actions

Copy link
Copy Markdown

Mythos delta-pass (auto)

NO FINDINGS across 2 Tier-5 file(s)

File Verdict Hypothesis
`` ✅ NO FINDINGS
`` ✅ NO FINDINGS

Auto-run via anthropics/claude-code-action@v1
(SHA-pinned) on the touched Tier-5 files, using the
maintainer's Max-plan OAuth token. See
.github/workflows/mythos-auto.yml and
scripts/mythos/discover.md.

@avrabe
avrabe merged commit 299878c into main Jul 15, 2026
18 of 19 checks passed
@avrabe
avrabe deleted the feat/338-const-expr-globalget branch July 15, 2026 04:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mythos-pass-done Mythos delta-pass completed on Tier-5 file changes; findings (or NO FINDINGS) attached to PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant