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

Bios submitted to a never-completing mlx controller park on mlx_bioq forever β€” unkillable D-state issuers and unbounded bioq growth (DF-5048/DF-5026 family, mlx instance)

Summary

The only dequeue restart is hardware completions or the 1Hz poll; mc_timeout is dead code (XXX never read) - a controller that accepts commands then stops completing pins every slot BUSY forever and all further bios park indefinitely (unkillable D-state, unbounded bioq growth). Hardware-fault trigger (no deterministic software path like ips DF-5026). 1Hz stall-watchdog in mlx_periodic fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5058 Β· 6 files
FileTypeDescriptionSize
README.md β€” 1.5 KB ↓ raw
VERDICT.md β€” 4.4 KB ↓ raw
env.txt β€” 415 B view raw
fix.diff β€” 2.2 KB view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.7 KB view raw

DF-5058 β€” bios submitted to a never-completing mlx controller park on

mlx_bioq forever: unkillable D-state issuers + unbounded bioq growth

file: sys/dev/raid/mlx/mlx_disk.c (submit path; parking logic in sys/dev/raid/mlx/mlx.c) bucket: base:dev (DF-5026 / DF-5048 family β€” the mlx instance, the last LD sibling) severity: Low (trigger = controller hardware fault: accepts commands then stops completing, or wedges with slots held)

Reproduce

Not runnable on this guest: no Mylex PCI hardware (env.txt), driver never attaches. Family twin DF-5048 (ida) was accepted with the same hardware-gated pack.

Expected behavior on real hardware

  1. Saturate /dev/mlxd0 with asynchronous I/O (e.g. several parallel dd/vinum readers).
  2. Cause the controller to stop delivering completions after accepting commands (firmware hang / surprise fault while BUSY).
  3. After the 256 slots fill, every further bio parks on sc->mlx_bioq (mlx.c:1739-1742). No timeout exists anywhere (mc_timeout is set at mlx.c:2086 and explicitly "not currently used"); mlx_periodic only polls mlx_done every second (mlx.c:1040) and never fails a stale command.
  4. Every issuer sleeps in biowait β€” unkillable D-state processes; the bioq grows without bound (kernel memory exhaustion).

Fix

fix.diff: stall watchdog inside the existing 1 Hz mlx_periodic callout β€” if parked bufs exist and mlx_done completed nothing for 60 consecutive seconds, fail every queued bio with ETIMEDOUT and fail new arrivals until completions resume.

VERDICT.md
↓ download raw

DF-5058 VERDICT β€” parked bios / no completion path on a wedged mlx controller

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

Reproduced?

No. Requires a physical Mylex DAC960 controller that stops completing I/O. Guest evidence (env.txt): no 0x1069 PCI function, no 0x1011:1065 function, /dev/mlx*//dev/mlxd* absent, driver compiled into the running kernel (X86_64_GENERIC:134). Untested = hardware absent β€” same basis the family accepted for DF-5048.

Why the finding is real (trace)

Completion-flow enumeration β€” there is no biodone for bios that enter mlx_submit_bio other than mlxd_intr via mlx_completeio:

  1. mlxd_strategy (mlx_disk.c:156-190) local error paths all biodone (mlx_disk.c:166-170, 173-177, 183-189) β€” those are fine (this is the DF-5012 class, already clean here).
  2. The submit path (mlx_disk.c:179-181) β†’ mlx_submit_bio (mlx.c:683-693): bioqdisksort(&sc->mlx_bioq, bio) + mlx_startio.
  3. mlx_startio (mlx.c:1713-1805): pops a bio only when mlx_alloccmd succeeds and mlx_getslot returns 0; out of command slots (mlx.c:1739-1742, limit - 4 reached) β†’ break, bio stays parked on mlx_bioq. Commands whose mlx_start fails outright are completed with WEDGED (mlx.c:1796-1800 β†’ mlx_completeio β†’ mlxd_intr) β€” the fast wedge path is handled; the parked path below is not.
  4. The ONLY restart for parked bios is mlx_done β†’ mlx_startio (mlx.c:2160-2161), reached from the hardware interrupt (mlx.c:668-676) or the 1 Hz mlx_periodic (mlx.c:1040) β€” both rely on the controller eventually delivering completions.
  5. No timeout exists anywhere: mc_timeout = time_uptime + 60 is set in mlx_start (mlx.c:2085-2086) under the comment "XXX not currently used"; nothing ever reads it. mlx_complete (mlx.c:2173-2223) only processes mc_status != MLX_STATUS_BUSY; the periodic enquiry/rebuild polls (mlx.c:977-1044) never fail stale commands.
  6. Therefore: controller accepts commands then never completes them β‡’ all slots (up to me_max_commands, mlx.c:1961-1966) stay BUSY forever β‡’ mlx_getslot EBUSY β‡’ every subsequent bio parks on mlx_bioq indefinitely β‡’ biowait sleepers are unkillable D-states (local DoS of every process touching the array; if root/swap live on mlxd the box is dead) and mlx_bioq grows unbounded β†’ kernel memory exhaustion. mlxd_strategy itself has no timeout/cancel path (mlx_disk.c:172-181).

Trigger requires a real controller fault (firmware hang after accept, surprise removal while BUSY, channel stuck paused) β€” hence Low, matching the DF-5048 (ida) precedent; DF-5026 (ips) was Medium because an adapter-offline config state parked bios deterministically.

Not a duplicate

  • DF-3951 (mlx.c): wedge path of mlx_start leaks mlx_busycmds / leaves the DMA map loaded β€” that is the fast-fail path; this finding is the parked-bioq/issuer-hang path (bios never dequeued at all).
  • DF-3950 (mlx.c): mlx_wait_command infinite 1 s-sleep loop for synchronous internal commands (mlxctl rebuild/check) β€” not disk bios.
  • DF-2074 / DF-5057: different mechanisms (bio_driver_info staleness, detach lifetime).
  • DF-5026 / DF-5048: the same defect in ips/ida; this is the mlx instance.

Fix assessment

fix.diff adds a stall watchdog to the existing 1 Hz mlx_periodic callout (no new callout needed): a stall-tick counter in struct mlx_softc (mlxvar.h) is reset whenever mlx_done completes anything (mlx.c:1040) and incremented while mlx_waitbufs > 0; 60 consecutive no-progress seconds β‡’ drain mlx_bioq failing every bio with ETIMEDOUT+B_ERROR and fail new arrivals in mlx_submit_bio until completions resume. 60 s is far beyond any healthy controller's worst-case completion latency under load (load always produces completions, which reset the counter). Commands already stuck in hardware keep their slots (fail-stop: the array stays EIO until reset) β€” noted as the conservative choice. 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 adds mlx_stall_ticks to struct mlx_softc (mlxvar.h), resets it whenever mlx_done completes anything (healthy load always produces completions, so no false positive under sustained I/O), and after 60 no-progress seconds with parked bufs drains mlx_bioq failing every bio with ETIMEDOUT+B_ERROR while mlx_submit_bio fails new arrivals β€” issuers become killable and the queue cannot grow unbounded. Commands stuck in hardware keep their slots (fail-stop until reset), the conservative choice. 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_periodic (watchdog) + mlx_submit_bio (wedged fast-fail) and mlxvar.h (mlx_stall_ticks)
↓ fix.diffper-fix-DF-5058

Confirmed kernel references

Detail

Exploit chain

saturate /dev/mlxdN with I/O + controller fault that stops completions after accept -> all me_max_commands slots stuck BUSY -> mlx_getslot EBUSY for every subsequent bio -> bios park on mlx_bioq forever -> unkillable D-state issuers + unbounded queue growth (kernel memory exhaustion). DoS only, hardware-fault-triggered; no privilege-escalation primitive.

Evidence (decisive lines)

env.txt (live guest: no Mylex PCI function, no /dev/mlx*, driver in kernel); VERDICT.md completion-flow enumeration with path:line for every hop (no other biodone exists for parked bios); fix.diff (git apply --check: APPLIES_CLEAN)

PoC changes

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

Verified recommended fix

Stall watchdog in the existing 1 Hz mlx_periodic callout: when parked bufs exist and mlx_done completed nothing for 60 consecutive seconds, fail every queued bio with ETIMEDOUT and fail new arrivals in mlx_submit_bio until completions resume.

Verdict

Genuine DF-5048/DF-5026-family parked-bio hang, mlx instance: mlxd_strategy (mlx_disk.c:156-190) -> mlx_submit_bio (mlx.c:683-693) queues every bio on sc->mlx_bioq; mlx_startio (mlx.c:1713-1805) dequeues only while a command slot is free β€” mlx_getslot EBUSY (mlx.c:1739-1742, limit-4 reached) parks the bio. The ONLY restart is mlx_done -> mlx_startio (mlx.c:2160-2161) from the hardware interrupt or the 1 Hz mlx_periodic (mlx.c:1040), both of which depend on the controller delivering completions. No timeout exists anywhere: mc_timeout is set in mlx_start (mlx.c:2085-2086) under 'XXX not currently used' and never read; mlx_complete (mlx.c:2173-2223) only processes status != BUSY; nothing ever fails a stale command or a queued bio. Therefore a controller that accepts commands and then stops completing (firmware hang after accept, surprise fault while BUSY) pins every command slot BUSY forever, after which every further bio parks on mlx_bioq indefinitely: biowait sleepers are unkillable D-states (system-wide hang if root/swap live on mlxd) and the bioq grows unbounded -> kernel memory exhaustion. The fast-fail wedge path IS handled (mlx_start failure -> mlx_completeio -> biodone(EIO), mlx.c:1796-1800); the parked path is not. Untested solely because the guest has no Mylex DAC960 PCI function (env.txt) although the driver is compiled into the running kernel. Severity Low matching DF-5048 (trigger requires a real controller fault, unlike ips DF-5026 whose offline config state parked bios deterministically).