v0.11.35 ARM: i64 unpack miscompile — mask/shift constants land in the live u64 register pair → silent wrong-code (k_sem_give drops its count update; engine_control bench hangs on silicon)
Found while re-baselining the sem dissolution on v0.11.35 (G474RE). Two repros, one minimal + one production, both also byte-identical on the PR #309 head — so the fix can ride v0.11.36.
Minimal repro (self-contained, 15 lines)
#include <stdint.h>
__attribute__((noinline)) uint64_t make(uint32_t a, uint32_t b) {
return (uint64_t)(a + b + 1) << 32 | 1u;
}
__attribute__((export_name("check"))) uint32_t check(uint32_t a, uint32_t b) {
uint64_t r = make(a, b);
uint32_t action = (uint32_t)(r & 0xFFu); /* expect 1 */
uint32_t val = (uint32_t)(r >> 32); /* expect a+b+1 */
return action == 1u ? val : 0xDEADu;
}
clang -O2 → wasm-ld → loom optimize --passes inline (1.1.11) → synth compile --target cortex-m4f --all-exports --relocatable (v0.11.35):
- wasmtime (ground truth):
check(3,4) = 8 ✔
- synth ARM under unicorn:
check(3,4) = 0xDEAD ✘ — the action byte read garbage. (Here loom inlined make, so this is the single-function i64 shift/mask unpack going wrong — no call boundary needed.)
Production hit — the dissolved z_impl_k_sem_give
The seam (sem module, post-bl-form of the same bug):
bl gale_k_sem_give_decide ; u64 packed decision returns in r0(lo)/r1(hi)
movw r0, #8 ; ← clobbers r0 = action byte
movw r1, #255 ; ← clobbers r1 = new_count
and.w r2, r0, ... ; unpack now operates on the constants
The mask/shift constants are materialized into r0/r1 — the registers holding the live u64 return value — before the unpack reads them. Unicorn write-trace: the body stores [sem+8] = 0 where the Verus-verified decide contract requires count+1 = 1 (no-waiter path). On silicon this is silent wrong-code: k_sem_give neither wakes nor increments, and the engine_control bench hangs at the first handoff (reader never observes count > 0). No fault, no diagnostic — the worst failure mode for an ASIL-D toolchain.
Class & suggested fix
Same family as #232 (div-guard constants into the live numerator, fixed by alloc_temp_avoiding in #233) and #255 (raw-imm encodings): constant materialization ignoring liveness, this time in the i64 unpack/shift-mask lowering (both the post-call r0/r1 pair and the in-function path). Suggest routing these constants through alloc_temp_avoiding (or reserving the pair until the unpack consumes it) wherever an i64 value is live.
Repro artifacts
Staged: u64repro.c + unicorn differential runner + the sem module (merged.both.wasm, faithful-shim revision) — happy to push any of it. Same-day re-measure on the G474RE the moment a fix lands; this currently blocks the sem re-baseline (the 907-cyc number's refresh) and any primitive whose decide function returns a packed u64 — i.e. the entire verified-decide pattern.
v0.11.35 ARM: i64 unpack miscompile — mask/shift constants land in the live u64 register pair → silent wrong-code (k_sem_give drops its count update; engine_control bench hangs on silicon)
Found while re-baselining the sem dissolution on v0.11.35 (G474RE). Two repros, one minimal + one production, both also byte-identical on the PR #309 head — so the fix can ride v0.11.36.
Minimal repro (self-contained, 15 lines)
clang -O2 → wasm-ld →
loom optimize --passes inline(1.1.11) →synth compile --target cortex-m4f --all-exports --relocatable(v0.11.35):check(3,4)= 8 ✔check(3,4)= 0xDEAD ✘ — the action byte read garbage. (Here loom inlinedmake, so this is the single-function i64 shift/mask unpack going wrong — no call boundary needed.)Production hit — the dissolved
z_impl_k_sem_giveThe seam (sem module, post-
bl-form of the same bug):The mask/shift constants are materialized into r0/r1 — the registers holding the live u64 return value — before the unpack reads them. Unicorn write-trace: the body stores
[sem+8] = 0where the Verus-verified decide contract requirescount+1 = 1(no-waiter path). On silicon this is silent wrong-code:k_sem_giveneither wakes nor increments, and the engine_control bench hangs at the first handoff (reader never observes count > 0). No fault, no diagnostic — the worst failure mode for an ASIL-D toolchain.Class & suggested fix
Same family as #232 (div-guard constants into the live numerator, fixed by
alloc_temp_avoidingin #233) and #255 (raw-imm encodings): constant materialization ignoring liveness, this time in the i64 unpack/shift-mask lowering (both the post-call r0/r1 pair and the in-function path). Suggest routing these constants throughalloc_temp_avoiding(or reserving the pair until the unpack consumes it) wherever an i64 value is live.Repro artifacts
Staged:
u64repro.c+ unicorn differential runner + the sem module (merged.both.wasm, faithful-shim revision) — happy to push any of it. Same-day re-measure on the G474RE the moment a fix lands; this currently blocks the sem re-baseline (the 907-cyc number's refresh) and any primitive whose decide function returns a packed u64 — i.e. the entire verified-decide pattern.