ida_v4_done()'s empty-reply-FIFO sentinel 'completed == -1' is dead code on LP64: bus_addr_t is uint64_t while the register read is 32-bit, so an empty 42xx FIFO is consumed as completion 0xFFFFFFFF β idahwqcbptov() wild kernel dereference (boot-time page fault on <4GB hosts, infinite ISR spin on >=4GB)
Summary
The 32-bit register read zero-extends into uint64_t bus_addr_t so the 0xFFFFFFFF empty-FIFO value never equals (bus_addr_t)-1 on 64-bit - the sentinel worked only on 32-bit FreeBSD. Every ida_command poll during attach exits instantly on the bogus completion and derefs hwqcbs+(0xFFFFFFFC-hwqcb_busaddr). VERIFIED on guest with verbatim driver code: Fatal trap 12 with the fault VA matching the arithmetic exactly; masked-compare fix validated (-Werror ida.ko rebuild, empty-FIFO returns 0). Driver is in GENERIC - v4 boards panic at boot.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5061 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| df5061_harness.c | β | 11.0 KB | view raw | |
| Makefile | β | 301 B | β download | |
| Makefile.ida | β | 282 B | β download | |
| build.sh | β | 115 B | view raw | |
| run.sh | β | 150 B | view raw | |
| build.log | β | 5.9 KB | view raw | |
| run.log | β | 3.2 KB | view raw | |
| fix_run.log | β | 5.4 KB | view raw | |
| fix_build.log | β | 2.2 KB | view raw | |
| panic.txt | β | 2.4 KB | view raw | |
| panic.firstboot.txt | β | 3.0 KB | view raw | |
| hwgate.txt | β | 26 B | view raw | |
| env.txt | β | 449 B | view raw | |
| fix.diff | β | 661 B | view raw | |
| verdict.json | β | 5.8 KB | view raw |
DF-5061 β ida_v4_done() empty-FIFO sentinel is dead code on LP64 β wild kernel pointer deref
File under audit: sys/dev/raid/ida/ida_pci.c (pass 2), lines 101β111.
TL;DR
ida_v4_done() tests if (completed == -1) where completed is
bus_addr_t (uint64_t on x86_64, sys/cpu/x86_64/include/bus_dma.h:36)
but is assigned from ida_inl() = bus_space_read_4() which returns
u_int32_t (sys/cpu/x86_64/include/bus_dma.h:130). The 42xx board's
empty-reply-FIFO value 0xFFFFFFFF zero-extends to 0x00000000FFFFFFFF,
which never equals 0xFFFFFFFFFFFFFFFF β the "fifo is empty" check is
dead code on every 64-bit DragonFly. Both consumers then treat the
empty FIFO as a completion:
ida_wait()(ida.c:457-467, the polling path taken by everyida_command()during attach while interrupts are disabled) exits its poll loop instantly withcompleted = 0xFFFFFFFF;ida_intr()(ida.c:482-491) enters its drain loop for the same value and would spin forever printingignoring completion ffffffff.
Both feed the value to idahwqcbptov() (ida.c:140-148), which computes
hwqcbs + (0xFFFFFFFC - hwqcb_busaddr) β with DMAP-mapped hwqcbs and its
32-bit physical hwqcb_busaddr this cancels to exactly DMAP + 0xFFFFFFFC
(physical 4 GB β 4) β and dereferences hwqcb->qcb. On any host with
< 4 GB RAM that is an unmapped DMAP page β kernel page fault. The driver
ships in the GENERIC kernel (device ida, X86_64_GENERIC:132), so a v4
board (Compaq Integrated Array / RAID LC2 / SA4200 / SA4250ES / SA431)
panics the machine during boot attach.
The PoC (harness KLD)
df5061_harness.c runs verbatim copies of ida_v4_done()
(ida_pci.c:101-111) and idahwqcbptov() (ida.c:140-148) against a mock
32-bit MMIO register file whose R_42XX_REPLY slot holds 0xFFFFFFFF β
exactly what a 42xx board presents on an empty reply FIFO. Every byte of
kernel-side logic (types, macros, arithmetic, deref) is the shipping
driver's code; only the board's register file is mocked.
Build
# on the DragonFly guest (source in /usr/src) sh build.sh # -> /tmp/df5061/df5061.ko (see build.log)
Run
kldload /tmp/df5061/df5061.ko sysctl hw.df5061_mode=1 # fixed variant: sentinel honored -> returns 0 sysctl hw.df5061_fire=1 # SAFE run (run.log / fix_run.log) sysctl hw.df5061_mode=2 # verbatim driver code -> wild deref sysctl hw.df5061_fire=1 # KERNEL PANIC (panic.txt) # (mode 3 prints the wild-pointer arithmetic without dereferencing; # note idahwqcbptov() itself must deref hwqcb->qcb to return, so even # mode 3 faults β which is itself the proof that the driver cannot # even *compute* the qcb without touching wild memory.)
Expected
- mode 1:
ida_v4_done_fixed() = 0β empty FIFO detected, no side-effect register write, no fault. - mode 2/3:
Fatal trap 12: page fault while in kernel mode,fault code = supervisor read data, page not present, faulting instruction = thehwqcb->qcbload (movq 0x110(%rax,%rdx,1),%r13), fault VA ==hwqcbs + (0xFFFFFFFC - hwqcb_busaddr) + 0x110.
Fix validation
fix.diff(git-apply-able againstsys/) masks the sentinel compare:if (completed == (bus_addr_t)0xffffffffU).- Patched real driver rebuilt clean (
ida.ko,-Werror) βfix_build.log. - Fixed variant re-run on guest: sentinel works, no fault β
fix_run.log.
Hardware gate (honest limits)
The guest has no Compaq SMART controller (pciconf -l: no 0e11 devices),
so the device path cannot run end-to-end in QEMU. The mock-register
harness executes the driver's own kernel-side code path verbatim; the
types/arithmetic/deref are identical to a real board returning
0xFFFFFFFF on an empty reply FIFO.
DF-5061 β VERDICT
Status: reproduced (kernel page fault from the shipping driver's own code path) Impact: panic (deterministic wild kernel-pointer dereference; DoS) Confidence: certain (types are statically provable; deref observed on guest)
Root cause (line-accurate)
sys/dev/raid/ida/ida_pci.c:106-108:c completed = ida_inl(ida, R_42XX_REPLY); if (completed == -1) return (0); /* fifo is empty */completedisbus_addr_t=uint64_ton x86_64 (sys/cpu/x86_64/include/bus_dma.h:36).ida_inlisbus_space_read_4(sys/dev/raid/ida/idavar.h:40-41), which returnsu_int32_t(sys/cpu/x86_64/include/bus_dma.h:130, body at :154-162:return (*(volatile u_int32_t *)(handle + offset));).-1promotes to0xFFFFFFFFFFFFFFFF; the register value0xFFFFFFFFzero-extends to0x00000000FFFFFFFF. The compare is false for every possible register value β the empty-FIFO sentinel is dead code on all 64-bit DragonFly (it worked on 32-bit FreeBSD wherebus_addr_t == uint32_t; the code is unchanged since FreeBSD 2001).
Consequence chain (all verified on the guest)
ida_v4_done()returns0xFFFFFFFFfor an empty reply FIFO and performs theida_outl(R_42XX_REPLY, 0)"confirm read" write on the empty FIFO (harness log:R_42XX_REPLY now 0x00000000).ida_wait()(sys/dev/raid/ida/ida.c:457):while ((completed = ida->cmd.done(ida)) == 0)β exits instantly with the bogus completion. This is the polling path taken by everyida_command()during attach (interrupts are disabled:ida_attach()βcmd.int_enable(ida, 0)at ida.c:242), i.e. the very firstCMD_GET_CTRL_INFOat boot.ida_wait()then callsidahwqcbptov(ida, completed & ~3)(ida.c:463);ida_intr()does the same at ida.c:483 β but note the interrupt variant (while (... != 0)) can never terminate once the FIFO drains: every empty read yields0xFFFFFFFFagain β infiniteignoring completion ffffffffloop (ISR hang) whenever the wild deref does not fault first.idahwqcbptov()(ida.c:140-148) computeshwqcb = hwqcbs + (0xFFFFFFFC - hwqcb_busaddr)and dereferenceshwqcb->qcb(offset 0x110 instruct ida_hardware_qcb, idavar.h:76-81). WithhwqcbsDMAP-mapped andhwqcb_busaddrits 32-bit physical address, the wild address is deterministic: DMAP + 0xFFFFFFFC (physical 4 GB β 4) β unmapped on every host with < 4 GB RAM.
Reproduction evidence
- Baseline run 1 (
panic.firstboot.txt, serial console): harness mode 2 β printssentinel DEAD,ida_v4_done() = 0x00000000ffffffff (empty FIFO reported as a completion!), the empty-FIFO confirm-write side effect, thenpanicwith the harness frame in the trace (trap 0xc). - Baseline run 2 (
panic.txt, serial console, fresh boot): fullFatal trap 12record:fault virtual address = 0xfffff801625a310c fault code = supervisor read data, page not present Stopped at df5061_fire_sysctl+0xb5: movq 0x110(%rax,%rdx,1),%r13Arithmetic check:0xfffff801625a310c == hwqcbs(0xfffff80118498000) + (0xFFFFFFFC β hwqcb_busaddr(0xb5ef5000)) + 0x110β exact match, the faulting instruction is thehwqcb->qcbload inside the verbatimidahwqcbptov(). - Fixed run (
run.log,fix_run.log):(uint32_t)completed == 0xffffffffvariant returns 0, leaves the register untouched, no fault; module unloads cleanly.
Exploitability (honest ceiling)
Wild-address read feeding a pointer compare; no write primitive, no
copyout of the garbage. On < 4 GB hosts: page fault panic (boot-time on
real boards β the driver is in GENERIC: device ida,
X86_64_GENERIC:132). On β₯ 4 GB hosts: garbage qcb pointer β
qcb->state read (mapped) β != QCB_ACTIVE β ida_intr spins forever
(ISR-level hang + console flood). Both are kernel DoS. Gated on Compaq
SMART-2/4x00-class PCI hardware being present β severity Low per rubric
(specific hardware), but the code defect itself is unconditional on
64-bit.
Why not false-positive / already-fixed
- Types verified in-tree and on the guest (
bus_addr_tuint64 vsbus_space_read_4u_int32_t). - Consumers verified at ida.c:457-467, 482-491;
ida_v4_doneis wired to every v4 board viaida_v4_access(ida_pci.c:138-144) andboard_id[](ida_pci.c:158-167). - No intermediate masking anywhere between register read and compare.
Fix
fix.diff β mask the compare to the register width:
if (completed == (bus_addr_t)0xffffffffU).
Validated: patched ida_pci.c rebuilt clean as ida.ko with -Werror
(fix_build.log); fixed logic re-run on the guest with correct behavior
(fix_run.log). git apply --check passes against the pristine tree.
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 applied inside the guest's /usr/src; the real ida driver (ida.c+ida_pci.c+ida_disk.c) rebuilt clean as ida.ko with -Werror (fix_build.log). The fixed logic variant ((uint32_t)completed == 0xffffffff) re-run on the guest returns 0 for the empty FIFO, performs no 'confirm read' write on it, and faults nothing (run.log, fix_run.log) - the previously-observed Fatal trap 12 does not occur. git apply --check passes against the pristine sys/ tree.
fix.diff; fix_build.log (ida.ko link line, -Werror clean); fix_run.log (fixed variant: 'ida_v4_done_fixed() = 0x0000000000000000 ... R_5061 untouched: 0xffffffff ... DF-5061: unloaded'); panic.txt vs panic.firstboot.txt (baseline traps absent under the fix).
Confirmed kernel references
- sys/dev/raid/ida/ida_pci.c:101
- sys/dev/raid/ida/ida_pci.c:107
- sys/dev/raid/ida/ida_pci.c:109
- sys/cpu/x86_64/include/bus_dma.h:36
- sys/cpu/x86_64/include/bus_dma.h:130
- sys/dev/raid/ida/idavar.h:40
- sys/dev/raid/ida/ida.c:457
- sys/dev/raid/ida/ida.c:463
- sys/dev/raid/ida/ida.c:482
- sys/dev/raid/ida/ida.c:140
- sys/dev/raid/ida/ida.c:146
Detail
Exploit chain
Compaq 42xx-class board present (PCI 0x0e11:0xae10/0x4040x0E11 subclass) -> GENERIC kernel ida_pci_attach at boot -> ida_attach() disables interrupts -> first ida_command(CMD_GET_CTRL_INFO) -> ida_wait() polls ida_v4_done() on an empty reply FIFO -> dead sentinel returns 0xFFFFFFFF as a completion (+ bogus 'confirm read' write to R_42XX_REPLY on the empty FIFO) -> idahwqcbptov(0xFFFFFFFC) -> deref at DMAP+0xFFFFFFFC -> page fault panic (<4GB RAM) or mapped-garbage qcb -> qcb->state != QCB_ACTIVE -> ida_intr spins forever (>=4GB RAM). No write primitive, no data returned to userspace: DoS ceiling.
Evidence (decisive lines)
panic.txt: 'Fatal trap 12 ... fault virtual address = 0xfffff801625a310c ... supervisor read data, page not present ... Stopped at df5061_fire_sysctl+0xb5: movq 0x110(%rax,%rdx,1),%r13' with 0xfffff801625a310c == hwqcbs(0xfffff80118498000) + (0xFFFFFFFC - hwqcb_busaddr(0xb5ef5000)) + 0x110 (the verbatim idahwqcbptov hwqcb->qcb load). panic.firstboot.txt: sentinel-DEAD prints + 'ida_v4_done() = 0x00000000ffffffff (empty FIFO reported as a completion!)' + empty-FIFO confirm-write side effect + panic. run.log/fix_run.log: fixed variant returns 0, register untouched, no fault. fix_build.log: patched ida.ko builds clean (-Werror). build.log: harness KLD build. env.txt/hwgate.txt: guest uname, 3.96GB RAM, no Compaq PCI device (mocked register file runs the driver's own kernel-side code verbatim).
PoC changes
No seed PoC existed (pass-2 new finding). Authored harness KLD embedding VERBATIM copies of ida_v4_done() (ida_pci.c:101-111) and idahwqcbptov() (ida.c:140-148) against a mock 32-bit MMIO register file (R_42XX_REPLY=0xFFFFFFFF, the documented empty-FIFO value). kmod.mk quirks handled: interface headers via SRCS+=device_if.h bus_if.h; -I must ride COPTS because kmod.mk:98 overwrites CFLAGS. A survivable 'probe' mode was added but even it faults - idahwqcbptov() must dereference hwqcb->qcb to return at all, which is itself proof the driver cannot compute the qcb without touching wild memory.
Verified recommended fix
Mask the empty-FIFO sentinel to the 32-bit register width: if (completed == (bus_addr_t)0xffffffffU) return (0);
Verdict
ida_v4_done()'s empty-reply-FIFO sentinel 'completed == -1' (ida_pci.c:107) is dead code on LP64: completed is bus_addr_t (uint64_t, bus_dma.h:36) assigned from bus_space_read_4() (u_int32_t, bus_dma.h:130), so the 42xx empty-FIFO value 0xFFFFFFFF zero-extends and never matches (bus_addr_t)-1. ida_wait()'s poll loop (ida.c:457, the path every ida_command() takes during attach with interrupts disabled) and ida_intr()'s drain loop (ida.c:482) then consume the empty FIFO as completion 0xFFFFFFFF; idahwqcbptov() (ida.c:140-148) computes hwqcbs + (0xFFFFFFFC - hwqcb_busaddr) == DMAP+0xFFFFFFFC and dereferences hwqcb->qcb. Verified on the guest with verbatim driver code and a mock register file returning 0xFFFFFFFF: Fatal trap 12, 'supervisor read data, page not present', faulting instruction movq 0x110(%rax,%rdx,1),%r13 (the hwqcb->qcb load), fault VA == hwqcbs + (0xFFFFFFFC - busaddr) + 0x110 exactly. Driver ships in GENERIC (X86_64_GENERIC:132) so v4 boards panic during boot attach; >=4GB hosts instead read mapped garbage and spin forever in the ISR. Read primitive only (no write/copyout) and hardware-gated (Compaq SMART-2/4x00 PCI IDs) => kernel DoS, Low severity. Fixed variant (masked compare) returns 0, performs no empty-FIFO confirm-write, no fault; patched driver rebuilds clean with -Werror.
No comments yet.