isp_attach_chan() cam_simq_release()s the shared devq once per channel while isp_attach held only one reference - premature cam_devq free + write-after-free on multi-channel adapters
Summary
isp_attach allocates one cam_devq (refcount=1) and hands the same pointer to every isp_attach_chan; each chan does cam_sim_alloc (which itself takes a per-sim reference) and then cam_simq_release(devq), dropping 'the caller's' reference once PER channel. For nchan=N the steady-state refcount is 1 instead of N. The first cam_sim_free during detach or attach-unwind drives the count to zero and kfrees the devq while N-1 sims still dereference sim->devq on every CCB; their own cam_sim_free then executes --freed->refcount (write-after-free, potential double kfree). Affected instances: dual-bus SCSI ISP1240/ISP12160 (nchan=2) and NPIV FC ISP2422/2432/2532 (nchan+=nvports, off by up to ~125); single-channel cards balance. Upstream FreeBSD releases exactly once after all sims are gone. Root-triggered teardown bug; heap-reuse steering needed beyond a panic. Device absent on guest: not_reproduced, arithmetic source-certain.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3661 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trig_devq.c | β | 2.2 KB | view raw | |
| build.sh | β | 90 B | view raw | |
| run.sh | β | 252 B | view raw | |
| build.log | β | 55 B | view raw | |
| run.log | β | 147 B | view raw | |
| env.txt | β | 386 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| VERDICT.md | β | 3.6 KB | β raw |
DF-3661 VERDICT β per-channel cam_simq_release on a once-referenced shared devq
Status: not_reproduced (no QLogic hardware on guest; arithmetic is source-certain)
Analysis
The devq reference protocol in DragonFly CAM:
cam_devq_allocβrefcount = 1(cam_queue.c:228).cam_sim_alloc(action, poll, name, softc, unit, lock, maxdev, maxtagged, queue)takes one reference per sim when a non-NULL queue is supplied (cam_sim.c:182-185:else cam_devq_reference(queue)).cam_sim_freeβcam_sim_releasedrops that sim's reference when the sim dies (cam_sim.c:246-252).cam_devq_releaseisif (--refcount == 0) { camq_fini x2; kfree(devq) }(cam_queue.c:248-256). No underflow protection.
The driver:
isp_attachallocates the devq once (isp_freebsd.c:213) and hands the same pointer to everyisp_attach_chan(isp_freebsd.c:219-223).- Each
isp_attach_chanβ right after itscam_sim_allocβ executescam_simq_release(devq)(isp_freebsd.c:95). This is patterned on single-channel drivers where the caller's reference is dropped exactly once after handing it to the (sole) sim. Applied per channel, every channel after the first decrements a reference owned by a live sim.
Net: for nchan = N the steady-state refcount is 1 instead of N.
Failure sequence at teardown (isp_detach, isp_freebsd.c:290-309)
- chan Nβ1:
cam_sim_free(sim)β--refcountβ 0 β devq kfree'd. - Between step 1 and the last
cam_sim_free, the remaining Nβ1 sims are still registered; their buses' CCB flow (xpt_run_devq, allocation openings) dereferencessim->devqβ use-after-free reads/writes ofalloc_queue/send_queuestate. - Each later
cam_sim_freecallscam_simq_release(sim->devq)β--freed->refcountβ write-after-free; if the decremented value is 0 the devq is kfree'd a second time.
Identical over-release executes in isp_attach's unwind (:235-251) when a
later channel fails to attach.
Affected hardware (this tree)
- PCI_QLOGIC_ISP1240, ISP12160 (dual SCSI,
isp_nchan = 2; isp_pci.c:732/749). - ISP2422/2432/2532 with NPIV virtual ports (
isp_nchan += isp_nvports; isp_pci.c:798/805 β nvports hint, up to ~126 channels). - Single-channel cards (the vast majority) balance exactly β N=1 is safe, which is why this survived: the common configuration never trips it.
Why not reproduced
Guest has zero QLogic PCI devices (vendor 0x1077) β isp never attaches, so neither the over-release nor the premature free can execute (run.log / env.txt). The refcount walk is fully determined by source; upstream FreeBSD (fetched 2026-09-15) releases the devq only once, after all sims are gone, in both unwind and detach β confirming the intended protocol.
Exploitability
Write-after-free of a fixed-size cam_devq (M_CAMDEVQ) triggered by root
teardown on specific multi-channel hardware; heap-reuse steering would be
required to convert to more than a panic. Not pursuable without the HBA.
Fix
fix.diff: delete cam_simq_release(devq) from isp_attach_chan; drop the
caller's single reference in isp_attach unwind and isp_detach after all
sims are freed (upstream pattern). Not runtime-validatable on this guest
(no hardware) β fix_status: not_testable.
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 restores correct accounting (release once, after all sims); cannot be behaviorally validated on this guest (no HBA).
findings/poc/DF-3661/fix.diff; FreeBSD HEAD isp_attach/isp_detach devq handling verified
Confirmed kernel references
Detail
Exploit chain
on affected hardware: kldunload/devctl-detach -> premature kfree(cam_devq) -> in-flight CCBs on surviving buses hit freed devq (UAF read/write of alloc/send queue state) -> last cam_sim_free writes to freed memory; heap-reuse steering would be needed beyond panic - not pursuable without the HBA
Evidence (decisive lines)
VERDICT.md refcount walk table; trig_devq.c header comment with the arithmetic; run.log/env.txt (0 QLogic devices on guest); fix.diff (upstream-pattern release)
PoC changes
documentation trigger written from scratch (runtime needs multi-channel HBA); compiled and ran no-op on guest
Verified recommended fix
Remove the per-channel cam_simq_release from isp_attach_chan and drop the single caller reference in isp_attach unwind / isp_detach after all sims are freed.
Verdict
isp_attach allocates one cam_devq (isp_freebsd.c:213) and passes it to every isp_attach_chan, each of which calls cam_simq_release(devq) (isp_freebsd.c:95) - but cam_sim_alloc already took a per-sim reference (cam_sim.c:185) and the caller held only one. On multi-channel isp adapters (dual SCSI ISP1240/12160, NPIV 2422/2432/2532 with the nvports hint) the live refcount is off by nchan-1: the FIRST cam_sim_free during detach/unwind drives it to zero and kfrees the devq (cam_queue.c:248-256) while other sims still dereference sim->devq on every CCB (UAF), and their own cam_sim_free then executes --freed->refcount (write-after-free, potential double kfree). Single-channel cards balance and are unaffected - why this survived. Upstream FreeBSD releases once after all sims are gone (verified vs FreeBSD HEAD). Guest has 0 QLogic PCI devices: sequence cannot execute -> not_reproduced (device absent), impact none observable on guest.
No comments yet.