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