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

No validation/stripping of dangerous user CCB flags allows privileged DMA to arbitrary physical memory and OOB reads

Summary

passsendccb at scsi_pass.c:565-568 gates cam_periph_mapmem purely on (flags & CAM_DATA_PHYS)==0 but never STRIPS the flag. When attacker sets CAM_DATA_PHYS, the entire cam_periph_mapmem path (useracc/MAXPHYS/bounce-buffer) is bypassed and csio.data_ptr (attacker-controlled) reaches the SIM as a physical address for DMA. XPT_SCSI_IO with CAM_DIR_IN writes device response to attacker-chosen physical address (arbitrary physical-memory write); CAM_DIR_OUT DMAs that physical address out. Same gate missing for CAM_SENSE_PHYS/CAM_CDB_PHYS/CAM_SENSE_PTR/CAM_CDB_POINTER. No bounds check of cdb_len against IOCDBLEN=16 for inline CDBs - cdb_len=255 makes SIM read 255 bytes from 16-byte cdb_bytes, over-reading csio/union ccb including msg_ptr kernel pointer. XPT driver proves kernel knows these are hazardous - cam_xpt.c:1133 rejects CAM_DATA_PHYS for user DEV_MATCH CCBs. pass.c omits ALL these protections. Requires SYSCAP_RESTRICTEDROOT; impactful in jail granted RESTRICTEDROOT with /dev/passN exposed (jail escape / securelevel bypass on hosts without IOMMU).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1088 Β· 17 files
FileTypeDescriptionSize
df1088_trigger.c trigger-source open()/CAMIOCOMMAND exerciser; proves devfs cap gate then proves CCB-shape bug as root 3.0 KB view raw
verify.sh trigger-source 7 static source-tree grep checks pinning the buggy line and constants 1.9 KB view raw
verify.log run-log verify.sh output on audit commit (all 7 pass) 446 B view raw
run.log run-log unprivileged run (devfs EACCES) 231 B view raw
run_root.log run-log root run on unpatched kernel: BUG confirmed (rc=0) 247 B view raw
run_root_stress.log run-log 3x root run on unpatched kernel - all BUG (rc=0) 579 B view raw
baseline.log run-log fresh with-src #0 baseline: BUG confirmed 179 B view raw
fix_run.log run-log patched #1 kernel: FIXED (EINVAL) 3/3 719 B view raw
fix.diff suggested-fix reject dangerous CCB flags + clamp cdb_len in passsendccb 2.3 KB view raw
fix_build.log build-log nativekernel output, rc=0, 35k lines 5.6 MB ↓ download
build.sh build-script cc -o df1088_trigger df1088_trigger.c 182 B view raw
run.sh run-script run as maxx then explain root invocation 376 B view raw
env.txt environment uname, cc, securelevel, /dev/pass0 mode, HW presence 731 B view raw
README.md readme how to reproduce + bug shape + impact 2.4 KB ↓ raw
VERDICT.md verdict full narrative: mechanism, reproduction, fix 5.2 KB ↓ 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
README.md readme how to reproduce + bug shape + impact
↓ download raw

DF-1088 β€” CAM pass-through (/dev/passN) missing CCB flag validation

Build

cc -o df1088_trigger df1088_trigger.c

Run

# unprivileged (demonstrates the privilege gate)
./df1088_trigger

# privileged (demonstrates the CCB-shape bug)
su root -c ./df1088_trigger

Expected (bug present)

  • Unprivileged: open /dev/pass0 fails with Permission denied (devfs mode 0600) β€” the trigger is privilege-gated.
  • Root (unpatched kernel): CAMIOCOMMAND with CAM_DIR_IN | CAM_DATA_PHYS and cdb_len=255 returns 0 β€” the kernel forwarded the dangerous CCB to the SIM without stripping or rejecting the flags, and did not clamp cdb_len to IOCDBLEN (16). The signature line is: [root] BUG: kernel forwarded attacker physical pointer to SIM without rejection (DF-1088 present)

Expected (after fix)

  • Root (patched kernel): CAMIOCOMMAND returns -1, errno=EINVAL β€” passsendccb rejects the dangerous flags / over-long CDB at the head of the function. Signature: [root] FIXED: kernel rejected dangerous CCB flags (DF-1088 patch present)

Static verification

sh verify.sh

runs 7 source-tree grep checks that pin the exact buggy line and constants.

Bug shape

passsendccb at sys/bus/cam/scsi/scsi_pass.c:565-568 gates cam_periph_mapmem() purely on (flags & CAM_DATA_PHYS) == 0 but never strips the flag and never rejects it; the kernel-internal XPT path (cam_xpt.c:1133) does reject CAM_DATA_PHYS for XPT_DEV_MATCH. A user-supplied CCB with CAM_DATA_PHYS therefore bypasses the cam_periph_mapmem useracc/MAXPHYS/bounce-buffer path and reaches the SIM with csio.data_ptr interpreted as a physical address for DMA. The same function also fails to clamp csio.cdb_len to IOCDBLEN (16) for inline CDBs, so cdb_len=255 makes the SIM read 255 bytes from the 16-byte cdb_bytes[] array, over-reading adjacent union ccb fields including the kernel msg_ptr / data_ptr.

Impact / preconditions

The device requires caps_priv_check_self(SYSCAP_RESTRICTEDROOT) and securelevel <= 1 to open (scsi_pass.c:308,335), and /dev/passN is mode 0600 in the default install. The bug is therefore not directly reachable by an unprivileged user on a default install; impact requires either jail-granted RESTRICTEDROOT with /dev/passN exposed, or a process already holding the RESTRICTEDROOT capability. The CVSS vector in the finding reflects this (AV:L/PR:L ...).

VERDICT.md verdict full narrative: mechanism, reproduction, fix
↓ download raw

DF-1088 β€” No validation/stripping of dangerous user CCB flags in CAM pass

Verdict

REPRODUCED (privilege-gated runtime). The bug exists verbatim in sys/bus/cam/scsi/scsi_pass.c:565-568; a root-originated CAMIOCOMMAND with CAM_DIR_IN | CAM_DATA_PHYS and csio.cdb_len = 255 is accepted by the unpatched kernel (rc=0, three-of-three deterministic runs) β€” i.e. the dangerous CCB is forwarded to the SIM with the user's csio.data_ptr interpreted as a physical address and the CDB read length un-clamped. After the fix.diff is applied the same CCB returns EINVAL.

The trigger is gated behind caps_priv_check_self(SYSCAP_RESTRICTEDROOT) (scsi_pass.c:308) plus a default mode-0600 devfs node, so an unprivileged user cannot reach the bug on a default install. The finding's realistic impact (jail escape / securelevel bypass on hosts without IOMMU) requires a jail granted RESTRICTEDROOT with /dev/passN exposed, or an already-capable process. The passsendccb defect itself is real, confirmed deterministically, and closed by the fix.

Mechanism (confirmed by source trace and runtime)

passsendccb merges the user CCB into a kernel CCB via xpt_merge_ccb (scsi_pass.c:546) and then conditionally maps user buffers:

/* scsi_pass.c:565-568 β€” the bug */
if (((ccb->ccb_h.flags & CAM_DATA_PHYS) == 0)
 && (((ccb->ccb_h.func_code == XPT_SCSI_IO)
    && ((ccb->ccb_h.flags & CAM_DIR_MASK) != CAM_DIR_NONE))
  || (ccb->ccb_h.func_code == XPT_DEV_MATCH))) {
    ...
    error = cam_periph_mapmem(ccb, &mapinfo);
    ...
}

When the user sets CAM_DATA_PHYS (cam_ccb.h:84, 0x00200000): - The gate is false β†’ cam_periph_mapmem is skipped entirely. - cam_periph_runccb (scsi_pass.c:600) still forwards the CCB through xpt_action to the SIM/HBA. - The SIM sees csio.data_ptr (cam_ccb.h:598) as a physical address (the flag's documented meaning) and DMAs to/from it. For CAM_DIR_IN the device response overwrites the attacker-chosen physical address.

The kernel-internal XPT path already knows this is unsafe β€” cam_xpt.c:1133 rejects CAM_DATA_PHYS for XPT_DEV_MATCH. The pass driver does not.

Two related defects sit on the same function:

  1. No cdb_len clamp. For inline CDBs (cdb_bytes[IOCDBLEN], cam_ccb.h:587, IOCDBLEN = CAM_MAX_CDBLEN = 16, cam.h:53), a cdb_len of 255 makes the SIM read 255 bytes from the 16-byte array, over-reading adjacent csio/union ccb fields including kernel pointers (msg_ptr, data_ptr). The same class of pointer-vs-array confusion affects CAM_CDB_POINTER, CAM_SENSE_PTR, CAM_SENSE_PHYS, CAM_CDB_PHYS β€” none of which the pass driver inspects.

  2. No privilege-boundary flag rejection. CAM_DATA_PHYS, CAM_CDB_PHYS, CAM_SENSE_PHYS are kernel-internal flags (set by kernel-allocated CCBs whose buffers the kernel already DMA-mapped). They have no legitimate meaning in a user-supplied CCB. Their presence is therefore always a request to bypass cam_periph_mapmem β€” and should be rejected.

Reproduction

$ ssh dfbsd-maxx 'cd poc/DF-1088 && ./df1088_trigger'      # as maxx (uid 1001)
[unpriv] open /dev/pass0: Permission denied (errno=13)
[unpriv] pass0 access denied at devfs (mode 0600) -- DF-1088 trigger is privilege-gated
[unpriv] re-run as root to exercise the CCB-shape signature via CAMIOCOMMAND

# as root (the only context that can open the device)
$ ./dfbsd-qemu/vm.sh run_root '/root/df1088_trigger'
[unpriv] open /dev/pass0: SUCCEEDED (fd=3) -- unexpected
[root] CAMIOCOMMAND CAM_DATA_PHYS|cdb_len=255: rc=0 errno=0 (Undefined error: 0)
[root] BUG: kernel forwarded attacker physical pointer to SIM without rejection (DF-1088 present)

The rc=0 (CCB forwarded) is the bug signature. The PoC was run three times β€” all three returned rc=0 with no rejection.

Fix

fix.diff (git-apply-able) adds two guards to passsendccb immediately after xpt_merge_ccb:

  1. Reject any user CCB carrying CAM_DATA_PHYS | CAM_CDB_PHYS | CAM_SENSE_PHYS | CAM_CDB_POINTER | CAM_SENSE_PTR (all kernel-internal flags) with EINVAL.
  2. For XPT_SCSI_IO/XPT_CONT_TARGET_IO, reject csio.cdb_len > IOCDBLEN.

The pre-existing CAM_DATA_PHYS == 0 test on the cam_periph_mapmem gate is now dead (we rejected the flag above) and is removed for clarity.

This mirrors the kernel-internal XPT pattern at cam_xpt.c:1133 and the upstream FreeBSD fix that adds similar flag rejections to its passsendccb.

Hardware / guest notes

  • /dev/pass0 is present on the audit guest (attached to QEMU DVD-ROM via atapci0/cd0). No special hardware required to demonstrate the CCB flag-shape bug β€” passsendccb is reached for any open of the device.
  • The DMA-to-arbitrary-physical-memory impact chain requires (a) an attacker-chosen physical data_ptr AND (b) a SCSI target that completes the I/O. (b) is present in the guest (the DVD-ROM), but the bug's real-world value is in a jail/securelevel context where a privileged capability is grantable and the device is exposed. The trigger PoC stops short of completing an actual DMA to a chosen physical address (no IOMMU bypass demonstration), but proves the kernel forwards the malicious CCB β€” which is the precise claim of the finding.

Fix verification

fixed

validated

kernel build rc=0 + harness before/after
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 19 19:24:29 UTC 2026

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (live). scsi_pass forwards dangerous CCB flags CAM_DATA_PHYS|cdb_len=255 to SIM without rejection. Root-only. Kernel rebuild fix.