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

mfi syspd lifetime UAF: AEN PD removal (MR_EVT_PD_REMOVED) deletes the syspd locklessly with no gate and mfi_syspd_detach frees the softc under open descriptors and bios parked on the controller bioq (DF-5013's syspd twin)

Summary

The detach gate refuses EBUSY only when OPEN && (keep_deleted_volumes||detaching) - both default 0, so a busy-open syspd is destroyed BY DESIGN: held fds' si_drv1 dangles (strategy/completion write freed devstat), parked bios carry bio_driver_info=sc into mfi_bio_command/mfi_build_syspdio reads, and the AEN walk itself iterates the TAILQ with NO locks/gate/disable/drain (detect_jbod_change=1 default). Hardware-event trigger, no privileged ioctl needed (same posture as DF-5013). HW-gated on guest; slab-groom->forge->uid0 chain design in pack. Open-EBUSY + DISABLED + drain + locked AEN fix compile-validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5049 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 7.0 KB ↓ raw
syspd_hold.c β€” 2.5 KB view raw
build.sh β€” 87 B view raw
run.sh β€” 108 B view raw
build.log β€” 87 B view raw
run.log β€” 96 B view raw
env.txt β€” 394 B view raw
fix.diff β€” 4.6 KB view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 4.3 KB view raw

DF-5049 β€” mfi syspd lifetime UAF: AEN PD removal frees the softc under open descriptors and parked bios

What this is

sys/dev/raid/mfi/mfi_syspd.c (raw system-PD / JBOD half of mfi(4)) lets the struct mfi_system_pd softc be freed while (a) userspace still holds an open descriptor on /dev/mfisyspdN and (b) bios referencing the softc via bio->bio_driver_info are still parked on the controller bioq. The AEN MR_EVT_PD_REMOVED trigger additionally walks the syspd list with no locks at all. This is the syspd twin of DF-5013 (mfi_disk.c LD half) β€” same family pattern, different file, different delete paths, different sinks.

Reproduce

./build.sh   # cc -O -o syspd_hold syspd_hold.c   (on the DF guest)
./run.sh     # ./syspd_hold /dev/mfisyspd0

On real hardware (LSI MegaRAID with a SYSTEM PD exposed): open succeeds, four readers issue pread(2); pull the drive (or let firmware raise MR_EVT_PD_REMOVED) β†’ mfi.c:1513-1532 deletes the syspd child β†’ softc freed (subr_bus.c:1944 via device_set_driver(dev, NULL)) β†’ the held fd keeps its si_drv1 dangling β†’ every subsequent pread(2)/close(2) writes into the freed slab (devstat_start_transaction(&sc->pd_devstat), completion devstat_end_transaction_buf(&sc->ld_devstat)), and parked bios are later dereferenced by mfi_bio_command (mfid->ld_flags) and mfi_build_syspdio (disk->pd_id).

On THIS QEMU guest (no MegaRAID PCI device β€” proven in run.log):

open(/dev/mfisyspd0) failed: No such file or directory  <- hardware-gated guest

The finding is source-proven end-to-end (VERDICT.md); the guest run documents the hardware gate, exactly as for DF-5013.

Fix

fix.diff β€” 2 parts: lock+mfi_syspd_disable() gate in the AEN MR_EVT_PD_REMOVED handler (mfi.c), and open-gate + DISABLED + bioq drain in mfi_syspd_detach with a DISABLED re-check in mfi_syspd_strategy (mfi_syspd.c). Compile-validated against stock X86_64_GENERIC on the guest (make nativekernel clean, see VERDICT.md).

VERDICT.md
↓ download raw

VERDICT β€” DF-5049

Status: not_reproduced (hardware-gated β€” no MegaRAID controller on the audit guest). Source-proven end-to-end. Fix compile-validated. Confidence in the bug itself: likely (same certainty class as DF-5013: every code path below is read at path:line; nothing is hypothetical except the physical trigger).

1. The claim

An mfi_system_pd softc is freed while three classes of reference survive:

  1. an open userspace descriptor whose si_drv1 points at the softc;
  2. bios parked on controller->mfi_bioq whose bio_driver_info points at the softc;
  3. concurrent walkers of mfi_syspd_tqh (the AEN removal walk itself is lockless, so it can chase a pd_link that another CPU is freeing).

2. Trigger paths (all reach mfi_syspd_detach with the softc referenced)

  • AEN MR_EVT_PD_REMOVED β€” mfi_decode_evt mfi.c:1513-1532, runs in the SWI event task (mfi_queue_evt β†’ taskqueue_swi, mfi.c:1554-1559). Walks sc->mfi_syspd_tqh and calls device_delete_child(sc->mfi_dev, syspd->pd_dev) with no mfi_config_lock, no mfi_io_lock, no mfi_syspd_disable() call, no drain. Live by default: mfi_detect_jbod_change = 1 (mfi.c:149-150; verified hw.mfi.detect_jbod_change: 1 on the stock guest). Trigger = drive removal / firmware event β€” no privileged ioctl needed (identical posture to DF-5013's LD-offline AEN).
  • mfi_syspdprobe delete β€” mfi.c:1344-1357 (AEN host-bus-scan / PD_INSERTED, mfi.c:1473-1485, 1534-1543). Holds both locks, but has no disable gate and no drain either.
  • ioctl path β€” MFI_DCMD_PD_STATE_SET β†’ mfi_syspd_disable() (mfi.c:2594-2607) only marks DISABLED; the actual child delete happens at the next syspdprobe. Root-only; listed for completeness.

3. The free

device_delete_child (kern/subr_bus.c:1284-1310) β†’ device_detach (subr_bus.c:2129-2154) β†’ mfi_syspd_detach (mfi_syspd.c:155-177) β†’ back in device_detach: device_set_driver(dev, NULL) β†’ kfree(dev->softc, M_BUS) (subr_bus.c:1943-1946) β€” the struct mfi_system_pd is freed synchronously inside the same device_delete_child call. mfi_syspd_detach also runs kfree(sc->pd_info, M_MFIBUF) (mfi_syspd.c:175).

4. The surviving references (the UAF sinks)

  • Open descriptor. The cdev is created UID_ROOT/GID_OPERATOR 0640 (subr_disk.c:740-742 via disk_create, mfi_syspd.c:133) β€” an operator-group user can hold it open. mfi_syspd_detach's only guard (mfi_syspd.c:162-167) refuses EBUSY only when OPEN && (mfi_keep_deleted_volumes || mfi_detaching) β€” both sysctls default 0 (mfi.c:731-735), so on the default config an open syspd is destroyed by design. disk_destroy() (subr_disk.c:876-880) only sends DISK_DISK_DESTROY to the dsched/disk machinery; it does not close held fds and nothing clears si_drv1. The next read(2)/write(2) on the held fd routes to mfi_syspd_strategy (mfi_syspd.c:233-285) which:
  • derefs sc->pd_controller (247) β€” freed slab read;
  • writes devstat_start_transaction(&sc->pd_devstat) (281) into freed memory;
  • stores bio->bio_driver_info = sc (278), so completion later writes devstat_end_transaction_buf(&sc->ld_devstat, bp) and reads sc->ld_disk.d_cdev in mfi_disk_complete (mfi_disk.c:304-320, shared by syspd bios via mfi_bio_complete mfi.c:2114). mfi_syspd_close (196-206) likewise takes sc->pd_controller->mfi_io_lock out of freed memory.
  • Parked bios. mfi_syspd_strategy enqueues bios with bio_driver_info = sc (mfi_syspd.c:278-280). Nothing in detach removes them. The next mfi_startio β†’ mfi_bio_command (mfi.c:1950-1970) dequeues and reads mfid->ld_flags (1962-1963, type-punned layout per mfivar.h:120-151), then mfi_build_syspdio reads disk->pd_id into the DMA frame (mfi.c:2008-2016) β€” a freed-slab value drives which physical device the firmware writes, and the completion writes into the freed devstat.
  • Lockless walk. The AEN walk at mfi.c:1520 runs with no io lock while mfi_syspd_attach inserts (mfi_syspd.c:122-124) and mfi_syspd_detach removes (mfi_syspd.c:172-174) under the lock β€” a concurrent TAILQ_REMOVE+softc free can hand the walker a stale pd_link, and it then calls device_delete_child on an already-freed softc.

5. Exploit chain design (why uid0 is plausible; not executable on this guest)

The victim object is the freed mfi_system_pd (~112 bytes, M_BUS/kmalloc slab). The post-free writes are devstat_start_transaction / devstat_end_transaction_buf on sc->pd_devstat and sc->pd_flags reads controlling an enqueue; the reads feed pd_id into a firmware DMA frame. Grooming: driver-softc-sized M_BUS allocations (device softcs, nameunit strings) or same-size general kmalloc; hold many /dev/mfisyspd* fds open (operator group), race drive removal, immediately spray to reoccupy the slab, then use the surviving fd's pread(2) as a repeatable write trigger into the reoccupied object. With no KASLR/SMAP on the audit-target class of systems, forging pd_controller to point at a fake mfi_softc with a controlled mfi_bioq/lock field gives a path to arbitrary kernel write via the devstat/statistics fields; the mfi_build_syspdio read path leaks slab bytes back through I/O placement. This mirrors the DF-5013 chain design; it cannot be executed here because no MegaRAID controller exists on the guest (run.log).

6. What was run on the guest

  • pciconf -lv β†’ no AMD/mfi/megaraid/LSI device; no /dev/mfisyspd*, /dev/mfid*; hw.mfi.detect_jbod_change: 1 (default live); mfisyspd string present in /boot/kernel/kernel (driver ships in stock GENERIC β€” hardware is the only gate). β†’ env.txt, run.log
  • syspd_hold.c compiled clean (cc -O), executed: open(/dev/mfisyspd0) failed: No such file or directory β†’ hardware gate demonstrated. run.log
  • fix.diff applied to the guest's /usr/src copy and compile-validated: make -DNO_CLEAN nativekernel KERNCONF=X86_64_GENERIC β†’ "Kernel build for X86_64_GENERIC completed", exit 0; source reverted afterwards (verified byte-identical to pre-patch copies). Baseline behavior could not be exercised (no hardware), so fix_status is not_testable operationally β€” the compile validation plus the drain/gate logic review is the evidence.

7. Residual risk after fix.diff

A bio already built into an in-flight firmware command at the instant of delete can still complete through mfi_disk_complete after the softc free (window: between drain and kfree while a cm referencing the syspd sits on the busy queue). Fully closing it needs per-syspd in-flight command accounting β€” shared follow-up with the DF-5013 LD fix.

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 applied cleanly to guest /usr/src copy and compile-validated (make -DNO_CLEAN nativekernel KERNCONF=X86_64_GENERIC -> 'Kernel build completed', exit 0); source reverted afterwards. Baseline/patched behavior not exercisable without MegaRAID hardware, so operational fix validation not_testable; logic reviewed line-by-line in VERDICT.md.

VERDICT.md section 6; fix.diff applies via patch -p1 (Hunk #1/#2/#3 succeeded, mfi.c + mfi_syspd.c)
↓ fix.diffper-fix-DF-5049

Confirmed kernel references

Detail

Exploit chain

operator-group user holds /dev/mfisyspdN open with readers in flight -> drive removal raises MR_EVT_PD_REMOVED -> lockless device_delete_child frees mfi_system_pd slab -> slab re-groomed via M_BUS/kmalloc spray of softc-sized objects -> held fd's pread(2) re-enters mfi_syspd_strategy which writes devstat_start_transaction(&sc->pd_devstat) into the reoccupied object and enqueues bios whose completion writes devstat_end_transaction_buf -> repeatable post-free write; forge pd_controller/pd_devstat toward arbitrary write (no KASLR/SMAP on audit-target class) -> uid0. Not executable on this guest (hardware gate).

Evidence (decisive lines)

run.log: open(/dev/mfisyspd0) ENOENT on guest; env.txt: no AMD/mfi PCI, hw.mfi.detect_jbod_change=1, mfisyspd in stock kernel; VERDICT.md sections 2-4: path:line proof of all three trigger paths, the synchronous softc free, and the three surviving-reference sinks; fix.diff + VERDICT.md section 6: nativekernel compile validation

PoC changes

Seed had no PoC; wrote syspd_hold.c (open-hold + 4 pread readers as the in-flight-bio precondition). One compile fix: missing for sig_atomic_t.

Verified recommended fix

AEN PD_REMOVED: take config+io locks and gate via mfi_syspd_disable(); mfi_syspd_detach: refuse while open (unless detaching), set DISABLED, drain parked bios referencing the softc; strategy: re-check DISABLED under the io lock (see fix.diff)

Verdict

Hardware-gated on the audit guest (no MegaRAID PCI device: pciconf empty, /dev/mfisyspd* absent β€” run.log/env.txt), so the UAF could not be executed. Source-proven end-to-end: AEN MR_EVT_PD_REMOVED (mfi.c:1513-1532, live by default via mfi_detect_jbod_change=1) deletes the syspd child locklessly with no mfi_syspd_disable() gate and no drain; mfi_syspd_detach (mfi_syspd.c:155-177) frees pd_info and the softc (subr_bus.c:1944 kfree in device_delete_child) while (a) operator-group fds hold the 0640 cdev with dangling si_drv1, (b) bios parked on mfi_bioq carry bio_driver_info=sc and are later dereferenced+written by mfi_bio_command/mfi_build_syspdio/mfi_disk_complete, and (c) the lockless TAILQ walk can chase a concurrently freed pd_link. Syspd twin of DF-5013. fix.diff (AEN lock+gate; detach open-gate+DISABLED+drain; strategy DISABLED re-check) compile-validated on stock X86_64_GENERIC via make nativekernel; behavior could not be exercised without hardware.