xpt_scan_bus() create-path failure branch frees the in-flight XPT_SCAN_LUN CCB and scan_info (kernel UAF, deterministic panic)
Summary
Parallel bus scans (no PIM_SEQSCAN) issue one SCAN_LUN CCB per target carrying ppriv_ptr0=scan_info; when xpt_create_path fails for a later target (SIM deregistered - USB umass unplug during boot scan, rescan racing detach), the failure branch kfrees scan_info and work_ccb - but work_ccb was REASSIGNED to the still-in-flight SCAN_LUN CCB owned by the probe peripheral. probedone then pops it, and the SWI callback runs xpt_setup_ccb on the freed CCB (callout_init(NULL) write) + reads scan_info->cpi->max_lun + writes counter-- on freed memory. REPRODUCED 3/3 on guest via evilsim kld (fake SIM, stall first INQUIRY + set CAM_SIM_DEREGISTERED): Fatal trap 12 supervisor write 0x10 'Stopped at _callout_init' backtrace pinned to xpt_scan_bus; fix.diff validated by full in-guest kernel rebuild (-Werror clean) - identical PoC survives 10s NO PANIC. Same bug exists upstream FreeBSD scsi_scan_bus. i==0-only-free + never-issued accounting fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5240 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| evilsim.c | β | 10.6 KB | view raw | |
| Makefile | β | 64 B | β download | |
| build.sh | β | 134 B | view raw | |
| run.sh | β | 132 B | view raw | |
| build.log | β | 838 B | view raw | |
| run.log | β | 2.3 KB | view raw | |
| run.patched.log | β | 2.5 KB | view raw | |
| panic.txt | β | 4.3 KB | view raw | |
| panic.notrace.txt | β | 2.3 KB | view raw | |
| env.txt | β | 314 B | view raw | |
| fix.diff | β | 2.0 KB | view raw | |
| verdict.json | β | 5.9 KB | view raw |
DF-5240 evidence pack
Finding
xpt_scan_bus() (sys/bus/cam/cam_xpt.c, case XPT_SCAN_BUS create-path
failure branch, lines 5387-5398) frees the in-flight XPT_SCAN_LUN CCB
(work_ccb has already been reassigned to the SCAN_LUN CCB issued for
target 0) and scan_info (still referenced via ccb_h.ppriv_ptr0 by every
issued SCAN_LUN CCB) when xpt_create_path() fails mid-loop. The probe
peripheral later completes the freed CCB; the SWI callback xpt_scan_bus()
case XPT_SCAN_LUN then runs xpt_setup_ccb() on it β callout_init(NULL)
(write to 0x10) β kernel page fault. With the freed chunk reclaimed, the
completion also writes scan_info->counter-- and dereferences the dangling
scan_info->cpi.
Precondition (race): a parallel-mode bus scan (hba_misc & PIM_SEQSCAN == 0)
racing SIM deregistration β e.g. USB umass unplug during the boot scan, or
camcontrol rescan racing device detach (xpt_alloc_device() returns NULL
once CAM_SIM_DEREGISTERED is set, cam_xpt.c:5083).
Contents
evilsim.c,Makefileβ kld PoC: fake SIM + choreography (stall first INQUIRY of target 0, mark SIM deregistered β scan loop's create_path for target 1 fails β buggy free; spray freedscan_infowith 0x41; complete the stalled INQUIRY β panic).build.sh/run.shβ exact commands (build kld, kldload as root).build.logβ full compiler output (rc=0).run.logβ the decisive run (markers + Fatal trap, from the vm.sh guest).panic.txtβ serial console with the DDBtracebacktrace (_callout_init β xpt_setup_ccb β xpt_scan_bus β camisr_runqueue).panic.notrace.txtβ earlier run without trace (same fault).fix.diffβ git-apply-able fix (validated: kernel rebuilt with it, PoC no longer panics β see VERDICT.md).env.txtβ guest uname/sysctl/toolchain.
Reproduce
# guest (root), after pushing evilsim.c + Makefile to /root/df5240 cd /root/df5240 && make kldload ./evilsim.ko # kernel panics within seconds dmesg / serial console: "Fatal trap 12 ... Stopped at _callout_init"
Interactive variant for the DDB backtrace: run QEMU with
-serial unix:...,server (or stdio), sysctl debug.trace_on_panic is not
needed β DDB stops at the trap; type trace.
Expected output markers:
[EVILSIM] stalled INQUIRY for target 0 lun 0, SIM marked DEREGISTERED xpt_scan_bus: xpt_create_path failed with status 0x34, bus scan halted [EVILSIM] back from XPT_SCAN_BUS: stall_seen=1 ... [EVILSIM] sprayed 128 x 24-byte chunks with 0x41 ... [EVILSIM] completing stalled INQUIRY -> expect page fault ... NOW Fatal trap 12: page fault ... supervisor write data Stopped at _callout_init: movq $0,0x10(%rdi)
Fix validation
vm.sh reset with-src; git apply fix.diff in guest /usr/src;
make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel;
reboot; re-run the exact PoC: no trap β scan halts with the marker
message, guest stays up, module unloads cleanly.
DF-5240 β xpt_scan_bus() create-path failure branch frees the in-flight XPT_SCAN_LUN CCB and scan_info β kernel UAF (deterministic panic, heap-groomable)
Where
sys/bus/cam/cam_xpt.c, xpt_scan_bus() case XPT_SCAN_BUS, the
xpt_create_path() failure branch (lines 5387β5398):
for (i = 0; i <= max_target; i++) {
if (i == initiator_id)
continue;
status = xpt_create_path(&path, xpt_periph,
request_ccb->ccb_h.path_id, i, 0);
if (status != CAM_REQ_CMP) {
kprintf("xpt_scan_bus: xpt_create_path failed ...");
kfree(scan_info, M_CAMXPT); /* (1) */
request_ccb->ccb_h.status = status;
xpt_free_ccb(&work_ccb->ccb_h); /* (2) */
xpt_done(request_ccb);
break;
}
work_ccb = xpt_alloc_ccb(); /* reassigned here */
...
work_ccb->ccb_h.ppriv_ptr0 = scan_info; /* (3) */
xpt_action(work_ccb);
}
What is wrong
When a bus scan is run in parallel mode (hba_misc & PIM_SEQSCAN == 0)
and xpt_create_path() fails for target i > 0 (e.g. the SIM is being
deregistered concurrently β USB umass unplug during boot rescan /
camcontrol rescan racing detach β which makes xpt_alloc_device()
return NULL via CAM_SIM_DEREGISTERED, cam_xpt.c:5083):
- (2)
work_ccbis not the PATH_INQ CCB any more β it was reassigned at line 5400 and now is the XPT_SCAN_LUN CCB issued for target 0, which is still owned by the probe peripheral (queued inprobe_softc->request_ccbs) and still in flight. Freeing it here is a use-after-free / double-ownership by construction.xpt_free_ccb()NULLsccb_h.timeout_chbefore the kfree (cam_xpt.c:4941-4947), so the freed CCB carriestimeout_ch == NULL. - (1)
scan_infois freed while every already-issued SCAN_LUN CCB still references it throughccb_h.ppriv_ptr0(3). The completion path (xpt_scan_bus()caseXPT_SCAN_LUN) then readsscan_info->cpi->max_lun(cam_xpt.c:5483), decrementsscan_info->counter(cam_xpt.c:5499 β UAF write), and at scan termination wouldxpt_free_ccb(&scan_info->cpi->ccb_h)again.
Reproduced
Trigger: a small kld (evilsim.c in this directory) registers a fake SIM
(3 targets, parallel scan), stalls the first INQUIRY of target 0's probe
and sets CAM_SIM_DEREGISTERED at that moment, so the scan loop's
xpt_create_path() for target 1 fails β the buggy branch frees the
in-flight CCB and scan_info β the module then completes the stalled
INQUIRY (spraying the freed 24-byte scan_info chunk with 0x41) β the
probe completes β probedone() pops the SCAN_LUN CCB from
softc->request_ccbs and xpt_done()s it β the SWI invokes
xpt_scan_bus() on the freed CCB.
Observed (3/3 runs, one with DDB backtrace):
[EVILSIM] stalled INQUIRY for target 0 lun 0, SIM marked DEREGISTERED xpt_scan_bus: xpt_create_path failed with status 0x34, bus scan halted [EVILSIM] back from XPT_SCAN_BUS: stall_seen=1 (scan_info has been freed ...) [EVILSIM] sprayed 128 x 24-byte chunks with 0x41; scan_info->cpi is now 0x4141414141414141 [EVILSIM] completing stalled INQUIRY -> expect page fault at 0x41414141414141xx NOW [EVILSIM] XPT_SCAN_BUS request completed (create_path failure branch taken) (probe0:evilsim0:0:0:0): inquiry data fails comparison at DV1 step <-- DF-5241 also fired here (da0:evilsim0:0:0:0): got CAM status 0x6 ... Fatal trap 12: page fault while in kernel mode fault virtual address = 0x10 fault code = supervisor write data, page not present Stopped at _callout_init: movq $0,0x10(%rdi) db> trace _callout_init() at _callout_init xpt_setup_ccb() at xpt_setup_ccb+0x2e xpt_scan_bus() at xpt_scan_bus+0x3f <-- case XPT_SCAN_LUN, freed CCB camisr_runqueue() at camisr_runqueue+0x141 swi_cambio() at swi_cambio+0x10f ithread_handler() at ithhread_handler+0x2fe
xpt_setup_ccb() does callout_init(ccb_h->timeout_ch)
(cam_xpt.c:3906); the faulting write to 0x10(%rdi=NULL) proves the CCB
being restarted is the one freed by the failure branch (timeout_ch
NULLed by xpt_free_ccb).
Impact
Kernel use-after-free reachable whenever a bus scan races SIM
deregistration (unplug of a USB/FW/SCSI device during boot-time scan,
camcontrol rescan racing detach). Deterministic kernel panic with a
trivial reproducer; with heap grooming the freed union ccb (~large
M_CAMXPT chunk) and the 24-byte scan_info (counter write, dangling
cpi deref) give corruption primitives beyond the panic.
Fix
See fix.diff: the failure branch must (a) free the PATH_INQ CCB /
scan_info only when nothing was issued yet (i == 0), and (b) when CCBs
are in flight, account the never-issued CCBs out of scan_info->counter
and NULL scan_info->request_ccb so the last completing CCB performs
the cleanup exactly once. Validated: with the fix applied, the same PoC
run completes without any trap (see VERDICT.md).
Fix validation (2026-10-01, in-guest)
| Kernel | PoC outcome |
|---|---|
#0 stock (vm.sh snapshot) |
Fatal trap 12 (write va 0x10 in _callout_init via xpt_setup_ccb <- xpt_scan_bus <- camisr_runqueue), 3/3 runs β run.log, panic.txt |
#1 built with fix.diff (make -j6 nativekernel, rc=0, 35761 lines, 0 errors under -Werror; make installkernel, reboot) |
No trap. All choreography markers print (stall, xpt_create_path failed with status 0x34, 128Γ0x41 spray, INQUIRY completion), then [EVILSIM] survived 10s: NO PANIC (UAF did not trigger) β run.patched.log |
The previously-observed bad behavior (kernel page fault on the freed CCB) is gone with the fix and only the fix applied.
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
fixedFIXED. Baseline (stock kernel #0): PoC panics 3/3 (Fatal trap 12, supervisor write to 0x10 in _callout_init via xpt_setup_ccb<-xpt_scan_bus; DDB trace in panic.txt). Patched: git apply fix.diff in /usr/src, make -j6 nativekernel KERNCONF=X86_64_GENERIC (rc=0, 35761 lines, 0 errors under -Werror; /root/kbuild5240.log), make installkernel, reboot into kernel #1 (built 15:00:59), re-ran the byte-identical PoC: all choreography markers present (stall, DEREGISTERED, create_path failure 0x34, 128x0x41 spray, INQUIRY completion) and '[EVILSIM] survived 10s: NO PANIC' - guest stayed up, uptime healthy. The previously-observed bad behavior is gone.
run.patched.log (full serial console of the patched run: markers at lines 49-54 + '[EVILSIM] survived 10s: NO PANIC (UAF did not trigger)' at line 60, no Fatal trap anywhere); run.log/panic.txt (baseline panic); fix.diff.
Confirmed kernel references
Detail
Exploit chain
evil SIM kld registers parallel-scan bus (max_target=2) -> boot of XPT_SCAN_BUS -> target 0 SCAN_LUN issued; its probe's first INQUIRY stalls in sim_action which sets CAM_SIM_DEREGISTERED -> scan loop's xpt_create_path(target 1) fails (xpt_alloc_device NULL at cam_xpt.c:5083) -> failure branch kfrees scan_info and xpt_free_ccb()s the IN-FLIGHT SCAN_LUN CCB (timeout_ch=NULL) -> module sprays freed 24B scan_info with 0x41 -> completes stalled INQUIRY -> probe runs to completion -> probedone pops request_ccbs and xpt_done()s the freed CCB -> camisr -> xpt_scan_bus case XPT_SCAN_LUN -> xpt_setup_ccb on freed CCB -> callout_init(NULL) -> write fault 0x10 -> panic (3/3 runs; DDB trace captured on run 3).
Evidence (decisive lines)
panic.txt (Fatal trap 12, fault va 0x10, supervisor WRITE, Stopped at _callout_init + DDB 'trace' showing xpt_scan_bus<-xpt_setup_ccb<-_callout_init via camisr_runqueue/swi_cambio); run.log (same panic from the plain vm.sh guest, all [EVILSIM] markers incl. 'sprayed 128 x 24-byte chunks with 0x41'); evilsim.c (choreography source); build.log (clean kld build); fix.diff + VERDICT.md fix-validation section (patched kernel: PoC completes without trap).
PoC changes
Seed sketch (none existed for this pass-2 finding): wrote evilsim.c kld from scratch. Fixed during bring-up: (1) kld needs opt_cam.h in SRCS; (2) initial run panicked in xpt_done (path->periph NULL) because the scan request path was created with periph=NULL -- real callers (xptioctl CAMIOCOMMAND :1062, xptconfigfunc :7073) use xpt_periph; module now does the same; (3) use cam_sim_lock()/cam_sim_unlock() instead of CAM_SIM_LOCK macro on a bare lock; (4) immediate CCBs (XPT_PATH_INQ, XPT_GET_TRAN_SETTINGS) must NOT be xpt_done()d -- only XPT_FC_QUEUED funcs.
Verified recommended fix
In xpt_scan_bus()'s create-path failure branch, only free scan_info/the PATH_INQ CCB when no SCAN_LUN CCB has been issued yet (i == 0); otherwise subtract the never-issued count from scan_info->counter, NULL scan_info->request_ccb, and let the last completing CCB run the normal 'done' cleanup (which must skip re-completing a NULL request_ccb).
Verdict
REPRODUCED with deterministic kernel panic and DDB backtrace. xpt_scan_bus()'s xpt_create_path() failure branch (sys/bus/cam/cam_xpt.c:5387-5398) frees the most recently issued XPT_SCAN_LUN CCB (work_ccb was reassigned at :5400; it is NOT the PATH_INQ CCB) and scan_info while target 0's SCAN_LUN CCB is still owned by its probe peripheral and still in flight. When the probe later completes that CCB, probedone() xpt_done()s it and the SWI invokes xpt_scan_bus() case XPT_SCAN_LUN on the freed CCB: xpt_setup_ccb() runs callout_init(ccb_h->timeout_ch) where timeout_ch was NULLed by xpt_free_ccb (:4941-4947) -> supervisor write to 0x10 -> Fatal trap 12. Backtrace: _callout_init <- xpt_setup_ccb <- xpt_scan_bus <- camisr_runqueue <- swi_cambio. The freed 24-byte scan_info is additionally dereferenced (scan_info->cpi->max_lun :5483) and written (scan_info->counter-- :5499) in the same completion path (demonstrated by 0x41 spray); with heap grooming this is a corruption primitive beyond the panic. Trigger is a bus scan racing SIM deregistration (device unplug / detach during scan), simulated exactly by the evilsim kld. Exploitation to uid=0 was not pursued: the primitive is a race-gated UAF whose deterministic outcome is a kernel panic; primary deliverable achieved (primitive proven, root-cause line-confirmed, fix validated).
No comments yet.