DHRS-error path bypasses the blockable filter: blocking error recovery (tsleep-based pwaits, 1-2s each) executes in hard-interrupt context
Summary
ahci_port_intr(ap, blockable==0) from ahci_intr() carefully defers blocking error sources to the port thread via blockable_mask ('If we can't block then we can't handle these here'). DHRS is deliberately not in that mask because normal DHRS handling is non-blocking - but its error sub-path (D2H FIS with ERR and ST clear) does 'goto process_error', executing ahci_port_stop (1s tsleep), ahci_port_clo (1s), and for NCQ ahci_port_start (2s) + ahci_port_read_ncq_error (1s) inside the PCI interrupt thread. DFly tsleep has no interrupt-context guard, so the interrupt thread deschedules for seconds with the port lock held, stalling every device sharing the IRQ vector. Repeatable device-triggered latency/DoS, bounded by pwait timeouts; no memory unsafety. Fix: defer to the port thread when blockable==0.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3775 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 2.2 KB | β raw | |
| env.txt | β | 551 B | view raw | |
| fix.diff | β | 802 B | view raw | |
| verdict.json | β | 2.2 KB | view raw |
DF-3775 β DHRS-error path bypasses the blockable filter and runs blocking error recovery (tsleep, 1β2 s polls) in hard-interrupt context
File: sys/dev/disk/ahci/ahci.c (pass 2, GLM 5.3)
What the bug is
ahci_port_intr(ap, blockable) is called with blockable == 0 from the real
interrupt handler (ahci_intr() β ahci_os_lock_port_nb() success,
ahci.c:2511-2513). For that case the driver carefully filters the interrupt
sources that need blocking recovery and defers them to the port thread:
if (blockable == 0) {
...
if (is & blockable_mask) { /* TFES|IFS|PCS|PRCS|HBFS|OFS|UFS */
ahci_pwrite(ap, AHCI_PREG_IE, 0);
ahci_os_signal_port_thread(ap, AP_SIGF_PORTINT);
return; /* ahci.c:2602-2606 β defer */
}
}
AHCI_PREG_IS_DHRS is deliberately not in blockable_mask (ahci.c:2565-2571)
because the normal DHRS handling is non-blocking. However the DHRS error
sub-path is fully blocking:
} else if (is & AHCI_PREG_IS_DHRS) {
...
if ((tfd & AHCI_PREG_TFD_STS_ERR) &&
(cmd & AHCI_PREG_CMD_ST) == 0) {
err_slot = ...;
goto process_error; /* ahci.c:2851 */
}
process_error (ahci.c:2649-2807) then executes, in the PCI interrupt
thread:
ahci_port_stop()(2671) βahci_pwait_clr(CR)βahci_pwait_eq()(ahci.c:3612-3632): after a 100 Β΅s hard loop it callsahci_os_softsleep()=tsleep()for up to AHCI_PWAIT_TIMEOUT (1000 ms);ahci_os_hardsleep(10)(2672) and possiblyahci_port_clo()(2675, another 1 s pwait);- for NCQ:
ahci_port_start()(2717, up to 2000 ms pwait) thenahci_port_read_ncq_error()(2718) βahci_poll()with 1000 ms of softsleeps, thenahci_port_stop()again (2719).
DragonFly's tsleep() has no interrupt-context guard (verified:
sys/kern/kern_synch.c β no TDF_INTTHREAD check), so the interrupt thread is
simply descheduled for seconds with the port lock held. On a shared IRQ
vector every other device on that vector is stalled for the duration; if the
timed-out register never clears, each pwait burns its full 1β2 s budget.
The design intent (the whole point of blockable_mask) is that none of this
runs in ISR context β the DHRS-error goto silently violates it.
Impact
Interrupt-thread stalls of up to ~5 s per occurrence with the port lock held; repeatable by any SATA device that raises a D2H FIS with ERR while the command processor is stopped (e.g. a device error coincident with a port stop β the same conditions CAM error recovery produces). Degraded-but-recoverable DoS / latency hazard, not memory unsafety β Low.
Why not reproduced on the guest
No AHCI controller on the audit guest (env.txt) β the interrupt path cannot be entered. Source analysis is line-accurate.
DF-3775 VERDICT β not_reproduced (no AHCI hardware on guest); blocking-in-ISR logic violation certain by source
Bottom line
The blockable filter at ahci.c:2596-2607 exists precisely so that error
recovery β which sleeps via ahci_os_softsleep()/tsleep() inside
ahci_pwait_eq() (ahci.c:3612-3632) and ahci_poll() β never runs in the
interrupt handler (ahci_intr() β ahci_port_intr(ap, 0), ahci.c:2511-2513).
The DHRS error sub-path (ahci.c:2843-2852: D2H FIS with ERR while ST is clear)
jumps straight into process_error (ahci.c:2649+) and thereby executes
ahci_port_stop (1 s pwait), ahci_port_clo (1 s pwait), for NCQ
ahci_port_start (2 s pwait) + ahci_port_read_ncq_error (1 s poll) β all in
the PCI interrupt thread, descheduling it (DFly tsleep has no
interrupt-context guard: sys/kern/kern_synch.c contains no TDF_INTTHREAD
check) with the port lock held and stalling any device sharing the IRQ vector.
Device-triggered (SATA device raising D2H ERR with the command processor stopped); latency/DoS only, bounded by the pwait timeouts. Low.
Not-reproduced rationale
Guest has no AHCI controller (env.txt). The path requires a real AHCI interrupt. Line-accurate source reasoning; the contradiction between the filter's comment/purpose ("If we can't block then we can't handle these here" ahci.c:2583-2587) and the DHRS-error goto is unambiguous.
Recommended fix
Make the DHRS error sub-path respect blockable: when blockable == 0, disable
the port interrupt and defer to the port thread exactly like the
blockable_mask path does (the state needed β tfd/cmd β is re-read in
process_error anyway):
if ((tfd & AHCI_PREG_TFD_STS_ERR) &&
(cmd & AHCI_PREG_CMD_ST) == 0) {
if (blockable == 0) {
ahci_pwrite(ap, AHCI_PREG_IE, 0);
ahci_os_signal_port_thread(ap, AP_SIGF_PORTINT);
return;
}
...goto process_error;
}
see fix.diff.
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_testableDeferral patch authored; no AHCI hardware to validate against.
fix.diff in this pack.
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md; ahci.c:2596-2607 (filter) vs ahci.c:2843-2852 (bypass); blocking calls at ahci.c:2671-2719; ahci.c:3612-3632 + ahci_dragonfly.c:221-230 (softsleep=tsleep inside pwaits); sys/kern/kern_synch.c tsleep (no interrupt-thread guard)
PoC changes
No runtime PoC - requires real AHCI interrupt with device-raised D2H ERR while ST clear.
Verified recommended fix
In the DHRS error sub-path, when blockable==0 disable port interrupts (PREG_IE=0) and defer to the port thread (AP_SIGF_PORTINT) instead of goto process_error - mirroring the blockable_mask deferral.
Verdict
The blockable filter in ahci_port_intr() (ahci.c:2596-2607) defers blocking error sources to the port thread when called with blockable==0 from ahci_intr() (ahci.c:2511-2513), but the DHRS error sub-path (ahci.c:2843-2852) jumps directly into process_error (ahci.c:2649-2807), which sleeps: ahci_port_stop (1s pwait), ahci_port_clo (1s), and for NCQ ahci_port_start (2s) + ahci_port_read_ncq_error (1s poll) - all executed in the PCI interrupt thread (DFly tsleep has no interrupt-context guard, sys/kern/kern_synch.c), descheduling it with the port lock held and stalling shared IRQ devices for seconds. Device-triggered latency/DoS only, bounded by pwait timeouts. Not reproduced: guest has no AHCI controller.
No comments yet.