Skip to content

constant-folding eliminates the mandatory div_s(INT_MIN,-1) overflow trap — optimized module returns INT_MIN where the original traps (i32 + i64) #273

Description

@avrabe

Summary

Loom's constant-folding pass folds i32.div_s(INT_MIN, -1) (and the i64 equivalent) to the constant INT_MIN, eliminating a mandatory integer-overflow trap. The optimized module returns a value where the original module traps — a silent, observable miscompile that changes program semantics.

Found on loom 1.1.18, cross-checked against wasmtime 42.0.1.

Repro

i32:

(module (func (export "f") (result i32)
  (i32.div_s (i32.const -2147483648) (i32.const -1))))
loom optimize f.wat --wat -o f.opt.wat

Optimized body becomes i32.const -2147483648.

wasmtime 42
original wasm trap: integer overflow
loom-optimized returns -2147483648

i64 (i64.div_s (i64.const -9223372036854775808) (i64.const -1)): original traps; optimized returns -9223372036854775808. Same divergence.

Spec

WebAssembly Core Spec, idiv_s(N, i1, i2): the operation traps when i2 = 0 or when the exact result 2^(N-1) is not representable — i.e. INT_MIN / -1. So -2^31 / -1 and -2^63 / -1 MUST trap; they have no defined value. An optimizer replacing a trapping op with a constant is unsound because a program may rely on the trap (e.g. INT_MIN/-1 guarding a subsequent operation).

Root cause

loom-shared/src/lib.rs:

  • i32 — ValueData::I32DivS arm, lines ~4704-4716:
(ValueData::I32Const { val: l }, ValueData::I32Const { val: r })
    if r.value() != 0 =>
{
    // Signed division with wrapping for overflow case (INT_MIN / -1)
    iconst32(Imm32(l.value().wrapping_div(r.value())))
}
  • i64 — ValueData::I64DivS arm, lines ~4776-4787 (same shape with Imm64).

The guard only excludes r == 0 (div-by-zero trap correctly preserved — verified: div_s(10, 0) is NOT folded). It then uses wrapping_div, which yields INT_MIN for the INT_MIN / -1 overflow case instead of preserving the trap. The comment documents the wrong intent.

Fix

Also decline to fold (fall through to idivs32/idivs64, preserving the runtime trap) when l == iN::MIN && r == -1:

(ValueData::I32Const { val: l }, ValueData::I32Const { val: r })
    if r.value() != 0 && !(l.value() == i32::MIN && r.value() == -1) =>
{
    iconst32(Imm32(l.value() / r.value()))
}

Scope check (these are correct, not affected)

  • div_s/div_u/rem_s/rem_u by 0 → not folded, trap preserved. ✓
  • rem_s(INT_MIN, -1) → correctly folds to 0 (spec: no trap, result 0). ✓
  • i32.add/mul wrap, rotr by 32 → folded correctly. ✓

Environment: loom 1.1.18 (--no-default-features build), wasmtime 42.0.1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions