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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4503 Β· 11 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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_done KKASSERT, 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_ENABLED the 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_write carries 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_read is AUTOSENSE-gated at entry, but the issue loop that runs from ahci_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_expired holds 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() trips KKASSERT(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/complete on the NEW owner's command (serial-number protection only covers the callout race, ahci.c:3762-3771) and the deferred-expiry loop overwrites ccb->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

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

Fix 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.
↓ fix.diffper-fix-DF-4503

Confirmed kernel references

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).