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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5240 Β· 14 files
FileTypeDescriptionSize
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 freed scan_info with 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 DDB trace backtrace (_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.

VERDICT.md
↓ download raw

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_ccb is 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 in probe_softc->request_ccbs) and still in flight. Freeing it here is a use-after-free / double-ownership by construction. xpt_free_ccb() NULLs ccb_h.timeout_ch before the kfree (cam_xpt.c:4941-4947), so the freed CCB carries timeout_ch == NULL.
  • (1) scan_info is freed while every already-issued SCAN_LUN CCB still references it through ccb_h.ppriv_ptr0 (3). The completion path (xpt_scan_bus() case XPT_SCAN_LUN) then reads scan_info->cpi->max_lun (cam_xpt.c:5483), decrements scan_info->counter (cam_xpt.c:5499 β€” UAF write), and at scan termination would xpt_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

fixed
baseline reproduced→ patch + rebuild →patched clean

FIXED. 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.
↓ fix.diffper-fix-DF-5240

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).