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

mfi LD lifetime UAF: device_delete_child frees the mfi_disk softc under open descriptors and in-flight bios (AEN offline path has no gate at all)

Summary

All three delete paths free the softc while reachable: the AEN volume-offline path (mfi.c:1494-1510) has NO gate - a hardware event (no privileged ioctl needed) kfree()s the softc under open fds (root:operator 0640) and in-flight bios; mfi_disk_complete then writes into freed memory and held fds deref si_drv1 into freed slab. HW-gated on this guest; source-proven end-to-end with slab-groom->forge->uid0 chain design. 3-file fix: in-flight accounting + DISABLED gate + drain-before-destroy + AEN disable gate.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5013 Β· 5 files
FileTypeDescriptionSize
README.md β€” 3.5 KB ↓ raw
VERDICT.md β€” 4.5 KB ↓ raw
fix.diff β€” 2.7 KB view raw
manifest.json β€” 986 B view raw
verdict.json β€” 4.5 KB view raw

DF-5013 β€” mfi LD lifetime: softc freed under open descriptors and in-flight bios

What

Every path that deletes an mfi logical-disk child device frees the struct mfi_disk softc (newbus: device_delete_child β†’ device_detach β†’ device_set_driver(dev, NULL) β†’ kfree(dev->softc), sys/kern/subr_bus.c:2127-2130, :2146) while the cdev may still be reachable and while bios for that volume may still be in flight:

  1. AEN path β€” no gate at all (mfi.c:1494-1510, mfi_decode_evt, MR_EVT_LD_STATE_CHANGE β†’ new_state OFFLINE): device_delete_child() directly β€” no mfi_disk_disable(), no OPEN check, no drain. A volume going offline (drive failure/pull, foreign import, firmware decision) frees the softc while userland holds open fds on /dev/mfidN (root:operator 0640 β€” sys/kern/subr_disk.c) and/or while its bios sit on mfi_bioq / the controller busy queue.
  2. ioctl path (mfi.c:2623-2638, mfi_check_command_post, MFI_DCMD_LD_DELETE / MFI_DCMD_CFG_CLEAR): the pre-check (mfi_check_command_pre, mfi.c:2570-2578) calls mfi_disk_disable(), which refuses when the ld is OPEN β€” but that flag is advisory and the post-path never re-checks; bios already in flight at pre-check time are never drained either.
  3. mfi_disk_detach() itself (mfi_disk.c:171-194): the OPEN check returns EBUSY only when mfi_keep_deleted_volumes || mfi_detaching (both default 0, mfi.c:729-734) β€” i.e. by default detach proceeds even with the disk OPEN β€” and nothing waits for ld_active-style in-flight accounting because none exists.

After the softc is freed: - mfi_disk_complete() (mfi_disk.c:304-320) later runs for any in-flight bio (bio->bio_driver_info = sc, set at mfi_disk.c:295) β†’ write-after-free (devstat_end_transaction_buf(&sc->ld_devstat, bp)) and read-after-free (diskerr(bio, sc->ld_disk.d_cdev, ...)). - any descriptor that raced open (devfs unlink is asynchronous relative to disk_destroy(), which itself only synchronizes on the disk-core message: subr_disk.c:876-880 β†’ devfs_core.c:755-763) β†’ next read/write/close runs mfi_disk_strategy/close against si_drv1 pointing into the freed softc.

Threat model

Raw mfid nodes are root:operator 0640. An operator-group user holding /dev/mfidN open (or with AIO in flight) when the volume goes OFFLINE β€” a hardware event, no privileged ioctl required β€” gets kernel heap UAF. The same operator can drive path 2 directly via MFI_CMD LD_DELETE (0660 on /dev/mfi0, mfi.c:721-724), sacrificing a scratch volume. Destructive but single-actor-controlled, and the corruption primitive (freed softc deref by strategy/completion) is attacker-timing-controlled.

Reproduction status

HW-gated: the audit guest has no MegaRAID controller, so the mfi attach/lifecycle cannot run here. The mechanism is source-proven end-to-end (softc kfree site, completion deref site, missing gates β€” see VERDICT.md). No runtime PoC possible on this guest; this matches the DF-2066/DF-3898 house treatment of HW-gated driver bugs.

Fix

fix.diff (git-apply-able, 3 files): - mfivar.h: ld_active in-flight counter (guarded by mfi_io_lock). - mfi_disk.c: strategy fails new bios with ENXIO once DISABLED; counts in-flight bios; mfi_disk_complete() decrements and wakes; mfi_disk_detach() sets DISABLED and drains (lksleep) before disk_destroy(). - mfi.c AEN path: mfi_disk_disable() gate before device_delete_child() (refuse while open), mirroring the ioctl pre-path.

VERDICT.md
↓ download raw

DF-5013 β€” VERDICT

Status: NOT REPRODUCED at runtime β€” hardware-gated (no LSI MegaRAID controller on the audit guest; mfi cannot attach). The defect is source-proven end-to-end: every link in the chain below is unconditional, cited control flow in the audited tree (same commit the guest kernel was built from). This is the same standard applied to DF-2066 and DF-3898.

Impact ceiling: kernel heap use-after-free (write-after-free via devstat_end_transaction_buf on the freed softc; read + indirect call through freed si_drv1 via the disk ops). Realistic weaponization: operator-group user (raw mfid nodes are root:operator 0640) racing an open fd / in-flight read against a volume-offline event or LD_DELETE β†’ freed-softc deref β†’ slab-groom β†’ object forge β†’ uid=0. The race window is the firmware round-trip of the deleting DCMD (milliseconds) for the ioctl path, and zero (no gate) for the AEN path β€” an fd that is already open when the AEN delete lands is enough; the next I/O on it hits the freed softc.

Chain (all citations from the audited tree)

  1. Softc free site: device_delete_child(sc->mfi_dev, ld->ld_dev) (mfi.c:1508 AEN path, mfi.c:2633 LD_DELETE post-path, mfi.c:2644 CFG_CLEAR post-path) β†’ device_detach (subr_bus.c:2140-2152) β†’ device_set_driver(dev, NULL) (subr_bus.c:2146) β†’ kfree(dev->softc, M_BUS) (subr_bus.c:2127-2130). The struct mfi_disk is the driver softc (mfi_disk.c:80-84, sizeof(struct mfi_disk)).
  2. Missing gates: - AEN: mfi.c:1494-1510 β€” device_delete_child with no mfi_disk_disable, no OPEN check, no drain (the old KASSERT here is commented out). - ioctl post: mfi.c:2630-2635 β€” no re-check between the pre-check (mfi.c:2570-2578) and the delete; pre-check's disable is advisory. - mfi_disk_detach: mfi_disk.c:178-185 β€” EBUSY only when mfi_keep_deleted_volumes || mfi_detaching (defaults 0, mfi.c:729-734); no in-flight accounting exists to drain even if it wanted to.
  3. UAF consumers: - mfi_disk_complete (mfi_disk.c:304-320), reached from mfi_bio_complete (mfi.c:2092-2115, called under mfi_io_lock from mfi_complete): writes sc->ld_devstat inside the freed softc (mfi_disk.c:310), reads sc->ld_disk.d_cdev (mfi_disk.c:314). - any raced-open descriptor: si_drv1 (set mfi_disk.c:149) still points into the freed softc; mfi_disk_strategy derefs sc->ld_controller (mfi_disk.c:264) and mfi_disk_open/close take &sc->ld_controller->mfi_io_lock (mfi_disk.c:202, 219) β€” lockmgr on a pointer read from freed memory. Devfs unlink is asynchronous (destroy_dev β†’ devfs_destroy_dev posts a message, devfs_core.c:755-763), and disk_destroy only synchronizes the disk-core message (subr_disk.c:876-880, :527-556) β€” neither revokes an fd that already holds a cdev reference.
  4. Bios in flight with OPEN clear: an in-flight bio does not require the fd's d_close to have happened (vnode/fp refs), but the final-in-flight / AIO / post-umount-buffer windows leave exactly that state; the AEN path (1) additionally doesn't care about OPEN at all.

Exploit chain (design, not exercised β€” no hardware on guest)

operator (or any user if an admin-configured volume goes offline while they hold an fd): hold O_RDONLY fd on /dev/mfidN β†’ trigger offline (pull last drive of RAID0 / operator LD_DELETE on scratch volume) β†’ AEN delete frees softc β†’ keep issuing pread()s on the held fd β†’ strategy derefs freed softc β†’ heap groom (the 0x...-sized softc slab) β†’ forge ld_controller pointer / victim object β†’ lockmgr/devstat writes give a controlled write primitive β†’ uid=0. Deterministic-UAF variant: park a large AIO read, force OFFLINE, completion path writes into freed softc.

Fix

fix.diff β€” in-flight accounting (ld_active) + DISABLED gate in strategy + drain-before-destroy in mfi_disk_detach + mfi_disk_disable() gate on the AEN delete path. git apply --check clean against the tree. Not built as a single-fix kernel: entry is hardware-gated (no mfi controller on guest), so a patched-kernel run could not exercise the path β€” fix validated by inspection only.

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 (git apply --check clean, 3 files) closes all three holes: DISABLED gate in strategy, drain-before-destroy in detach, mfi_disk_disable gate on the AEN path. Single-fix kernel build not meaningful: entry is hardware-gated on this guest (no MegaRAID controller), so neither baseline nor patched kernel can exercise the lifecycle here; fix validated by inspection against the traced lock protocol (mfi_disk_complete runs under mfi_io_lock via mfi_complete).

fix.diff; VERDICT.md 'Fix' section
↓ fix.diffper-fix-DF-5013

Confirmed kernel references

Detail

Exploit chain

design only (no hardware): operator holds O_RDONLY fd on /dev/mfidN -> volume goes OFFLINE (drive pull / operator LD_DELETE on a scratch volume via MFI_CMD on /dev/mfi0) -> AEN mfi_decode_evt deletes the child -> softc kfree'd -> subsequent pread()/close() on the held fd deref si_drv1 into freed slab (mfi_disk_strategy reads sc->ld_controller and lockmgrs on it) and/or in-flight bio completion writes into the freed softc -> groom the freed softc slab -> forge ld_controller / victim object -> controlled kernel write -> uid=0

Evidence (decisive lines)

VERDICT.md full chain with citations; the decisive sites are mfi.c:1508 (ungated device_delete_child), mfi_disk.c:178-185 (conditional-only EBUSY), subr_bus.c:2127-2130 (softc kfree inside delete), mfi_disk.c:304-320 (post-free completion consumers), mfi.c:721-724 (operator-reachable ioctl node)

PoC changes

authored from source analysis; no runnable PoC possible on this guest (no mfi hardware), following the DF-2066/DF-3898 house treatment of HW-gated driver findings

Verified recommended fix

Track in-flight bios per ld (ld_active under mfi_io_lock), fail new bios with ENXIO once DISABLED, drain in mfi_disk_detach before disk_destroy, and gate the AEN offline-deletion path with mfi_disk_disable() (refuse while open) - see fix.diff (mfivar.h + mfi_disk.c + mfi.c).

Verdict

Hardware-gated: no LSI MegaRAID controller exists on the audit guest, so the mfi attach/lifecycle (LD probe, AEN handling, MFI_CMD LD_DELETE) cannot run and no runtime trigger was possible. The defect is source-proven end-to-end: device_delete_child on an ld child (AEN path mfi.c:1494-1510 with no gate at all; LD_DELETE/CFG_CLEAR post-path mfi.c:2630-2645 with an advisory-only pre-check) frees the mfi_disk softc via device_detach -> device_set_driver(NULL) -> kfree(dev->softc) (subr_bus.c:2127-2130,2146) while (a) bios still in flight are completed later by mfi_disk_complete against the freed softc (write-after-free via devstat_end_transaction_buf, mfi_disk.c:310; read-after-free via sc->ld_disk.d_cdev, mfi_disk.c:314), and (b) a raced-open descriptor keeps si_drv1 pointing into freed memory (devfs unlink is async vs disk_destroy, devfs_core.c:755-763; mfi_disk_detach's EBUSY is conditional on mfi_keep_deleted_volumes||mfi_detaching, both default 0). Impact ceiling: kernel heap UAF reachable by an operator-group user (raw mfid nodes are root:operator 0640; /dev/mfi0 is 0660) racing an open fd/in-flight I/O against a volume-offline event or LD_DELETE - memcorrupt/privesc class, not exercised at runtime here.