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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3775 Β· 5 files
FileTypeDescriptionSize
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 calls ahci_os_softsleep() = tsleep() for up to AHCI_PWAIT_TIMEOUT (1000 ms);
  • ahci_os_hardsleep(10) (2672) and possibly ahci_port_clo() (2675, another 1 s pwait);
  • for NCQ: ahci_port_start() (2717, up to 2000 ms pwait) then ahci_port_read_ncq_error() (2718) β†’ ahci_poll() with 1000 ms of softsleeps, then ahci_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.

VERDICT.md
↓ download raw

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.

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_testable
baseline no→ patch + rebuild →patched clean

Deferral patch authored; no AHCI hardware to validate against.

fix.diff in this pack.
↓ fix.diffper-fix-DF-3775

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.