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

Mapped CTIO bounce pbuf leaked forever by targdone()'s not-enabled free path and targdtor(): vm.pbuf_mem_count drain β†’ permanent system-wide raw-I/O wedge (unkillable wswbuf5)

Summary

targsendccb maps daemon CTIO data via cam_periph_mapmem but targdone's not-enabled discard and targdtor's drain call targfreeccb WITHOUT cam_periph_unmapmem (only targreturnccb releases). Guest-verified: one pbuf leaked per enable/CTIO/disable/late-completion cycle; draining all 394 leaves dd on the REAL system disk unkillable in D0/wswbuf5 - system-wide raw physio I/O dead until reboot. Route-to-user-queue + unmap-in-dtor fix validated (v1/v2 designs honestly refuted live).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5377 Β· 15 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 3.1 KB ↓ raw
targsim.c β€” 13.9 KB view raw
Makefile β€” 64 B ↓ download
poc5377.c β€” 6.0 KB view raw
build.sh β€” 182 B view raw
run.sh β€” 189 B view raw
build.log β€” 1.4 KB view raw
run.log β€” 611 B view raw
run.drain.log β€” 1.0 KB view raw
run.patched.log β€” 1.7 KB view raw
fix.diff β€” 1.6 KB view raw
fix_build.log β€” 390 B view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 4.6 KB view raw

DF-5377 β€” mapped CTIO bounce pbuf leaked forever by targdone()'s

not-enabled free path (and targdtor): vm.pbuf_mem_count drain β†’

system-wide raw-I/O wedge

What

sys/bus/cam/scsi/scsi_target.c

  • targsendccb() maps daemon CTIO data buffers via cam_periph_mapmem() (:788), which takes a bounce pbuf with getpbuf_mem() (cam_periph.c:643-767) and records it in descr->mapinfo (released only by cam_periph_unmapmem).
  • The correct release is in targreturnccb() (:973-974) β€” but targdone()'s "no longer enabled, throw away CCB" path (:828-832) and targdtor()'s user_ccb_queue drain (:541-548) both call targfreeccb() directly, dropping mapinfo->bp[0] on the floor.
  • A pbuf is never returned to the pool: vm.pbuf_mem_count (guest: 394) drops by one per leaked CTIO. Once drained, every getpbuf_mem() caller β€” raw physio I/O (dd if=/dev/daX), disk-label/GPT probes β€” blocks forever in tsleep(..., "wswbuf5") (vm/vm_pager.c:505-528).

Choreography (no race, no close; DF-1045 not involved)

per cycle on ONE fd: TARGIOCENABLE β†’ write() CTIO (CAM_DIR_IN, dxfer 512) β†’ targsim holds it β†’ TARGIOCDISABLE (targsim fails the XPT_ABORT with CAM_UA_ABORT so the CTIO survives; EN_LUN disable succeeds β†’ LUN_ENABLED cleared) β†’ sysctl hw.targsim.complete=1 (rig xpt_done()s the held CTIO) β†’ targdone() sees state not enabled β†’ targfreeccb() β†’ pbuf leaked. Repeat (re-enable works on the same softc).

Reproduce (guest, root)

cd /root/targpwn
make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC
cc -O0 -g -o poc5377 poc5377.c
kldload ./targsim.ko; sleep 2
sysctl vm.pbuf_mem_count          # baseline (394 on this guest)
./poc5377 8                       # expect count -8 exactly
./poc5377 386 drain               # drain pool, then dd hangs forever

Expected

vm.pbuf_mem_count decreases by exactly the cycle count and never recovers; in drain mode dd if=/dev/da1 stays in the process table stuck on wswbuf5.

Files

  • poc5377.c β€” leak loop + drain mode
  • build.sh / run.sh
  • build.log / run.log / run.drain.log
  • fix.diff β€” cam_periph_unmapmem() before targfreeccb() in targdone() and targdtor

Fix-validation mode

./poc5377 8 x readback β€” on a kernel carrying fix.diff, the late CTIO completions are delivered via user_ccb_queue (not discarded in SWI context) and targdtor() releases the mapping in the daemon's context: the pool returns to baseline (394) and stays there. Note the fix went through three designs β€” v1 (unmapmem in targdone) and v2 (relpbuf in targdone) were REFUTED live (copyout is illegal in SWI context; the pbuf is BUF_LOCKed by the mapping thread) β€” see run.patched.log; the validated v3 routes the release into daemon context per the API contract in scsi_targetio.h.

VERDICT.md
↓ download raw

DF-5377 VERDICT

Status: REPRODUCED (permanent pbuf leak β†’ system-wide raw-I/O DoS),

fix validated (after two live-refuted intermediate fix designs)

Root cause

targsendccb() maps the daemon's CTIO data buffer through cam_periph_mapmem() (scsi_target.c:788; cam_periph.c:643-767), which allocates a bounce pbuf with getpbuf_mem() and records it in descr->mapinfo. The only release site is targreturnccb() (scsi_target.c:973-974). Two teardown paths free CCBs without releasing the mapping:

  • targdone() not-enabled path (scsi_target.c:828-832): targfreeccb(softc, done_ccb) β€” drops mapinfo->bp[0];
  • targdtor() user_ccb_queue drain (scsi_target.c:541-544): same.

Each dropped pbuf is never returned to the pool (vm.pbuf_mem_count, 394 on this guest); once the pool is empty every getpbuf_mem() caller sleeps forever in tsleep(..., "wswbuf5") (vm/vm_pager.c:505-528) β€” raw physio I/O (dd/fdisk/label ops on any disk) wedges system-wide until reboot.

Guest evidence (baseline kernel #0, targsim rig)

Choreography per cycle on one fd (no race, no close β†’ DF-1045 not involved): TARGIOCENABLE β†’ write() CTIO (CAM_DIR_IN, dxfer 512) β†’ targsim holds it β†’ TARGIOCDISABLE (rig fails the XPT_ABORT with CAM_UA_ABORT so the CTIO survives; EN_LUN disable clears LUN_ENABLED) β†’ sysctl hw.targsim.complete=1 β†’ rig xpt_done()s the held CTIO β†’ targdone sees not-enabled β†’ targfreeccb without unmap.

  • run.log: vm.pbuf_mem_count: 394 β†’ 386 after exactly 8 cycles, never recovers; hw.targsim.held_total: 8.
  • run.drain.log: 386 further cycles β†’ pbuf_mem_count=0; dd if=/dev/vbd0 of=/dev/null bs=64k count=1 then sits unkillable in D0 on wchan wswbuf5 β€” permanent system-wide raw-I/O wedge.

Fix engineering (documented honestly)

  • v1 (cam_periph_unmapmem() inside targdone): REFUTED live β€” targdone runs in the cambio SWI where copyout() is illegal: Fatal trap 12 at copyout+0x14 (current process Idle).
  • v2 (relpbuf-only inside targdone): REFUTED live β€” the pbuf is BUF_LOCK()ed by the mapping thread: panic: lockmgr: pid -1, not exclusive lock holder at relpbuf ← targdone ← camisr_runqueue.
  • v3 (validated): route not-enabled completions to user_ccb_queue (per the documented API contract in scsi_targetio.h) and cam_periph_unmapmem() in targdtor() (daemon thread context, where copyout and the pbuf unlock are legal).

Fix validation (kernel #3, Sat Oct 3 18:32:37 2026)

./poc5377 8 x readback on the patched kernel: at most one pbuf outstanding during the run (393 at the measurement point), pool back to 394 baseline with zero permanent loss; guest healthy. [+] FIXED: pool fully restored after daemon read the completions. versus baseline βˆ’1-per-cycle-forever and the 394β†’0 drain wedge.

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: -1 pbuf per cycle permanently; full drain (394->0) leaves dd in unkillable D0/wswbuf5. Patched v3 kernel: after the identical 8-cycle choreography the pool returns to baseline 394 (at most one outstanding during the run), '[+] FIXED: pool fully restored'. v1/v2 intermediate fix designs were themselves refuted live (copyout trap in SWI; lockmgr owner assert in relpbuf from SWI) and are documented in run.patched.log.

run.log + run.drain.log (baseline leak + wedge); run.patched.log (v3 kernel, pool restored).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #3: Sat Oct 3 18:32:37 UTC 2026 x86_64

Confirmed kernel references

Detail

Exploit chain

privileged daemon loop (one fd): enable -> write CTIO w/ mapped 512B buffer -> disable (abort fails by rig, disable succeeds) -> late CTIO completion -> targdone not-enabled path -> targfreeccb drops pbuf -> repeat N times -> vm.pbuf_mem_count 394 -> 0 -> every raw physio I/O (dd/fdisk/label probes on any disk) blocks forever in getpbuf_mem ('wswbuf5'), unkillable D state; system raw-I/O DoS until reboot.

Evidence (decisive lines)

run.log (394->386 after exactly 8 cycles, no recovery); run.drain.log (pool to 0; dd unkillable D0 wswbuf5); run.patched.log (final-fix kernel: pool returns to 394, '[+] FIXED'); targsim.c (abort_ok=0 + hw.targsim.complete choreography knobs).

PoC changes

No seed existed. Rig adds: XPT_ABORT -> CAM_UA_ABORT policy (held CTIO survives TARGIOCDISABLE), sysctl hw.targsim.complete to xpt_done held CCBs late, hw.targsim.held_total so userland waits deterministically for targstart. readback mode added for fix validation (drain user_ccb_queue via read()).

Verified recommended fix

Route not-enabled targdone() completions to user_ccb_queue (instead of freeing in SWI context) and cam_periph_unmapmem() any still-mapped buffers in targdtor() before targfreeccb().

Verdict

REPRODUCED: permanent kernel resource leak -> system-wide raw-I/O wedge. targsendccb() maps daemon CTIO data via cam_periph_mapmem() (getpbuf_mem bounce pbuf recorded in descr->mapinfo, cam_periph.c:643-767), but the teardown paths that discard CCBs -- targdone()'s not-enabled free path (scsi_target.c:828-832) and targdtor()'s user_ccb_queue drain (:541-548) -- call targfreeccb() directly, dropping mapinfo->bp[0]; only targreturnccb() (:973-974) unmaps. Deterministic choreography (no race, no close, DF-1045 not involved): per cycle TARGIOCENABLE -> write() CTIO (CAM_DIR_IN, 512B) -> targsim holds it -> TARGIOCDISABLE (rig fails the XPT_ABORT with CAM_UA_ABORT so the CTIO survives; EN_LUN disable clears LUN_ENABLED) -> rig completes the held CTIO -> targdone sees not-enabled -> targfreeccb without unmap -> pbuf leaked. Guest proof: vm.pbuf_mem_count drops by exactly one per cycle (394 -> 386 after 8) and NEVER recovers; draining all 394 wedges every getpbuf_mem() consumer -- dd if=/dev/vbd0 sits unkillable in D0 on wchan 'wswbuf5' (vm/vm_pager.c:505-528), i.e. raw physio I/O system-wide is dead until reboot. Fix validated after two refuted intermediate designs (v1: cam_periph_unmapmem in targdone is illegal from the SWI -- copyout trap; v2: relpbuf in targdone trips lockmgr owner assert since the pbuf is BUF_LOCKed by the mapping thread): final fix routes not-enabled completions to user_ccb_queue and unmaps in targdtor; patched kernel keeps the pool at baseline 394 across the same cycles.