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)
PoC verification
Evidence pack
findings/poc/DF-4059 Β· 21 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 liveamr_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.
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 run78 f8 13 18 00 f8 ff ff), - runs of
0x5aand0xa5β the rig caller frames' pattern buffers (rig_nest0/rig_nestpads) immediately belowamr_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 0printed, no NULL store, no decrement,busycmd[0]intact (write_after == write_before,decr_after == decr_before); - leak run:
excessive completion count 255printed, 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 rigbootA_serial.logβ 4059B + leak runs + 4060 evidence + 4060 finale panicbootB_serial.logβ 4059A idle-controller panicpanic_4060.txt,panic_4059A.txtβ panic excerptsleak2.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
fixedfix.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
Confirmed kernel references
- sys/dev/raid/amr/amr.c:1786
- sys/dev/raid/amr/amr.c:1788
- sys/dev/raid/amr/amr.c:1789
- sys/dev/raid/amr/amr.c:1795
- sys/dev/raid/amr/amr.c:1798
- sys/dev/raid/amr/amr.c:1500
- sys/dev/raid/amr/amr.c:1510
- sys/dev/raid/amr/amr.c:196
- sys/dev/raid/amr/amrreg.h:433
- sys/dev/raid/amr/amrvar.h:230
- sys/dev/raid/amr/amrvar.h:231
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.
No comments yet.