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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3660 Β· 8 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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_sleep loop) and whose isp_detach() sets is_exiting and callout_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_testable
baseline no→ patch + rebuild →patched clean

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

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.