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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5061 Β· 17 files
FileTypeDescriptionSize
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 every ida_command() during attach while interrupts are disabled) exits its poll loop instantly with completed = 0xFFFFFFFF;
  • ida_intr() (ida.c:482-491) enters its drain loop for the same value and would spin forever printing ignoring 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 = the hwqcb->qcb load (movq 0x110(%rax,%rdx,1),%r13), fault VA == hwqcbs + (0xFFFFFFFC - hwqcb_busaddr) + 0x110.

Fix validation

  • fix.diff (git-apply-able against sys/) 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.

VERDICT.md
↓ download raw

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 */
  • completed is bus_addr_t = uint64_t on x86_64 (sys/cpu/x86_64/include/bus_dma.h:36).
  • ida_inl is bus_space_read_4 (sys/dev/raid/ida/idavar.h:40-41), which returns u_int32_t (sys/cpu/x86_64/include/bus_dma.h:130, body at :154-162: return (*(volatile u_int32_t *)(handle + offset));).
  • -1 promotes to 0xFFFFFFFFFFFFFFFF; the register value 0xFFFFFFFF zero-extends to 0x00000000FFFFFFFF. 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 where bus_addr_t == uint32_t; the code is unchanged since FreeBSD 2001).

Consequence chain (all verified on the guest)

  1. ida_v4_done() returns 0xFFFFFFFF for an empty reply FIFO and performs the ida_outl(R_42XX_REPLY, 0) "confirm read" write on the empty FIFO (harness log: R_42XX_REPLY now 0x00000000).
  2. 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 every ida_command() during attach (interrupts are disabled: ida_attach() β†’ cmd.int_enable(ida, 0) at ida.c:242), i.e. the very first CMD_GET_CTRL_INFO at boot.
  3. ida_wait() then calls idahwqcbptov(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 yields 0xFFFFFFFF again β†’ infinite ignoring completion ffffffff loop (ISR hang) whenever the wild deref does not fault first.
  4. idahwqcbptov() (ida.c:140-148) computes hwqcb = hwqcbs + (0xFFFFFFFC - hwqcb_busaddr) and dereferences hwqcb->qcb (offset 0x110 in struct ida_hardware_qcb, idavar.h:76-81). With hwqcbs DMAP-mapped and hwqcb_busaddr its 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 β€” prints sentinel DEAD, ida_v4_done() = 0x00000000ffffffff (empty FIFO reported as a completion!), the empty-FIFO confirm-write side effect, then panic with the harness frame in the trace (trap 0xc).
  • Baseline run 2 (panic.txt, serial console, fresh boot): full Fatal trap 12 record: fault virtual address = 0xfffff801625a310c fault code = supervisor read data, page not present Stopped at df5061_fire_sysctl+0xb5: movq 0x110(%rax,%rdx,1),%r13 Arithmetic check: 0xfffff801625a310c == hwqcbs(0xfffff80118498000) + (0xFFFFFFFC βˆ’ hwqcb_busaddr(0xb5ef5000)) + 0x110 β€” exact match, the faulting instruction is the hwqcb->qcb load inside the verbatim idahwqcbptov().
  • Fixed run (run.log, fix_run.log): (uint32_t)completed == 0xffffffff variant 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_t uint64 vs bus_space_read_4 u_int32_t).
  • Consumers verified at ida.c:457-467, 482-491; ida_v4_done is wired to every v4 board via ida_v4_access (ida_pci.c:138-144) and board_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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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).
↓ fix.diffper-fix-DF-5061

Confirmed kernel references

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.