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

amr_done trusts the controller completion list twice over: completion id 0 yields idx = -1 and reads amr_busycmd[-1] (OOB before the array -> garbage ac pointer deref), and on standard firmware the 8-bit mb_nstatus iterates mb_completed[46] out to 255 entries (stack OOB read past the local mailbox) β€” distinct from DF-1358 (the WRITE overflow inside amr_quartz_get_work)

Summary

The completion loop indexes amr_busycmd[mbox.mb_completed[i] - 1] with no validation: id 0 (never allocated) yields -1 and reads the softc field preceding the array, yielding a garbage ac pointer immediately dereferenced (amr_freeslot -> ac->ac_slot). On std firmware the whole shared mailbox (attacker/controller-chosen 8-bit mb_nstatus) is bcopy'd into the 82-byte stack mbox and the loop reads up to 255 entries from mb_completed[46] - a 209-byte stack overread feeding the index. Sibling of DF-1358 in amr_done itself. Clamp + id-validation fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4059 Β· 21 files
FileTypeDescriptionSize
README.md β€” 2.9 KB ↓ raw
VERDICT.md β€” 9.3 KB ↓ raw
dfamr_rig.c β€” 33.3 KB view raw
dfamr_ctl.c β€” 10.3 KB view raw
Makefile β€” 108 B ↓ download
build.sh β€” 143 B view raw
run_bootA.sh β€” 1.1 KB view raw
run_bootB.sh β€” 642 B view raw
run_patched.sh β€” 1.3 KB view raw
bootA_serial.log β€” 30.7 KB view raw
bootB_serial.log β€” 13.6 KB view raw
panic_4059A.txt β€” 654 B view raw
panic_4060.txt β€” 630 B view raw
leak2.serial.txt β€” 7.9 KB view raw
leak7.serial.txt β€” 7.9 KB view raw
dmesg_patched.txt β€” 9.0 KB view raw
env.txt β€” 316 B view raw
fix.diff β€” 4.1 KB view raw
fix_build.log β€” 5.7 MB ↓ download
fix_run.log β€” 2.7 KB view raw
verdict.json β€” 6.7 KB view raw

DF-4059 β€” amr_done trusts the completion list: id 0 β†’ amr_busycmd[-1] wild

pointer; mb_nstatus > 46 β†’ stack overread disclosed via "bad slot" prints

Status: REPRODUCED (panic + controlled OOB write + 209-byte kernel stack disclosure) β€” production-handler KLD dispatch rig, no MegaRAID hardware needed. Fix validated on a rebuilt kernel. See VERDICT.md.

What runs

dfamr_rig.c (KLD, root-only /dev/dfamr0) fabricates the post-attach softc of a std-firmware amr controller with REAL bus_dma (mailbox64 / s-g table / ccb array exactly as amr_pci.c allocates them), REAL locks, the production queue setup and the production std handler trio, then dispatches the shipped kernel's own code: amr_done (global link), amr_start, amr_std_get_work, amr_std_poll_command, amr_std_submit_command (nm-resolved, objdump prologue-verified). The "card" is a memory register file + completions written into the real DMA mailbox; rig_get_work wraps production amr_std_get_work only to model the card-side interrupt-line drop. Command clusters are a verbatim copy of amr_alloccmd_cluster (gcc-8.3 inlined that symbol into amr_attach).

Build (on the audit guest, from repo root)

findings/poc/DF-4059/build.sh        # scp + make + cc dfamr_ctl

Run

# baseline boot A: controlled write + stack disclosure + DF-4060 finale
./dfbsd-qemu/vm.sh run_root 'cd /root/rig && kldload ./dfamr_rig.ko && \
    ./dfamr_ctl prime && ./dfamr_ctl offs && ./dfamr_ctl magic && \
    ./dfamr_ctl b && ./dfamr_ctl leak 2 && ./dfamr_ctl leak 7 && \
    ./dfamr_ctl tmo 0 && ./dfamr_ctl late'
./dfbsd-qemu/vm.sh run_root 'cd /root/rig && ./dfamr_ctl tmo 1'   # panics
# baseline boot B: idle-controller id-0 panic
... kldload, prime, offs, magic ...
./dfbsd-qemu/vm.sh run_root 'cd /root/rig && ./dfamr_ctl a'       # panics
# fix validation (after applying fix.diff + nativekernel + reboot):
./dfbsd-qemu/vm.sh run_root 'cd /root/rig && kldload ./dfamr_rig.ko && \
    ./dfamr_ctl prime && ./dfamr_ctl offs && ./dfamr_ctl magic && \
    ./dfamr_ctl b && ./dfamr_ctl leak 2 && ./dfamr_ctl tmo 1 && \
    ./dfamr_ctl late && ./dfamr_ctl a && uptime'                  # all benign

Expected results

  • baseline a: panic: amr: slot 0 not busy? (amr_done+0x134)
  • baseline b: NULL store at softc+3088+8*slot, atomic decrement at softc+5128 (1208 bytes past the 3920-byte softc), mb_status stored into the live amr_busycmd[0] pointer β€” guest survives
  • baseline leak: 255 "bad slot %d" prints disclosing 209 stack bytes past the 82-byte mailbox (kernel heap pointers, caller frame patterns; bytes differ between call depths 2 and 7)
  • patched: bad completion id 0, excessive completion count 255, 46 entries max, zero stores, zero disclosure, guest alive

Fix

fix.diff (git-apply clean): clamp the completion loop to nitems(mbox.mb_completed) and reject completion id 0 in amr_done.

VERDICT.md
↓ download raw

DF-4059 β€” VERDICT

REPRODUCED (panic + controlled OOB write + kernel stack disclosure; fix validated on a rebuilt kernel).

Production-handler KLD dispatch rig (dfamr_rig.c, the DF-4240 dpt class): the guest's X86_64_GENERIC kernel contains the amr driver (nm /boot/kernel/kernel: amr_done T, amr_start/amr_std_get_work/ amr_std_poll_command/amr_std_submit_command/amr_complete t) but no MegaRAID HBA exists, so the rig fabricates the post-amr_pci_attach softc (real bus_dma tags, real DMA-mapped mailbox64/s-g table/ccb array, real lockinit of both locks, production amr_init_qhead/queue setup, production std-firmware handler trio at amr_attach:212-214) and stands in only for the card: a memory-mapped register file (X86_64_BUS_SPACE_MEM β€” the AMR_SGET_ISTAT/AMR_SPOST_COMMAND accesses are plain volatile memory ops), completions written into the real DMA mailbox, and the card-side "deassert the interrupt line once the driver took the mailbox" semantic (which a memory file cannot express) modeled by rig_get_work() wrapping the production amr_std_get_work. Command clusters are built by a verbatim copy of amr_alloccmd_cluster (amr.c:1922-1979 β€” gcc 8.3 inlined that symbol into amr_attach, so it cannot be dispatched by address; everything else on the vulnerable path is dispatched from the shipped kernel image, addresses nm-resolved and prologue-byte-verified against objdump β€” all four OK).

Everything at issue runs as shipped-kernel production code: amr_done with its inlined amr_freeslot, driven by production amr_std_get_work over production amr_alloccmd/amr_start/amr_releasecmd structures.

Measured alias (empirical, not assumed)

offs ioctl on the fabricated-but-production-shaped softc: offsetof(softc, amr_ready)=1536, amr_busycmd=1552, amr_busyslots=3592, sizeof(softc)=3920; offsetof(command, ac_sc)=8, ac_slot=16, ac_status=20, sizeof(command)=232; sizeof(amr_mailbox)=82 (mb_completed[46] at +18). amr_busycmd[-1] empirically equals &sc->amr_ready.stqh_first (busykm1_value == ready_stqh_last, match=1) β€” a non-NULL kernel pointer on every production attach (amr.c:197 amr_init_qhead(&sc->amr_ready)).

Reproduction 1 β€” completion id 0, idle controller: instant panic (bootB)

One mailbox: {mb_nstatus=1, mb_completed[0]=0} raised as the card would:

[amrrig] DF-4059A: idle controller, completion id 0 -> amr_freeslot derefs busycmd[0]=NULL as softc
panic: amr: slot 0 not busy?
amr_done() at amr_done+0x134

(bootB_serial.log). Mechanism, line-verified: idx = mb_completed[i] - 1 = -1 (amr.c:1788) β†’ ac = sc->amr_busycmd[-1] (amr.c:1789) = &amr_ready.stqh_first β‰  NULL β†’ amr_freeslot(ac) (amr.c:1795) reads ac->ac_sc (+8) = amr_ready.stqh_last = softc-interior pointer &amr_ready, and ac->ac_slot (+16) = low byte of amr_busycmd[0] = 0 β†’ sc2->amr_busycmd[0] reads *(softc+3088) = amr_busycmd[192] = NULL (production maxio ≀ 120, slot 192 can never be busy) β†’ production panic("amr: slot %d not busy?") (amr.c:1510). One hostile completion byte = guaranteed kernel panic on the interrupt path.

Reproduction 2 β€” completion id 0, slot 0 busy: controlled corruption (bootA)

Real production cluster + production amr_start on two commands (busycmd[0]=ac0? β€” the freelist is LIFO so slots 2 and 1 went busy; slot 0 idle β†’ forged ac->ac_slot byte = 0). Rig pre-poisons only the qword production will test/clear so the "not busy?" guard does not fire first (0x0000133700001337 at softc+3088; 0x4000 at softc+5128). After one production amr_done:

[4059B] NULL store @softc+3088: 0x0000133700001337 -> 0x0000000000000000
[4059B] busyslots decr @softc+5128: 0x4000 -> 0x3fff
[4059B] ac_status store corrupted busycmd[0] byte4: 0x5a (mb_status 0x5a)
[4059B] real sc busyslots after: 2      (fake sc2 never touched the real counter)

What production executed: amr_freeslot treated &amr_ready as a softc and (1) wrote NULL through it at &amr_ready + off_busycmd + 8*slot (&amr_ready + 1552 + 8*slot = softc+3088+8slot β€” for heap-groomed slot bytes β‰₯ 63 this leaves the amr_busycmd[] array entirely and clears arbitrary softc fields: amr_freecmds, amr_cmd_clusters, cam pointers); (2) atomic_subtract_int(&sc2->amr_busyslots, 1) at &amr_ready+3592 = softc+5128 β€” 1208 bytes past the end of the 3920-byte softc (on a real host: whatever follows the device softc in the heap); (3) back in amr_done, ac->ac_status = mb_status (amr.c:1798) stored the controller-chosen status byte at &amr_ready+20 = softc+1556 β€” corrupting the amr_busycmd[0] command pointer inside the live softc*. The slot steering byte is the low byte of the slot-0 command's cluster address (16 KB kmalloc β€” heap-groomable on a real host), exactly the "wild kernel pointer deref" the finding claims, with a write primitive attached.

Reproduction 3 β€” std-firmware stack overread + log disclosure (bootA)

mb_nstatus = 255 (8-bit, controller-chosen) with mb_completed[0..45] set to 0x80..0xab. Production amr_done loop iterates 255 times reading mb_completed[i] for i = 46..254 β€” 209 bytes past the 82-byte stack mailbox β€” and each byte becomes idx = byte - 1 printed by device_printf(... "bad slot %d completed") (amr.c:1802) unless NULL. 255 disclosure lines per run captured (leak2/leak7 marker regions in dmesg_bootA.txt, bootA_serial.log). Decoded bytes beyond the mailbox include:

  • a canonical kernel heap pointer 0xfffff8001813f878 (byte run 78 f8 13 18 00 f8 ff ff),
  • runs of 0x5a and 0xa5 β€” the rig caller frames' pattern buffers (rig_nest0/rig_nest pads) immediately below amr_done's frame,
  • zero bytes (idx βˆ’1) and pointer-fragment bytes throughout.

Two runs at different call depths (2 vs 7) disclose different bytes (... 113 23 ... / 55 242 ... vs ... 207 114 ... / 103 244 ...) β€” the overread demonstrably reads live kernel stack, and every byte lands in the console/message buffer. This is a kernel-stack info disclosure of up to 209 bytes per completion mailbox to anyone who can read the log, driven purely by controller-supplied data.

Accidental third facet (production-faithful, first rig iteration)

While shaking down the rig (virtual card left the mailbox-busy flag stuck), the second amr_start took the production EBUSY path (amr_setup_data:1597-1600 β†’ amr_requeue_ready) which left amr_ready.stqh_last = &requeued_cmd.ac_link. In that state amr_busycmd[-1] resolves to a real amr_command (the requeued one), so a completion id 0 makes amr_freeslot free that live command's slot a second time β†’ panic("amr: slot 1 not busy?") (observed on serial; corrected register model since). I.e. under ordinary driver states the id-0 alias is either a softc-interior pointer (repro 1/2) or a real queued command (double-free/double-complete) β€” both non-NULL, both weaponized by the same missing validation.

Why this is reachable (threat model)

All inputs are controller-supplied: a malicious/faulty MegaRAID (firmware bug, transient, or passed-through/virtualized device) or DMA of the shared mailbox. The path is the ordinary interrupt/poll completion (amr_pci_intr:469, amr_std_poll_command:1353, amr_rescan_drives:462 — root-only ioctls on /dev/amr0 reach the poll loops). Not unpriv→root: the device node is 0600 root:operator and no HBA exists on the audit guest; the ceiling is one-byte-to-panic plus heap-directed NULL-store/decrement/ pointer-corruption and stack disclosure on hosts that have the card.

Fix validation (patched kernel #1, nativekernel gcc 8.3, rc=0)

fix.diff = clamp i < nitems(mbox.mb_completed) (46) + reject mb_completed[i] < 1 in amr_done (amr.c:1786-1789). On the rebuilt kernel (run_patched.sh / fix_run.log):

  • 4059B: bad completion id 0 printed, no NULL store, no decrement, busycmd[0] intact (write_after == write_before, decr_after == decr_before);
  • leak run: excessive completion count 255 printed, exactly 46 entries processed (bad slot 127..172), zero stack-derived bytes in the log;
  • 4059A: bad completion id 0 + guest alive (former panic gone).

Baseline behaviors gone; benign completions unaffected (tmo/late scenarios still complete real ids through the patched loop).

Not re-verified here / adjacent

amr_quartz_get_work's write overflow into its own completed[46] is DF-1358 (separate fix); the patched amr_done clamp also protects the quartz-driven downstream loop, but the quartz write itself is out of scope for this pack.

Artifacts

  • dfamr_rig.c / dfamr_ctl.c / Makefile / build.sh β€” the rig
  • bootA_serial.log β€” 4059B + leak runs + 4060 evidence + 4060 finale panic
  • bootB_serial.log β€” 4059A idle-controller panic
  • panic_4060.txt, panic_4059A.txt β€” panic excerpts
  • leak2.dmesg / leak7.dmesg β€” full disclosure runs (re-captured on the patched-build baseline boot; identical mechanism)
  • fix.diff (git-apply clean), fix_build.log, fix_run.log

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (git-apply clean) applied to guest /usr/src, nativekernel KERNCONF=X86_64_GENERIC rebuilt (RC=0, 37878-line log) and installed; rig re-dispatched the PATCHED production handlers (amr_std_poll_command moved +0x50, prologues re-verified): id-0 completion rejected ('bad completion id 0') with zero stores; nstatus=255 clamped ('excessive completion count 255', exactly 46 entries, zero stack bytes disclosed); former 4059A panic gone; guest alive

fix.diff; fix_build.log (full nativekernel output, RC=0/INSTALL_RC=0); fix_run.log (baseline-vs-patched contrast per scenario); dmesg_patched.txt
↓ fix.diffper-fix-DF-4059

Confirmed kernel references

Detail

Exploit chain

controller firmware/DMA writes completion mailbox id 0 -> amr_std_get_work bcopy -> amr_done idx=-1 -> amr_busycmd[-1] (= &amr_ready.stqh_first, or &requeued_cmd after EBUSY) treated as amr_command* -> amr_freeslot deref/write through forged ac_sc (softc-interior) with heap-byte-steered slot -> NULL store into arbitrary softc field + atomic decrement 1208B past the softc + status byte into the live busycmd[] pointer; idle variant = production panic; nstatus>46 variant = 209-byte stack overread disclosed to the log. No unpriv->root: 0600 root device + hardware precondition; ceiling is one-byte-panic + heap-directed corruption + stack info disclosure.

Evidence (decisive lines)

VERDICT.md (full narrative + numbers); bootB_serial.log / panic_4059A.txt (idle-controller panic: 'panic: amr: slot 0 not busy?' amr_done+0x134); bootA_serial.log (4059B controlled stores: NULL @softc+3088, decr 0x4000->0x3fff @softc+5128, busycmd[0] byte4=0x5a); leak2.serial.txt / leak7.serial.txt (255 entries each, 209 stack bytes, differing at 6/255 positions between depths); dmesg_patched.txt + fix_run.log (bad completion id 0 / excessive completion count 255 / 46 entries / no stores / guest alive)

PoC changes

No seed sources existed (pack was source-only 'hardware-gated'). Built the DF-4240-class production-handler KLD dispatch rig from scratch: dfamr_rig.c + dfamr_ctl.c. Key rig facts: amr compiled into X86_64_GENERIC (nm shows statics); amr_alloccmd_cluster was inlined by gcc 8.3 so its body is a verbatim copy; the virtual card uses a X86_64_BUS_SPACE_MEM register file, and rig_get_work wraps production amr_std_get_work only to model the card-side interrupt-line drop (a memory file cannot react to the driver's ack write); struct offsets measured at runtime (ac_link is ONE pointer: ac_sc=8, ac_slot=16 -- the pass-2 layout guesses in early rig iterations were corrected by the measured offs ioctl and one production panic that exposed the real alias chain).

Verified recommended fix

In amr_done, clamp the completion loop to nitems(mbox.mb_completed) and reject completion ids < 1 before indexing amr_busycmd

Verdict

REPRODUCED via production-handler KLD dispatch rig (DF-4240 class): the shipped X86_64_GENERIC kernel's own amr_done/amr_start/amr_std_get_work (nm-resolved, prologue-verified) execute over a fabricated post-attach softc with real bus_dma and production command clusters. (A) Idle controller + one completion id 0: idx=-1 makes amr_done read amr_busycmd[-1] -- empirically &sc->amr_ready.stqh_first, non-NULL after production attach -- and drive it through amr_freeslot as an amr_command; ac->ac_sc lands on the softc interior and the slot byte on amr_busycmd[0], so the production NULL-slot guard fires: panic("amr: slot 0 not busy?") in amr_done+0x134. One controller-supplied byte = guaranteed kernel panic on the interrupt path. (B) Slot busy: same forged pointer steers a NULL store at &amr_ready+1552+8slot (arbitrary softc fields for heap-groomed slot bytes >= 63), an atomic decrement at softc+5128 -- 1208 bytes past the 3920-byte softc -- and stores mb_status into the live amr_busycmd[0] pointer (all observed, guest alive). Accidental third facet: after a production EBUSY requeue, amr_busycmd[-1] is a REAL amr_command, so id 0 double-frees a live command's slot (observed panic "amr: slot 1 not busy?"). (C) std-firmware mb_nstatus=255: the loop reads mb_completed[46..254], 209 bytes past the 82-byte stack mailbox, and every byte is disclosed via the production "bad slot %d completed" print -- kernel heap pointers (0xfffff8xxxxxxxxx) and caller-frame patterns captured, 6/255 bytes differ between call depths 2 and 7 (live stack). Threat model: malicious/faulty MegaRAID or mailbox DMA; root-only /dev/amr0 poll paths. Fix (clamp loop to nitems(mb_completed)=46 + reject id 0) validated on a rebuilt nativekernel: id rejected, zero stores, 46 entries max, zero stack disclosure, guest alive.