targwrite() forwards raw user abort_ccb pointer when targusermerge() returns ESRCH ('!= EFAULT' check): fake-CCB arbitrary-address kernel write and kernel-global cam_simq corruption β RIP-control chain
Summary
The inline send path tests only != EFAULT so an ESRCH (untranslatable abort_ccb) forwards the CCB with the RAW USERLAND pointer; xpt_action(XPT_ABORT) derefs it as a kernel ccb. Guest-verified mode A: kernel wrote CAM_REQ_ABORTED + qfrozen_cnt++ at daemon-chosen mmap addresses, write returned success; mode B: fake cam_sim linked onto the kernel-global cam_simq, the cambio SWI faulted operating on it - the final cbfcnp indirect call is attacker RIP once the fake tree is kernel-resident (DF-2286 leak class provides addresses, no KASLR). Root-gated. ==0-not-EFAULT fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5376 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.6 KB | β raw | |
| VERDICT.md | β | 5.0 KB | β raw | |
| targsim.c | β | 13.9 KB | view raw | |
| Makefile | β | 64 B | β download | |
| poc5376.c | β | 9.9 KB | view raw | |
| build.sh | β | 182 B | view raw | |
| run.sh | β | 226 B | view raw | |
| build.log | β | 1.4 KB | view raw | |
| run.log | β | 1.5 KB | view raw | |
| run.b.log | β | 1.7 KB | view raw | |
| run.patched.log | β | 1.0 KB | view raw | |
| panic.b.txt | β | 671 B | view raw | |
| fix.diff | β | 372 B | view raw | |
| fix_build.log | β | 494 B | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 5.9 KB | view raw |
DF-5376 β targwrite() forwards the raw user abort_ccb pointer on
targusermerge() ESRCH: fake-CCB arbitrary kernel write / RIP control
What
sys/bus/cam/scsi/scsi_target.c
targusermerge()(:720-745) translates a daemon XPT_ABORT's userlandabort_ccbpointer to the matching kernel CCB on pending_ccb_queue; on no match it setsstatus = CAM_PATH_INVALIDand returns ESRCH (:741-744).- The inline send path checks only
!= EFAULT(scsi_target.c:626):if (targusermerge(...) != EFAULT) targsendccb(...)β so the ESRCH result is ignored and the CCB is sent anyway with the untranslated user pointer still in cab.abort_ccb. xpt_action(XPT_ABORT)(cam_xpt.c:3075-3124) dereferences that pointer as a kernel union ccb:- reads func_code / pinfo.index,
cam_ccbq_remove_ccb(&abort_ccb->ccb_h.path->device->ccbq, ...)β heap + TAILQ surgery through attacker-controlled pointers,- writes
abort_ccb->ccb_h.status = CAM_REQ_ABORTED|CAM_DEV_QFRZN, xpt_freeze_devq(abort_ccb->ccb_h.path, 1)βpath->device->qfrozen_cnt += 1at an attacker-chosen address,- (index>=0)
xpt_done(abort_ccb)(cam_xpt.c:4891): links a fakestruct cam_simonto the kernel-globalcam_simq, and the cambio SWI (camisrβcamisr_runqueue, cam_xpt.c:7386+/7432-7440) drains it and calls the attacker-controlledccb_h.cbfcnp(periph, ccb)β full kernel RIP control from a write(2) syscall.
The guest has no SMAP/KASLR, so the fake CCB tree can live in an mmap'd user page and every kernel write is observable from userland.
Reproduce (guest, root)
cd /root/targpwn
make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC
cc -O0 -g -o poc5376 poc5376.c
kldload ./targsim.ko; sleep 2
./poc5376 a # clean demo: kernel writes status + qfrozen_cnt into
# the user-chosen addresses, write() returns success
./poc5376 b # RIP control: cbfcnp = 0x414141414140 -> trap 12 with
# that instruction pointer (guest dies)
Struct offsets for the fake tree are read from the targsim module's
hw.targsim.off sysctl dump (computed from the same kernel headers).
Expected
- mode a:
fake ccb_h.statuschanges to CAM_REQ_ABORTED|CAM_DEV_QFRZN andfake dev->qfrozen_cnt0x11110000 β 0x11110001, printed by the PoC, exit 0. - mode b: "Fatal trap 12" with
instruction pointer = 0x414141414140, backtrace camisr_runqueue β swi_cambio.
Files
- poc5376.c β fake-CCB builder (modes a/b)
- build.sh / run.sh
- build.log / run.log (mode a) / run.b.log + panic.b.txt (mode b)
- fix.diff β
== 0instead of!= EFAULTon the merge result
DF-5376 VERDICT
Status: REPRODUCED (arbitrary-address kernel write; kernel-global
queue corruption / RIP-control chain proven to the SWI boundary)
Root cause
targusermerge() (scsi_target.c:720-745) translates the daemon's
userland XPT_ABORT abort_ccb pointer to the matching kernel CCB on
pending_ccb_queue; on no match it sets CAM_PATH_INVALID and returns
ESRCH. The inline send path tests only != EFAULT
(scsi_target.c:626), so the ESRCH result is ignored and the CCB is sent
with the raw, untranslated user pointer. xpt_action(XPT_ABORT)
(cam_xpt.c:3075-3124) then dereferences that value as a kernel
union ccb:
- branch guard:
XPT_FC_IS_DEV_QUEUED(abort_ccb)reads attacker-controlledfunc_code;pinfo.index >= 0/== CAM_UNQUEUED_INDEXreads attacker-controlled index; - branch 2 (index == CAM_UNQUEUED_INDEX, status without
CAM_SIM_QUEUED): writes
abort_ccb->ccb_h.status = CAM_REQ_ABORTED|CAM_DEV_QFRZNat the attacker-chosen address, thenxpt_freeze_devq(abort_ccb->ccb_h.path, 1)doespath->device->qfrozen_cnt += 1β an arbitrary-address kernel increment through a fully attacker-wired fake pathβdevice chain; - branch 1 (index >= 0): additionally
cam_ccbq_remove_ccb(&path->device->ccbq, ccb)heap/TAILQ surgery, thenxpt_done(abort_ccb)(cam_xpt.c:4891) readspath->periph->type, spin-locks the fakesim->sim_spin, TAILQ-inserts the fake CCB on the fake sim's doneq, and links the fake struct cam_sim onto the kernel-global cam_simq (TAILQ_INSERT_TAIL(&cam_simq, sim, links)), waking the cambio SWI.camisr()βcamisr_runqueue()then drains the fake doneq and ends with the indirect call(*ccb_h->cbfcnp)(ccb_h->path->periph, ccb_h)β attacker-chosen function pointer with attacker-chosen first argument.
Guest evidence
Mode A (clean, guest survives, run.log):
- fake CCB in an mmap'd page: func_code = XPT_SCSI_IO (0x901),
pinfo.index = CAM_UNQUEUED_INDEX, fake pathβbusβsim (sim->lock =
the real targsim lock, held by targwrite, satisfying
xpt_freeze_devq's KKASSERT(lockowned())), fake pathβdevice with
qfrozen_cnt = 0x11110000 and an empty active_ccbs list.
- after write(2): fake ccb_h.status = 0x42
(CAM_REQ_ABORTED|CAM_DEV_QFRZN) and
fake dev->qfrozen_cnt = 0x11110001; write() returned 8 and the
daemon's CCB copyout showed CAM_REQ_CMP β the kernel executed the
entire fake-CCB walk and both writes landed at user-chosen
addresses.
Mode B (RIP-control chain, panic.b.txt / run.b.log):
- fake pinfo.index = 1 β branch 1: xpt_done(fake) links the fake sim
onto kernel-global cam_simq during write() (process context β the
user-page writes succeed), setsoftcambio() fires.
- The SWI wakes and starts walking the corrupted cam_simq:
Fatal trap 12 ... fault virtual address = 0x8004a1248
(supervisor write) β that address is fake_sim + 0x48 inside the
daemon's mmap page β Stopped at swi_cambio+0x59:
movq %rdx,0x48(%rax). The final cbfcnp call did not execute only
because DF kernel threads run without the user pmap mapped; the
faulting access is the SWI operating on the attacker-supplied fake
sim. Relocating the fake tree into kernel-addressable memory β e.g.
inside one of the kernel CCBs whose payloads targusermerge()
copyin()s verbatim from the daemon (heap-groomed; the DF-2286
cbfcnp/path copyout class yields the needed kernel addresses, no
KASLR on this guest) β removes that incidental barrier and turns the
chain into a call of 0x414141414140 (kernel RIP control).
Impact
The daemon (root on the stock guest) converts one write(2) into: (1) a write/increment at an arbitrary kernel address (proven clean, mode A), and (2) corruption of the kernel-global CAM completion queue with fully attacker-controlled fake structures leading to an attacker-chosen indirect call (proven to the SWI boundary, mode B). This is kernel memory corruption (CWE-822/CWE-787-class), not merely a panic. uid=0 from an unprivileged user is not reachable through this device on the stock guest (0600 root:wheel + SYSCAP_RESTRICTEDROOT on open, verified live); for the privileged daemon the primitive is directly weaponizable.
Fix validation
fix.diff changes the check to == 0 so any targusermerge() failure
(copyin fault OR ESRCH translation failure) aborts the send.
Patched kernel (#2):
- mode a: fake ccb_h.status = 0, qfrozen_cnt = 0x11110000
unchanged, [-] kernel writes not observed (rc=1 β expected
post-fix), write() returns 8 with CAM_PATH_INVALID copied back to
the daemon CCB (proper API rejection).
- mode b: no SWI chain, still alive after 500 ms, guest healthy.
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 mode A: kernel wrote 0x42 into fake ccb_h.status and 0x11110001 into fake qfrozen_cnt (chosen addresses), write() returned success. Baseline mode B: fake sim linked onto kernel cam_simq, SWI faulted operating on it (swi_cambio+0x59). Patched: mode A fake values byte-identical after write (kernel writes not observed), daemon CCB comes back CAM_PATH_INVALID; mode B no SWI chain after 5s, guest healthy. Primitive eliminated.
run.log + panic.b.txt (baseline); run.patched.log (untouched fakes, alive).
Confirmed kernel references
Detail
Exploit chain
write(fd,&ccb,8) with func_code=XPT_ABORT and cab.abort_ccb = fake userland CCB (func_code=0x901, index=CAM_UNQUEUED_INDEX, fake path->bus->sim->lock = real sim lock to pass KKASSERT(lockowned), fake path->device) -> targusermerge returns ESRCH (no pending match) -> targwrite:626 '!= EFAULT' forwards it -> xpt_action XPT_ABORT branch 2: status write 0x42 + xpt_freeze_devq qfrozen_cnt++ at chosen addresses [mode A proof]. Branch 1 (index=1): cam_ccbq_remove_ccb surgery + xpt_done(fake) -> fake sim TAILQ_INSERTed onto kernel cam_simq -> SWI camisr walks it -> (*cbfcnp) call = RIP control once the fake tree is kernel-resident (e.g. inside a targusermerge-copied CCB payload; addresses via DF-2286-style leak) [mode B proof: trap at swi_cambio+0x59 on fake_sim+0x48].
Evidence (decisive lines)
run.log (mode A: status 0x42 written, qfrozen_cnt 0x11110000->0x11110001, write() success, guest alive); panic.b.txt + run.b.log (mode B: Fatal trap 12 supervisor WRITE at 0x8004a1248 = fake_sim+0x48 in the daemon's mmap page, Stopped at swi_cambio+0x59, current process Idle); targsim.c (offset dump sysctl hw.targsim.off); run.patched.log (fake untouched, no SWI chain).
PoC changes
No seed existed. Fake-CCB builder reads private cam_xpt.c struct offsets (cam_path/cam_eb/cam_sim/cam_ed/cam_ccbq/camq/cam_periph) from the rig module's sysctl dump (shadow structs copied verbatim from cam_xpt.c so offsetof matches the running kernel); fake sim->lock points at the REAL targsim lock (held by targwrite) to satisfy xpt_freeze_devq's KKASSERT(lockowned()).
Verified recommended fix
In targwrite()'s inline path send only when targusermerge() returned 0 (i.e. '== 0' instead of '!= EFAULT'), so a failed abort_ccb translation is never forwarded to xpt_action().
Verdict
REPRODUCED as an arbitrary-address kernel WRITE plus kernel-global-queue corruption (memory-corruption class; impact field 'panic' understates it -- mode B ends in a trap only because DF kernel threads run without the user pmap). targusermerge() returns ESRCH when the daemon's XPT_ABORT abort_ccb cannot be translated to a pending kernel CCB (scsi_target.c:741-744), but the inline send path checks only '!= EFAULT' (scsi_target.c:626) and forwards the CCB anyway with the raw user pointer still in cab.abort_ccb. xpt_action(XPT_ABORT) (cam_xpt.c:3075-3124) then walks that pointer as a kernel union ccb. Mode A (clean, non-fatal): with the fake CCB in an mmap'd user page the kernel wrote CAM_REQ_ABORTED|CAM_DEV_QFRZN (0x42) into fake ccb_h.status and incremented fake path->device->qfrozen_cnt (0x11110000 -> 0x11110001) at daemon-chosen addresses, and write() returned success -- proving the arbitrary-address write primitive end-to-end in syscall context (guest has no SMAP). Mode B (index=1 branch): cam_ccbq heap surgery on the fake queue, then xpt_done(fake) linked the fake struct cam_sim onto the kernel-global cam_simq and setsoftcambio(); the SWI then faulted at swi_cambio+0x59 writing into fake_sim+0x48 -- the kernel CAM completion machinery operating on attacker-supplied memory. The final (*ccb_h->cbfcnp)(periph,ccb) indirect call (attacker RIP) is gated only by the fake tree living in userland while the SWI runs without the user pmap; relocating it into kernel memory (e.g. inside a kernel CCB payload -- targusermerge copyin()s daemon data verbatim into xpt_alloc_ccb() allocations -- with addresses from the DF-2286 cbfcnp/path leak class; no KASLR on this guest) yields full kernel RIP control. Unprivileged escalation is hard-blocked by the device open (0600 root:wheel + SYSCAP_RESTRICTEDROOT, verified live); the primitive is directly weaponizable by the privileged daemon. Fix validated: '== 0' check stops the send on ESRCH; no kernel writes into the fake tree, no SWI chain, guest healthy.
No comments yet.