Skip to content

v0.11.35 arm: i64 unpack miscompile — mask constants materialize into the live u64 pair (r0/r1); silent wrong-code in k_sem_give #311

Description

@avrabe

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.

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