Skip to content

aarch64: linear-memory load/store emit NO bounds check and --safety-bounds is a no-op (all modes byte-identical) — OOB reads/writes up to 4GiB past the guest memory instead of trapping #865

Description

@avrabe

Summary

The new aarch64 linear-memory lowering (#855, v0.51.0) emits no bounds check at all, and --safety-bounds is a no-op on the aarch64 backend — none, software, mask, and mpu all produce byte-identical output. A guest i32.load/i32.store becomes a bare add x9, x28, w0, uxtw; ldr/str [x9], so any out-of-bounds guest address is dereferenced directly against host memory instead of trapping. wasmtime traps the same access.

Because the address is zero-extended (uxtw), a guest address of 0xFFFFFFFF reaches x28 + 4 GiB − 1 — i.e. the guest can read and write up to 4 GiB past its linear memory. The store form gives an arbitrary-write primitive.

This is the "flag exists but doesn't enforce" class (cf. #651 mask-mode, kiln#411) — on a backend that, as of v0.51.0, is advertised as running real WASM modules.

Reproduction (synth 0.51.0)

(module (memory 1) (func (export "f") (param i32) (result i32) (i32.load (local.get 0))))
synth compile t.wat -b aarch64 -n f --relocatable --safety-bounds software -o t.o
llvm-objdump -d --triple=aarch64 t.o
0: 8b204389   add  x9, x28, w0, uxtw     ; x28 = linear-memory base
4: b9400129   ldr  w9, [x9]              ; <-- no bounds check, no trap path
8: aa0903e0   mov  x0, x9
c: d65f03c0   ret

All four modes are byte-identical (same md5 for --safety-bounds none|software|mask|mpu), so the flag has no effect on this backend:

none      78af70bc77f24e0145c992652bff89fe
software  78af70bc77f24e0145c992652bff89fe
mask      78af70bc77f24e0145c992652bff89fe
mpu       78af70bc77f24e0145c992652bff89fe

The store form is identical (add x9, x28, w0, uxtw; str w1, [x9]) — an unchecked write.

Executed natively (arm64 host, MAP_JIT, x28 = a real memory region), module declares (memory 1) = 64 KiB:

guest address synth (aarch64) wasmtime
65532 (in bounds) 0 0
65536 (1 byte past) 0 (reads host memory) trap (out of bounds)
100000 0 (reads host memory) trap
−1 (0xFFFFFFFF) -829685755 (garbage from x28+4GiB−1) trap

Why it matters

  • Silent: no diagnostic, no trap — the guest just reads/writes outside its memory.
  • --safety-bounds implies a choice of enforcement strategy; on aarch64 every mode (including software and mpu) silently degrades to none.
  • With uxtw zero-extension the reachable window is the full 4 GiB above x28, in both directions of access (load = disclosure, store = arbitrary write).

Suggested fix

Emit the bounds check on the aarch64 load/store path (the memory-size register/limit compare + trap, mirroring the thumb-2 software mode), and make --safety-bounds select the strategy on aarch64 — or, if aarch64 is not yet meant to enforce, hard-error on --safety-bounds software|mask|mpu for -b aarch64 rather than silently accepting the flag and emitting unchecked accesses.

Context

Found while extreme-testing the new v0.51.0 aarch64 surface. The rest of the new lowering is solid — I executed sub-word load/store sign/zero-extension + truncation + adjacent-byte safety, unaligned access, aliasing stores, high local pressure mixed with memory, a 4000-iteration loop-over-memory array sum, and a 1000-node pointer-chasing linked-list walk: all bit-exact vs wasmtime. This is specifically the bounds/--safety-bounds gap. (Separately: select is not yet lowered on aarch64, which blocks branchless algorithms — minor, mentioning in case it's in scope for the same milestone.)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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