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)
PoC verification
Evidence pack
findings/poc/DF-5375 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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) readsfunc_codefrom the user CCB viafuword32()and forwards ANY value to the CCB engine: ATIO/INOT get queued as target resources; every other code takes the default branch where onlyXPT_FC_QUEUEDis 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 bytargsendccb()(:769) β only expects XPT_IMMED_NOTIFY/XPT_ACCEPT_TARGET_IO/XPT_CONT_TARGET_IO andpanic("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)
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
fixedBaseline: 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).
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.
No comments yet.