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

iir_shutdown: shared flush ucmd freed (wrong malloc type) while still queued/in flight, and re-queued for the next drive while a gccb owns it β€” use-after-free at shutdown

Summary

iir_shutdown allocates ONE gdt_ucmd_t with M_GDTBUF and reuses it for every present host drive: queue-insert, dispatch (queue-removes it while the owning gccb keeps gc_ucmd pointing at it), then tsleep at most 10s. If a flush does not complete in time, the next iteration overwrites and re-queues the same still-in-flight ucmd (double dispatch), and the unconditional kfree at the end - with the wrong type - frees it while the pending gccb still references it. The later completion writes status/info, bcopies up to 3072 bytes into the freed ucmd->data, TAILQ-links the freed object, and wakeups it. A controller stalling >=10s at shutdown turns this into ~3KB-class write into freed heap. One-ucmd-per-drive + free-only-completed fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4173 Β· 7 files
FileTypeDescriptionSize
README.md β€” 1.2 KB ↓ raw
VERDICT.md β€” 3.0 KB ↓ raw
gate_check.sh β€” 779 B view raw
gate_check.log β€” 577 B view raw
fix.diff β€” 1.3 KB view raw
manifest.json β€” 963 B view raw
verdict.json β€” 2.6 KB view raw

DF-4173 β€” iir(4) iir_shutdown: flush ucmd freed while in flight (+ wrong malloc type, double-queue reuse)

Build / Run

There is no userland trigger: the flaw is in the shutdown_post_sync event handler (iir_shutdown, sys/dev/raid/iir/iir.c:1478-1520) and manifests when the controller is slow/hung at shutdown.

To document the hardware gate on the audit guest, run:

sh gate_check.sh          # no /dev/iir, no iir PCI device, iir never attached

Expected on hardware-less guest: no iir/isp PCI device present.

Expected on a machine with iir hardware and a controller that fails to complete a GDT_FLUSH within 10 s: the shared ucmd is (a) re-inserted into sc_ucmd_queue for the next host drive while the previous dispatch still owns it, and (b) unconditionally kfreed (with the wrong type M_DEVBUF) while a pending gccb still points at it β†’ the next GDT_GCF_IOCTL completion (iir.c:1599-1633) writes ucmd->status, bcopys up to 3 KB into the freed ucmd->data, and calls wakeup(ucmd) β†’ use-after-free write into freed kernel heap.

Fix

fix.diff allocates one ucmd per host drive, frees only completed ones, and frees with the correct type (M_GDTBUF).

VERDICT.md
↓ download raw

DF-4173 VERDICT β€” iir_shutdown ucmd use-after-free / double-queue / wrong free type

Status: untested on this guest (hardware-gated: shutdown path of the iir driver, no controller present). Bug existence: certain from code trace.

Reproduced?

No runtime reproduction: the guest has no iir controller (gate_check.log), and the buggy path is the system-shutdown handler, driven by controller completion latency. The gate is proven; the code trace below is line-accurate.

Code trace

  1. iir_shutdown (iir.c:1478-1520) allocates one gdt_ucmd_t with kmalloc(..., M_GDTBUF, ...) (iir.c:1493).
  2. For each present host drive it fills that single ucmd, queues it (TAILQ_INSERT_TAIL(&gdt->sc_ucmd_queue, ucmd, links), iir.c:1509), dispatches via gdt_next(), and waits at most 10 s (tsleep(..., 10*hz), iir.c:1514).
  3. gdt_next() removes the ucmd from the queue when dispatching it (iir.c:706) and the owning gccb keeps gccb->gc_ucmd = ucmd (iir.c:1006). Until the controller completes the command the ucmd is owned by that in-flight gccb.
  4. Defect A (double-queue): if the 10 s sleep expires with complete_flag == FALSE, the next loop iteration overwrites the same ucmd's fields while the controller still owns it, and inserts it into sc_ucmd_queue again β€” one ucmd now referenced by an in-flight gccb and queued for a second gccb.
  5. Defect B (UAF): after the loop, kfree(ucmd, M_DEVBUF) (iir.c:1518) frees the ucmd unconditionally β€” including while it is still in flight and/or still linked into sc_ucmd_queue. The later GDT_GCF_IOCTL completion path (iir.c:1599-1633) then does: - ucmd->status = gdt->sc_status; ucmd->info = …; ucmd->complete_flag = TRUE; (iir.c:1608-1610) β€” writes into freed memory; - bcopy(gccb->gc_scratch, ucmd->data, cnt) with cnt up to GDT_SCRATCH_SZ = 3072 (iir.c:1613-1626) β€” a 3 KB-class write into a freed ~3.1 KB heap chunk (heap corruption if reallocated); - TAILQ_INSERT_HEAD(&gdt->sc_ucmd_queue, ucmd, links) on the BSY path (iir.c:1604) β€” links freed memory into a live list; - wakeup(ucmd) (iir.c:1632).
  6. Defect C (type mismatch): allocated with M_GDTBUF, freed with M_DEVBUF (iir.c:1493 vs 1518) β€” benign without MALLOC_DEBUG, but wrong.

Impact ceiling

Shutdown-time UAF write / list corruption, requiring an iir controller that stalls β‰₯10 s on a flush. Not software-triggerable. Severity Low; bucket: memcorrupt.

Fix validation

fix.diff (one ucmd per drive, free-only-when-complete, correct type) passes git apply --check. No kernel-build/runtime validation possible without hardware; fix_status = not_testable.

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.diff restructures the flush loop (per-drive ucmd + correct free discipline) and passes git apply --check; runtime validation requires iir hardware

findings/poc/DF-4173/fix.diff (git apply --check clean)
↓ fix.diffper-fix-DF-4173

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (full path:line trace); gate_check.log (hardware absent); fix.diff (per-drive ucmd, free-only-complete, correct type, git apply --check clean)

PoC changes

no PoC source written: the defect is in the system-shutdown handler and requires a controller that stalls >=10s on GDT_FLUSH; only a hardware gate check was run

Verified recommended fix

Allocate one ucmd per host drive, free only completed ones with M_GDTBUF, never free/requeue an in-flight ucmd (see fix.diff)

Verdict

Bug certain from code trace, not runtime-testable: iir_shutdown (shutdown_post_sync handler) allocates one gdt_ucmd_t (iir.c:1493), reuses it across all host drives - re-queueing it for drive N+1 while drive N's dispatch still owns it if the 10s tsleep expires (iir.c:1514) - and unconditionally kfrees it with the wrong type (M_DEVBUF vs M_GDTBUF, iir.c:1518) while an in-flight gccb may still hold gc_ucmd pointing at it. The later GDT_GCF_IOCTL completion then writes ucmd->status/info/complete_flag (iir.c:1608-1610), bcopies up to 3KB into the freed ucmd->data (iir.c:1613-1626), TAILQ-links the freed object on the BSY path (iir.c:1604) and wakeups it (iir.c:1632): use-after-free heap write at shutdown. Guest has no iir controller (gate_check.log) so the driver never attaches.