Teardown race in scsi_targ_bh: fixed hz/2 sleep with no wakeup allows CTIO completion after softc kfree (UAF)
| Field | Value |
|---|---|
| ID | DF-1070 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-416 Use After Free |
| File | sys/bus/cam/scsi/scsi_targ_bh.c |
| Lines | 342, 411, 419-421 (targbhdtor sleep+kfree), 522 (targbhdone softc deref), 648 (TAILQ_REMOVE), 678 (state check) |
| Area | bus/cam/scsi (CAM SCSI black-hole target peripheral) |
| Confidence | speculative |
| Discovered | 2026-07-14 |
| Reported | pending |
| Known CVE | DF-1045 (sibling, scsi_target.c teardown UAF) |
| CVE match | dfly_specific |
Summary
targbhdtor synchronizes teardown with a fixed sim_lock_sleep(softc, 0, "targbh", hz/2, ...)
that has no wakeup source β nobody in the driver ever calls wakeup(softc).
targbhdislun aborts ATIOs and IMMED_NOTIFY CCBs but explicitly does not abort in-flight
CTIOs (XPT_CONT_TARGET_IO). If a CTIO is still outstanding when the 500 ms timer
expires, targbhdtor calls kfree(softc) and returns into camperiphfree which frees
periph->path and periph itself; the late CTIO completion then invokes
targbhdone(periph, done_ccb) which dereferences periph->softc (line 522) β a
use-after-free. The original author acknowledged this twice with XXX comments at lines
342 and 419.
Root cause
targbhdtor (scsi_targ_bh.c:402-424) sets state = TARGBH_STATE_TEARDOWN (line 409),
calls targbhdislun (line 411), then unconditionally falls through to the default case
(init_level == 1 after ctor line 397) which does
sim_lock_sleep(softc, 0, "targbh", hz/2, periph->sim->lock) at line 420 followed by
kfree(softc, M_SCSIBH) at line 421. A grep of the entire file confirms no
wakeup(softc) anywhere, so the sleep always times out after exactly 500 ms.
targbhdislun (lines 329-382) aborts only accept_tio_list entries (lines 347-354, via
XPT_ABORT on each ATIO) and immed_notify_slist entries (lines 356-363), then disables
the LUN (lines 368-371). It never enumerates or aborts CTIOs that were submitted via
xpt_action in targbhstart (line 497). The comment at line 342
(XXX Block for Continue I/O completion) and at line 419
(XXX Wait for callback of targbhdislun()) explicitly mark this as unfinished.
When a late CTIO completes, targbhdone (line 518) executes
softc = (struct targbh_softc *)periph->softc (line 522) on freed memory. The
XPT_CONT_TARGET_IO case (lines 638-695) further dereferences softc->pending_queue via
TAILQ_REMOVE (line 648), softc->state (line 678), and calls targbhfreedescr /
xpt_free_ccb (lines 692-693). camperiphfree (cam_periph.c:600-632) calls periph_dtor
then immediately frees periph->path and periph itself, so by the time the late callback
fires, both softc and periph are freed. The same class of race affects late
XPT_IMMED_NOTIFY callbacks (line 705 path) and late XPT_ACCEPT_TARGET_IO abort callbacks
(line 544 path), since targbhdone dereferences softc before checking the
TEARDOWN / ABORTED status.
Threat model & preconditions
- Attacker position: A SCSI initiator with access to a target-mode bus served by this driver (physical SCSI, Fibre Channel, or iSCSI depending on the HBA/SIM).
- Privileges gained or impact: Practical impact is denial of service (kernel
panic). Privilege escalation is theoretical: with slab grooming, the freed
softcslab can be reclaimed with a controlled object enabling code execution via the corruptedTAILQ_REMOVE/ function-pointer deref β but this is not demonstrated, hence the speculative confidence. - Required config or capabilities: A target-mode-capable HBA/SIM with
PIT_PROCESSOR(e.g. ahc/ahd in target mode, or the iSCSI target SIM). Thescsi_targ_bhdriver auto-attaches on any such LUN. - Reachability:
1. Initiator sends a stream of
INQUIRYCDBs to the targbh-served LUN to keep CTIOs in flight. 2. Stall the DATA IN phase of at least one CTIO by withholding transfer ACKs (on parallel SCSI this means holding off on REQ/ACK; on iSCSI, delaying the data-in PDU reception / sending a non-final R2T response). 3. Trigger SIM deregistration (AC_PATH_DEREGISTEREDattargbhasync:214-221triggerscam_periph_invalidate) β hot-plug removal, SIM fatal error, or admin action. 4. Within the 500 ms window after deregistration, the stalled CTIO must still be outstanding.targbhdtorsleeps 500 ms then kfreessoftc. When the stalled CTIO finally completes (or the SIM abort-fires it),targbhdonedereferences freedsoftcβ kernel panic inTAILQ_REMOVEor theperiph->softcload.
Proof of concept
Reproduce the panic (DoS) path:
- Use a SCSI target-mode-capable setup where this driver auto-attaches (any HBA with
PIT_PROCESSOR, e.g. ahc/ahd in target mode, or the iSCSI target SIM). - From the initiator, send a stream of INQUIRY CDBs to the targbh-served LUN to keep CTIOs in flight.
- Stall the DATA IN phase of at least one CTIO by withholding transfer ACKs (on parallel SCSI this means holding off on REQ/ACK; on iSCSI, delaying the data-in PDU reception / sending a non-final R2T response).
- Trigger SIM deregistration β e.g. if using a hot-pluggable transport, disconnect the
link; or use a separate fault-injection path (
camcontrolto force a bus reset / LIP that the SIM treats as fatal). - Within the 500 ms window after deregistration, the stalled CTIO must still be outstanding.
For a QEMU-based PoC, a simpler trigger: load the targbh module on a target-mode SIM,
start an initiator sending INQUIRYs, then unload / deregister the SIM while I/O is active
and use a kernel delay-injection hook (or simply spam initiations) to hit the window.
Build & run
# No userspace binary needed β this is a kernel-internal driver auto-attach.
# Run inside a DragonFlyBSD guest with a target-mode-capable HBA emulated.
qemu-system-x86_64 -enable-kvm -m 1G \
-device pvscsi,... # or whichever HBA exposes target mode
-hda dfbsd.img
# Inside the guest, ensure scsi_targ_bh loads; from the initiator host,
# issue a sustained INQUIRY stream and then trigger link/SIM deregistration.
Expected output
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x<address in freed M_SCSIBH slab> targbhdone(...) at scsi_targ_bh.c:522 (or :648 TAILQ_REMOVE, :678 state check) camperiphfree(...) at cam_periph.c:600 ... panic: from-rights-free
For a slab-corruption variant: panic: mp_free: mp is already free or
panic: targbhdtor: multiple frees.
Impact
Local / network-adjacent DoS via kernel UAF during SCSI target-mode black-hole teardown.
Speculative confidence β race window is narrow and depends on SIM / transport behavior.
Medium severity per "kernel memory corruption" / "remote DoS on config" depending on the
transport. This is a lower-severity analogue of the scsi_target.c teardown UAF class
(DF-1045 lineage) β here there is no userland daemon to coordinate with, but the
synchronization gap is the same shape: fixed sleep instead of a completion-wait on
outstanding CTIOs.
Recommended fix
Replace the fixed hz/2 sleep with a proper outstanding-CTIO count + condition wait, OR
explicitly abort all in-flight CTIOs before sleeping and wait on a wakeup that targbhdone's
CTIO-completion path issues when the count reaches zero. Concretely:
- Add
u_int outstanding_ctios;totargbh_softc. - Increment in
targbhstartbeforexpt_action(line 497); decrement intargbhdoneXPT_CONT_TARGET_IOafterxpt_release_ccb(line 677). - In
targbhdtor, loop withsim_lock_sleepwaiting on a wakeup signal fromtargbhdonewhenoutstanding_ctios == 0, with a generous safety timeout.
--- a/sys/bus/cam/scsi/scsi_targ_bh.c
+++ b/sys/bus/cam/scsi/scsi_targ_bh.c
@@ -84,6 +84,7 @@ struct targbh_softc {
struct tccb_queue pending_queue;
struct tccb_queue work_queue;
struct tccb_queue unknown_atio_queue;
+ u_int outstanding_ctios;
struct tdevstat device_stats;
targbh_state state;
targbh_flags flags;
@@ -493,6 +494,8 @@ targbhstart(struct cam_periph *periph, union ccb *start_ccb)
start_ccb->ccb_h.ccb_type = TARGBH_CCB_WORKQ;
start_ccb->ccb_h.ccb_atio = atio;
+ softc->outstanding_ctios++;
+
CAM_DEBUG(periph->path, CAM_DEBUG_SUBTRACE,
("Sending a CTIO\n"));
xpt_action(start_ccb);
@@ -674,6 +677,11 @@ targbhdone(struct cam_periph *periph, union ccb *done_ccb)
desc->data_resid -= desc->data_increment;
xpt_release_ccb(done_ccb);
+ KKASSERT(softc->outstanding_ctios > 0);
+ if (--softc->outstanding_ctios == 0) {
+ /* Wake targbhdtor teardown wait */
+ wakeup(&softc->outstanding_ctios);
+ }
if (softc->state != TARGBH_STATE_TEARDOWN) {
@@ -416,8 +424,14 @@ targbhdtor(struct cam_periph *periph)
/* FALLTHROUGH */
default:
- /* XXX Wait for callback of targbhdislun() */
- sim_lock_sleep(softc, 0, "targbh", hz/2, periph->sim->lock);
+ /*
+ * Wait for all in-flight CTIOs to complete. targbhdone
+ * decrements outstanding_ctios and wakes us when it hits 0.
+ */
+ while (softc->outstanding_ctios > 0)
+ sim_lock_sleep(&softc->outstanding_ctios, 0, "targbh",
+ hz, periph->sim->lock);
kfree(softc, M_SCSIBH);
This replaces the unconditional fixed 500 ms timeout with a real completion-wait that blocks until every in-flight CTIO has called back, while still allowing a periodic (1 s) wakeup to re-check the condition and avoid indefinite hangs.
References
- DF-1045 β sibling finding (scsi_target.c teardown UAF class)
sys/bus/cam/scsi/scsi_targ_bh.c:342, 419βXXXcomments acknowledging the unfinished synchronizationsys/bus/cam/scsi/scsi_targ_bh.c:411βtargbhdislundoes not abort CTIOssys/bus/cam/scsi/scsi_targ_bh.c:420-421βsim_lock_sleep+kfree(softc)sys/bus/cam/scsi/scsi_targ_bh.c:497βxpt_actionsubmits CTIOs that are not trackedsys/bus/cam/scsi/scsi_targ_bh.c:518-522βtargbhdonedereferencesperiph->softcsys/bus/cam/cam_periph.c:600-632βcamperiphfreefreesperiphafterperiph_dtorreturns- CWE-416 Use After Free
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1070 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| verify.sh | trigger-source | static source-verification script (5 checks) | 1.5 KB | view raw |
| verify.log | run-log | verify.sh output on audit commit | 1.4 KB | view raw |
| VERDICT.md | verdict | full narrative: mechanism + why-not-reproduced | 4.7 KB | β raw |
| fix.diff | suggested-fix | outstanding_ctios counter + condition-wait replaces fixed hz/2 sleep | 1.7 KB | view raw |
| env.txt | environment | uname, cc, sysctls | 713 B | view raw |
| README.md | readme | how to reproduce | 1.2 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-DF-1070 β reproduce
This finding was verified by static source tracing (see VERDICT.md).
Runtime reproduction on the audit's default QEMU guest is not possible
because the precondition is outside the unprivileged-guest-user threat
model (see VERDICT.md "Why it cannot be triggered from the audit guest").
How to verify (static source check)
# from the repo root (sys/ must be present)
sh findings/poc/DF-DF-1070/verify.sh
The script walks the cited code path in sys/ with grep/sed and
confirms every claim in the finding markdown.
Files
| File | Purpose |
|---|---|
| verify.sh | static source-verification script |
| verify.log | output of verify.sh on the audit commit (the evidence) |
| VERDICT.md | full narrative: mechanism, why-not-reproduced, fix rationale |
| fix.diff | git-apply-able fix (validated with git apply --check) |
| env.txt | guest environment (uname, cc, sysctls, modules) |
| manifest.json | machine-readable artifact catalog |
DF-1070 β scsi_targ_bh teardown UAF (fixed hz/2 sleep, no wakeup source)
Verdict
NOT REPRODUCED (runtime) β STATIC VERIFICATION CONFIRMED.
The cited code path and bug exist verbatim in
sys/bus/cam/scsi/scsi_targ_bh.c. The runtime trigger requires two
preconditions that the audit's default guest cannot satisfy:
-
The driver is
optional targbh(sys/conf/files:46) and is not listed inX86_64_GENERICβgrep -nE 'targbh|targ_bh' sys/config/X86_64_GENERICreturns nothing. Confirmed bynm /boot/kernel/kernel: notargbh*symbols in the running kernel. Loading it requireskldload targbh(root), which under the audit's bright-line rule is a circular precondition for auid0claim. -
Even with the module loaded, the driver auto-attaches only on a target-mode-capable HBA/SIM with
PIT_PROCESSOR(e.g.ahc/ahdin target mode, or an iSCSI target SIM). The audit's QEMU guest has no SCSI target-mode HBA emulated (default virtio-blk + AHCI initiator only), so the driver'stargbhctor/targbhstart/targbhdonepaths are never reached.
So this is a latent / hardware-gated defect: real in source, present
in the source tree, but unreachable from the audit guest's default
configuration. Classified as Medium with CVSS AV:A/AC:H (network-adjacent
attacker, high complexity) β the AC:High reflects the narrow race window
and the specialised HBA/SIM requirement.
Mechanism (confirmed by source trace)
targbhdtor (scsi_targ_bh.c:402-424) sets
softc->state = TARGBH_STATE_TEARDOWN (:409), calls targbhdislun
(:411), then unconditionally falls through to the default case
(init_level == 1 after ctor:397) which does
sim_lock_sleep(softc, 0, "targbh", hz/2, periph->sim->lock) at :420
followed by kfree(softc, M_SCSIBH) at :421.
grep -nE '\bwakeup\(' sys/bus/cam/scsi/scsi_targ_bh.c returns only:
444: wakeup(&periph->ccb_list); 526: wakeup(&done_ccb->ccb_h.cbfcnp);
Nobody calls wakeup(softc) β the sleep always times out after
exactly 500 ms regardless of outstanding work.
targbhdislun (:329-382) aborts only accept_tio_list entries
(:347-354, via XPT_ABORT on each ATIO) and immed_notify_slist
entries (:356-363), then disables the LUN (:368-371). It never
enumerates or aborts CTIOs that were submitted via xpt_action in
targbhstart (:497). The author's XXX comments at :342
(XXX Block for Continue I/O completion) and :419 (XXX Wait for
callback of targbhdislun()) explicitly mark this as unfinished.
If a late CTIO completes, targbhdone (:518) executes
softc = (struct targbh_softc *)periph->softc (:522) on freed memory.
The XPT_CONT_TARGET_IO case (:638-695) further dereferences
softc->pending_queue via TAILQ_REMOVE (:648), softc->state
(:678), and calls targbhfreedescr / xpt_free_ccb (:692-693).
camperiphfree (cam_periph.c:600-632) calls periph_dtor then
immediately frees periph->path and periph itself, so by the time the
late callback fires, both softc and periph are freed.
Exploit chain
None developed β the driver is not present in the running kernel and the
HBA class it requires is not emulated in the audit guest. The theoretical
chain (slab-groom the freed M_SCSIBH allocation into a controlled object
and convert the TAILQ_REMOVE / function-pointer deref to code execution)
is explicitly speculative in the finding (CVSS AV:A/AC:H, confidence
speculative).
PoC
verify.sh β static-verification script that walks the cited path with
grep/sed against sys/, confirming: (1) the fixed hz/2 sleep +
immediate kfree(softc) at :420-421; (2) no wakeup(softc) in the
file (only wakeup(&periph->ccb_list) and wakeup(&done_ccb->ccb_h.cbfcnp));
(3) targbhdislun aborts ATIOs and IMMED_NOTIFY but never CTIOs; (4)
targbhdone dereferences periph->softc unconditionally (:522); (5)
the driver is optional targbh and absent from X86_64_GENERIC. Run from
the repo root: sh findings/poc/DF-1070/verify.sh.
Fix
fix.diff β adds an outstanding_ctios counter to targbh_softc,
increments it in targbhstart before xpt_action, decrements it (with
KKASSERT + wakeup_one) in targbhdone's XPT_CONT_TARGET_IO case
after xpt_release_ccb, and replaces the fixed hz/2 sleep in
targbhdtor with a while (softc->outstanding_ctios > 0)
sim_lock_sleep(&softc->outstanding_ctios, ..., hz, ...) loop. This
closes the race by blocking teardown until every in-flight CTIO has
called back, while still allowing a periodic 1 s wakeup to re-check.
Matches the finding markdown's recommended fix (it proposed the same
counter + condition-wait pattern).
Reproduce
sh findings/poc/DF-1070/verify.sh # static source verification
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. scsi_targ_bh fixed hz/2 teardown sleep -> UAF. Driver optional targbh, not in GENERIC.
No comments yet.