Source code
Revision control
Copy as Markdown
Other Tools
// PoC for stale alias-analysis dependency() surviving wasm loop unrolling
// (js/src/jit/UnrollLoops.cpp), letting the post-unroll GVN run merge a
// reload of a mutable global across the cloned aliasing stores.
//
// Pipeline walkthrough:
// 1. Alias analysis: the load L (global.get $g) picks the *last aliasing
// store* as its dependency. The branch arm containing S1
// (global.set $g 7777) precedes L, so dep(L) = S1.
// 2. Range analysis proves (i & 7) >= 8 is always false (beta node with
// empty range), marks the arm unreachable, and prepareForUCE rewrites
// the branch condition to a constant.
// 3. The UCE GVN run executes with DontUpdateAliasAnalysis: it folds the
// test, removes the arm block and DISCARDS S1. dep(L) now dangles, and
// because this GVN run must not update alias analysis, the dangling
// pointer is deliberately kept ("it's still valid for the purposes of
// detecting congruent loads", ValueNumbering.cpp).
// 4. UnrollLoops clones the loop body (peel + unroll x3 by default). Its
// dependency fixup (UnrollLoops.cpp:1170-1188) remaps a clone's
// dependency only if valueTable.findInRow(0, originalDep) finds the
// dependency among the *current* instructions of the original body.
// S1 was discarded, so the lookup fails and EVERY copy of L keeps the
// SAME stale dependency pointer.
// 5. The post-unroll GVN run (also DontUpdateAliasAnalysis) now sees the
// four copies of L as congruent (same operand, identical dependency())
// and replaces the loop copies with the peeled copy -- even though a
// cloned aliasing store S2 (global.set $g i) executes between them.
//
// Result: with the Ion/optimized wasm compiler (loop unrolling on by
// default) every iteration accumulates the stale value of $g instead of
// reloading it.
const wat = `(module
(global $g (export "g") (mut i32) (i32.const 1000))
(func (export "f") (param $n i32) (param $x i32) (result i32)
(local $i i32)
(local $acc i32)
(loop $cont
;; Dead-at-runtime store; provably dead only via range analysis
;; ((x % 7) is in [0,6], so >= 8 is always false; i32.rem_u maps to
;; MMod which is range-aware, unlike the Wasm-specific bitwise ops).
;; It becomes the alias-analysis dependency of the global.get below.
(if (i32.ge_s (i32.rem_u (local.get $x) (i32.const 7)) (i32.const 8))
(then (global.set $g (i32.const 7777)))
)
;; L: reload of $g -- dep = S1 (the store above)
(local.set $acc (i32.add (local.get $acc) (global.get $g)))
;; S2: aliasing store after L
(global.set $g (local.get $i))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $cont (i32.lt_s (local.get $i) (local.get $n)))
)
(local.get $acc)
)
)`;
const inst = wasmEvalText(wat);
const actual = inst.exports.f(10, 3);
// Reference (interpreter semantics):
// iter 0: acc += 1000, g = 0
// iter k: acc += k-1, g = k (k = 1..9)
// acc = 1000 + (0+1+...+8) = 1036
const expected = 1036;
assertEq(actual, expected);