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

Bios queued to an IPS_OFFLINE adapter are never biodone'd β€” ips_start_io_request swallows ips_get_free_cmd's EIO; every issuer parks in an unkillable D-state and bio_queue grows unbounded

Summary

Terminal EIO conflated with transient EBUSY at ips_commands.c:246-247 + no IPS_OFFLINE gate in strategy: every bio to the wedged adapter parks forever (unkillable D-state, unbounded queue growth, root-on-ipsd wedges the box). Same DF-2066/DF-5012 systemic class. Sink-level REPRODUCED on stock guest: pread parks 7 bios, survives kill -9, only external drain releases; A/B fix returns EIO immediately. Two-hunk fail-fast+drain fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5026 Β· 15 files
FileTypeDescriptionSize
df5026.c β€” 6.4 KB view raw
df5026_fixed.c β€” 6.8 KB view raw
df5026_trigger.c β€” 1.2 KB view raw
Makefile β€” 52 B ↓ download
build.sh β€” 115 B view raw
run.sh β€” 161 B view raw
run_ab.sh β€” 1.6 KB view raw
build.log β€” 6.7 KB view raw
run.log β€” 1.4 KB view raw
run_fixed_ab.log β€” 942 B view raw
fix.diff β€” 2.4 KB view raw
fix_validation.log β€” 1.8 KB view raw
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 2.4 KB ↓ raw
verdict.json β€” 4.3 KB view raw

DF-5026 β€” ips: bios queued to an offline adapter are never completed (ips_disk.c)

File: sys/dev/raid/ips/ips_disk.c (pass 2, with the swallowed error in sys/dev/raid/ips/ips_commands.c:246-247) Lines: ips_disk.c:128-143 (strategy, no fail-fast), ips_commands.c:246-247 (error swallowed), ips.c:207-209/220 (EIO/EBUSY sources), ips.c:354-358 (IPS_OFFLINE set after failed reinit) Class: missing-completion DoS β€” the ips instance of the DF-5012/DF-2066 systemic pattern.

Build (guest, root)

sh build.sh

Run

sh run.sh          # original pattern: wedge + kill -9 + poke
sh run_ab.sh       # A/B: original vs fixed pattern

Expected output (decisive lines from run.log)

   PID STAT   WCHAN    COMMAND
  1275 D3     physstr  /tmp/poc/df5026_trigger wedge /dev/df50260
-- after SIGKILL, still present? --
  1275 D3     physstr  ...            <-- survives kill -9
df5026: poke -- draining 7 parked bio(s) with EIO
gone                                 <-- released only by external completion

And from run_fixed_ab.log (FIXED pattern):

wedge: pread@2097152 r=-1 errno=5(Input/output error)   <-- immediate EIO
no wedge process remains (fixed pattern)

Mechanism

ips_timeout() (ips.c:354-358) declares the adapter IPS_OFFLINE after a failed forced reinit ("AIEE! adapter reset failed"). From then on ips_get_free_cmd() returns EIO (ips.c:207-209) β€” but ips_start_io_request() discards the error code entirely (ips_commands.c:246-247) and returns, leaving the bio on sc->bio_queue forever; ipsd_strategy() (ips_disk.c:134-141) keeps accepting and enqueuing new bios with no fail-fast. No biodone() ever fires: every issuer (including fs-flush on close) parks in uninterruptible biowait, and bio_queue grows without bound. The unload-time observation in run.log ("draining 6 parked bio(s)" AFTER the poke drained 7) shows the queue keeps accumulating from incidental IO while offline.

Why a sink module

Guest has no ServeRAID adapter (env.txt). Per the DF-4986/DF-5012 precedent, df5026.c wires the verbatim ipsd_strategy/ips_start_io_request structure (lock, bioqdisksort, swallow-on-error) to a raw D_DISK device; "offline" reproduces IPS_OFFLINE. The D-state/unkillable/external-release behavior demonstrated is exactly the failure mode the driver-side omission inflicts.

Artifacts

df5026.c, df5026_fixed.c (fix pattern), df5026_trigger.c, Makefile, build.sh, run.sh, run_ab.sh, build.log, run.log, run_fixed_ab.log, env.txt, fix.diff.

VERDICT.md
↓ download raw

DF-5026 Verification Verdict

Status: reproduced (sink-level, house precedent DF-4986/DF-5012) β€” impact dos

Reproduced

run.log (original pattern, stock guest kernel, uname in env.txt):

  1. Adapter taken "offline" (simulating ips_timeout's failed reinit, ips.c:354-358).
  2. A single pread parks its bio(s): issuing process lands in D3/physstr (biowait) β€” PID 1275.
  3. kill -9 delivered β†’ process still present 2 s later (unkillable D-state).
  4. Recovery poke drains the parked bios with EIO β†’ process exits immediately ("gone").
  5. The unload log line ("draining 6 parked bio(s)" after the poke's 7) demonstrates continued accumulation: every bio arriving while offline parks; none are ever completed by the driver pattern itself.

run_fixed_ab.log (A/B): with the fix pattern (strategy fail-fast + ips_start_io_request draining on terminal error), the same pread returns immediately r=-1 errno=5 (EIO) β€” no D-state, no residual process.

Source chain (cited)

  • ips_disk.c:128-143 β€” ipsd_strategy enqueues unconditionally; no IPS_OFFLINE/IPS_TIMEOUT gate.
  • ips_commands.c:246-247 β€” if (ips_get_free_cmd(sc, &command, 0) != 0) return; β€” the error code (EIO offline / EBUSY busy-or-resetting) is discarded; EBUSY-retry is correct behavior, EIO is terminal.
  • ips.c:207-209 (EIO when OFFLINE), ips.c:220 (EBUSY when TIMEOUT), ips.c:137 & ips.c:358 (IPS_OFFLINE setters).

Impact ceiling: with a filesystem on ipsd, every page-in/write/fsync parks β€” local DoS that wedges the box (and root-on-ipsd = whole-system hang); attacker-relevant when the hardware faults or is made to fault (malicious device / broken HBA). Bucket: base:dev (same as DF-5012/DF-2066 twins).

Fix validation

  • Behavioral: A/B above β€” the fixed pattern removes the wedge entirely (fix_baseline_reproduced=true, fix_patched_reproduced=false).
  • Compile: fix.diff (ips_disk.c fail-fast + ips_commands.c drain) applied to the guest's /usr/src copy; full ips.ko module rebuilt -Werror clean (fix_validation.log). Sources restored afterward.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Behavioral A/B at sink level (run_fixed_ab.log): baseline pattern wedges unkillably (D-state survives SIGKILL); fixed pattern returns EIO immediately with no residual process. fix.diff also compile-validated against the guest's /usr/src copy: full ips.ko module rebuilt with -Werror clean (fix_validation.log; sources restored). The real ips entry path is hardware-gated on this guest; the demonstrated failure mode and its removal are properties of the driver-side completion pattern.

run_fixed_ab.log (A/B: 'SURVIVES SIGKILL (original pattern)' vs 'r=-1 errno=5 ... no wedge process remains (fixed pattern)'); fix_validation.log (patched ips.ko -Werror clean); fix.diff
↓ fix.diffper-fix-DF-5026

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log 'PID 1275 D3 physstr' + survives 'kill -9' + 'gone' only after 'poke -- draining 7 parked bio(s) with EIO'; dmesg tail 'df5026: unload -- draining 6 parked bio(s)' (continued accumulation); run_fixed_ab.log 'wedge: pread@2097152 r=-1 errno=5(Input/output error)' + 'no wedge process remains (fixed pattern)'

PoC changes

authored fresh from source analysis (no seed); control offsets muxed in front of the verbatim strategy (DF-5012 style) so the demo self-drives and the guest returns to a clean state; unload hook drains anything still parked

Verified recommended fix

ipsd_strategy: fail-fast (B_ERROR/EIO/b_resid/biodone) when sc->state & IPS_OFFLINE; ips_start_io_request: distinguish ips_get_free_cmd errors -- keep returning on EBUSY (transient, retried on next completion) but drain-and-fail every queued bio with the error on terminal EIO (fix.diff).

Verdict

Sink-level reproduction on the stock guest kernel (DF-4986/DF-5012 house precedent): a raw D_DISK device wired to the verbatim ipsd_strategy (ips_disk.c:128-143) + ips_start_io_request (ips_commands.c:238-253) structure demonstrates that once the adapter is IPS_OFFLINE (set by ips_timeout after a failed reinit, ips.c:354-358; ips_get_free_cmd then returns EIO, ips.c:207-209), every bio is parked in bio_queue forever: the error code is swallowed at ips_commands.c:246-247 and ipsd_strategy has no fail-fast, so no biodone() ever fires. The issuing process landed in D3/physstr (biowait), SURVIVED kill -9, and was released only by an external drain (the poke) -- 7 bios parked from a single pread, and 6 more accumulated from incidental IO during the seconds before unload (unbounded queue growth). With the fix pattern transplanted into the sink (strategy fail-fast + start_io_request draining on terminal error) the same pread returns immediately r=-1 errno=5 (EIO): no D-state, no residual process. The real ips entry is hardware-gated on this guest (no ServeRAID adapter, env.txt) but the completion pattern is driver-side unconditional control flow; impact ceiling is a permanent unkillable-hang DoS for every issuer (whole-system wedge when ipsd carries root) whenever the hardware faults or is made to fault.