β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3929

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3929 Β· 5 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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:

  1. Direct writes of firmware-fed values: mc_status (u16), mc_sense (u8), mc_resid (u32) β€” into btl fields.
  2. mly_remove_busy(mc) β†’ TAILQ_REMOVE(&sc->mly_busy, mc, mc_link) (mlyvar.h:345-352): reads mc->mc_link.tqe_next/tqe_prev from btl memory β€” mb_flags/mb_name[]/mb_state regions. mb_state/mb_speed/mb_width are firmware-fed ints (mly.c:928-930 from GETPHYSDEVINFOVALID); then executes if (tqe_next) tqe_next->tqe_prev = tqe_prev; *tqe_prev = tqe_next; β€” write-what-where style kernel writes with firmware-influenced pointers.
  3. mly_enqueue_complete(mc) threads the fake entry onto the complete queue; mly_complete() (mly.c:1612-1634) then invokes mc_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_testable
baseline no→ patch + rebuild →patched clean

Fix 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
↓ fix.diffper-fix-DF-3929

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.