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)
PoC verification
Evidence pack
findings/poc/DF-5049 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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:
- an open userspace descriptor whose
si_drv1points at the softc; - bios parked on
controller->mfi_bioqwhosebio_driver_infopoints at the softc; - concurrent walkers of
mfi_syspd_tqh(the AEN removal walk itself is lockless, so it can chase apd_linkthat another CPU is freeing).
2. Trigger paths (all reach mfi_syspd_detach with the softc referenced)
- AEN
MR_EVT_PD_REMOVEDβmfi_decode_evtmfi.c:1513-1532, runs in the SWI event task (mfi_queue_evtβtaskqueue_swi, mfi.c:1554-1559). Walkssc->mfi_syspd_tqhand callsdevice_delete_child(sc->mfi_dev, syspd->pd_dev)with nomfi_config_lock, nomfi_io_lock, nomfi_syspd_disable()call, no drain. Live by default:mfi_detect_jbod_change = 1(mfi.c:149-150; verifiedhw.mfi.detect_jbod_change: 1on the stock guest). Trigger = drive removal / firmware event β no privileged ioctl needed (identical posture to DF-5013's LD-offline AEN). mfi_syspdprobedelete β 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 viadisk_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 whenOPEN && (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 clearssi_drv1. The nextread(2)/write(2)on the held fd routes tomfi_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 writesdevstat_end_transaction_buf(&sc->ld_devstat, bp)and readssc->ld_disk.d_cdevinmfi_disk_complete(mfi_disk.c:304-320, shared by syspd bios viamfi_bio_completemfi.c:2114).mfi_syspd_close(196-206) likewise takessc->pd_controller->mfi_io_lockout of freed memory. - Parked bios.
mfi_syspd_strategyenqueues bios withbio_driver_info = sc(mfi_syspd.c:278-280). Nothing in detach removes them. The nextmfi_startioβmfi_bio_command(mfi.c:1950-1970) dequeues and readsmfid->ld_flags(1962-1963, type-punned layout per mfivar.h:120-151), thenmfi_build_syspdioreadsdisk->pd_idinto 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_attachinserts (mfi_syspd.c:122-124) andmfi_syspd_detachremoves (mfi_syspd.c:172-174) under the lock β a concurrentTAILQ_REMOVE+softc free can hand the walker a stalepd_link, and it then callsdevice_delete_childon 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);mfisyspdstring present in/boot/kernel/kernel(driver ships in stock GENERIC β hardware is the only gate). β env.txt, run.logsyspd_hold.ccompiled clean (cc -O), executed:open(/dev/mfisyspd0) failed: No such file or directoryβ hardware gate demonstrated. run.logfix.diffapplied to the guest's/usr/srccopy 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 isnot_testableoperationally β 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_testablefix.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)
Confirmed kernel references
- sys/dev/raid/mfi/mfi_syspd.c:155
- sys/dev/raid/mfi/mfi_syspd.c:162
- sys/dev/raid/mfi/mfi_syspd.c:278
- sys/dev/raid/mfi/mfi_syspd.c:281
- sys/dev/raid/mfi/mfi.c:149
- sys/dev/raid/mfi/mfi.c:1513
- sys/dev/raid/mfi/mfi.c:1344
- sys/dev/raid/mfi/mfi.c:1962
- sys/dev/raid/mfi/mfi_disk.c:304
- sys/kern/subr_bus.c:1944
- sys/kern/subr_disk.c:740
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
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.
No comments yet.