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)
PoC verification
Evidence pack
findings/poc/DF-2421 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
- 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).
- passsendccb (scsi_pass.c:565-568) invokes cam_periph_mapmem ONLY when
(flags & CAM_DIR_MASK) != CAM_DIR_NONEfor 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. - ahci_xpt_scsi_disk_io (ahci_cam.c:1083) takes
rdata = csio->data_ptrand the synthetic handlers write it unconditionally: INQUIRYbzero(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)+bcopyat :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.hostnametruncated 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.versiontruncated 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+0x2bwrite 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
fixedfixed
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)
Confirmed kernel references
Detail
Exploit chain
operator/root opens /dev/passN (0600 root:operator) -> CAMIOCOMMAND XPT_SCSI_IO, CAM_DIR_NONE, data_ptr=
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.
No comments yet.