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

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 softc slab can be reclaimed with a controlled object enabling code execution via the corrupted TAILQ_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). The scsi_targ_bh driver auto-attaches on any such LUN.
  • Reachability: 1. Initiator sends a stream of INQUIRY CDBs 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_DEREGISTERED at targbhasync:214-221 triggers cam_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. targbhdtor sleeps 500 ms then kfrees softc. When the stalled CTIO finally completes (or the SIM abort-fires it), targbhdone dereferences freed softc β†’ kernel panic in TAILQ_REMOVE or the periph->softc load.

Proof of concept

Reproduce the panic (DoS) path:

  1. 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).
  2. From the initiator, send a stream of INQUIRY CDBs to the targbh-served LUN to keep CTIOs in flight.
  3. 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).
  4. Trigger SIM deregistration β€” e.g. if using a hot-pluggable transport, disconnect the link; or use a separate fault-injection path (camcontrol to force a bus reset / LIP that the SIM treats as fatal).
  5. 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.

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; to targbh_softc.
  • Increment in targbhstart before xpt_action (line 497); decrement in targbhdone XPT_CONT_TARGET_IO after xpt_release_ccb (line 677).
  • In targbhdtor, loop with sim_lock_sleep waiting on a wakeup signal from targbhdone when outstanding_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

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1070 Β· 8 files
FileTypeDescriptionSize
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
README.md readme how to reproduce
↓ download 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
VERDICT.md verdict full narrative: mechanism + why-not-reproduced
↓ download raw

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:

  1. The driver is optional targbh (sys/conf/files:46) and is not listed in X86_64_GENERIC β€” grep -nE 'targbh|targ_bh' sys/config/X86_64_GENERIC returns nothing. Confirmed by nm /boot/kernel/kernel: no targbh* symbols in the running kernel. Loading it requires kldload targbh (root), which under the audit's bright-line rule is a circular precondition for a uid0 claim.

  2. Even with the module loaded, the driver auto-attaches only on a target-mode-capable HBA/SIM with PIT_PROCESSOR (e.g. ahc/ahd in 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's targbhctor/targbhstart/targbhdone paths 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_testable

compile 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.