NULL pointer dereference of xa when ahci_ata_get_xfer() returns NULL
Summary
ahci_xpt_scsi_disk_io() and ahci_xpt_scsi_atapi_io() obtain ata_xfer with xa=ahci_ata_get_xfer(ap atx) but never check return for NULL. ahci_ata_get_xfer returns NULL when per-port CCB free list (ap_ccb_free) empty. Every subsequent deref (xa->fis xa->complete xa->atascsi_private xa->tag xa->flags ahci_ata_put_xfer) NULL deref panics kernel. CAM devq throttle does not strictly guarantee free ahci slot available particularly after NCQ-slot-pruning loop in ahci_cam_probe permanently removes slots from free list when device ncqdepth < sc_ncmds. Local DoS reachable by any user issuing disk I/O against AHCI-attached SATA disk with ncqdepth < sc_ncmds.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2420 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df2420_rig.c | β | 5.4 KB | view raw | |
| Makefile | β | 209 B | β download | |
| build.sh | β | 195 B | view raw | |
| run.sh | β | 186 B | view raw | |
| flood2.sh | β | 846 B | view raw | |
| up_ahci.sh | β | 919 B | view raw | |
| run.log | β | 2.4 KB | view raw | |
| build.log | β | 249 B | view raw | |
| panic-disk.txt | β | 822 B | view raw | |
| panic-atapi.txt | β | 808 B | view raw | |
| flood.log | β | 311 B | view raw | |
| flood2.log | β | 5.4 KB | view raw | |
| env.txt | β | 2.1 KB | view raw | |
| fix.diff | β | 2.0 KB | view raw | |
| VERDICT.md | β | 2.9 KB | β raw | |
| verdict.json | β | 3.7 KB | view raw |
DF-2420 VERDICT β unchecked ahci_ata_get_xfer() NULL in both IO dispatchers
Bottom line
REPRODUCED on live AHCI hardware via the unmodified production path: with the per-port ccb free list empty, a single ordinary pass(4) XPT_SCSI_IO CCB panics the kernel inside the production dispatchers β the exact NULL derefs the finding describes. The free-list exhaustion that occurs naturally when a device's ncqdepth < sc_ncmds (probe prune loop) cannot be produced by QEMU's emulated disk (it advertises ncqdepth 32 == sc_ncmds), so the exhaustion state was produced on the REAL port by holding every free ata_xfer through the driver's own exported symbols.
Live evidence (run.log, panic-disk.txt, panic-atapi.txt)
- Port 0 (DISK da0/pass0) drained to
ap_ccb_free EMPTYβ one READ_10 pass CCB βStopped at ahci_xpt_scsi_disk_io+0x8f9: movl $0x1,0x60(%r12)β fault VA 0x60, supervisor WRITE =xa->flags = ATA_F_READ(ahci_cam.c:1334) with xa == NULL (r12=0), i.e. the very first post-switch deref in the READ_10 path. - Port 1 (ATAPI cd0/pass1) drained β one 6-byte ATAPI CCB β
Stopped at ahci_xpt_action+0x2d6: movq (%rax),%raxβ fault VA 0x0, supervisor READ =fis = xa->fis(ahci_cam.c:1525, inlined) with xa == NULL. - Both panics are in production code reached through the ordinary CAM pipeline (user ioctl β passsendccb β xpt_action β ahci_xpt_action β dispatcher); the rig (df2420_rig.c) only manipulated free-list state beforehand, using the exported get/put symbols.
Negative control (live churn, flood2.log)
48 concurrent full-disk reads (48 x 512MB) + 19,639 pass READ_10 CCBs: no
crash, all clean. On this hardware tags=32/32 β the prune loop
(ahci_cam.c:386-392) keeps all 32 slots free-listed and CAM's devq
(cam_simq_alloc(sc_ncmds-1), ahci_cam.c:112-115) never exhausts them.
This pins the natural precondition to real disks reporting ncqdepth <
ncmds (e.g. queue depth 8/16 SATA drives, exactly as the finding states),
or >ncmds-1 simultaneous in-flight commands on such pruned ports.
Impact
Immediate kernel panic (local DoS) on AHCI-attached storage under slot exhaustion; any user issuing disk I/O when the port is in that state hits it (the CCB source β filesystem, raw device, pass β is irrelevant).
Fix validation (fix.diff: NULL checks + CAM_RESRC_UNAVAIL in both
dispatchers)
Patched kernel re-built and booted; identical drain + pass CCB trigger:
no panic, CCB returns cleanly (CAM_RESRC_UNAVAIL), subsequent normal I/O
on the port works after sysctl dev.df2420rig=release.
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
fixedfixed
fix-validation.log: patched kernel #1, same drain+CCB triggers -> cam_status 0x34 (CAM_RESRC_UNAVAIL|SIM_QUEUED), no panic; release -> normal CAM_REQ_CMP I/O; normal da0/dd I/O unaffected
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log (verbatim sessions), panic-disk.txt (Stopped at ahci_xpt_scsi_disk_io+0x8f9, fault 0x60 write), panic-atapi.txt (ahci_xpt_action+0x2d6, fault 0x0 read), flood2.log (negative control, tags=32/32), df2420_rig.c (live-state rig, no driver logic), fix-validation.log (patched kernel #1 survives all triggers with CAM_RESRC_UNAVAIL)
PoC changes
original finding had no runnable PoC; built (1) ahcipoc.c userland pass(4) CAMIOCOMMAND trigger (built against guest /usr/src CAM headers), (2) df2420rig.ko live-state rig that drains the real free list via the exported symbols and devclass_find("ahci"), (3) attached real AHCI hardware to the guest at QEMU launch (up_ahci.sh) - QEMU's AHCI bus rejects drive hotplug, and the earlier boot-hang noted in DF-4203's first round was an invocation artifact solved by explicit bootindex ordering
Verified recommended fix
check ahci_ata_get_xfer() result for NULL in ahci_xpt_scsi_disk_io() and ahci_xpt_scsi_atapi_io() and complete the CCB with CAM_RESRC_UNAVAIL (fix.diff, validated)
Verdict
REPRODUCED on live AHCI hardware via the unmodified production path: with the per-port ccb free list drained on the real ahci0 port (rig uses only the driver's exported ahci_ata_get_xfer/put_xfer symbols), a single ordinary pass(4) XPT_SCSI_IO READ_10 CCB panics the kernel at ahci_xpt_scsi_disk_io+0x8f9 'movl $0x1,0x60(%r12)' (fault VA 0x60, supervisor WRITE = xa->flags = ATA_F_READ with xa==NULL, ahci_cam.c:1334), and an ATAPI CCB on drained port 1 panics at ahci_xpt_action+0x2d6 (inlined ahci_xpt_scsi_atapi_io, fis=xa->fis, fault VA 0x0). Live-churn negative control: 48 concurrent full-disk reads + 19639 pass CCBs survive clean because QEMU's disk advertises ncqdepth 32 == sc_ncmds (the probe prune loop ahci_cam.c:386-392 removes no slots), pinning the natural precondition to real disks with ncqdepth < ncmds, exactly as the finding states. Fix (NULL checks -> CAM_RESRC_UNAVAIL) validated on a rebuilt kernel: same drain triggers return cleanly, no panic, normal I/O restored after release.
No comments yet.