ahci_pci_detach() has no interlock against an in-flight ahci_intr() on another CPU β hardirq-context UAF of sc_ports[]/ap and access to the already-unmapped ABAR during controller detach
Summary
Detach clears AHCI_F_INT_GOOD, tears down intr, then immediately kfrees every port and unmaps the ABAR - but DFly unregister_int never drains a handler running on another CPU, so an ahci_intr past the flag check walks kfree'd ports/CCBs and touches unmapped MMIO. In-source XXX admits it. Root/physical trigger only (kldunload/devctl detach/hotplug surprise-removal); hw-gated (no AHCI on guest). Active-count+drain fix (atomic inc/recheck/dec + wakeup, detach waits).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4744 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.9 KB | β raw | |
| VERDICT.md | β | 8.2 KB | β raw | |
| trigger_devctl_detach.sh | β | 1.3 KB | view raw | |
| fix.diff | β | 2.6 KB | view raw | |
| env.txt | β | 818 B | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 3.9 KB | view raw |
DF-4744 β ahci_pci_detach() lacks an interlock against an in-flight ahci_intr() on another CPU
File under audit: sys/dev/disk/ahci/ahci_attach.c (pass 2)
What the bug is
ahci_pci_detach() (sys/dev/disk/ahci/ahci_attach.c:580-647) tears a live AHCI
controller down in this order:
sc->sc_flags &= ~AHCI_F_INT_GOOD(ahci_attach.c:592)ahci_write(sc, AHCI_REG_GHC, 0)(ahci_attach.c:594)bus_teardown_intr(...)(ahci_attach.c:596-599)- free every port:
ahci_cam_detach(ap); ahci_port_free(sc, i)β kfree ofap, its CCB array, DMA maps,atarray (ahci_attach.c:604-610 β ahci.c:979-1044) bus_release_resource(SYS_RES_IRQ)andbus_release_resource(SYS_RES_MEMORY)β the ABAR MMIO mapping is destroyed (ahci_attach.c:615-628)
ahci_intr() (sys/dev/disk/ahci/ahci.c:2472-2502) guards itself with a single
non-atomic check of AHCI_F_INT_GOOD and then walks sc->sc_ports[], reads
ABAR registers and processes CCBs.
The interlock between (1) and (4) is missing. DragonFly's
bus_teardown_intr() β pci_teardown_intr() (sys/bus/pci/pci.c:3384, masks
INTx/MSI at the PCI config level) β nexus_teardown_intr() β
unregister_int() (sys/kern/kern_intr.c:416-497) unlinks the handler under a
local critical section and returns; it never waits for a handler that is
already executing on another CPU. Masking the vector at the interrupt
controller does not affect a handler that has already been entered.
So a CPU that entered ahci_intr() just before step (1) can still be inside
the handler body while steps (4)/(5) free every structure the handler touches
and unmap the register window it is about to read:
ahci_read(sc, AHCI_REG_IS)after (5) β read of unmapped MMIO,sc->sc_ports[port]/ahci_port_intr(ap, ...)after (4) β use-after-free ofstruct ahci_port(kfree'd at ahci.c:1041) in hard-interrupt context,ahci_os_lock_port_nb(ap)on a kfree'd/freed lock (lockuninit'd inahci_os_stop_port, ahci_dragonfly.c:297).
The driver authors know: XXX interlock last interrupt? at ahci_attach.c:589-591.
Reachability / threat
Trigger requires a controller detach while the controller is generating interrupts:
kldunload ahci.ko(ordevctl detach ahciN) β root only, or- physical surprise-removal (express-card/thunderbolt-style AHCI HBA) β physical access.
There is no unprivileged-local or remote trigger. Race window is the duration of the handler body vs. the teardown sequence β needs an interrupt to land during detach of an active controller (I/O in flight).
Impact ceiling: kernel use-after-free in hardirq context (panic; potentially worse given a sufficiently controllable heap), but only under privileged or physical action β Low per the audit rubric.
Why this pack has no run logs (status: untested)
The audit QEMU guest has no AHCI controller: pciconf -l shows no SATA/AHCI
class device (root disk is if=virtio on an i440fx machine; verified in
env.txt). ahci_pci_detach therefore cannot be entered on the guest at all.
The race itself additionally requires interrupt traffic during the teardown
window. A deterministic demonstration needs (a) a real/instantiated AHCI
controller with a drive under I/O and (b) an instrumented kernel (artificial
delay in ahci_intr after the flag check) β a recipe is given in VERDICT.md
for the team to run on iron that has the hardware. Per contract, Low-severity
privileged-trigger findings do not mandate guest verification; nothing here is
claimed as reproduced.
Contents
VERDICT.mdβ full narrative + line-by-line trace + repro recipetrigger_devctl_detach.shβ the detach-race exerciser for real hardware (NOT RUN on this guest; no AHCI controller present)fix.diffβ proposed driver-side interlock (NOT applied to sys/)env.txtβ guest environment proof (no ahci device)manifest.json,verdict.json
DF-4744 VERDICT β ahci_pci_detach() / in-flight ahci_intr() interlock missing
Machine verdict: untested (reproduced=0, impact=none observed).
Classification of the vulnerability itself (source-proven): race β
hard-interrupt-context use-after-free / unmapped-MMIO read; trigger is
privileged (root detach) or physical (surprise removal) β severity Low,
confidence likely (mechanism fully provable from source; runtime window
not demonstrable on this guest β no AHCI hardware).
1. Full source trace
The handler side β what an in-flight ahci_intr() touches
ahci_intr() β sys/dev/disk/ahci/ahci.c:2472:
void ahci_intr(void *arg)
{
struct ahci_softc *sc = arg;
...
if ((sc->sc_flags & AHCI_F_INT_GOOD) == 0) // ahci.c:2482 β the ONLY guard
return;
is = ahci_read(sc, AHCI_REG_IS); // ahci.c:2484 β ABAR MMIO read
if (is == 0 || is == 0xffffffff) return;
is &= sc->sc_portmask;
ahci_write(sc, AHCI_REG_IS, is); // ABAR MMIO write
for (ack = 0; is; is &= ~(1 << port)) {
port = ffs(is) - 1;
ap = sc->sc_ports[port]; // ahci.c:2497 β walks port array
if (ap == NULL) continue;
if (ahci_os_lock_port_nb(ap) == 0) { // lockmgr on ap->ap_lock
ahci_port_intr(ap, 0); // CCBs, DMA maps, port regs...
ahci_os_unlock_port(ap);
} else {
ahci_pwrite(ap, AHCI_PREG_IE, 0); // ap MMIO
ahci_os_signal_port_thread(ap, AP_SIGF_PORTINT); // ap fields
}
}
}
Everything after the single flag check reads the ABAR mapping (sc_iot/sc_ioh
out of sc_regs) and the per-port heap objects. Note the else branch also
dereferences ap β grabbing the port lock first does not shield the
teardown.
The teardown side
ahci_pci_detach() β sys/dev/disk/ahci/ahci_attach.c:580:
sc->sc_flags &= ~AHCI_F_INT_GOOD; // :592
if (sc->sc_regs)
ahci_write(sc, AHCI_REG_GHC, 0); // :594
if (sc->sc_irq_handle) {
bus_teardown_intr(dev, sc->sc_irq, sc->sc_irq_handle); // :597
sc->sc_irq_handle = NULL;
}
for (i = 0; i < AHCI_MAX_PORTS; i++) { // :604
ap = sc->sc_ports[i];
if (ap) {
ahci_cam_detach(ap);
ahci_port_free(sc, i); // kfrees ap and everything in it
}
}
... bus_release_resource(irq) ... // :615
... bus_release_resource(SYS_RES_MEMORY) ... // :624 β ABAR unmapped
Why bus_teardown_intr() does not save you (the crux)
Chain on DragonFly:
bus_teardown_intrβpci_teardown_intr(sys/bus/pci/pci.c:3384): masks INTx (PCIM_CMD_INTxDIS) or disables MSI at the PCI config level, then delegates tobus_generic_teardown_intrβ nexus.unregister_int(sys/kern/kern_intr.c:416-497): undercrit_enter()on the calling CPU it unlinks theintrec, callsmachintr_intr_teardown()if this was the last handler (masks the vector β ioapic_abi.c:681 / icu_abi.c:282),crit_exit(), thenkfree(rec).
There is no wait anywhere in this chain for a handler already executing on
another CPU. A critical section only guards the local CPU; masking a vector
does not affect an entered handler. (Compare FreeBSD's intr_event teardown,
which drains active handlers β DFly has no equivalent.)
The interleaving that breaks it
CPU0 (detach thread) CPU1 (hardirq)
--------------------------------------- ------------------------------------
<intr fires; ahci_intr entered>
sc_flags & AHCI_F_INT_GOOD != 0 (pass)
sc_flags &= ~AHCI_F_INT_GOOD (:592) ...
ahci_write(GHC, 0) (:594) ...
bus_teardown_intr (:597) ...
(no drain; returns) is = ahci_read(sc, AHCI_REG_IS)
ahci_port_free() x N (:604-610) ap = sc->sc_ports[port] β UAF
kfree(ap), free maps/CCBs ahci_port_intr(ap, 0) / pwrite(ap,...)
bus_release_resource(MEMORY) (:624) ahci_read/ahci_write ... β unmapped MMIO
The in-source XXX interlock last interrupt? (ahci_attach.c:589-591) shows
this is a known-but-unhandled gap.
What is NOT affected (checked and killed)
- The attach-failure unwind paths that call
ahci_pci_detachmid-attach (ahci_attach.c:303, 317, 328, 352, 431, 523): in every one of them eitherbus_setup_intrhas not run yet (no handler registered β no race) or no ports exist yet. Verified individually. - Port-thread lifetime vs
ahci_port_free:ahci_os_stop_port(ahci_dragonfly.c:279-298) signalsAP_SIGF_STOPand waits forap->ap_threadto clear before the kfrees β thread side is interlocked. - The interrupt-vs-port-init race at attach is handled correctly:
bus_setup_intrat :515 runs beforeAHCI_F_INT_GOODis set at :546 and before GHC_IE is enabled at :545; the port-thread sync loop (:533-538) precedes the master enable. No window. - MSI release on early failure:
pci_alloc_1intrat :296 can allocate MSI beforebus_alloc_resource_anyfails;ahci_pci_detach:621-622 callspci_release_msiβ covered. - Partial DMA-tag failure unwind: softc is zero-allocated; detach NULL-checks and NULLs every tag (:630-645) β idempotent, no double destroy.
1 << iport map walk with i<32 (AHCI_MAX_PORTS=32, ahci.h:400): fits.- Per-port sysctl tree (
link_pwr_mgmt,link_pwr_state, ahci_dragonfly.c:250-267) issysctl_ctx_free'd inahci_os_stop_portbeforekfree(ap)β no dangling sysctl handler (unlike the DF-4647 pattern). ahci_lookup_device(ahci_attach.c:128-161) matches listed VID/DID entries without checking PCI class β binds ahci to e.g. IDE-mode MCP controllers; hardware-selection quirk, not a security boundary (a malicious PCIe device is already a DMA attacker). Not filed.- Adjacent-file observation (not this file, ahci.c already audited pass 2):
ahci_initleaks its temporaryap(ahci.c:186 alloc, ahci.c:197-201return (1)without kfree) whenbus_space_subregionfails β one-time few-KB boot-time leak on broken hardware.
2. Why untested (not run on the guest)
pciconf -l in the audit guest (env.txt) contains no SATA/AHCI class
device β the guest boots from if=virtio storage on an i440fx machine, so
ahci_pci_attach/ahci_pci_detach are never even entered. Even with a
controller, the window requires an interrupt to land inside the
microseconds-scale teardown of an active controller.
3. Reproduction recipe for hardware that has AHCI (for the team)
- Instrument a debug kernel: in
ahci_intr(ahci.c:2482), after theAHCI_F_INT_GOODcheck, addDELAY(100000);(100 ms) to hold the handler inside the window deterministically. - Boot it on a machine whose boot disk is NOT on the ahci controller under
test; put a secondary AHCI disk under streaming I/O (
ddfrom the raw device). devctl detach ahciN(orkldunload ahci) while the I/O runs.- Expected on an unpatched kernel: panic in hardirq context (NULL/UAF deref
in
ahci_intr/ahci_port_intr, or a fault on the unmapped ABAR) within seconds. Withfix.diffapplied: clean detach, no panic.
trigger_devctl_detach.sh in this pack encodes steps 2-3.
4. Fix
fix.diff β driver-side interlock (authoritative recommendation is to also
give unregister_int() a real drain, FreeBSD-intr_event-style; the driver
patch stands alone and is self-contained):
ahci.h: addu_int sc_intr_active; u_int sc_intr_wakeup;to the softc.ahci.c ahci_intr(): after passing the flag check, atomically incrementsc_intr_active, re-check the flag, bail (decrement + wakeup) if teardown began; on body exit decrement and wakeup.ahci_attach.c ahci_pci_detach(): afterbus_teardown_intr(), issue a full memory fence (cpu_mfence()) and wait forsc_intr_activeto drain before freeing ports / releasing the register mapping.
Fix validation was not possible on this guest (no AHCI controller; no
baseline reproduction possible): fix_status: not_testable,
fix_baseline_reproduced: 0, fix_patched_reproduced: 0.
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_testablefix.diff (driver-side active-handler interlock with cpu_mfence + drain wait in ahci_pci_detach) was authored from the line-accurate root cause but could not be validated: with no AHCI controller on the guest there is no baseline behavior to compare against, and a kernel rebuild would prove nothing the source trace does not.
findings/poc/DF-4744/fix.diff (three-hunk proposal: ahci.h softc fields, ahci_intr() in-flight counting with flag re-check, ahci_pci_detach() drain wait before port frees).
Confirmed kernel references
- sys/dev/disk/ahci/ahci_attach.c:589
- sys/dev/disk/ahci/ahci_attach.c:592
- sys/dev/disk/ahci/ahci_attach.c:596
- sys/dev/disk/ahci/ahci_attach.c:604
- sys/dev/disk/ahci/ahci_attach.c:624
- sys/kern/kern_intr.c:416
- sys/dev/disk/ahci/ahci.c:2472
- sys/dev/disk/ahci/ahci.c:2482
- sys/bus/pci/pci.c:3384
- sys/dev/disk/ahci/ahci.c:979
Detail
Evidence (decisive lines)
VERDICT.md carries the full interleaving trace (CPU0 detach steps :592/:597/:604/:624 vs CPU1 handler body ahci.c:2484-2501), the teardown-chain proof (pci_teardown_intr sys/bus/pci/pci.c:3384 -> unregister_int kern_intr.c:416 with no drain), the hardware-absence proof (env.txt), and the instrumented-kernel reproduction recipe for real iron (Β§3). trigger_devctl_detach.sh encodes the exerciser; fix.diff is the proposed interlock.
PoC changes
No seed PoC existed; none could be built β the target code path (ahci_pci_detach) is unreachable on the audit guest (no AHCI controller). A hardware exerciser script and an instrumented-kernel recipe were authored instead and clearly marked NOT RUN.
Verified recommended fix
Drain in-flight handlers before freeing ports: add an sc_intr_active counter (inc, re-check AHCI_F_INT_GOOD, dec+wakeup) around ahci_intr()'s body and have ahci_pci_detach() mfence+wait for the count to hit zero after bus_teardown_intr(); longer term give unregister_int() a real drain like FreeBSD's intr_event teardown.
Verdict
Source-proven race, not runtime-demonstrable on this guest: ahci_pci_detach() (ahci_attach.c:580) clears AHCI_F_INT_GOOD, calls bus_teardown_intr() and then immediately kfrees every port (ahci.c:979) and unmaps the ABAR (ahci_attach.c:624). DragonFly's unregister_int() (kern_intr.c:416-497) unlinks and masks the vector but never drains a handler already executing on another CPU, so an ahci_intr() that passed the single flag check at ahci.c:2482 can walk kfree'd sc_ports[] entries and read/write the unmapped register window β a hard-interrupt-context UAF acknowledged in-source by 'XXX interlock last interrupt?' (ahci_attach.c:589). Trigger requires root (kldunload/devctl detach) or physical surprise-removal, and the audit guest has no AHCI controller at all (pciconf: no class 0x0106 device, storage is virtio β env.txt), so no run was possible; hence untested/reproduced=0 rather than any reproduced claim.
No comments yet.