isp_detach() never terminates or reaps the per-channel FC kthreads nor stops ldt/gdt callouts; isp_pci_detach() then frees the lock, isp_param and bus resources they still use - teardown use-after-free
Summary
isp_attach_chan spawns isp_kthread per FC channel; its for(;;) lksleep loop has no exit condition and nothing ever terminates it. isp_detach frees paths/sims without stopping the kthreads or the per-channel ldt/gdt callouts (only isp_osinfo.tmo). isp_pci_detach then lockuninit()s isp_osinfo.lock, releases the MMIO/IO resource and kfrees isp_param and pc.ptr; newbus frees the softc. The still-scheduled kthread/callout/task code then runs entirely on freed state: lksleep on the dead lock, FCPARAM() UAF on freed isp_param, isp_prt on a freed device, and ISP_READ/ISP_WRITE wild MMIO through the stale bus_space handle. Upstream FreeBSD reaps (is_exiting + num_threads + callout_drain) - DragonFly has none of it (verified vs FreeBSD HEAD). kldunload isp / devctl detach / reprobe of any attached isp HBA gives a deterministic teardown-order UAF: a resident kernel thread continues read-modify-writing a freed ~500KB known-size M_DEVBUF block on a timer cadence plus wild MMIO through a stale handle. The sim->refcount>2 EBUSY guard does not help - the kthread holds no sim reference. Device absent on guest: not_reproduced, source-certain.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3660 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trig_detach.c | β | 2.5 KB | view raw | |
| build.sh | β | 94 B | view raw | |
| run.sh | β | 214 B | view raw | |
| build.log | β | 59 B | view raw | |
| run.log | β | 193 B | view raw | |
| env.txt | β | 386 B | view raw | |
| fix.diff | β | 2.1 KB | view raw | |
| VERDICT.md | β | 3.8 KB | β raw |
DF-3660 VERDICT β teardown UAF: FC kthread + ldt/gdt callouts outlive the softc
Status: not_reproduced (no QLogic hardware on the guest; defect source-certain)
The lifetime chain, line by line
| Step | Where | What happens |
|---|---|---|
| 1 | isp_freebsd.c:159 | THREAD_CREATE(isp_kthread, fc, &fc->kthread, ...) per FC channel |
| 2 | isp_freebsd.c:4187-4279 | for (;;) { ...; lksleep(fc, &isp->isp_osinfo.lock, 0, "ispf", slp*hz); } β no exit flag, no termination path anywhere in the driver |
| 3 | isp_freebsd.c:265-323 | isp_detach(): frees paths, xpt_bus_deregister, cam_sim_free per channel; stops ONLY isp_osinfo.tmo (286); never touches kthreads or fc->ldt/fc->gdt |
| 4 | isp_pci.c:1012-1029 | isp_uninit; bus_teardown_intr; lockuninit(&isp->isp_osinfo.lock) (1017); bus_release_resource (1022); kfree(isp->isp_param) (1024); kfree(pc.ptr) (1028) |
| 5 | newbus | softc (struct isp_pcisoftc, containing the ispsoftc) freed |
| 6 | still-scheduled code | kthread wakes on the dead lock; isp_fc_runstate β FCPARAM() on freed isp_param; isp_prt β freed isp_dev; mailbox path β ISP_READ/ISP_WRITE on released bus resources; isp_ldt/isp_gdt β tasks on freed fc |
Every element of step 6 is a use-after-free; the mailbox path is a wild-MMIO write primitive into whatever the released resource region became.
Reachability / trigger
- Requires an isp(4) HBA that attached, then a detach event:
kldunload isp(modular kernel),devctl detach, reprobe. Root-initiated on this hardware class, hence Medium, not High. sim->refcount > 2 β EBUSY(isp_freebsd.c:281) only guards active CCBs β the kthread holds no sim reference, so it does not block detach.
Why not reproduced here
pciconf -l: 0 devices with vendor 0x1077 β isp(4) never attaches β no/dev/isp*, no isp bus, nothing to detach (run.log).- isp is compiled into X86_64_GENERIC (sys/config/X86_64_GENERIC:89), not loadable as a module on this guest, so even the kldunload route is closed.
- The defect does not depend on input data β it is pure teardown ordering,
provable by inspection and by contrast with upstream FreeBSD, whose
isp_detach_chan()reaps threads (num_threads+wakeup(fc)+mtx_sleeploop) and whoseisp_detach()setsis_exitingandcallout_drains the timer (verified against FreeBSD HEAD 2026-09-15).
Exploitability assessment (primary-objective analysis)
The primitive would be: after kfree(isp_param) (a M_DEVBUF allocation of
sizeof(fcparam)*nchan β for FC that is a large, well-known-sized heap
block, fcparam is ~500 KB with portdb[512]), a resident kernel thread
continues read-modify-writing it on a timer cadence, plus wild MMIO through a
stale bus_space handle. Practical weaponization requires controlling heap
reuse of the freed softc/isp_param and surviving the lock corruption β on the
audit guest's no-SMEP/no-KASLR profile that would be plausible with hardware;
without hardware it cannot be pursued. Classified Medium (root-triggered
teardown UAF), memcorrupt bucket.
Fix
fix.diff: upstream-style is_exiting flag + num_threads reaping +
callout_stop(ldt/gdt) + fc->ready = 0 before any frees, and
kthread_exit() in the worker. Not runtime-validated on this guest
(fix_status: not_testable) because neither baseline nor patched kernel can
attach the absent HBA; the change mirrors FreeBSD's shipped solution.
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
not_testablefix.diff mirrors FreeBSD's shipped reap machinery; cannot be behaviorally validated on this guest (no HBA to attach/detach).
findings/poc/DF-3660/fix.diff; FreeBSD HEAD isp_detach_chan/num_threads verified
Confirmed kernel references
Detail
Exploit chain
on hardware: kldunload isp -> kfree(softc/isp_param) -> resident kthread wakes on dead lock, continues read-modify-writing freed fcparam (~500KB known-size M_DEVBUF block) on timer cadence and issuing wild MMIO via stale bus_space handle -> heap-reuse control of isp_param would be required to steer; not pursuable without the HBA on this guest
Evidence (decisive lines)
VERDICT.md lifetime-chain table with path:line; fix.diff (upstream-modeled reap machinery); run.log (0 QLogic devices, isp compiled into kernel)
PoC changes
documentation trigger written from scratch (runtime detach needs the HBA); compiled/ran the no-op on the guest and captured absence evidence
Verified recommended fix
Add is_exiting/num_threads, break+exit the kthread, and stop ldt/gdt + wake/reap all channel threads in isp_detach before freeing anything (see fix.diff).
Verdict
isp_attach_chan spawns an isp_kthread per FC channel (isp_freebsd.c:159) whose loop (4187-4279) has no exit condition; isp_detach (265-323) never stops or reaps these threads, never stops the per-channel ldt/gdt callouts (only isp_osinfo.tmo at 286), and frees sims/paths. isp_pci_detach then lockuninit()s isp_osinfo.lock (isp_pci.c:1017), releases the bus resources (1022) and kfrees isp_param (1024) and pc.ptr (1028); newbus frees the softc. The still-scheduled kthread/callout/task code then executes against all of it: lksleep on a dead lock, FCPARAM() UAF on freed isp_param, isp_prt on a freed device, and ISP_READ/ISP_WRITE wild MMIO through the released bus_space handle. Deterministic teardown-order UAF on kldunload/devctl-detach of an isp-attached device (root). Upstream FreeBSD reaps (is_exiting + num_threads + callout_drain) - DragonFly lacks all of it. Guest: 0 QLogic PCI devices, isp compiled into X86_64_GENERIC, no detach possible -> not_reproduced (device absent), impact none observable on guest.
No comments yet.