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

No func_code whitelist in targwrite(): arbitrary queued XPT opcode injected by the daemon β†’ deterministic targdone() panic (and raw kernel pointers forwarded to SIMs)

Summary

targwrite forwards ANY user func_code to xpt_action: queued codes (e.g. XPT_SCSI_IO) complete into targdone whose default is panic('impossible xpt opcode') - one write(2) = guaranteed panic (guest-verified with a targsim rig: 'panic: targdone: impossible xpt opcode 0x901'). The same missing filter forwards opcodes carrying raw kernel pointers (cdb_ptr/data_ptr) that targsendccb never maps - upstream FreeBSD's rewrite whitelists exactly these. Opcode-whitelist fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5375 Β· 16 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 2.6 KB ↓ raw
targsim.c β€” 13.9 KB view raw
Makefile β€” 64 B ↓ download
poc5375.c β€” 2.4 KB view raw
build.sh β€” 182 B view raw
build_poc.sh β€” 155 B view raw
run.sh β€” 136 B view raw
build.log β€” 1.4 KB view raw
run.log β€” 995 B view raw
run.patched.log β€” 485 B view raw
panic.txt β€” 647 B view raw
fix.diff β€” 860 B view raw
fix_build.log β€” 494 B view raw
manifest.json β€” 922 B view raw
verdict.json β€” 3.8 KB view raw

DF-5375 β€” no func_code whitelist in targwrite(): arbitrary queued XPT

opcode injected by the daemon β†’ deterministic targdone() panic

What

sys/bus/cam/scsi/scsi_target.c

  • targwrite() (:584-632) reads func_code from the user CCB via fuword32() and forwards ANY value to the CCB engine: ATIO/INOT get queued as target resources; every other code takes the default branch where only XPT_FC_QUEUED is inspected β€” queued codes go targstartβ†’targusermergeβ†’targsendccbβ†’xpt_action, inline codes go straight to xpt_action.
  • targdone() (:837-850) β€” the completion callback stamped on every forwarded CCB by targsendccb() (:769) β€” only expects XPT_IMMED_NOTIFY/XPT_ACCEPT_TARGET_IO/XPT_CONT_TARGET_IO and panic("targdone: impossible xpt opcode %#x") on the default case.
  • So a daemon that writes an 8-byte pointer to a CCB with func_code = XPT_SCSI_IO (0x901, XPT_FC_DEV_QUEUED, cam_ccb.h:117) gets the CCB executed as an initiator I/O by the SIM and, when it completes, a guaranteed kernel panic. Same for XPT_SCAN_LUN, XPT_RESET_DEV, XPT_TRIM, XPT_ENG_EXEC, ...
  • Secondary consequence of the same missing whitelist: opcodes whose payloads carry kernel pointers (XPT_SCSI_IO with CAM_CDB_POINTER cdb_io.cdb_ptr, data_ptr, DEV_MATCH cookies) are never validated or mapped β€” targsendccb() only maps CTIO/DEV_MATCH (:783-799) β€” so raw daemon-supplied kernel addresses are dereferenced by the SIM (CWE-822). FreeBSD's rewritten scsi_target.c restricts targwrite() to target-mode opcodes for exactly this reason.

Rig

targsim.c kld: fake SIM with target_sprt = PIT_PROCESSOR so TARGIOCENABLE succeeds; probe traffic answered so a device exists at (bus,0,0); CCBs with ccb_h.targ_descr != NULL (i.e. forwarded by targ) are completed CAM_REQ_CMP.

Reproduce (guest, root; guest dies)

cd /root/targpwn
make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC   # targsim.ko
cc -O0 -g -o poc5375 poc5375.c
kldload ./targsim.ko; sleep 2
./poc5375

Expected

panic: targdone: impossible xpt opcode 0x901 on the serial console, backtrace targdone <- camisr_runqueue <- swi_cambio. Guest dies (DDB).

Files

  • targsim.c / Makefile β€” target-capable fake SIM rig
  • poc5375.c β€” trigger
  • build.sh / build_poc.sh / run.sh
  • build.log / run.log / panic.txt
  • fix.diff β€” whitelist func codes in targwrite (EINVAL otherwise)
VERDICT.md
↓ download raw

DF-5375 VERDICT

Status: REPRODUCED (panic via injected queued XPT opcode), fix validated

Root cause

targwrite() (scsi_target.c:584-632) accepts ANY 32-bit func_code fetched with fuword32() from the daemon's CCB. Non-ATIO/INOT codes take the default branch where only XPT_FC_QUEUED is checked; queued codes are forwarded through targstart β†’ targusermerge (whole-CCB copyin from user memory) β†’ targsendccb β†’ xpt_action(). targdone() (scsi_target.c:837-850) β€” the completion callback stamped on every forwarded CCB β€” only expects INOT/ATIO/CTIO and panic("targdone: impossible xpt opcode %#x") otherwise. FreeBSD's rewritten scsi_target.c restricts the injectable opcodes for exactly this reason; DragonFly's 2003-vintage driver never got that filter.

Secondary consequence of the same missing whitelist (not separately PoC'd): opcodes whose payloads contain kernel pointers β€” XPT_SCSI_IO with CAM_CDB_POINTER (cdb_io.cdb_ptr), data_ptr, XPT_DEV_MATCH cookies β€” are forwarded raw because targsendccb() maps only CTIO/DEV_MATCH (scsi_target.c:783-799); SIMs then dereference daemon-supplied kernel addresses (CWE-822).

Guest evidence (baseline, targsim rig, bus 2)

./poc5375 β€” TARGIOCENABLE(targsim 0:0:0) then write() of a CCB with func_code = XPT_SCSI_IO (0x901): - [TARGSIM] EN_LUN enable=1 grp6=0 grp7=0 -> OK - [TARGSIM] SCSI_IO via targ (descr=0xfffff801167edd40): completing - panic: targdone: impossible xpt opcode 0x901 - backtrace: targdone() <- camisr_runqueue() <- swi_cambio() (panic.txt, run.log). Deterministic; the SWI fires within milliseconds of the write(2).

Impact

Reliable kernel panic (local DoS) from the target daemon with a single write(2); plus a pointer-trust path turning daemon CCB contents into raw kernel-address dereferences by the transport. The daemon normally runs as root; the panic is therefore a robustness/availability break of the target subsystem (and full host DoS), not an unpriv escalation on the stock guest (device 0600 root:wheel, verified).

Fix validation

fix.diff whitelists func codes at the top of the write loop (ATIO/INOT/CTIO/ABORT allowed; anything else β†’ EINVAL, write(2) returns the error). Patched kernel (#2): write returned -1 (errno=22 EINVAL) - no panic, guest alive, [-] still alive - NOT reproduced. FIXED.

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

Baseline: panic 'targdone: impossible xpt opcode 0x901' (trace targdone <- camisr_runqueue <- swi_cambio), guest dead. Patched (whitelist): write() returns -1 errno=22 (EINVAL), no CCB forwarded, no panic, guest alive after 3s. Bad behavior gone.

panic.txt + run.log (baseline); run.patched.log (EINVAL, alive).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #3: Sat Oct 3 18:32:37 UTC 2026 x86_64

Confirmed kernel references

Detail

Exploit chain

open /dev/targ -> TARGIOCENABLE(targsim bus,0,0) -> write(fd, &ccb_ptr, 8) where user ccb has func_code=0x901 -> queued path targstart -> targusermerge copyin of ccb_scsiio -> targsendccb -> xpt_action(XPT_SCSI_IO) -> SIM completes -> xpt_done -> SWI camisr_runqueue -> targdone default case -> panic("targdone: impossible xpt opcode 0x901").

Evidence (decisive lines)

panic.txt (panic: targdone: impossible xpt opcode 0x901; trace targdone/camisr_runqueue/swi_cambio); run.log (EN_LUN OK, SCSI_IO via targ, panic); targsim.c (target-capable SIM rig: PIT_PROCESSOR); run.patched.log (errno=22 EINVAL, no panic, guest alive).

PoC changes

No seed existed. Rig (targsim.c) extended from the DF-5240 evilsim pattern: target_sprt=PIT_PROCESSOR, XPT_EN_LUN accepted, forwarded CCBs detected via ccb_h.targ_descr (== periph_priv.entries[1].ptr, a scsi_target.c-local macro), SCSI_IO with targ_descr completed CAM_REQ_CMP. Three -Werror bring-up fixes (targ_descr macro, cam_periph.h include, drop unused statics).

Verified recommended fix

Whitelist func codes in targwrite() (XPT_ACCEPT_TARGET_IO/XPT_IMMED_NOTIFY/XPT_CONT_TARGET_IO/XPT_ABORT only) and reject everything else with EINVAL, matching upstream FreeBSD's rewrite.

Verdict

REPRODUCED: targwrite() has no func_code whitelist -- ANY 32-bit opcode from the daemon's CCB is forwarded to xpt_action(). A write(2) of a CCB with func_code=XPT_SCSI_IO (0x901, XPT_FC_DEV_QUEUED) executes as an initiator I/O and its completion lands in targdone() whose default case panics: guest shows 'panic: targdone: impossible xpt opcode 0x901' with backtrace targdone <- camisr_runqueue <- swi_cambio. Deterministic (SWI fires within milliseconds), repeatable, guest dead. Same missing whitelist also forwards opcodes carrying raw kernel pointers (SCSI_IO CAM_CDB_POINTER cdb_io.cdb_ptr, data_ptr; targsendccb maps only CTIO/DEV_MATCH at scsi_target.c:783-799) into SIMs -- CWE-822; upstream FreeBSD's rewritten scsi_target.c restricts targwrite to target-mode opcodes for exactly this reason. Root-gated on the stock guest (0600 root:wheel + SYSCAP_RESTRICTEDROOT), so impact = privileged-daemon-triggered kernel panic/DoS + pointer-trust surface, not unpriv escalation. Fix validated: patched kernel returns EINVAL (errno 22) from write(), no panic.