ahci_pm_read/write recycle the ata_xfer while the command is still programmed on the chip (deferred timeout returns ATA_S_ONCHIP): CCB freed and re-issuable while chip owns the slot -> ahci_ata_cmd_done KKASSERT panic or cross-command completion corruption
Summary
Both PM register accessors call put_xfer on ANY non-COMPLETE return. When the poll timeout defers because co-issued commands are still active, ahci_ata_cmd returns with state==ATA_S_ONCHIP - and the CCB is returned to the free list while the chip still owns the slot. The eventual completion then operates on a CCB in PUT or re-issued to an unrelated command (KKASSERT panic; deferred-expiry overwrites state on a re-issued CCB - premature completion of unrelated I/O). Reachable because PM register commands run outside the exclusive bracket and FBSS raises the issue limit. Drain-until-off-chip helper fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4503 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.4 KB | β raw | |
| VERDICT.md | β | 7.2 KB | β raw | |
| trigger_DF4503.c | β | 2.6 KB | view raw | |
| build.sh | β | 118 B | view raw | |
| run.sh | β | 165 B | view raw | |
| build.log | β | 176 B | view raw | |
| run.log | β | 518 B | view raw | |
| env.txt | β | 1.1 KB | view raw | |
| fix.diff | β | 2.0 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw | |
| verdict.json | β | 4.3 KB | view raw |
DF-4503 β ahci_pm_read/write recycle an on-chip ata_xfer after deferred timeout
ahci_pm_read()/ahci_pm_write() call ahci_ata_put_xfer() on ANY non-COMPLETE return. When the poll timeout DEFERS ("Deferred timeout until its safe", ahci.c:3881) because co-issued commands are still active β possible on FBSS-enabled PM ports, where PM register commands run outside exclusive access and the issue limit is 2-4 β ahci_ata_cmd() returns with xa->state == ATA_S_ONCHIP: the command is still programmed on the chip while its CCB is recycled to the free list. The later completion/expiry hits a CCB in ATA_S_PUT or re-issued to another command β KKASSERT panic (ahci_ata_cmd_done, ahci.c:3775) or cross-command completion corruption.
Reproduce (PM + FBS hardware required)
build.sh # builds trigger_DF4503 ./trigger_DF4503 /dev/da1 600 & # keep a PM target disk busy while :; do camcontrol rescan <bus>; sleep 1; done # notify storm
Expected output (vulnerable)
...: CMD TIMEOUT state=2 slot=N ... Deferred timeout until its safe, slot N assertion "xa->state != ATA_S_ONCHIP && xa->state != ATA_S_PUT" failed in ahci_ata_cmd_done
On the audit guest
No AHCI controller (PIIX3 IDE + virtio only); QEMU has no PM/FBS emulation β build + load-generator verified only (build.log, run.log). Full line-cited control-flow proof in VERDICT.md.
DF-4503 β ahci_pm_read/ahci_pm_write recycle the ata_xfer while the command is still on the chip (deferred timeout)
Verdict: INCONCLUSIVE at runtime (feature absent on guest); code path proven by trace, likelihood HIGH for FBSS PM hardware
- status:
inconclusiveβ requires an ahci controller with a port multiplier whose FBS mode is enabled (AP_F_FBSS_ENABLED) plus concurrent I/O; the audit guest has no AHCI controller at all and QEMU does not emulate port multipliers/FBS (see env.txt). The control-flow argument below is mechanical; the only empirical unknown is how often real PM hardware hits the timeout window. - expected impact on real hardware: kernel panic
(
ahci_ata_cmd_doneKKASSERT, INVARIANTS kernels) in the common case; a narrower race allows a CCB that was returned to the free list and re-issued for an unrelated command to be completed/prematurely finalized by the stale PM-register completion or the deferred-expiry loop (cross-command completion confusion β a memory-corruption-class primitive, though the PM register commands themselves are datalen=0, so no direct DMA corruption from them).
Root cause (line-cited)
ahci_pm_read() (ahci_pm.c:971) and ahci_pm_write() (ahci_pm.c:1008)
send a PM register command and then unconditionally recycle the xfer:
xa = ahci_ata_get_xfer(ap, ap->ap_ata[15]); /* ahci_pm.c:976 / 1013 */
...
xa->flags = ATA_F_POLL | ATA_F_AUTOSENSE; /* read: 987 (write: ATA_F_POLL only, 1028) */
xa->timeout = 1000; /* 988 / 1029 */
if (ahci_ata_cmd(xa) == ATA_S_COMPLETE) { ... } /* 990 / 1031 */
/* fallthrough on ANY other state */
ahci_ata_put_xfer(xa); /* 1000 / 1035 */
ahci_ata_cmd() (ahci.c:3702) returns xa->state after
ahci_poll(ccb, xa->timeout, ahci_ata_cmd_timeout) (ahci.c:3732).
ahci_poll() (ahci.c:2137) exits its loop on timeout and calls the
timeout handler; ahci_ata_cmd_timeout() (ahci.c:3811) defers when
the port cannot be safely stopped:
ap->ap_expired |= 1 << ccb->ccb_slot; /* ahci.c:3873 */
if ((ahci_pread(ap, AHCI_PREG_CMD) & AHCI_PREG_CMD_ST) &&
(ap->ap_active | ap->ap_sactive) != ap->ap_expired) {
kprintf("%s: Deferred timeout until its safe ...\n"); /* ahci.c:3881 */
return; /* state stays ATA_S_ONCHIP */
}
ahci_poll then returns ATA_S_ONCHIP (ahci.c:2183) β i.e. the command
is STILL PROGRAMMED ON THE CHIP β and ahci_pm_read/write treat any
non-COMPLETE state alike and call ahci_ata_put_xfer() β
ahci_put_ccb() (ahci.c:3316), which only checks
state != ATA_S_PUT and puts the CCB on ap->ap_ccb_free.
Why PM register commands can be co-active with other I/O (FBS)
- The port state machine processes PM hotplug/notify events OUTSIDE
ahci_beg_exclusive_access():ahci_pm_check_good()is called at ahci.c:891 and the EINFO bitmap read at ahci.c:841, both before the exclusive-access bracket at ahci.c:938. The state machine holds the port lock (ahci_cam.c:888-890 / ahci_dragonfly.c:371-375) but disk I/O issued earlier is still on-chip. - With
AP_F_FBSS_ENABLEDthe standard-command issue limit is 2-4 (ahci.c:2426-2433) instead of 1, so PM register commands can be co-issued / co-active with ordinary disk I/O: ahci_pm_writecarries neither ATA_F_EXCLUSIVE nor ATA_F_AUTOSENSE (ahci_pm.c:1028), so the exclusivity gate at ahci.c:2339-2358 lets it be issued while other standard commands are active (limit permitting);ahci_pm_readis AUTOSENSE-gated at entry, but the issue loop that runs fromahci_start()(ahci.c:2250) continues past an AUTOSENSE/EXCLUSIVE leader (ahci.c:2458-2466 checks only the FOLLOWER's flags) and appends pending normal commands in the same batch when FBSS raises the limit.- When the 1s PM-register timeout then expires while the co-issued disk
I/O is still outstanding,
(ap_active|ap_sactive) != ap_expiredholds and the timeout defers β leaving the PM command ONCHIP as it is being freed.
Failure cascade
- The chip later completes the PM slot (or another command's timeout
processing drains
ap_expired, ahci.c:3894-3910): ahci_ata_cmd_done()tripsKKASSERT(xa->state != ATA_S_ONCHIP && xa->state != ATA_S_PUT)(ahci.c:3775) β panic on INVARIANTS kernels (guest config has INVARIANTS, X86_64_GENERIC:56).- If the CCB was re-acquired from the free list in the meantime, the
stale completion runs
ccb_done/completeon the NEW owner's command (serial-number protection only covers the callout race, ahci.c:3762-3771) and the deferred-expiry loop overwritesccb->ccb_xa.state = ATA_S_TIMEOUT(ahci.c:3899) on a CCB that now belongs to someone else β cross-command completion corruption.
Distinction from known findings
- DF-1659/DF-2423 cover the NULL deref when
ahci_ata_get_xfer()returns NULL (pool exhausted) at the SAME two functions. This finding is a different defect at a different site: the CCB is acquired successfully, then recycled while still owned by the chip. - DF-4203 covers a missing port lock in the CAM probe path; this finding is about polled-command lifetime vs. the deferred-timeout machinery.
Trigger
trigger_DF4503.c (I/O load half) + camcontrol rescan loop (notify
storm half), run.sh documents the procedure. On PM+FBS hardware the
panic signature would be:
ahci0.: CMD TIMEOUT state=2 slot=N ... Deferred timeout until its safe, slot N ... assertion "xa->state != ATA_S_ONCHIP && xa->state != ATA_S_PUT" failed in ahci_ata_cmd_done
Why not reproduced (honest accounting)
Same as DF-4502: no AHCI controller on the guest (PIIX3 IDE + virtio only) and QEMU has no PM/FBS emulation, so the co-active window cannot be constructed. Compile of both the trigger and the load generator is verified (build.log); the load generator runs (run.log).
Fix validation
Not performable (no emulatable hardware). fix.diff is authored against
the read-only sys/ tree (never applied): it keeps ownership semantics β
if the command comes back still ONCHIP (deferred), the accessor waits
(bounded) and drives the timeout machinery until the command is pulled
off the chip, and only then recycles the xfer. The deeper hardening
(fixing the inverted exclusivity check at ahci.c:2458-2466 so
EXCLUSIVE/AUTOSENSE leaders stop batching followers) is noted for
upstream but is outside this file's diff.
References
- sys/dev/disk/ahci/ahci_pm.c:976-1001 (read), 1013-1036 (write)
- sys/dev/disk/ahci/ahci.c:2137-2184 (ahci_poll), 3811-3930 (timeout/defer/expire)
- sys/dev/disk/ahci/ahci.c:3316-3328 (ahci_put_ccb), 3749-3780 (ahci_ata_cmd_done, KKASSERT at 3775)
- sys/dev/disk/ahci/ahci.c:841,891,938 (state machine: PM cmds outside exclusive access)
- sys/dev/disk/ahci/ahci.c:2339-2358, 2424-2467 (issue gating; FBSS limit; follower batching)
- sys/config/X86_64_GENERIC:56 (INVARIANTS)
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 authored (fix.diff introducing ahci_pm_cmd() drain helper; git-apply --check clean) but not kernel-buildable into a testable configuration: requires FBSS-capable PM hardware absent from all available emulation (guest has no AHCI controller; QEMU has no PM/FBS emulation), so baseline-vs-patched comparison cannot be performed.
fix.diff (git apply --check OK on the read-only sys/ tree; never applied); VERDICT.md 'Fix validation' section.
Confirmed kernel references
- sys/dev/disk/ahci/ahci_pm.c:976-1001
- sys/dev/disk/ahci/ahci_pm.c:1013-1036
- sys/dev/disk/ahci/ahci.c:2137-2184
- sys/dev/disk/ahci/ahci.c:3316-3328
- sys/dev/disk/ahci/ahci.c:3749-3780
- sys/dev/disk/ahci/ahci.c:3811-3930
- sys/dev/disk/ahci/ahci.c:841
- sys/dev/disk/ahci/ahci.c:891
- sys/dev/disk/ahci/ahci.c:938
- sys/dev/disk/ahci/ahci.c:2339-2358
- sys/dev/disk/ahci/ahci.c:2424-2467
Detail
Evidence (decisive lines)
VERDICT.md (line-cited proof: deferral condition ahci.c:3873-3884 vs the unconditional put at ahci_pm.c:1000/1035; the FBSS co-issue analysis ahci.c:2339-2358/2424-2467); run.log (load generator executes); env.txt (no AHCI on guest); fix.diff (git-apply --check OK)
PoC changes
No seed sketch; wrote the I/O-load trigger from scratch (built first try on the guest). The notify-storm half is the camcontrol rescan loop documented in run.sh.
Verified recommended fix
Never recycle an ata_xfer whose state is still ATA_S_ONCHIP: run PM register commands through a helper that drives the deferred-timeout machinery until the command comes off the chip before ahci_ata_put_xfer() (see fix.diff); upstream should additionally fix the inverted exclusivity break at ahci.c:2458-2466.
Verdict
ahci_pm_read() (ahci_pm.c:976-1001) and ahci_pm_write() (ahci_pm.c:1013-1036) call ahci_ata_put_xfer() on ANY non-ATA_S_COMPLETE return. When ahci_poll()'s timeout handler defers because co-issued commands are still active ("Deferred timeout until its safe", ahci.c:3873-3884), ahci_ata_cmd() returns with xa->state == ATA_S_ONCHIP (ahci.c:2183) -- the command is still programmed on the chip -- and the PM accessors then recycle the CCB into ap->ap_ccb_free (ahci.c:3316-3328). The eventual chip completion or deferred-expiry processing (ahci.c:3894-3910) operates on a CCB in ATA_S_PUT (KKASSERT panic at ahci.c:3775 on INVARIANTS kernels) or re-issued to an unrelated command (cross-command completion corruption: state overwrite at ahci.c:3899, premature done/complete). Co-activity requires an FBSS-enabled PM port (issue limit 2-4, ahci.c:2426-2433) and PM register commands running outside exclusive access (state machine: ahci.c:841, 891 vs the exclusive bracket at 938); PM writes carry no exclusivity flags at all (ahci_pm.c:1028) and PM reads get followers appended after the AUTOSENSE leader by the issue loop (ahci.c:2458-2466). Runtime reproduction impossible on the audit guest: no AHCI controller (env.txt) and QEMU does not emulate port multipliers or FBS. Load-half trigger builds and runs (build.log/run.log); full control-flow proof in VERDICT.md. Expected real-hardware impact: kernel panic (common) with a narrower cross-command corruption window; classified likely (code path proven, hardware timing window unmeasurable here).
No comments yet.