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

atapi_action() indexes softc->atadev[tid] with unbounded CAM target_id in XPT_SCSI_IO and XPT_PATH_INQ β€” tid=5 deterministically reads state_lock.lk_lockholder (curthread, non-NULL while the lock is held) and dereferences it as struct ata_device *, panicking the kernel

Summary

target_id is not bounded by the SIM's advertised max_target=1 before indexing the 2-element atadev[]; xpt_compile_path accepts any target id, so a crafted path with tid 2..15 reaches the SIM. state_lock immediately follows atadev[2]; tid=5 reads lk_lockholder which is curthread - non-NULL by construction because the lock was acquired before the slot read - so the NULL guard passes and the follow-on device_get_parent faults. REPRODUCED LIVE on the guest (its ATAPI DVD runs through this driver): camcontrol rescan 1:5:0 -> Fatal trap 12, fault VA=0x28, stopped at device_get_parent; 1:0:0 control clean. Fix kernel built and validated. Root-gated trigger (/dev/xpt0 0600). Bounds-check-all-three-paths fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4557 Β· 11 files
FileTypeDescriptionSize
README.md β€” 3.3 KB ↓ raw
VERDICT.md β€” 5.9 KB ↓ raw
run.sh β€” 146 B view raw
build.sh β€” 117 B view raw
run.log β€” 661 B view raw
run.2.log β€” 1.0 KB view raw
panic.txt β€” 2.1 KB view raw
env.txt β€” 483 B view raw
fix.diff β€” 1.3 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 3.9 KB view raw

DF-4557 β€” atapi-cam unbounded CAM target_id β†’ softc->atadev[tid] OOB read β†’ wild-pointer deref (kernel panic)

File

sys/dev/disk/nata/atapi-cam.c β€” atapi_action() XPT_SCSI_IO (:490, :615, :618) and XPT_PATH_INQ (:385, :391).

Root cause

struct atapi_xpt_softc.atadev is a 2-element array (atadev[2], atapi-cam.c:73). CAM target ids routed to the SIM are not bounded by the SIM's advertised max_target = 1 (atapi-cam.c:367): xpt_compile_path() (sys/bus/cam/cam_xpt.c:3993) allocates a cam_et for any target id a CCB path names β€” including 2..15. atapi_action() then uses the raw id as an array index:

  • XPT_PATH_INQ (atapi-cam.c:385,391): softc->atadev[tid] β€” OOB read, NULL-checked but not bounds-checked; a non-NULL OOB qword is dereferenced (->mode).
  • XPT_SCSI_IO (atapi-cam.c:490,615,618): softc->atadev[tid] β€” same OOB read; if the OOB qword is non-NULL, ->mode is read and ->dev is loaded and handed to ata_queue_request() β†’ wild-pointer deref.

state_lock (a struct lock, sys/sys/lock.h:67-73) immediately follows atadev[2] in struct atapi_xpt_softc (atapi-cam.c:72-74). struct lock field offsets: lk_flags|lk_timo@0, lk_count@8, lk_wmesg@16, lk_lockholder@24. Hence:

  • tid=2 β†’ lk_flags|lk_timo (0 β†’ benign DEV_NOT_THERE)
  • tid=3 β†’ lk_count (0 when uncontended β†’ benign)
  • tid=4 β†’ lk_wmesg ("atapicamattach" rodata ptr β†’ non-NULL β†’ garbage ->dev = ASCII "atapicam" β†’ wild deref β†’ same crash class)
  • tid=5 β†’ lk_lockholder β€” and in the XPT_SCSI_IO path state_lock is HELD (acquired at :482) by the time :490 reads the slot, so lk_lockholder == curthread β€” non-NULL by construction β†’ passes the == NULL check β†’ atadev[5]->dev = first qword of struct thread = run-queue link, NULL for a running thread β†’ request->dev = NULL β†’ ata_queue_request() β†’ device_get_parent(NULL) β†’ supervisor read fault at VA 0x28. This is exactly the signature observed in panic.txt.

Reachability

/dev/xpt0 is crw------- root operator on the guest β†’ root-only trigger (same privilege gate as the known DF-1695/DF-2501 family, which cover only XPT_RESET_DEV). The XPT core's own traffic is bounded by max_target, so this needs a deliberately crafted path β€” e.g. the stock base-system command:

camcontrol rescan 1:5:0

camcontrol rescan <b:t:l> issues XPT_SCAN_LUN (whitelisted in xptioctl, sys/bus/cam/cam_xpt.c:1052) on the crafted path; xpt_scan_lun() first sends XPT_PATH_INQ (OOB read #1) and then the probe periph sends XPT_SCSI_IO INQUIRY (OOB read #2 β†’ wild deref β†’ panic).

The guest (QEMU PIIX3 IDE, ATAPI DVD on ata1 β†’ scbus1) runs this exact driver live: cd0/pass0/sg0 are the atapicam SIM's devices.

Build

No build: uses stock /sbin/camcontrol + /dev/xpt0.

Run

./run.sh          # as root on the DragonFly guest

Expected result (baseline, unpatched)

Guest panics ("Fatal trap ... page fault in supervisor mode") with a backtrace ending in the ata layer (ata_queue_request path) β€” ssh connection dies, vm.sh status β†’ down. Serial console shows the panic text.

With the fix applied (tid bounds-checked): camcontrol rescan 1:5:0 returns cleanly, no panic, guest stays up (probe just finds no device).

VERDICT.md
↓ download raw

DF-4557 VERDICT β€” unbounded CAM target_id β†’ atadev[tid] OOB read β†’ wild-pointer deref

Bottom line: REPRODUCED (kernel panic, live guest), FIX VALIDATED (panic gone)

reproduced=1, impact=panic. An out-of-bounds read primitive past softc->atadev[2] converts, deterministically, into a wild-pointer dereference inside the kernel with a base pointer the caller (root, via /dev/xpt0) selects from 14 fixed slots past the array.

How it reproduced (baseline, clean with-src snapshot, unpatched kernel)

Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 2026, QEMU PIIX3 IDE, ATAPI DVD on ata1 β†’ the atapicam SIM registers scbus1 (cd0/pass0/sg0 live through this driver; acd0 (atapi-cd) coexists on the same channel).

  1. camcontrol rescan 1:0:0 β†’ "Re-scan ... successful" (legitimate target, sanity baseline). RC=0.
  2. camcontrol rescan 1:5:0 β†’ ssh session dies silently (command never returns); vm.sh status β†’ down. Serial console (panic.txt):

    Fatal user address access from kernel mode from camcontrol at ffffffff8068d140 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x28 fault code = supervisor read data, page not present instruction pointer = 0x8:0xffffffff8068d140 current process = 1086 (camcontrol) Stopped at device_get_parent: movq 0x28(%rdi),%rax

Exact chain (why tid=5 is deterministic)

  • camcontrol rescan 1:5:0 β†’ scanlun_or_reset_dev() opens /dev/xpt0 and issues XPT_SCAN_LUN on path (1,5,0) β€” allowed by xptioctl's whitelist (sys/bus/cam/cam_xpt.c:1052). xpt_compile_path() does NOT validate target_id against the SIM's max_target (cam_xpt.c:3972-4004): it happily allocates the cam_et/cam_ed for target 5 and routes CCBs there.
  • xpt_scan_lun() (cam_xpt.c:5619) sends XPT_PATH_INQ (OOB read #1, survives β€” only reads ->mode) and then the probe periph sends XPT_SCSI_IO INQUIRY on the crafted path.
  • atapi_action() XPT_SCSI_IO (sys/dev/disk/nata/atapi-cam.c):
  • :482 lockmgr(&softc->state_lock, LK_EXCLUSIVE) β€” state_lock now held
  • :490 if (softc->atadev[tid] == NULL) β€” tid=5 reads 40 bytes past atadev[2], i.e. state_lock + 24 = lk_lockholder (struct lock: lk_flags@0, lk_timo@8, lk_count@16, lk_wmesg@24, lk_lockholder@32 β€” sys/sys/lock.h:67-73). Because state_lock is held, lk_lockholder == curthread β€” non-NULL by construction β†’ the guard passes.
  • :615 softc->atadev[tid]->mode β€” reads curthread + offsetof(ata_device, mode) (~in-slab, mapped) β€” garbage value, harmless.
  • :618 request->dev = softc->atadev[tid]->dev β€” dev is at offset 0 of struct ata_device (ata-all.h:415), so this loads the FIRST qword of struct thread = the run/sleep queue link td_qq.tqe_next β€” NULL for a running thread β†’ request->dev = NULL.
  • :639 ata_queue_request(request) β†’ device_get_softc(device_get_parent( request->dev)) β†’ device_get_parent(NULL) reads 0x28(%rdi) with rdi=0 β†’ supervisor read fault at VA 0x28 β†’ trap 12. Matches panic.txt exactly.
  • Neighbouring ids: tid=4 β†’ lk_wmesg ("atapicamattach" rodata ptr) β†’ ->dev = first 8 bytes of the string (garbage 0x6d61636170697461) β†’ same crash class; tid=6 β†’ other in-slab qwords; tid=2/3 β†’ zero qwords (benign DEV_NOT_THERE). The XPT core never sends these itself (scan bounded by max_target), so the crafted path is required β€” hence privileged-only.

Impact ceiling β€” why impact=panic and not uid0

The primitive is an OOB read of one of 14 fixed qwords past the softc, then a double-deref whose final pointer feeds ata_queue_request. A write chain would require heap-grooming the softc-adjacent slab so atadev[tid] resolves to attacker-controlled memory that survives the ->dev→device_get_parent→ device_get_softc→ata-channel chase — but the trigger itself is gated on /dev/xpt0 (crw------- root operator, 0600) or pass/sg (0600): an unprivileged user cannot create the crafted CAM path at all. For the privileged user who can, the guest is already an intentionally permissive target. So the realistic ceiling is a privileged-triggered kernel panic / DoS — which is what was reproduced. (Distinct from DF-1695/DF-2501, which cover only XPT_RESET_DEV's atadev[tid] deref; the SCSI_IO and PATH_INQ paths carry NULL checks but no bounds check, and tid=5's non-NULL guarantee is state_lock-held curthread — a stronger trigger than RESET_DEV's plain-NULL crash.)

Fix validation (mandatory for memory-corruption class)

fix.diff bounds-checks tid (0..1) in XPT_PATH_INQ (wildcard-exempt), XPT_RESET_DEV, and XPT_SCSI_IO before any atadev[] index.

  • Applied to the guest's /usr/src with patch -p1 (3 hunks, clean); make -j8 nativekernel KERNCONF=X86_64_GENERIC β†’ BUILD_DONE_RC=0; make installkernel β†’ OK; rebooted into X86_64_GENERIC #1: Mon Sep 21 13:22:39 UTC 2026.
  • Re-ran the exact baseline sequence (run.2.log):
  • camcontrol rescan 1:0:0 β†’ successful (legit path unharmed);
  • camcontrol rescan 1:5:0 β†’ error 0x8 (CAM_DEV_NOT_THERE), RC=1, no panic, guest up; same for tids 2,3,4,6,9,14; 3x repeat stable;
  • boot + cd0 probe prove wildcard PATH_INQ traffic unaffected.

Baseline panic vs patched clean-error: behavior change is exactly the fix.

Artifacts

panic.txt (serial console capture), run.log (baseline), run.2.log (patched), env.txt (guest env), fix.diff (git-apply checked), README.md. Guest was vm.sh reset with-src after validation β€” returned clean.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel (X86_64_GENERIC #1 Mon Sep 21 13:22:39 2026, fix.diff applied via patch -p1, nativekernel RC=0) re-ran the exact PoC: camcontrol rescan 1:5:0 (and tids 2,3,4,6,9,14) now returns error 0x8 CAM_DEV_NOT_THERE with RC=1, no panic, guest up; legitimate rescan 1:0:0 still succeeds; 3x repeat stable.

findings/poc/DF-4557/run.2.log (patched behavior), /root/build.log on guest had BUILD_DONE_RC=0, fix.diff in pack (git apply --check OK)
↓ fix.diffper-fix-DF-4557

Confirmed kernel references

Detail

Exploit chain

root opens /dev/xpt0 -> XPT_SCAN_LUN on (scbus1,tid=5,lun0) -> probe XPT_SCSI_IO -> atadev[5]=state_lock.lk_lockholder=curthread -> ->dev=NULL -> device_get_parent(NULL) -> trap 12 page fault (panic). Escalation beyond panic not applicable: trigger already requires root.

Evidence (decisive lines)

panic.txt (Fatal trap 12, VA=0x28, Stopped at device_get_parent, process=camcontrol); run.log (baseline: 1:0:0 OK then 1:5:0 kills guest); run.2.log (patched: error 0x8 for all OOB tids, STILL_ALIVE); VERDICT.md (full chain + fix validation); fix.diff (3 hunks, git apply --check OK)

PoC changes

No code PoC needed: the stock base-system camcontrol binary is the trigger (libcam/userland headers absent on guest; a hand-rolled CCB program would add nothing). run.sh encodes baseline+trigger; note csh used for guest-side scripts.

Verified recommended fix

Bounds-check ccb_h->target_id against NELEM(softc->atadev) in XPT_SCSI_IO, XPT_PATH_INQ (wildcard-exempt) and XPT_RESET_DEV before indexing atadev[]; return CAM_DEV_NOT_THERE

Verdict

Unbounded ccb_h->target_id indexes softc->atadev[2] in atapi_action XPT_SCSI_IO (:490,:615,:618) and XPT_PATH_INQ (:385,:391); tid=5 deterministically reads state_lock.lk_lockholder (= curthread while the lock is held at :482, non-NULL by construction), whose ->dev (first qword of struct thread, a NULL queue link) becomes request->dev=NULL -> ata_queue_request -> device_get_parent(NULL) -> supervisor read fault at VA 0x28. Reproduced live: 'camcontrol rescan 1:5:0' (XPT_SCAN_LUN is whitelisted in xptioctl cam_xpt.c:1052; xpt_compile_path does not bound target_id cam_xpt.c:3993) panicked the guest with exactly that signature. Trigger is root-only (/dev/xpt0 is 0600 root:operator), so impact ceiling is privileged panic/DoS, not uid0. fix.diff (bounds-check tid 0..1, wildcard-exempt in PATH_INQ) build-validated (nativekernel RC=0) and behavior-validated on guest: patched kernel returns CAM_DEV_NOT_THERE (0x8) for tids 2,3,4,5,6,9,14, legit 1:0:0 still rescans, guest stays up, boot/wildcard PATH_INQ unaffected.