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)
PoC verification
Evidence pack
findings/poc/DF-5026 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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):
- Adapter taken "offline" (simulating ips_timeout's failed reinit, ips.c:354-358).
- A single
preadparks its bio(s): issuing process lands inD3/physstr(biowait) β PID 1275. kill -9delivered β process still present 2 s later (unkillable D-state).- Recovery poke drains the parked bios with EIO β process exits immediately ("gone").
- 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_strategyenqueues 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-Werrorclean (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
fixedBehavioral 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
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.
No comments yet.