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

INQUIRY/READ_CAPACITY/page_inquiry dereference user-controlled data_ptr without CAM_DIR validation

Summary

ahci_xpt_scsi_disk_io() and ahci_xpt_page_inquiry() unconditionally dereference csio->data_ptr (as rdata) for write via bzero()/bcopy() in INQUIRY READ_CAPACITY VPD-page paths. Driver never checks (ccbh->flags & CAM_DIR_MASK) but cam_periph_mapmem skips allocation entirely when CAM_DIR_NONE set leaving csio->data_ptr as raw user-supplied value. Operator-privileged user can submit INQUIRY/READ_CAPACITY CCB with CAM_DIR_NONE and data_ptr=NULL (panic) or data_ptr=arbitrary kernel address (kernel writes zeros to that address).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2421 Β· 12 files
FileTypeDescriptionSize
ahcipoc.c β€” 5.0 KB view raw
build.sh β€” 122 B view raw
run.sh β€” 212 B view raw
up_ahci.sh β€” 919 B view raw
run.log β€” 3.2 KB view raw
build.log β€” 122 B view raw
panic-null.txt β€” 286 B view raw
panic-addr.txt β€” 1.0 KB view raw
env.txt β€” 2.1 KB view raw
fix.diff β€” 2.0 KB view raw
VERDICT.md β€” 3.8 KB ↓ raw
verdict.json β€” 3.9 KB view raw
VERDICT.md
↓ download raw

DF-2421 VERDICT β€” raw csio->data_ptr dereference via CAM_DIR_NONE CCBs

Bottom line

REPRODUCED on live AHCI hardware: operator-privileged arbitrary kernel address write (zeros + synthetic-response bytes), silent, repeatable, plus NULL-deref / chosen-address panic variants. Every code claim of the finding verified line-for-line and exercised at runtime.

Mechanism (verified in source, then live)

  1. A userland XPT_SCSI_IO submitted through /dev/passN is merged wholesale into the kernel CCB by xpt_merge_ccb (cam_xpt.c:3888-3900 β€” flags and the whole csio payload including data_ptr are copied from the user).
  2. passsendccb (scsi_pass.c:565-568) invokes cam_periph_mapmem ONLY when (flags & CAM_DIR_MASK) != CAM_DIR_NONE for XPT_SCSI_IO; with CAM_DIR_NONE no mapping/validation happens (cam_periph.c:677 returns 0 immediately). csio->data_ptr keeps the raw user-supplied value.
  3. ahci_xpt_scsi_disk_io (ahci_cam.c:1083) takes rdata = csio->data_ptr and the synthetic handlers write it unconditionally: INQUIRY bzero(rdata, rdata_len) + field writes (:1109-1126), READ_CAPACITY/READ_CAPACITY_16 (:1159-1180), and EVPD page-inquiry (bzero(ccb->csio.data_ptr, dxfer_len) + bcopy at :1693-1694). No CAM_DIR check exists anywhere in the file.

Live evidence (run.log, panic-null.txt, panic-addr.txt)

  • Surgical non-fatal writes with userland readback oracle (kernel symtab present, no KASLR):
  • hostname (kernel bss, 0xffffffff81193360): one INQUIRY CCB with data_ptr=hostname+8, dxfer_len=36 β†’ kern.hostname truncated from 64 chars to exactly 8 (zeroed [8..43], INQUIRY byte0 = T_DIRECT = 0 terminates). CCB returned CAM_REQ_CMP β€” silent.
  • version (kernel .data): version+64..+99 zeroed β†’ sysctl kern.version truncated exactly at +64.
  • EVPD page 0x80 (page_inquiry path): hostname+40 β†’ exactly 40 chars + NUL.
  • READ_CAPACITY (bzero + capacity/512 fields): hostname+16 β†’ 16 chars.
  • NULL deref: data_ptr=NULL β†’ Fatal trap 12, fault VA 0x0, supervisor WRITE, Stopped at memset+0x2b (bzero(rdata,...)).
  • Chosen kernel VA: data_ptr=0xfffff800deadf000 β†’ trap 0xc at the same memset+0x2b write instruction (kernel faults the raw pointer, panicking in vm_object_hold_shared).

Threat model / escalation notes

/dev/passN is 0600 root:operator β€” matches the finding's "operator-privileged" premise (also: any root context). The write gives full control of WHERE and LENGTH (dxfer_len up to 2^16 bytes for READ_CAPACITY paths; INQUIRY caps at sizeof(inquiry_data)); content = zeros + fixed header fields + disk-IDENTIFY-derived strings (model/serial β€” attacker-influenced with physical/easily-swapped media; on emulated disks fully chosen). uid=0 escalation was NOT completed in this run: the primitive is write-only (no read oracle from the same interface to locate dynamic targets such as a process ucred), and content control beyond the identify strings is limited. The demonstrated surgical corruption of live kernel .data/.bss at arbitrary addresses is the finding's claim verbatim.

Fix validation (see fix section in README / fix.diff)

Patched kernel (fix.diff: reject CAM_DIR_NONE with dxfer_len > 0 in both IO dispatchers) re-built and booted in the guest: * inquiry-null / inquiry-addr / surgical: no panic, no corruption β€” CCB returns CAM_REQ_INVALID (ioctl error), hostname and kern.version unchanged. * normal I/O (da0 reads, pass read10, TEST_UNIT_READY with dxfer_len=0) unaffected.

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

fixed

fix-validation.log: patched kernel, all five attack modes return cam_status 0x6 (CAM_REQ_INVALID), hostname/version unchanged, read10/dd normal (260-720 MB/s)
↓ fix.diffper-fix-DF-2421

Confirmed kernel references

Detail

Exploit chain

operator/root opens /dev/passN (0600 root:operator) -> CAMIOCOMMAND XPT_SCSI_IO, CAM_DIR_NONE, data_ptr=, dxfer_len=, INQUIRY/READ_CAPACITY/EVPD -> passsendccb skips mapmem -> ahci_xpt_scsi_disk_io bzero/bcopy at the raw pointer -> silent arbitrary kernel-address write (demonstrated on hostname[]/version[]) or panic (NULL / unmapped VA)

Evidence (decisive lines)

run.log (verbatim: hostname 64->8/16/40 chars, version truncated at +64, all CAM_REQ_CMP), panic-null.txt (fault 0x0, memset+0x2b), panic-addr.txt (chosen VA fault), fix-validation.log (patched kernel: all 5 attack modes -> CAM_REQ_INVALID 0x6, zero corruption, normal I/O unaffected)

PoC changes

no runnable PoC existed; wrote ahcipoc.c (modes inquiry-null / inquiry-addr / readcap-addr / evpd-addr / surgical / read10 / atapi), built against the guest's /usr/src CAM headers (/usr/include/cam is not installed); required live AHCI hardware attached at QEMU launch (up_ahci.sh)

Verified recommended fix

reject CAM_DIR_NONE CCBs with dxfer_len > 0 in ahci_xpt_scsi_disk_io() and ahci_xpt_scsi_atapi_io() before any data_ptr dereference (fix.diff, validated: CAM_REQ_INVALID)

Verdict

REPRODUCED on live AHCI hardware: an operator-privileged XPT_SCSI_IO submitted via /dev/pass0 with CAM_DIR_NONE carries a raw user-supplied csio->data_ptr into ahci_xpt_scsi_disk_io (passsendccb skips cam_periph_mapmem for DIR_NONE, scsi_pass.c:565-568; cam_periph.c:677 returns untouched), where the synthetic handlers write it unconditionally. Surgical non-fatal proof: single INQUIRY CCBs zeroed+overwrote kernel bss hostname[] and .data version[] at byte-exact chosen offsets, each read back via sysctl (kern.hostname truncated from 64 to exactly 8/16/40 chars; kern.version cut at +64), CCBs returning CAM_REQ_CMP silently; READ_CAPACITY (:1159) and EVPD page_inquiry (:1693) paths proven the same way. Panic proofs: data_ptr=NULL -> Fatal trap 12 fault VA 0x0 supervisor WRITE 'Stopped at memset+0x2b' (= bzero(rdata,rdata_len)); data_ptr=0xfffff800deadf000 -> trap 0xc at the same write instruction, kernel panics faulting the caller-chosen address. uid=0 escalation not completed: primitive is write-only with constrained content (zeros + inquiry header + IDENTIFY-derived strings), no kernel-read oracle from the same interface to locate dynamic targets; the arbitrary-address write itself (where+length fully controlled) is demonstrated against live kernel .data/.bss.