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

OsSendCommand bcopy's user-controlled cdb_len (<=255) into UCHAR Cdb[16] - kernel stack overflow

Summary

Every READ/WRITE/VERIFY CCB entering hpt_action has its CDB copied into a 16-byte stack array using the user-supplied cdb_len with no clamp; up to 239 bytes of chosen data smash OsSendCommand's kernel stack frame. cdb_len is copied verbatim from the user CCB (no CAM layer clamps it; mapped_ioctl copyins the full union ccb). Root/operator-with-SYSCAP_RESTRICTEDROOT on a host with an RR1820 controller: kernel stack corruption -> ring-0 code execution. Gated by privilege AND hardware, hence Low despite the clean primitive.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3920 Β· 7 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 3.2 KB ↓ raw
df3920.c β€” 3.0 KB view raw
fix.diff β€” 385 B view raw
env.txt β€” 1.2 KB view raw
manifest.json β€” 1.0 KB view raw
verdict.json β€” 2.5 KB view raw

DF-3920 β€” hptmv OsSendCommand copies user-controlled cdb_len into UCHAR Cdb[16] (kernel stack overflow)

What

sys/dev/raid/hptmv/entry.c:2771-2794 (function OsSendCommand, invoked from hpt_action XPT_SCSI_IO, entry.c:2676-2899):

UCHAR Cdb[16];                      /* :2771 */
UCHAR CdbLength;
CdbLength = csio->cdb_len;          /* :2777 β€” u_int8_t, attacker-supplied, never clamped */
if ((ccb->ccb_h.flags & CAM_CDB_POINTER) != 0)
        bcopy(csio->cdb_io.cdb_ptr, Cdb, CdbLength);   /* :2782 */
else
        bcopy(csio->cdb_io.cdb_bytes, Cdb, CdbLength); /* :2793 */

csio.cdb_len is copied verbatim from the user's CCB by passsendccb β†’ xpt_merge_ccb (sys/bus/cam/scsi/scsi_pass.c:546); no layer of DragonFly CAM clamps it (verified: no cdb_len validation in scsi_pass.c, cam_periph.c or cam_xpt.c for XPT_SCSI_IO). A user-supplied cdb_len of 17..255 overflows the 16-byte stack array by up to 239 bytes with fully attacker-chosen contents (the CDB pointer for CAM_CDB_POINTER is the user's kernel-copied ioctl buffer β€” mapped_ioctl() copies the whole union ccb into kernel heap, sys/kern/sys_generic.c:674-676).

Impact

Kernel stack memory corruption (CWE-787/121) β†’ potential ring-0 code execution. Gated twice: requires (1) an attached hptmv controller with a present target (the audit guest has none β€” see env.txt), and (2) root / operator-with- SYSCAP_RESTRICTEDROOT, because /dev/passN is 0600 root:operator and passopen checks caps_priv_check_self(SYSCAP_RESTRICTEDROOT) (scsi_pass.c:308). Hence severity Low per the project rubric ("already-privileged user + specific hardware"), though the primitive itself is a clean kernel stack smash.

Reproduce (on hardware that has an RR1820)

cc -Wall -o df3920 df3920.c          # trigger sketch in this directory
sudo ./df3920 <pass-unit-of-hptmv-disk> 200
# expected on vulnerable driver: kernel panic / stack corruption in OsSendCommand

Cannot run on the audit guest (no 11ab:5081 device) β€” verdict not_reproduced with source-level certainty.

Fix

Clamp the copy (and reject oversized CDBs) before the bcopy:

--- a/sys/dev/raid/hptmv/entry.c
+++ b/sys/dev/raid/hptmv/entry.c
@@ -2774,6 +2774,11 @@ OsSendCommand(_VBUS_ARG union ccb *ccb)
            PCommand pCmd = AllocateCommand(_VBUS_P0);
            HPT_ASSERT(pCmd);

+           if (csio->cdb_len > sizeof(Cdb)) {
+               ccb->ccb_h.status = CAM_REQ_INVALID;
+               xpt_done(ccb);
+               break;
+           }
            CdbLength = csio->cdb_len;

(An imin(csio->cdb_len, sizeof(Cdb)) clamp also works; rejecting is safer because READ_6/10/16 decoding never needs more than 16 bytes.)

VERDICT.md
↓ download raw

DF-3920 VERDICT

status: not_reproduced (hardware-gated) | impact observed: none | source-level certainty: certain

Was it reproduced?

No. The trigger requires an attached HighPoint RR1820 (PCI 11ab:5081) controller with a logical device so that XPT_SCSI_IO CCBs are routed to the hptmv SIM (hpt_action β†’ OsSendCommand). The audit guest has no such PCI device and no hptmv module (env.txt: pciconf -l filter empty, no /dev/hptmv, no module in kldstat). No emulator device model exists for this card, so the path cannot be exercised at runtime on this guest. The trigger program (df3920.c) documents the exact CCB construction but was deliberately not run against the guest's AHCI pass device, because on that SIM the bug does not exist (different driver) β€” running it there proves nothing.

Why the bug is certain anyway (path:line trace)

  1. sys/kern/sys_generic.c:674-685 β€” mapped_ioctl() copyin()s the whole user union ccb (CAMIOCOMMAND arg) into a kernel buffer; csio.cdb_len (u_int8_t, cam_ccb.h:603) and cdb_io.cdb_ptr arrive verbatim.
  2. sys/bus/cam/scsi/scsi_pass.c:546 β€” xpt_merge_ccb(ccb, inccb) copies user CCB fields into the kernel CCB. No clamp of cdb_len (verified by grep across sys/bus/cam: only scsi_sg.c derives cdb_len itself via scsi_group_len ≀ 16).
  3. sys/bus/cam/scsi/scsi_pass.c:600 β€” cam_periph_runccb β†’ xpt_action routes the CCB to the owning SIM: hpt_action (entry.c:2246).
  4. entry.c:2266 β€” target validated; entry.c:2285 OsSendCommand(_VBUS_P ccb).
  5. entry.c:2777 β€” CdbLength = csio->cdb_len; (≀255), entry.c:2782/2793 β€” bcopy(..., Cdb, CdbLength) into UCHAR Cdb[16] (entry.c:2771). Overflow of up to 239 bytes, contents fully attacker-chosen.

Only the read/write/verify CDB opcodes (0x88/0x8a/0x13/0x2f/READ_6/10/WRITE_6) enter this block; the copied bytes past Cdb[15] overwrite CdbLength, pCmd, _VBUS_INST locals and the return frame of OsSendCommand (non-leaf, called under driver_lock from hpt_action).

Privilege gate

/dev/passN open requires caps_priv_check_self(SYSCAP_RESTRICTEDROOT) (scsi_pass.c:308-309) and the node is crw------- root operator (0600; operator group lists only root β€” env.txt). So the trigger is root-class, not unprivileged. Combined with the hardware requirement β†’ severity Low.

Exploit chain (if the card were present)

root β†’ open /dev/passN on hptmv bus β†’ CAMIOCOMMAND with cdb_len=200 and a 255-byte CDB β†’ 239-byte kernel stack smash in OsSendCommand (no SMEP/SMAP/ KASLR on the standard audit snapshot) β†’ ROP β†’ ring-0. Not pursued further: the primitive is uninteresting on this guest (no hardware) and the actor is already root-class.

Fix validation

Not performed (requires the controller to observe baseline vs patched behavior). fix.diff in this pack is authored against the read-only sys/ tree and never applied there.

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored (entry.c: reject csio->cdb_len > sizeof(Cdb)); validation impossible on this guest - no hptmv controller to observe baseline vs patched behavior

fix.diff in this pack; never applied to sys/ (read-only audit tree)
↓ fix.diffper-fix-DF-3920

Confirmed kernel references

Detail

Exploit chain

root -> /dev/passN on hptmv bus -> CAMIOCOMMAND cdb_len=200 -> 239-byte kernel stack smash -> ROP -> ring-0 (no SMEP/SMAP/KASLR on standard snapshot); not pursued: no hardware on guest, actor already root-class

Evidence (decisive lines)

VERDICT.md path:line trace (sys_generic.c:685 copyin -> scsi_pass.c:546 merge -> entry.c:2777/2782); env.txt documents absent 11ab:5081 PCI device; df3920.c is the exact CCB construction

PoC changes

n/a (first evidence pack; trigger authored fresh from the verified CAM path)

Verified recommended fix

Reject cdb_len > sizeof(Cdb) with CAM_REQ_INVALID before the bcopy (or clamp with imin())

Verdict

Kernel stack overflow is certain at source level: csio.cdb_len (u_int8_t, user-supplied via /dev/passN CAMIOCOMMAND) is bcopy'd verbatim into UCHAR Cdb[16] at entry.c:2782/2793 with no clamp anywhere in CAM (scsi_pass.c:546 xpt_merge_ccb copies it verbatim). Not reproduced at runtime because the audit guest has no HighPoint RR1820 controller (env.txt) and the trigger additionally requires SYSCAP_RESTRICTEDROOT (scsi_pass.c:308). Up to 239 bytes of chosen data smash the OsSendCommand stack frame.