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

mlxd lifetime TOCTOU: MLX_DETACH_DRIVE checks MLXD_OPEN then sleeps in mlx_flush before device_delete_child; mlxd_detach has no recheck and no in-flight drain β€” softc freed under racing open fds / in-flight bios (DF-5025/DF-5046 family, last LD sibling)

Summary

Check-then-sleep-then-act: the flush polls up to 15M MMIO iterations with MLX_STATE_SHUTDOWN not raised - a concurrent open succeeds mid-window and the subsequent delete frees the softc under the fd and in-flight bios; mlx_shutdown deletes ALL children with no open-check on reboot. Root-only trigger (no unprivileged event-driven delete) - Low like DF-5025/5046. Raise-SHUTDOWN-for-the-window + detach-recheck fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5057 Β· 6 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 5.4 KB ↓ raw
env.txt β€” 415 B view raw
fix.diff β€” 1.4 KB view raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 5.3 KB view raw

DF-5057 β€” mlxd lifetime TOCTOU: MLX_DETACH_DRIVE deletes the mlxd child

across a sleeping flush with no open-gate raised, and mlxd_detach neither rechecks MLXD_OPEN nor drains in-flight bios β†’ softc UAF

file: sys/dev/raid/mlx/mlx_disk.c (+ trigger window in sys/dev/raid/mlx/mlx.c) bucket: memcorrupt (DF-5013 / DF-5025 / DF-5046 family β€” the last LD sibling) severity: Low (trigger requires root: /dev/mlxN control node is 0600 root:operator, mlx.c:466-468; no unprivileged runtime delete path exists in mlx, unlike mfi's AEN path)

Reproduce

Not runnable on this guest: the driver is compiled into the running kernel (device mlx, X86_64_GENERIC:134; kldload mlx β†’ "already loaded or in kernel") but the guest has no Mylex PCI function (env.txt: vendor 0x1069 absent, 0x1011:1065 absent) so no controller ever attaches and /dev/mlxd* does not exist. This is the same hardware-gated situation as the family twins DF-5046/DF-5048 (packs contain the live-guest probe output).

Expected behavior on real hardware

  1. mlxcontrol detach <unit> (or any root caller of ioctl(mlxctl, MLX_DETACH_DRIVE, unit)).
  2. Concurrently, open /dev/mlxdN in a loop (any user with the device permission, e.g. operator group).
  3. The open that lands inside the flush window succeeds β†’ device_delete_child frees the mlxd_softc while the fd (and any bio it then submits) references it β†’ mlxd_close / mlxd_strategy / mlxd_ioctl write into freed kernel memory; a late controller completion runs mlxd_intr β†’ devstat_end_transaction_buf(&sc->mlxd_stats) into freed memory.

Fix

fix.diff (git-apply-able against this tree): * MLX_DETACH_DRIVE raises MLX_STATE_SHUTDOWN for the whole flush+delete window so the existing mlxd_open gate (mlx_disk.c:98) rejects racing opens with ENXIO (the flag is cleared again at detach_out). * mlxd_detach rechecks MLXD_OPEN and returns EBUSY (belt-and-suspenders for the check-then-act gap at mlx.c:782β†’794).

Full remediation additionally needs an in-flight drain (wait for mlx_waitbufs == 0 / outstanding mlxd commands before disk_destroy).

VERDICT.md
↓ download raw

DF-5057 VERDICT β€” mlxd detach/delete lifetime TOCTOU + no in-flight drain

status: untested (hardware-gated) | impact: none demonstrated (memcorrupt class) | confidence: certain (code trace)

Reproduced?

No. Requires a physical Mylex DAC960 family adapter (PCI vendor 0x1069, or 0x1011:1065 for the type-5 interface, mlx_pci.c:80-83). Guest evidence (env.txt, live run): no such PCI function exists, /dev/mlx*//dev/mlxd* absent, yet the driver is compiled into the running X86_64_GENERIC kernel (device mlx at X86_64_GENERIC:134; kldload mlx β†’ already loaded). Same hardware-gated situation the family accepted for DF-5046 (ida) and DF-5048.

Why the finding is real (trace)

The delete paths and their gates:

  1. MLX_DETACH_DRIVE (mlx.c:774-804): checks mlxd->mlxd_flags & MLXD_OPEN at mlx.c:782, then calls mlx_flush(sc) at mlx.c:788 before device_delete_child at mlx.c:794. mlx_flush (mlx.c:1516-1549) runs mlx_poll_command (mlx.c:1677-1703) which busy-spins polling the controller for up to 15,000,000 iterations (mlx.c:1690-1694) β€” a window of milliseconds to seconds, during which the controller is NOT marked shut down: MLX_STATE_SHUTDOWN is only set by mlx_shutdown (mlx.c:599), never by MLX_DETACH_DRIVE.
  2. mlxd_open (mlx_disk.c:86-115) has exactly one gate β€” sc->mlxd_controller->mlx_state & MLX_STATE_SHUTDOWN at mlx_disk.c:98 β€” which is clear during the entire MLX_DETACH_DRIVE window, so a concurrent open succeeds and sets MLXD_OPEN (mlx_disk.c:113) after the check at mlx.c:782 has already passed. No lock is held by either side (crit_enter() in mlx.c:780? β€” none; MLX_DETACH_DRIVE runs with no crit/lock at all).
  3. mlxd_detach (mlx_disk.c:290-301) then runs devstat_remove_entry + disk_destroy with no MLXD_OPEN recheck and no drain of bios already queued on the controller (sc->mlx_bioq, set by mlxd_strategy β†’ mlx_submit_bio at mlx_disk.c:180 / mlx.c:688) or of commands already in the controller's mlx_work queue.
  4. disk_destroy (subr_disk.c:545-549 β†’ DISK_DISK_DESTROY at subr_disk.c:873-897) only interlocks against disk-enumeration refs (dp->d_refs); the cdev teardown (destroy_dev) is asynchronous and the open descriptor keeps the cdev alive with si_ops = mlxd_ops and si_drv1 = sc (set at mlx_disk.c:259-260) dangling into freed memory once newbus frees the mlxd_softc after detach β€” the exact mechanism established for this family in DF-5046 (same disk_create/ devfs teardown: subr_disk.c:875-881, devfs_core.c:759-763, devfs_core.c:1440-1459, kern_device.c:355-389).
  5. Post-free touch points: mlxd_close writes sc->mlxd_flags &= ~MLXD_OPEN (mlx_disk.c:127); mlxd_strategy reads sc->mlxd_drive/mlxd_controller and calls devstat_start_transaction(&sc->mlxd_stats) + submits with freed pointers (mlx_disk.c:161,173,179-180); mlxd_ioctl dereferences sc->mlxd_controller (mlx_disk.c:143); and a late controller completion for an in-flight bio runs mlx_completeio (mlx.c:1811-1841) β†’ mlxd_intr β†’ devstat_end_transaction_buf(&sc->mlxd_stats, bp) (mlx_disk.c:204) β€” a write into freed kernel memory.
  6. Second leg: mlx_shutdown (mlx.c:611-617) deletes all children unconditionally β€” the MLXD_OPEN pre-check exists only in mlx_detach (mlx.c:558-566), not in mlx_shutdown itself. mlx_pci.c:54 wires mlx_shutdown as device_shutdown, so the system reboot/shutdown path deletes every mlxd child under open descriptors and in-flight bios with no gate at all.

Not a duplicate

  • DF-2074 (this file) = stale bio_driver_info in strategy β€” different root cause (no device lookup), no detach involvement.
  • DF-1303/DF-1304/DF-3949–DF-3952 (mlx.c) = firmware-bound trust, DCDB fixup, NEXT_CHILD NULL panic, wait-command loop, wedge-path slot leak, dmamap panic β€” none covers the child lifetime/TOCTOU.
  • DF-5013/DF-5025/DF-5046/DF-5049 = same family defect in mfi/ips/ida/mfi- syspd; this is the mlx instance (the last LD sibling), with the nuance that mlx has an open-check but it is check-then-sleep-then-act.

Severity rationale

Low, matching DF-5025 (ips): initiation requires root (MLX_DETACH_DRIVE on the 0600 root:operator control node, or reboot). There is no mfi-style unprivileged event-driven delete in mlx (the periodic enquiry only prints state changes, mlx.c:1126-1154). The reboot leg (leg 2) fires on any machine with a mounted mlxd root/swap, but requires physical DAC960 hardware.

Fix assessment

fix.diff: (1) MLX_DETACH_DRIVE raises MLX_STATE_SHUTDOWN for the whole flush+delete window and clears it at detach_out β€” closes the open-race window using mlxd_open's existing gate; (2) mlxd_detach rechecks MLXD_OPEN and returns EBUSY. The remaining in-flight-bio leg needs a drain (wait on mlx_waitbufs / outstanding commands) before disk_destroy β€” noted in the diff header as the complete fix. Cannot be build/boot-validated (no hardware); diff is git-apply-clean against sys/.

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 closes the open-race leg (SHUTDOWN flag raised across the sleeping flush so mlxd_open fails with ENXIO inside the window; flag cleared at detach_out since the controller keeps running) and adds an EBUSY recheck in mlxd_detach. The in-flight-bio leg additionally needs a drain (wait on mlx_waitbufs / outstanding commands) before disk_destroy β€” flagged in the diff comment as the complete fix. Cannot be build/boot validated: no Mylex hardware on the guest. Diff is git-apply-clean against sys/.

fix.diff (git apply --check: APPLIES_CLEAN); hunks at mlx.c MLX_DETACH_DRIVE (SHUTDOWN set/clear) and mlx_disk.c mlxd_detach (thread2.h include + EBUSY recheck)
↓ fix.diffper-fix-DF-5057

Confirmed kernel references

Detail

Exploit chain

root ioctl(MLX_DETACH_DRIVE) on /dev/mlxN + racing open('/dev/mlxdN') landing inside the mlx_flush window -> device_delete_child -> mlxd_detach frees softc under the open fd -> mlxd_close/mlxd_strategy write freed memory (mlxd_flags clear, devstat_start_transaction, submit) and late completions write via devstat_end_transaction_buf. Not an escalation primitive: the initiating side is root-only; on hardware this is a root-triggerable kernel UAF write with heap-groom potential.

Evidence (decisive lines)

env.txt (live guest: no Mylex PCI function, no /dev/mlx*, driver in kernel); VERDICT.md full path:line trace of both legs; fix.diff (git apply --check: APPLIES_CLEAN)

PoC changes

No runnable PoC exists (hardware-gated). Pack contains hardware-absence evidence and the line-accurate code trace; fix.diff generated via edited copies + diff -u and validated with git apply --check.

Verified recommended fix

Raise MLX_STATE_SHUTDOWN for the whole MLX_DETACH_DRIVE flush+delete window (closes the open race via mlxd_open's existing gate) and recheck MLXD_OPEN in mlxd_detach returning EBUSY; full fix also drains in-flight bios before disk_destroy.

Verdict

Genuine DF-5025/DF-5013/DF-5046-family detach lifetime TOCTOU, mlx instance: MLX_DETACH_DRIVE (mlx.c:774-804) checks mlxd_flags & MLXD_OPEN at mlx.c:782, then SLEEPS in mlx_flush (mlx.c:788 -> mlx_poll_command spinning up to 15M MMIO polls, mlx.c:1690-1694) before device_delete_child (mlx.c:794) β€” and MLX_STATE_SHUTDOWN (mlxd_open's only gate, mlx_disk.c:98) is never raised on this path, so a concurrent mlxd_open succeeds inside the window and sets MLXD_OPEN (mlx_disk.c:113) after the check passed. mlxd_detach (mlx_disk.c:290-301) has no recheck and no drain: devstat_remove_entry + disk_destroy free path runs while the racing fd (si_drv1 = sc set at mlx_disk.c:259-260) and any bios already queued on the controller are alive; disk_destroy only interlocks disk-enumeration refs (subr_disk.c:875-881), cdev teardown keeps the open cdev with si_drv1 dangling (DF-5046 mechanism), and a late completion runs mlx_completeio (mlx.c:1811-1841) -> mlxd_intr -> devstat_end_transaction_buf(&sc->mlxd_stats) (mlx_disk.c:204) writing freed memory; mlxd_close (mlx_disk.c:127) and mlxd_strategy (mlx_disk.c:161,173,179-180) likewise touch the freed softc. Second leg: mlx_shutdown (mlx.c:611-617) deletes ALL children with no open-check of its own (pre-check only in mlx_detach, mlx.c:558-566) and is wired to device_shutdown (mlx_pci.c:54), so reboot deletes children under open descriptors and in-flight bios. Untested solely because the guest has no Mylex DAC960 PCI function (env.txt: no 0x1069/0x1011:1065 device, no /dev/mlx*) although the driver is compiled into the running X86_64_GENERIC. Severity Low matching DF-5025: trigger requires root (MLX_DETACH_DRIVE on the 0600 root:operator control node mlx.c:466-468, or reboot); mlx has no mfi-style unprivileged event-driven delete (periodic enquiry only prints, mlx.c:1126-1154).