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

asr(4) unbounded polled waits: wait-forever ASR_resetIOP retry spin inside a critical section (asr_timeout-reachable), zero-timeout ASR_initOutBound reply poll reachable from the I2OUSRCMD ioctl, deadline-less ASR_queue_s poll, PCATCH-less ioctl wait

Summary

ASR_reset executes 'while (ASR_resetIOP(sc) == 0);' inside crit_enter() - a wedged IOP spins that CPU with interrupts blocked; ASR_initOutBound busy-loops a DMA'd status word with no timeout at all and is called directly from the I2OUSRCMD ioctl path; ASR_queue_s polls asr_intr without a deadline on every synchronous config path; ASR_queue_i waits in tsleep without PCATCH. The vendor changelog documents adapter lock-ups as an expected field condition. Transiently locked-up adapter -> CPU hang inside critical section (system-wide) or unkillable D-state ioctl threads. Bound the loops.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3825 Β· 11 files
FileTypeDescriptionSize
asr_ioctl_poll.c β€” 2.5 KB view raw
build.sh β€” 102 B view raw
run.sh β€” 107 B view raw
build.log β€” 193 B view raw
run.log β€” 396 B view raw
run.2.log β€” 396 B view raw
env.txt β€” 323 B view raw
fix.diff β€” 1.8 KB view raw
README.md β€” 3.0 KB ↓ raw
VERDICT.md β€” 2.5 KB ↓ raw
verdict.json β€” 2.7 KB view raw

DF-3825 β€” asr(4) unbounded polled waits: a transiently wedged adapter hard-hangs the kernel (wait-forever spin inside a critical section, zero-timeout reply polls, unkillable ioctl sleep)

  • File: sys/dev/raid/asr/asr.c
  • Lines: 1253-1255 (ASR_reset β€” while (ASR_resetIOP(sc) == 0); inside crit_enter()), 1892 (ASR_initOutBound β€” while (*Reply_Ptr < I2O_EXEC_OUTBOUND_INIT_REJECTED); with no timeout at all, directly reachable from the I2OUSRCMD ioctl at :3211-3219), 727-729 (ASR_queue_s β€” no-timeout interrupt poll), 3499-3523 (ASR_queue_i β€” 1 s tsleep loop without PCATCH)
  • Class: CWE-834/CWE-667 β€” unbounded busy-wait on untrusted device state β†’ system hang / unkillable threads
  • Reach: adapter lock-up (the V1.05 changelog documents the exact failure mode this driver still hangs on) or, for the initOutBound leg, root via ioctl("/dev/asr0", I2OUSRCMD).

Analysis

  1. ASR_reset() (asr.c:1226-1275) runs under crit_enter(); if the IOP fails to re-arm it executes

c 1253: /* Wait Forever */ 1254: while (ASR_resetIOP(sc) == 0);

an infinite spin β€” ASR_resetIOP itself polls MMIO/DMA with bounded delays (asr.c:577-586) but the retry loop never gives up. Reached from asr_timeout() (callout, asr.c:1297/1314) and XPT_RESET_DEV/I2ORESETCMD. A spinning thread inside a critical section blocks that CPU's interrupts β€” the whole system degrades to a watchdog hang. 2. ASR_initOutBound() (asr.c:1854-1929):

c 1892: while (*Reply_Ptr < I2O_EXEC_OUTBOUND_INIT_REJECTED);

busy-loops on a DMA'd status word with no timeout, holding the adapter's interrupts disabled. Besides boot/ASR_reset, this is reachable verbatim from user space: ASR_queue_i() special-cases function I2O_EXEC_OUTBOUND_INIT (asr.c:3211-3219) and calls it directly. 3. ASR_queue_s() (asr.c:715-733) polls asr_intr(sc) until the ccb completes β€” no deadline; a lost completion spins the caller (kernel thread / ioctl) at full tilt. Every synchronous configuration path (LCT, HRT, params, attach INQUIRY) funnels through it. 4. ASR_queue_i() (asr.c:3499-3523) waits with tsleep(..., hz) but no PCATCH and no bound β€” an unkillable D-state thread per outstanding ioctl when the card stops replying (blink-LED check is the only exit).

The changelog at asr.c:46-50 ("System locked up if the Adapter locked up") shows the vendor knew the class; these loops are the remaining instances.

Build / Run (guest trigger)

cc -O2 -Wall -o asr_ioctl_poll asr_ioctl_poll.c
./asr_ioctl_poll /dev/asr0

Expected on hardware: issuing I2O_EXEC_OUTBOUND_INIT while the IOP is deaf wedges the caller in the zero-timeout loop. Observed on the audit guest: open("/dev/asr0") β†’ ENOENT β€” no I2O HBA present (root and unprivileged, see run.log/run.2.log; env.txt).

Verdict

not_reproduced β€” hardware-gated (device absent); source-certain.

VERDICT.md
↓ download raw

VERDICT β€” DF-3825

Status: not_reproduced (the audit guest has no DPT/Adaptec I2O controller: /dev/asr0 β†’ ENOENT for both root and an unprivileged user; zero PCI devices with vendor 0x1044; the asr driver is compiled into X86_64_GENERIC but never attaches). Source-level confidence: certain.

The four unbounded waits

# site loop caller context effect when adapter is deaf
1 asr.c:1254 (ASR_reset) while (ASR_resetIOP(sc) == 0); crit_enter() region; reached from asr_timeout callout (:1297/:1314), XPT_RESET_DEV (:2744), I2ORESETCMD ioctl (:3793) infinite spin with interrupts blocked on that CPU β†’ system-wide hang/watchdog
2 asr.c:1892 (ASR_initOutBound) while (*Reply_Ptr < I2O_EXEC_OUTBOUND_INIT_REJECTED); β€” zero timeout boot, ASR_reset, and directly the I2OUSRCMD ioctl via ASR_queue_i (:3211-3219) unbounded busy loop on a DMA'd word with card interrupts masked
3 asr.c:727 (ASR_queue_s) while (status == CAM_REQ_INPROG) (void)asr_intr(sc); every synchronous config path (LCT/HRT/params/attach inquiry) full-spin poll, no deadline
4 asr.c:3499-3523 (ASR_queue_i) tsleep(ccb, 0, "asr", hz) loop, no PCATCH I2OUSRCMD ioctl (root) unkillable D-state thread per wedged ioctl

The driver's own changelog (asr.c:46-50, V1.05: "System locked up if the Adapter locked up ... The fail outstanding command discovery loop was flawed") establishes that transient IOP lock-ups are an expected field condition, not an attacker fiction.

Guest run

Trigger built on the guest (cc 8.3, build.log) and executed as root and as user maxx: both exit(2) with open("/dev/asr0"): No such file or directory β€” honest proof the surface is hardware-gated here (run.log, run.2.log, env.txt).

Fix

fix.diff: - bounds the ASR_resetIOP retry loop (60 attempts), - gives the initOutBound reply poll a ~3 s deadline, - gives ASR_queue_s a ~30 s budget after which the ccb fails with CAM_CMD_TIMEOUT instead of spinning.

Fix validation

not_testable β€” exercising any leg requires an IOP that stops replying; no asr hardware on the guest.

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

Both baseline and patched behavior require a physically present, non-responding IOP; absent on the guest. fix.diff adds the three bounds.

fix.diff
↓ fix.diffper-fix-DF-3825

Confirmed kernel references

Detail

Exploit chain

wedges: adapter stops replying during asr_timeout/ASR_reset -> infinite spin in crit section -> CPU interrupts blocked -> system hang; or root I2OUSRCMD with I2O_EXEC_OUTBOUND_INIT -> ASR_initOutBound zero-timeout poll -> unkillable thread.

Evidence (decisive lines)

run.log + run.2.log (trigger ENOENT as root and user); build.log (cc 8.3 on guest); env.txt (no vendor-0x1044 PCI devices); VERDICT.md table of the four sites; fix.diff (bounded retries/deadlines)

PoC changes

Trigger written fresh: I2OUSRCMD = _IO('D',76) per osd_unix.h, arg passed as pointer-to-pointer per asr.c:3789.

Verified recommended fix

Bound every polled wait: retry cap in ASR_reset, ~3s deadline in ASR_initOutBound, ~30s budget + CAM_CMD_TIMEOUT in ASR_queue_s (asr.c:727,1254,1892).

Verdict

Four unbounded waits on adapter state: ASR_reset spins 'while (ASR_resetIOP(sc) == 0);' inside crit_enter() (asr.c:1254) reachable from the asr_timeout callout and I2ORESETCMD; ASR_initOutBound busy-loops a DMA'd status word with zero timeout (asr.c:1892) and is directly reachable from the I2OUSRCMD ioctl (asr.c:3211-3219); ASR_queue_s polls asr_intr with no deadline (asr.c:727) on every synchronous config path; ASR_queue_i tsleeps without PCATCH (asr.c:3499-3523). A transiently wedged IOP - a field condition the driver changelog itself documents (asr.c:46-50) - therefore hard-hangs a CPU inside a critical section or strands unkillable D-state ioctl threads. Trigger built and run on the guest as root and unprivileged: open('/dev/asr0') -> ENOENT (no I2O HBA), so the path cannot be entered; source analysis certain.