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

mfip_detach frees the CAM sim without draining mfi_cam_ccbq or clearing mfi_cam_start: queued passthrough ccbs leak permanently (unkillable hang) and are later dispatched through the stale callback into deregistered/freed CAM state

Summary

The SIM-private queue is invisible to xpt_bus_deregister (in-source XXX admits it) and there is no XPT_ABORT case - queued ccbs leak with the issuing periph blocked forever, and the still-live parent later pops them through the stale mfip_start (post-unload = call into unmapped module text; the late xpt_done completes against deregistered CAM state). DF-4438-style choreography mirror: 3 violations in stock mode, 0 fixed; drain+clear fix compiles into mfip.ko -Werror. MegaRAID hw-gated, root-triggered detach.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4971 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
mfip_detach_mirror.c β€” 10.7 KB view raw
build.sh β€” 109 B view raw
run.sh β€” 136 B view raw
build.log β€” 74 B view raw
run.log β€” 1.0 KB view raw
run.2.log β€” 652 B view raw
env.txt β€” 942 B view raw
fix.diff β€” 889 B view raw
fix_build.log β€” 1.2 KB view raw
manifest.json β€” 897 B view raw
verdict.json β€” 4.1 KB view raw

DF-4971 β€” mfip_detach leaks queued CAM ccbs and leaves a stale mfi_cam_start callback

sys/dev/raid/mfi/mfi_cam.c:142-159 β€” mfip_detach() deregisters the bus and frees the sim but (a) never drains sc->mfi_sc->mfi_cam_ccbq β€” ccbs the SIM accepted via mfip_cam_action XPT_SCSI_IO (mfi_cam.c:242) but the controller had no free command for β€” and (b) never clears sc->mfi_sc->mfi_cam_start (set at mfi_cam.c:116). xpt_bus_deregister cannot recover SIM-private ccbs (cam_xpt.c:4383 XXX), and mfip_cam_action has no XPT_ABORT support, so the leaked ccbs hang forever; the still-live parent controller's mfi_startio (invoked from mfi_intr at mfi.c:1245 and from mfi_shutdown, both running after child deletion in mfi_pci_detach mfi_pci.c:328-335) then pops them through the stale callback and completes them via xpt_done on a deregistered bus (path->bus->sim == &cam_dead_sim, cam_xpt.c:4456) with periphs already destroyed β€” a CAM-state UAF shape; on kldunload the callback target is unmapped module text.

Files

  • mfip_detach_mirror.c β€” choreography mirror: DFly CAM refcount semantics transcribed from cam_sim.c/cam_xpt.c + verbatim mfip_attach/action/start detach structure; stock vs fixed detach
  • build.sh, run.sh, build.log, run.log, run.2.log
  • fix.diff β€” drain + clear-callback fix
  • fix_build.log β€” patched mfip.ko builds on the guest with -Werror
  • env.txt β€” guest environment (no LSI MegaRAID HBA)

Reproduce

sh build.sh && sh run.sh

Expected decisive lines (BUGGY mode):

post-detach: sim freed=1 bus sim=dead_sim queued-nonempty=YES mfi_cam_start=STALE (mfip_start)
V1 VIOLATED: 3 ccb(s) leaked in mfi_cam_ccbq ...
V2 VIOLATED: mfi_cam_start still set after sim freed
    [xpt_done] UAF: ccb 1 completed after its periph was destroyed (dispatch sim=0x... dead_sim)
    [mfip_start] UAF: call into unmapped module text after kldunload
BUGGY: 4 violation(s)   exit=1

FIXED mode: FIXED: 0 violation(s) exit=0.

Kernel-path reproduction is impossible on this guest (no LSI 0x1000 PCI device; QEMU has no MegaRAID model) β€” see VERDICT.md.

VERDICT.md
↓ download raw

DF-4971 VERDICT β€” mfip_detach lifecycle bug: static trace complete + choreography mirror; kernel path not instantiable on guest

Bottom line

mfip_detach() (mfi_cam.c:142-159) frees the sim without (a) draining the SIM-private CAM queue mfisc->mfi_cam_ccbq and without (b) clearing mfisc->mfi_cam_start. Both consequences are statically certain and are demonstrated by a userspace mirror of the exact choreography run on the DragonFly guest:

  1. V1 β€” ccb leak / unrecoverable hang. ccbs accepted by mfip_cam_action XPT_SCSI_IO (mfi_cam.c:242) park on mfi_cam_ccbq whenever the controller has no free command (mfip_start returns NULL at mfi_cam.c:268-269 without dequeuing). xpt_bus_deregister walks the devq/device ccbqs (cam_xpt.c:4402-4421) β€” it never sees SIM-private entries; the XXX at cam_xpt.c:4383 states AC_LOST_DEVICE "does not precisely abort the pending requests". And mfip_cam_action has no XPT_ABORT case (default β†’ CAM_REQ_INVALID, mfi_cam.c:246-248), so neither CAM nor the SIM ever completes the leaked ccb: the issuing peripheral (probe/pass/ses) blocks forever, the ccb memory leaks, and mfip_detach/kldunload cannot be completed cleanly.
  2. V2 β€” stale callback. mfi_pci_detach deletes children first (mfi_pci.c:328) and only afterwards quiesces the controller (EVENTHANDLER_DEREGISTER/mfi_shutdown/mfi_free at :332-335). In that window every completion calls mfi_startio (mfi.c:1245), which pops mfi_cam_ccbq through mfisc->mfi_cam_start (mfi.c:2132-2133) β€” still mfip_start, whose sim was just freed. On kldunload of mfip/mfi the callback target is unmapped linker text (call to freed kernel text).
  3. V3 β€” completion through deregistered CAM state. The late mfip_done runs xpt_done(ccb) (mfi_cam.c:378) after the bus was deregistered: xpt_done dispatches via ccb->ccb_h.path->bus->sim (cam_xpt.c:4898), retargeted to cam_dead_sim at cam_xpt.c:4456, and reads path->periph->type β€” periphs of "aborted" ccbs were destroyed by the deregistration asyncs; a ccb CAM believed aborted (but which was actually leaked in our queue) completes against freed periph state β€” a use-after-free-shaped CAM state violation.

Why the mirror is faithful

  • cam_sim_alloc takes its own devq ref (cam_sim.c:185) and never fails (M_INTWAIT); cam_simq_release after alloc is correct β€” modeled.
  • xpt_bus_register bumps sim refcount (cam_xpt.c:4322); deregister retargets bus->sim to dead_sim (4456) and drops the ref (4484); cam_sim_free (cam_sim.c:229-234) frees at refcount 1 β€” modeled.
  • mfip_attach/mfip_cam_action/mfip_start/mfi_startio structures are the verbatim stock code paths (pointers/locks simplified, queue and callback semantics exact).

Result (guest, 2 runs + fixed variant)

BUGGY: 4 violations (V1 leak count 3, V2 stale cb, V3 xpt_done UAF line, unmapped-text call). FIXED (drain + mfi_cam_start = NULL): 0 violations. fix.diff additionally compiles into mfip.ko on the guest with -Werror (fix_build.log).

Why not a kernel-path reproduction

Requires an LSI MegaRAID SAS controller: mfip only exists as a child of mfi (mfi.c:737), which only probes on LSI PCI IDs. The guest has zero 0x1000-vendor PCI devices and QEMU has no MegaRAID device model (env.txt). Status inconclusive per the family precedent (DF-4438, same-shape mrsas_cam finding).

Impact ceiling (honest)

On real hardware: permanent uninterruptible hang of any thread waiting on a passthrough ccb + kernel ccb leak at every detach with queued ccbs (boot-scan INQUIRYs and camcontrol traffic both queue here), and post-detach dispatch of stale ccbs through freed sim/unmapped module text / deregistered periph state β€” a UAF-shaped escalation surface, though no controlled write was demonstrated. Trigger requires MegaRAID hardware + pending mfip-bus ccb at detach/unload (root-initiated action, or surprise removal / shutdown race).

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

inconclusive
baseline no→ patch + rebuild →patched clean

Kernel-level fix validation impossible (no MegaRAID HBA on guest). Validated at two levels: (1) the mirror's fixed mfip_detach drains the queue and clears the callback - 0 violations where stock shows 4 (run.log); (2) fix.diff applies cleanly to the guest /usr/src copy and mfip.ko builds with -Werror (fix_build.log). Guest sources restored after the build.

run.log FIXED section ('FIXED: 0 violation(s) exit=0'); fix_build.log ('Hunk #1/#2 succeeded', cc -Werror, ld -r mfip.ko, RESTORED_CLEAN)
↓ fix.diffper-fix-DF-4971

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: 'post-detach: ... queued-nonempty=YES mfi_cam_start=STALE', 'V1 VIOLATED: 3 ccb(s) leaked', 'V2 VIOLATED', '[xpt_done] UAF: ccb 1 completed after its periph was destroyed (dispatch sim=... dead_sim)', '[mfip_start] UAF: call into unmapped module text after kldunload', 'BUGGY: 4 violation(s) exit=1' vs 'FIXED: 0 violation(s) exit=0'; fix_build.log shows patched mfip.ko linking with -Werror and source restore

PoC changes

mirror authored fresh (no seed); DragonFly userland queue.h needs plain TAILQ field names (kernel camq_entry union uses sim_links.tqe); post-detach free-command replenishment added to model mfi_intr completing a disk bio before mfi_startio

Verified recommended fix

In mfip_detach, under mfi_io_lock: drain mfi_cam_ccbq completing each ccb with CAM_REQ_ABORTED via xpt_done, set sc->mfi_sc->mfi_cam_start = NULL, then xpt_bus_deregister + cam_sim_free

Verdict

Static trace complete; kernel path not instantiable on the audit guest (no LSI 0x1000 PCI device, QEMU has no MegaRAID model; mfip exists only as a child of mfi, mfi.c:737). mfip_detach (mfi_cam.c:142-159) frees the sim without draining mfisc->mfi_cam_ccbq and without clearing mfisc->mfi_cam_start (set at mfi_cam.c:116). Queued ccbs (parked whenever the controller had no free command, mfi_cam.c:268) are invisible to xpt_bus_deregister (cam_xpt.c:4383 XXX) and unrecoverable through the sim (no XPT_ABORT case, mfi_cam.c:246 default CAM_REQ_INVALID): permanent hang + ccb leak. The still-live parent then dispatches them through the stale callback: mfi_pci_detach deletes children first (mfi_pci.c:328) and mfi_intr->mfi_startio (mfi.c:1245->2132-2133) runs before mfi_free, popping the leaked ccbs via mfip_start whose sim is freed; on kldunload the callback is unmapped module text; the late xpt_done (mfi_cam.c:378) completes the ccb against deregistered CAM state (path->bus->sim==&cam_dead_sim per cam_xpt.c:4456) with periphs destroyed - UAF-shaped. Guest-compiled choreography mirror (CAM refcount semantics transcribed from cam_sim.c/cam_xpt.c) shows all violations in stock mode and zero with the drain+clear fix; fix.diff builds into mfip.ko on the guest with -Werror.