ahci_beg_exclusive_access() drain loop never processes deferred timeouts -> permanent port wedge with the port lock held
Summary
The exclusive-access drain loop spins on ap_active/ap_sactive calling only ahci_port_intr(); a deferred-expired CCB (timeout fired while other commands active, CCB not completed) can never be retired because its hw CI/SACT bit never clears and ahci_check_active_timeouts() is never called - even though ahci_poll()'s equivalent loop calls it for exactly this reason (callout deadlocked under port lock). Result: infinite spin holding ap->ap_lock; the ISR then deflects all port interrupts to the also-blocked port thread - port permanently dead, kernel thread wedged. PM with >=2 targets; hang target 0 mid-read + hot-(re)plug target 1 -> permanent soft hang of the SATA port. Hardware-conditioned DoS. One-line fix mirroring ahci_poll.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3773 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 2.3 KB | β raw | |
| env.txt | β | 551 B | view raw | |
| fix.diff | β | 621 B | view raw | |
| verdict.json | β | 2.1 KB | view raw |
DF-3773 β ahci_beg_exclusive_access() drain loop never processes deferred timeouts β permanent port-thread wedge with the port lock held
File: sys/dev/disk/ahci/ahci.c (pass 2, GLM 5.3)
What the bug is
void
ahci_beg_exclusive_access(struct ahci_port *ap, struct ata_port *at)
{
KKASSERT((ap->ap_flags & AP_F_EXCLUSIVE_ACCESS) == 0);
ap->ap_flags |= AP_F_EXCLUSIVE_ACCESS;
while (ap->ap_active || ap->ap_sactive) { /* ahci.c:2266 */
ahci_port_intr(ap, 1);
ahci_os_softsleep();
}
}
The loop assumes every active command will eventually complete via
ahci_port_intr(). That is false for a CCB whose timeout has fired but was
deferred: ahci_ata_cmd_timeout() (ahci.c:3873-3884) sets
ap->ap_expired and returns without completing whenever the port is still
running and other non-expired commands are active. The only other places a
deferred-expired CCB gets processed are ahci_check_active_timeouts()
(ahci.c:2192-2207) and the completion walk in ahci_port_intr (ahci.c:3186) β
neither of which can retire a CCB whose hardware CI/SACT bit is still set
(device hung: ~ci_saved & *active never includes it, ahci.c:3152).
The driver knows this and handles it everywhere else it spins:
ahci_poll() calls ahci_check_active_timeouts(ap) inside its own wait loop
(ahci.c:2160-2163, comment: "When polling we have to check if the currently
active CCB(s) have timed out as the callout will be deadlocked while we hold
the port lock"). ahci_beg_exclusive_access() β which holds the same port
lock and can spin for the same reasons β does not.
Consequence
ahci_beg_exclusive_access() is called from the PM state machine
(ahci.c:938) which runs under the port lock from ahci_xpt_action
XPT_ENG_EXEC (ahci_cam.c:888-890) or the port thread's initial probe
(ahci_dragonfly.c:375). If a command to a PM target is deferred-expired and
the device never completes it:
- the loop spins forever holding
ap->ap_lock; - every subsequent I/O to the port blocks on that lock;
ahci_intr()can no longer take the lock non-blocking, so it defers all port interrupts to the port thread (ahci.c:2514-2517) β which also blocks on the lock β the port is permanently dead and a kernel thread is wedged.
Preconditions: port multiplier attached, a command to one target hangs
(timeout deferred), then a hot-plug/state-machine event on another target
enters ahci_beg_exclusive_access. Hardware-dependent DoS β Low.
Why not reproduced on the guest
No AHCI controller and no PM on the audit guest (env.txt). Source-level logic is definite; reachability requires a hung SATA device behind a PM.
Reproduce on suitable hardware
PM with β₯2 targets; target 0 disk hangs mid-read (its timeout defers while
target 1 has an in-flight command β or vice versa); then hot-(re)plug target 1
so the state machine calls ahci_beg_exclusive_access. Observe the port thread
spin at 100% in the loop with ap_expired != 0 and the port lock held.
DF-3773 VERDICT β not_reproduced (no AHCI/PM hardware on guest); logic gap definite by source
Bottom line
ahci_beg_exclusive_access() (ahci.c:2262-2270) spins on
while (ap->ap_active || ap->ap_sactive) calling only ahci_port_intr(). A
deferred-expired CCB β one whose timeout fired while other commands were active
(ap->ap_expired set, CCB not completed: ahci.c:3873-3884) β can never be
retired by that loop if the device never completes it, because
ahci_port_intr() only retires slots whose hardware CI/SACT bit cleared
(ahci.c:3152) and ahci_check_active_timeouts() is never called. The driver
demonstrates the correct discipline in ahci_poll()'s wait loop
(ahci.c:2160-2163), whose comment states the exact reason: the callout cannot
run while the port lock is held. ahci_beg_exclusive_access() holds the port
lock too (callers: ahci.c:938 via the state machine, run under ap_lock from
ahci_cam.c:888-890 / ahci_dragonfly.c:375), so the deferred timeout is
deadlocked by construction and the loop never exits.
Result: permanent wedge of the calling thread with ap->ap_lock held; the port
interrupt handler then deflects all port interrupts to the port thread
(ahci.c:2514-2517) which also blocks on the lock β the port is dead and one
kernel thread spins forever. DoS only (no memory unsafety). Preconditions:
port multiplier + hung target + deferred timeout + PM state-machine event.
Not-reproduced rationale
The audit guest has no AHCI controller at all (env.txt: pciconf shows none, root on virtio-blk, ahci static in GENERIC). The path needs real AHCI + PM hardware misbehavior. Source reasoning is line-accurate and the asymmetry with ahci_poll() is unambiguous.
Recommended fix
Add timeout processing to the drain loop, mirroring ahci_poll():
while (ap->ap_active || ap->ap_sactive) {
ahci_port_intr(ap, 1);
ahci_os_softsleep();
ahci_check_active_timeouts(ap); /* NEW: retire deferred-expired CCBs */
}
see fix.diff (one line + comment).
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_testableOne-line fix authored; no hardware to validate against.
fix.diff in this pack.
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md; ahci.c:2262-2270 (loop without timeout processing) vs ahci.c:2154-2168 (ahci_poll loop WITH it); ahci.c:3873-3884 (deferred timeout); ahci.c:3152 (completion walk cannot retire hung slots); ahci.c:938 (caller under port lock)
PoC changes
No runtime PoC possible - needs AHCI + port-multiplier hardware with a hung target.
Verified recommended fix
Call ahci_check_active_timeouts(ap) inside the beg_exclusive_access drain loop (one line), mirroring ahci_poll().
Verdict
ahci_beg_exclusive_access() (ahci.c:2262-2270) drains active commands with only ahci_port_intr(), but a deferred-expired CCB (timeout fired while other commands active - ap->ap_expired set, CCB not completed, ahci.c:3873-3884) can never be retired by that loop because its hardware CI/SACT bit never clears and ahci_check_active_timeouts() is never called - even though ahci_poll()'s equivalent loop does call it (ahci.c:2160-2163) for exactly this reason. The loop then spins forever holding the port lock; the port interrupt handler deflects to the also-blocked port thread (ahci.c:2514-2517), permanently wedging the port and a kernel thread. Requires PM hardware plus a hung target - DoS only. Not reproduced: guest has no AHCI controller.
No comments yet.