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)
PoC verification
Evidence pack
findings/poc/DF-5377 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 viacam_periph_mapmem()(:788), which takes a bounce pbuf withgetpbuf_mem()(cam_periph.c:643-767) and records it indescr->mapinfo(released only bycam_periph_unmapmem).- The correct release is in
targreturnccb()(:973-974) β buttargdone()'s "no longer enabled, throw away CCB" path (:828-832) andtargdtor()'s user_ccb_queue drain (:541-548) both calltargfreeccb()directly, droppingmapinfo->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, everygetpbuf_mem()caller β raw physio I/O (dd if=/dev/daX), disk-label/GPT probes β blocks forever intsleep(..., "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.
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)β dropsmapinfo->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=1then sits unkillable in D0 on wchanwswbuf5β 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 holderatrelpbuf β targdone β camisr_runqueue. - v3 (validated): route not-enabled completions to
user_ccb_queue(per the documented API contract in scsi_targetio.h) andcam_periph_unmapmem()intargdtor()(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
fixedBaseline: -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).
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.
No comments yet.