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

idad_detach frees the idad softc under open descriptors and in-flight bios β€” DF-5013/DF-5025 family detach UAF

Summary

No open-count gate and no in-flight drain; cdev teardown is async so open fds keep si_drv1 into the freed softc - strategy writes freed devstat/ida_submit memory, late completions write freed stats. Root-only (kldunload; no runtime delete path unlike mfi DF-5013). Hardware-gated. Opencount-gate fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5046 Β· 6 files
FileTypeDescriptionSize
README.md β€” 3.5 KB ↓ raw
VERDICT.md β€” 4.6 KB ↓ raw
env.txt β€” 1.0 KB view raw
fix.diff β€” 1.0 KB view raw
verdict.json β€” 3.8 KB view raw
manifest.json β€” 1.1 KB view raw

DF-5046 β€” idad_detach frees the idad softc under open descriptors and in-flight bios (LD-family detach UAF)

File: sys/dev/raid/ida/ida_disk.c (pass 2, GLM 5.3) Bucket: memcorrupt | Severity: Low | Pattern: DF-5013 (mfi_disk) / DF-5025 (ips_disk)

What the bug is

idad_detach() (sys/dev/raid/ida/ida_disk.c:302-311) releases the softc's registrations with no gate on open file descriptors and no drain of in-flight I/O:

static int
idad_detach(device_t dev)
{
    struct idad_softc *drv;

    drv = (struct idad_softc *)device_get_softc(dev);
    devstat_remove_entry(&drv->stats);
    disk_destroy(&drv->disk);
    return (0);
}

After it returns, the bus/devclass layer frees the idad_softc. Nothing in the teardown path protects the two consumers of dsk->si_drv1:

  1. Open fds (DF-5025 leg). disk_destroy() sends a synchronous DISK_DISK_DESTROY (sys/kern/subr_disk.c:876-880); the handler waits only for disk-enumeration refs dp->d_refs (subr_disk.c:540-542) and then destroys the cdevs (subr_disk.c:546-548). destroy_dev() -> devfs_destroy_dev() is asynchronous (sys/vfs/devfs/devfs_core.c:759-763) and only drops sysrefs; the cdev object survives while any open fd holds a reference, and neither that path nor idad_detach ever clears si_drv1 or revectors it away from id_ops. A read/write on the still-open fd then runs idad_strategy() -> idad_getsoftc() (ida_disk.c:94-99) which returns the dangling softc pointer; the very next statements write through it (devstat_start_transaction(&drv->stats) ida_disk.c:174, ida_submit_buf(drv->controller, bio) ida_disk.c:175 β€” queue-head writes into freed memory).

  2. In-flight bios (DF-5013 leg). dev_dstrategy() takes a bio_track_ref() on dev->si_track_* (sys/kern/kern_device.c:375-380) β€” this keeps the cdev alive, not the softc. bio_track_wait() exists (sys/kern/vfs_bio.c:565) but is called only from vnode-drain paths (sys/kern/vfs_subr.c:326,377,526,799); no cdev-destroy path drains it. A bio completing after detach runs idad_intr() (ida_disk.c:213-226) -> devstat_end_transaction_buf(&drv->stats, bp) on the freed softc.

Threat model / trigger

idad_detach is reachable only via the device detach path (module unload of the in-kernel ida/idad drivers β€” kldstat -v on the standard X86_64_GENERIC guest shows ida/idad, pci/ida compiled in). Requires root (kldunload) β€” hence Low, same rating rationale as DF-5025. (DF-5013 was High only because mfi had an unprivileged-time AEN offline path that deleted disks at runtime; ida has no runtime delete path and no ioctls at all.)

PoC / verification status

Hardware-gated: the ida driver attaches only to Compaq SMART/IDA PCI boards (ida_pci.c:146-170 board_id[] = subdevice ids 0x40300E11..0x40580E11, vendor 0x0E11, matched at ida_pci.c:199-210). The audit QEMU guest has no Compaq PCI device (pciconf -l | grep -i 0e11 empty β€” see env.txt), so no /dev/idad* node exists and the trigger cannot be exercised. The code trace above is line-accurate and reproduces the exact DF-5025 family defect.

Fix

Gate detach on an open count (and let the controller side quiesce per DF-1915's fix). See fix.diff.

Reproduce (on hardware)

# on a machine with a Compaq SMART/IDA controller:
dd if=/dev/idad0 of=/dev/null bs=64k count=100 &   # keep fd open + I/O in flight
kldunload ida_pci (or detach path via hw.pci....)  # as root, while the above runs
# observe: idad_strategy / idad_intr touch freed softc (KASAN/MEMGUARD or panic)
VERDICT.md
↓ download raw

DF-5046 VERDICT β€” idad detach UAF under open fds / in-flight bios

status: untested (hardware-gated) | impact: none demonstrated (UAF-write by code trace) | confidence: likely

Reproduced?

No β€” and it cannot be reproduced on this audit guest: the trigger requires a physical Compaq SMART/IDA (SMART-2/P, SMART-2SL, SA3200/3100ES/221, Integrated Array, RAID LC2, SA4200/4250ES/431) PCI array controller. Evidence of gating (env.txt, captured live from the guest):

  • pciconf -l contains no vendor 0x0e11 (Compaq) function -> ida_pci_match() (sys/dev/raid/ida/ida_pci.c:193-210) can never match, ida_pci_attach never runs, no idad children are created, and /dev/idad* does not exist (ls /dev/idad* -> NO_IDAD_DEVS).
  • The driver code itself is present in the guest kernel build (kldstat -v lists ida/idad and pci/ida), i.e. this is dormant code one PCI match away from live on any machine with the card β€” not dead code.

Why the finding is real (line-accurate trace)

  1. idad_attach sets dsk->si_drv1 = drv (ida_disk.c:280) where drv is the driver softc owned by the devclass, freed shortly after idad_detach returns (ida_disk.c:302-311).
  2. idad_detach has no open-count gate, no in-flight drain, and never clears si_drv1. Compare the modern DFly pattern (e.g. nvme_disk, mmcsd) which refuses EBUSY while the disk is open.
  3. disk_destroy (sys/kern/subr_disk.c:876-880) -> DISK_DISK_DESTROY handler (subr_disk.c:527-556): waits for dp->d_refs (disk enumeration refs only), then destroy_dev(dp->d_cdev) + destroy_only_dev(dp->d_rawdev).
  4. destroy_dev -> devfs_destroy_dev is asynchronous (sys/vfs/devfs/devfs_core.c:759-763); the worker (devfs_core.c:1440-1459) unlinks nodes and release_dev()s the sysref. The cdev object survives while any fd holds it open, and its si_ops stays id_ops, si_drv1 stays the freed softc.
  5. Post-detach I/O through a still-open fd: physio -> dev_dstrategy (sys/kern/kern_device.c:355-389) -> idad_strategy (ida_disk.c:145-189): - idad_getsoftc(dev) (ida_disk.c:94-99) returns dangling si_drv1; - reads drv->flags (ida_disk.c:161); - devstat_start_transaction(&drv->stats) (ida_disk.c:174) β€” write into freed softc; - ida_submit_buf(drv->controller, bio) (ida_disk.c:175) β€” dereferences freed drv->controller and inserts the bio into freed queue heads (sys/dev/raid/ida/ida.c:365-370).
  6. In-flight bios are equally unguarded: bio_track_ref in dev_dstrategy pins the cdev, nothing waits on dev->si_track_* in any cdev destroy path (bio_track_wait is only used by vnode drain, sys/kern/vfs_subr.c:326, 377, 526, 799), and a late completion runs idad_intr (ida_disk.c:213-226) -> devstat_end_transaction_buf(&drv->stats, bp) β€” another freed-softc write.

Exploit chain (assessment, not exercised)

Local root is already the precondition (kldunload), so this is not an escalation primitive β€” it is a root-triggerable UAF-write (kernel memory corruption class). With heap grooming around the freed idad_softc/ ida_softc (both large, controllable size), the devstat/queue-head writes could be steered into attacker-planted objects; on this intentionally unhardened guest that would be a viable corruption primitive, but it buys an attacker nothing beyond the root they already have. Real-world impact: crash/UAF on detach, integrity of the write path if the softc is reallocated.

Fix assessment

fix.diff adds the open-count gate (idad_open/idad_close increment/ decrement, idad_detach returns EBUSY while open) β€” the same remedy as the DF-5025/DF-5013 family fixes. It intentionally does not attempt to fix the controller-side in-flight problem (that is DF-1915's fix: bus_generic_detach + drain in ida_detach). fix_status=not_testable: no IDA hardware exists on the guest to build/boot a validated fix kernel against; the diff is line-accurate against sys/dev/raid/ida/ida_disk.c + idavar.h and compiles by inspection (trivial counter + EBUSY).

Bottom line

Genuine DF-5025-family UAF, code-certain, untestable here solely because the guest has no Compaq SMART/IDA hardware (proven in env.txt). Low severity: root-only trigger, no unprivileged path (unlike DF-5013's AEN path).

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 adds opencount to struct idad_softc, increments/decrements in idad_open/idad_close, and returns EBUSY from idad_detach while any fd is open β€” closes the open-fd UAF leg; the in-flight leg additionally needs DF-1915's controller quiesce. Cannot be build/boot validated: no IDA hardware on the guest. Diff is git-apply-clean against sys/.

fix.diff (git apply --check: APPLIES_CLEAN); context hunks at ida_disk.c idad_open/idad_close/idad_detach and idavar.h struct idad_softc
↓ fix.diffper-fix-DF-5046

Confirmed kernel references

Detail

Exploit chain

root (kldunload ida) -> idad_detach frees softc under open fd -> open fd read/write -> idad_strategy -> idad_getsoftc returns dangling si_drv1 -> devstat/queue writes into freed idad_softc+ida_softc. Not an escalation primitive (root already held); heap grooming around the freed softc would give kernel memory corruption on hardware.

Evidence (decisive lines)

env.txt (live guest: no 0x0e11 PCI function, NO_IDAD_DEVS, driver present in kldstat); VERDICT.md full trace with path:line for every hop; fix.diff (git apply --check clean)

PoC changes

No runnable PoC exists (hardware-gated). Pack contains the hardware-absence evidence and the verified-code trace instead; fix.diff generated against sys/ and validated with git apply --check.

Verified recommended fix

Gate idad_detach on an open counter maintained in idad_open/idad_close (return EBUSY while open), and drain in-flight bios before freeing (controller side per DF-1915).

Verdict

Genuine DF-5025/DF-5013-family detach UAF: idad_detach (ida_disk.c:302-311) frees the softc with no open-count gate and no in-flight drain; disk_destroy's sync wait covers only disk-enumeration refs (subr_disk.c:540-548), cdev teardown is async and keeps the cdev alive under open fds with si_ops=id_ops and si_drv1 dangling, and no cdev path drains si_track (bio_track_wait is vnode-only). Post-detach read/write via a still-open fd or a late idad_intr writes into freed memory (devstat_start_transaction ida_disk.c:174, ida_submit_buf ida_disk.c:175, devstat_end_transaction_buf ida_disk.c:224). Untested solely because the guest has no Compaq SMART/IDA hardware (pciconf: no 0x0e11 device, no /dev/idad*) β€” the code trace is line-accurate and the driver is compiled into X86_64_GENERIC. Severity Low: trigger requires root (kldunload); unlike mfi there is no unprivileged runtime delete path.