mly_done() completion slot has no lower-bound check: command_id 0 indexes mly_command[-1] β OOB field writes + TAILQ pointer writes + indirect call through firmware-fed memory
Summary
Both status walks gate only slot < MLY_SLOT_MAX (257) with MLY_SLOT_START=1, so firmware completions carrying slot/command_id 0 compute &mly_command[-1]. The code then writes mc_status/mc_sense/mc_resid and calls TAILQ_REMOVE on the fake entry overlaying firmware-fed memory; mly_complete finally calls the garbage mc_complete - corruption to RIP control with a hostile device. Arrives unsolicited with interrupts; distinct surface from DF-1290. Lower-bound-check fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3929 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1020 B | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| fix.diff | β | 864 B | view raw | |
| env.txt | β | 543 B | view raw | |
| verdict.json | β | 2.9 KB | view raw |
DF-3929 β mly: mly_done() completion slot lacks lower-bound check β mly_command[-1] OOB write + TAILQ pointer writes
Build / Run
Triggered purely by the controller's completion traffic (hardware mailbox status register read or memory-mailbox status ring walk). On hardware, a card returning command_id 0 in a completion (buggy/hostile firmware) hits it from the very first interrupt β no user action required beyond having the driver attached.
Lab observation: QEMU device model returning status_mailbox value 0x0000 (or an mmbox status entry with command_id=0) β immediate OOB write on completion.
Expected result
mc = &sc->mly_command[0 - MLY_SLOT_START] = mly_command[-1] β three direct
field writes plus TAILQ_REMOVE() pointer writes into the tail of mly_btl[],
then the fake command is threaded onto the complete queue and its
mc_complete (garbage from controller-fed btl fields) is called.
Hardware-gated (no mly controller in the audit guest): source-certain. See VERDICT.md.
DF-3929 verdict narrative
Reproduced?
No (hardware-gated). Source-level certainty: certain β pure register/ring walk arithmetic.
Root cause chain (path:line)
Slot regulation is 1-based on purpose:
mlyvar.h:153-154
#define MLY_SLOT_START 1
#define MLY_SLOT_MAX (MLY_SLOT_START + MLY_MAX_COMMANDS) /* 257 */
("We can't use slot 0 due to the memory mailbox implementation.")
Both completion paths check only the UPPER bound:
mly.c:1533-1543 (hardware mailbox):
c
slot = MLY_GET_REG2(sc, sc->mly_status_mailbox);
if (slot < MLY_SLOT_MAX) { /* no slot >= MLY_SLOT_START */
mc = &sc->mly_command[slot - MLY_SLOT_START]; /* slot==0 -> index -1 */
mc->mc_status = ...
mc->mc_sense = ...
mc->mc_resid = ...
mly_remove_busy(mc); /* TAILQ_REMOVE */
mly.c:1554-1582 (memory mailbox ring walk): identical
if (slot < MLY_SLOT_MAX) at :1564 with sc->mly_command[slot -
MLY_SLOT_START] at :1565.
slot is u_int16_t fed by firmware (16-bit status mailbox read at :1534, or
sp->status.command_id at :1563). slot==0 passes < 257 and yields
&sc->mly_command[-1] β one struct mly_command (~104B) BEFORE the array,
which overlays the tail of mly_btl[5][*] (softc layout, mlyvar.h:206-209).
What lands there:
- Direct writes of firmware-fed values:
mc_status(u16),mc_sense(u8),mc_resid(u32) β into btl fields. mly_remove_busy(mc)βTAILQ_REMOVE(&sc->mly_busy, mc, mc_link)(mlyvar.h:345-352): readsmc->mc_link.tqe_next/tqe_prevfrom btl memory βmb_flags/mb_name[]/mb_stateregions. mb_state/mb_speed/mb_width are firmware-fed ints (mly.c:928-930 from GETPHYSDEVINFOVALID); then executesif (tqe_next) tqe_next->tqe_prev = tqe_prev; *tqe_prev = tqe_next;β write-what-where style kernel writes with firmware-influenced pointers.mly_enqueue_complete(mc)threads the fake entry onto the complete queue;mly_complete()(mly.c:1612-1634) then invokesmc_complete(mc)β a function pointer read from the same btl memory β firmware-influenced indirect call.
Same-surface aggravators (same trust boundary, not separately filed)
- Duplicate completion for an already-completed slot:
mly_remove_busy()on a command that is on the free list corrupts both TAILQs (mly.c:1540/1569). - The mmbox walk loop (mly.c:1555-1582) is bounded (each iteration clears the flag), so no unbounded walk there.
Threat model
Same as DF-1290/DF-1289: malicious/buggy controller. Note this one is remotely reachable without any local user action β completions arrive from the card on their own.
Impact
Kernel memory corruption inside the softc with pointer-write and indirect-call primitives derived from firmware-fed fields; panic to potential code execution given a hostile device.
Fix
slot >= MLY_SLOT_START && slot < MLY_SLOT_MAX in both paths β fix.diff.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testableFix not validated on the guest: the trigger path requires mly/age hardware absent from the audit guest. fix.diff is mechanical (bounds/termination-only) and authored post-verification against the cited lines.
findings/poc/DF-3929/fix.diff + VERDICT.md
Confirmed kernel references
Detail
Exploit chain
malicious controller returns command_id 0 (or duplicate completions) in a status entry -> mly_command[-1] OOB writes + TAILQ pointer-write primitives + indirect call through firmware-fed memory -> corruption/RIP control
Evidence (decisive lines)
findings/poc/DF-3929/VERDICT.md; fix.diff (lower-bound check in both paths)
PoC changes
No seed PoC; hardware-gated interrupt-path defect - lab reproduction documented (status_mailbox/command_id = 0).
Verified recommended fix
Check slot >= MLY_SLOT_START in both mly_done completion paths.
Verdict
Hardware-gated (no mly controller in the guest); source-certain. Both completion walks in mly_done check only the upper bound: 'if (slot < MLY_SLOT_MAX)' (mly.c:1535 HM, :1564 AM) then index sc->mly_command[slot - MLY_SLOT_START] with MLY_SLOT_START==1 (mlyvar.h:153) - a firmware completion carrying command_id 0 yields mly_command[-1], one struct (~104B) before the array, overlaying the tail of mly_btl[][]. The driver then writes mc_status/mc_sense/mc_resid (firmware values) and executes TAILQ_REMOVE via mly_remove_busy (mlyvar.h:345-352) on that fake entry - dereferencing and writing tqe_next/tqe_prev pointers read from firmware-fed btl fields (mb_state/mb_speed/mb_width from GETPHYSDEVINFOVALID, mly.c:928-930) - and finally mly_complete (mly.c:1612-1634) calls the garbage mc_complete function pointer threaded from the same region. Distinct from known DF-1290 (event channel/target) : this is the status-walk slot index, reachable with no user action as completions arrive unsolicited. Runtime reproduction requires the card; filed not_reproduced, certain at source level.
No comments yet.