ahci_port_free() teardown use-after-free: armed per-CCB timeout callouts never cancelled and in-flight CCBs never failed before kfree
Summary
ahci_port_free() frees ap_ccbs, DMA memory and the ahci_port without cancelling armed ccb->ccb_timeout callouts (armed for every non-poll command with 30-60s CAM timeouts) and without failing CCBs still active in ap_active/ap_sactive - unlike ahci_port_hardstop() which performs exactly this cleanup on hot-unplug. A late callout executes ahci_ata_cmd_timeout_unserialized(): a UAF write of ccb->ccb_xa.flags into the freed ap_ccbs block plus lockmgr/wakeup on the freed ahci_port. kldunload ahci.ko while a command is on-chip (hung/slow device) leaves a 30-60s callout pointing at freed kernel heap -> kernel UAF write / panic. Root-privileged trigger, hence Low. Not runnable on the guest (no AHCI controller, ahci static in GENERIC so detach unreachable).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3771 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| env.txt | β | 551 B | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| verdict.json | β | 2.6 KB | view raw |
DF-3771 β ahci_port_free(): armed CCB timeout callouts + in-flight CCBs not cancelled/drained on detach β use-after-free
File: sys/dev/disk/ahci/ahci.c (pass 2, GLM 5.3)
What the bug is
ahci_port_free() (ahci.c:979-1046) tears a port down on driver detach but:
-
Never cancels the per-CCB timeout callouts. Every non-polled command arms
ccb->ccb_timeoutviaahci_start_timeout()(ahci.c:2212-2220, timeouts are CAM defaults, typically 30-60 s). The callout is only cancelled on the completion/error paths (ahci_ata_cmd_doneahci.c:3762-3771,ahci_port_hardstopahci.c:1884-1927).ahci_port_freehas nocallout_cancel()at all beforekfree(ap->ap_ccbs)(ahci.c:1013) andkfree(ap)(ahci.c:1044). -
Never fails/drains CCBs still in
ap_ccb_pending/ap_active/ap_sactive. Contrastahci_port_hardstop()(ahci.c:1884-1936) which walks all three masks, cancels callouts and completes each CCB β nothing equivalent runs before the memory is freed. Pending CCBs leak (their CAM ccb is neverxpt_done'd), and active CCBs keep armed callouts.
After the frees, a still-armed callout fires ahci_ata_cmd_timeout_unserialized()
(ahci.c:3789-3799) which writes ccb->ccb_xa.flags in freed heap and calls
ahci_os_signal_port_thread(ap) β lockmgr(&ap->ap_sig_lock, ...) +
wakeup(&ap->ap_thread) on the freed ahci_port β a kernel use-after-free
write + lock operation on reallocated memory.
Trigger / preconditions
- ahci built as a loadable module (
sys/dev/disk/ahci/Makefileβ ahci.ko) andkldunload ahciwhile a command is in flight (slow/hung device, or mid-I/O unload). Requires root. - Note: the stock X86_64_GENERIC kernel compiles ahci in, so the detach path is reachable only on module builds (or a custom kernel with ahci omitted, then kldload/kldunload). This narrows practical reachability β rated Low.
Why not reproduced on the guest (honest status)
The audit guest has no AHCI controller at all β it boots from virtio-blk
(vbd0s1d on hammer2), pciconf -l shows no SATA/AHCI class device, and ahci
is compiled into the kernel (not unloadable) β see env.txt. The trigger
requires in-flight ahci I/O + runtime driver detach, neither of which exists on
this guest. Source-level certainty only; verdict not_reproduced (device
absent).
Reproduce on suitable hardware (for upstream)
- Build kernel without ahci; build ahci.ko.
- Attach a SATA disk (ideally one that can be made to hang mid-command, e.g.
sg-style stall or a delay-injecting bridge). - Saturate the disk with I/O;
kldunload ahciwhile commands are on-chip. (e.g. a delay-injecting bridge, or simply unplug the disk mid-I/O then unload) - Within
ccb->ccb_xa.timeoutseconds the freed-CCB callout fires β UAF (panic with debug.mpsafe; silent corruption otherwise).
DF-3771 VERDICT β not_reproduced (guest lacks AHCI hardware); defect certain by source analysis
Bottom line
ahci_port_free() (sys/dev/disk/ahci/ahci.c:979-1046) frees the per-port CCB
array, DMA maps, ap_ata[] and the ahci_port itself without (a) cancelling
any armed ccb->ccb_timeout callouts and without (b) failing/completing CCBs
still queued in ap_ccb_pending or active in ap_active/ap_sactive.
Any CCB that was on-chip or pending at detach leaves a live callout pointing
into freed memory. The callout body,
ahci_ata_cmd_timeout_unserialized() (ahci.c:3789-3799), writes
ccb->ccb_xa.flags (offset into the freed ap_ccbs block) and then calls
ahci_os_signal_port_thread(ap, ...) (ahci.c:3798) which executes
lockmgr(&ap->ap_sig_lock, LK_EXCLUSIVE) + wakeup(&ap->ap_thread)
(ahci_dragonfly.c:306-312) on the freed ahci_port β use-after-free write
plus a lock operation on reallocated kernel heap.
This is exactly the teardown discipline ahci_port_hardstop() implements for
the hot-unplug path (ahci.c:1884-1936: walk ap_active/ap_sactive/
ap_ccb_pending, callout_cancel each, complete each CCB) β the detach path
simply never performs the equivalent cleanup.
Why it was not reproduced (environment, not doubt)
- Guest
uname -a: DragonFly dfbsd 6.5-DEVELOPMENT #0 X86_64_GENERIC (see env.txt) pciconf -l: no AHCI/SATA controller present; root disk is virtio-blk (vbd0s1d, hammer2).- ahci is compiled into X86_64_GENERIC (
kldstat -vshows27 pci/ahciregistered from the static kernel) β there is no ahci.ko to kldunload, and DragonFly has no runtime PCI driver unbind, so the detach path cannot be entered on this guest at all. - No QEMU knob in
dfbsd-qemu/vm.shto attach an ich9-ahci device, and DFly does not rescan PCI at runtime, so hot-adding one via the monitor would not attach the driver either.
Classification: not_reproduced, reproduced=0, impact=none on this
guest. The defect itself is certain by inspection (no cancellation code
exists anywhere on the free path β verified by
rg 'callout_' sys/dev/disk/ahci/ahci.c: cancel sites are 1892/1913/3764 only,
all outside ahci_port_free).
Exploit chain (theoretical)
Root-only trigger (module unload), so privesc is moot. Primitive: UAF write of
flag bits + lockmgr on freed-but-reallocatable ahci_port (~2.4 KB kmalloc,
M_DEVBUF zone shared with many drivers). Realistic outcome: panic or silent
corruption; a determined attacker with root already has simpler paths.
Severity therefore Low.
Recommended fix
In ahci_port_free(), before freeing anything: walk ap_active and
ap_sactive; for each slot cancel the callout (callout_cancel_sync) and
complete the CCB with ATA_S_TIMEOUT via a small helper
(ahci_port_free_fail_ccb()), mirroring ahci_port_hardstop(). Pending-queue
CCBs need no callout handling (their callouts are only armed at the ONCHIP
transition β ahci.c:2378-2379/2444-2445 β and their CAM ccb leak is bounded by
the pool size).
fix.diff included (generated from a scratch git worktree; verified to apply
cleanly against the pristine tree with git apply --check; never applied to
sys/).
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 authored from line-accurate source analysis mirroring ahci_port_hardstop() cleanup; cannot be build-validated on this guest (no AHCI controller, no module build path exercised).
fix.diff in this pack; cleanup pattern copied from ahci.c:1884-1936.
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-3771/VERDICT.md (path:line analysis); env.txt (guest pciconf/kldstat proving device absence); ahci.c:997-1046 (free path, no callout handling) vs ahci.c:1884-1936 (hardstop shows required cleanup); ahci.c:2212-2220 (arming); ahci.c:3789-3799 (late callout body)
PoC changes
No PoC possible: guest lacks AHCI hardware and ahci is compiled into the kernel (no kldunload path). Source-level verification only.
Verified recommended fix
In ahci_port_free, before freeing: fail all outstanding CCBs (walk ap_active/ap_sactive/ap_ccb_pending like ahci_port_hardstop) and callout_cancel_sync each ccb_timeout; also lockuninit(ap_ccb_lock).
Verdict
ahci_port_free() (sys/dev/disk/ahci/ahci.c:979-1046) frees ap_ccbs, DMA memory and the ahci_port without cancelling armed ccb->ccb_timeout callouts and without failing CCBs still in ap_ccb_pending/ap_active/ap_sactive. A late callout executes ahci_ata_cmd_timeout_unserialized() (ahci.c:3789-3799) which writes flags into the freed CCB and takes ap_sig_lock on the freed port - a kernel use-after-free write. Trigger requires runtime driver detach (module kldunload) with in-flight I/O, root-privileged. Not reproduced: the audit guest has no AHCI controller (virtio-blk root, pciconf shows no SATA device) and ahci is compiled into GENERIC so the detach path cannot be entered; defect is certain from source (no callout_cancel exists anywhere on the free path).
No comments yet.