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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3661 Β· 8 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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_release drops that sim's reference when the sim dies (cam_sim.c:246-252).
  • cam_devq_release is if (--refcount == 0) { camq_fini x2; kfree(devq) } (cam_queue.c:248-256). No underflow protection.

The driver:

  • isp_attach allocates the devq once (isp_freebsd.c:213) and hands the same pointer to every isp_attach_chan (isp_freebsd.c:219-223).
  • Each isp_attach_chan β€” right after its cam_sim_alloc β€” executes cam_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)

  1. chan Nβˆ’1: cam_sim_free(sim) β†’ --refcount β†’ 0 β†’ devq kfree'd.
  2. 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) dereferences sim->devq β†’ use-after-free reads/writes of alloc_queue/send_queue state.
  3. Each later cam_sim_free calls cam_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_testable
baseline no→ patch + rebuild →patched clean

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

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.