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.)
Summary
The new aarch64 linear-memory lowering (#855, v0.51.0) emits no bounds check at all, and
--safety-boundsis a no-op on the aarch64 backend —none,software,mask, andmpuall produce byte-identical output. A guesti32.load/i32.storebecomes a bareadd 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 of0xFFFFFFFFreachesx28 + 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)
All four modes are byte-identical (same md5 for
--safety-bounds none|software|mask|mpu), so the flag has no effect on this backend: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:Why it matters
--safety-boundsimplies a choice of enforcement strategy; on aarch64 every mode (includingsoftwareandmpu) silently degrades tonone.uxtwzero-extension the reachable window is the full 4 GiB abovex28, 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
softwaremode), and make--safety-boundsselect the strategy on aarch64 — or, if aarch64 is not yet meant to enforce, hard-error on--safety-bounds software|mask|mpufor-b aarch64rather 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-boundsgap. (Separately:selectis not yet lowered on aarch64, which blocks branchless algorithms — minor, mentioning in case it's in scope for the same milestone.)