Skip to content

v0.11.40 arm: spill-slot collision miscompiles dissolved k_mutex_unlock — live mutex-ptr arg clobbered by unpend() result, lock_count=0 store misses (silicon deadlock) #331

Description

@avrabe

Summary

On synth v0.11.40, the dissolved k_mutex_unlock body compiles but silently miscompiles: the incoming mutex-pointer argument and a call return value are assigned the same spill slot ([sp,#0x24]) despite overlapping live ranges. The mutex pointer is clobbered by gale_w_unpend_first_thread's result, so a later field store that reloads the slot writes to the wrong address.

Concretely: in the no-waiter unlock path, mutex->lock_count = 0 is emitted but lands on linmem[0+12] instead of linmem[mutex+12], leaving lock_count = 1. On silicon (NUCLEO-G474RE, 170 MHz) this deadlocks: the next k_mutex_lock(&m, K_FOREVER) sees lock_count≠0 ∧ owner≠current → blocks forever on an ownerless mutex → CPU idles. Observed hung state: owner=0, lock_count=1 (an impossible-by-construction inconsistent state that this bug uniquely produces).

This is the same "live value clobbered → silent wrong-code" family as #311 (closed, i64 pair clobbered by mask-constant materialization), but a different mechanism: spill-slot collision between a live incoming-arg pointer and a call result, rather than a register clobber. Likely related to #326 (register-exhaustion at this same site) — the call-result-pair pressure that forces the spill is what then collides.

Evidence — disassembly of the actual compiled body.o (synth v0.11.40, --target cortex-m4f --native-pointer-abi --relocatable)

Entry spills the mutex ptr (arg0) to its home slot:

   8: f8cd 0024   str.w r0, [sp, #0x24]     ; HOME slot for mutex ptr (live to 0x1e2)

Tag-1 (release + unpend) path:

 128: f8dd 7024   ldr.w r7, [sp, #0x24]     ; r7 = mutex (register copy)
 136: f7ff fffe   bl    gale_w_unpend_first_thread   ; r0 = new_owner (NULL if no waiter)
 13a: f8cd 0024   str.w r0, [sp, #0x24]     ; *** CLOBBERS mutex home slot with call result ***
 156: f84b 100c   str.w r1, [r11, r12]      ; (r12=r7+8) owner = new_owner  -- OK, uses r7
 168: d137        bne   0x1da               ; new_owner==0 -> no-waiter sub-path

No-waiter sub-path — reloads the clobbered slot as the mutex base:

1e2: f8dd 3024   ldr.w r3, [sp, #0x24]      ; r3 = [clobbered slot] = unpend result = 0 (no waiter)
1ea: f103 0c0c   add.w r12, r3, #0xc        ; r3 + 12  (intended: mutex + lock_count_off)
1ee: f84b 500c   str.w r5, [r11, r12]       ; lock_count = 0  -> writes linmem[0+12], NOT mutex->lock_count

The only writes to [sp,#0x24] between entry and 0x1e2 are 0x13a (unpend result) and 0x1aa (the other, has-waiter branch). In the no-waiter path the slot therefore holds 0, so the store misses. (Note the has-waiter path at 0x16a: ldr r7,[sp,#0x24] then 0x172: add r7,#0xc; str is also corrupted — it computes new_owner+12 for the lock_count=1 store — just not exercised by the single-thread selfcheck.) Relocation 0x136 R_ARM_THM_CALL gale_w_unpend_first_thread confirms the clobbering value's identity.

r7 still holds the valid mutex pointer at 0x1e2 — the allocator had a live copy available and chose the clobbered slot instead.

Kill-criterion

This claim is wrong if: a clean re-decode of synth_k_mutex_unlock_body on a fixed build shows the no-waiter lock_count store deriving its base from a slot/reg that holds the mutex pointer (not the unpend result) at 0x1e2; or if on-silicon k_mutex_lock/unlock round-trips without deadlock and lock_count reads 0 after a no-waiter unlock.

Fix direction

The mutex pointer (arg0) is live across the gale_w_unpend_first_thread call and used again afterward (no-waiter lock_count store, and the has-waiter lock_count store). The allocator must not reuse arg0's spill slot for a call return value while arg0 is still live — either keep arg0 in a callee-saved register across the whole body, or give the call result a distinct slot. This is a spill-slot extension of the register-level live-range tracking that #311/#226/#231/#232 exercised; the slot allocator appears to treat the home slot as dead once the value is also in a register, but then reloads the (now-clobbered) slot rather than the register.

Repro shape

k_mutex_lock(&m, K_FOREVER); k_mutex_unlock(&m); single-threaded (no waiter) on the dissolved/--native-pointer-abi mutex shim, cortex-m4f. Compiles clean; deadlocks at runtime. Faithful Zephyr v4.4.0 k_mutex layout (owner@8, lock_count@12).

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