DF-5377 / verdict.json
{ "finding_id": "DF-5377", "status": "reproduced", "reproduced": true, "impact": "dos", "confidence": "certain", "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.", "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": "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).", "kernel_refs": [ "sys/bus/cam/scsi/scsi_target.c:828", "sys/bus/cam/scsi/scsi_target.c:541", "sys/bus/cam/scsi/scsi_target.c:973", "sys/bus/cam/scsi/scsi_target.c:788", "sys/bus/cam/cam_periph.c:643", "sys/vm/vm_pager.c:505" ], "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()).", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Sat Oct 3 17:18:54 UTC 2026 x86_64 (baseline) / #2,#3 (fix iterations)", "runtime_sec": 2400, "guest_dirty": 0, "build_cmd": "cd /root/targpwn && make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC && cc -O0 -g -o poc5377 poc5377.c", "run_cmd": "cd /root/targpwn && kldload ./targsim.ko && sleep 2 && sysctl vm.pbuf_mem_count && ./poc5377 8 && ./poc5377 386 drain", "code_hash": "3ab75bdf049fb7d316054ab6f026d460162d8b67d0419e4c15dd810300ffdd8f", "notes": "The unpatched kernel never returns the pbuf (verified: count stays low across minutes). Fix engineering lesson recorded: releases of these pbufs must run in the daemon thread context (copyout + BUF_UNLOCK ownership constraints); v3 fix achieves that via user_ccb_queue delivery + targdtor unmap, matching the API contract in scsi_targetio.h.", "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().", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "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.", "fix_evidence": "run.log + run.drain.log (baseline leak + wedge); run.patched.log (v3 kernel, pool restored).", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #3: Sat Oct 3 18:32:37 UTC 2026 x86_64" } |