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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5376 Β· 16 files
FileTypeDescriptionSize
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 userland abort_ccb pointer to the matching kernel CCB on pending_ccb_queue; on no match it sets status = CAM_PATH_INVALID and 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 += 1 at an attacker-chosen address,
  • (index>=0) xpt_done(abort_ccb) (cam_xpt.c:4891): links a fake struct cam_sim onto the kernel-global cam_simq, and the cambio SWI (camisr β†’ camisr_runqueue, cam_xpt.c:7386+/7432-7440) drains it and calls the attacker-controlled ccb_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.status changes to CAM_REQ_ABORTED|CAM_DEV_QFRZN and fake dev->qfrozen_cnt 0x11110000 β†’ 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 β€” == 0 instead of != EFAULT on the merge result
VERDICT.md
↓ download raw

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-controlled func_code; pinfo.index >= 0 / == CAM_UNQUEUED_INDEX reads 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_QFRZN at the attacker-chosen address, then xpt_freeze_devq(abort_ccb->ccb_h.path, 1) does path->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, then xpt_done(abort_ccb) (cam_xpt.c:4891) reads path->periph->type, spin-locks the fake sim->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

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline 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).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #3: Sat Oct 3 18:32:37 UTC 2026 x86_64

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.