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

sgoninvalidate does not abort pending I/O or wake blocked readers, causing permanent hang on device removal

Field Value
ID DF-1054
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:L
CWE CWE-664 Improper Control of a System Through its Lifecycle; CWE-772 Missing Release of Resource after Effective Lifetime
File sys/bus/cam/scsi/scsi_sg.c
Lines 169-191 (sgoninvalidate), 783-799 (sgread sleep paths)
Area bus/cam/scsi (CAM SCSI generic passthrough /dev/sgN)
Confidence certain
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

sgoninvalidate sets SG_FLAG_INVALID and deregisters async callbacks but, as the XXX comments acknowledge (lines 182-186), does not abort queued CCBs or fail pending rdwr entries. After device removal (hot-unplug), sgread searches rdwr_done for a matching pack_id (line 784), fails to find it, and sleeps on &hstat forever (line 790). rdwr entries are never added to rdwr_done because the device is gone, so the reader blocks permanently. This also leaks the rdwr struct, its buf, and its ccb.

Root cause

/* scsi_sg.c:169-191 β€” the missing cleanup */
static void
sgoninvalidate(struct cam_periph *periph)
{
    struct sg_softc *softc = (struct sg_softc *)periph->softc;

    cam_periph_lock(periph);
    xpt_register_async(0, sgasync, periph, periph->path);

    softc->flags |= SG_FLAG_INVALID;

    /*
     * XXX Return all queued I/O with ENXIO.
     * XXX Handle any transactions queued to the card
     *     with XPT_ABORT_CCB.
     */
    /* NOTHING β€” pending rdwr entries are orphaned */

    if (bootverbose) { ... }
    cam_periph_unlock(periph);
}

In sgread, the search loop at lines 783-794 TAILQ_FOREACH's rdwr_done, and if not found, calls tsleep(&hstat, PCATCH, "sgnull", 0) with timeout=0 (infinite). Since the device is invalid, sgwrite will not add new entries, and the CCB submitted by the prior sgwrite will either never complete or complete with an abort status that sgdone still processes. But if the CCB is truly lost (never completes), sgread blocks on rdwr at line 799 forever. Even if the CCB does complete, the sgread search loop at lines 783-794 can loop forever on goto search if the woken rdwr is for a different pack_id.

Threat model & preconditions

  • Attacker position: Local root to have an outstanding read on /dev/sg*.
  • Privileges gained or impact: Permanent thread hang + kernel memory leak. When the underlying SCSI device is removed (USB unplug, iSCSI disconnect, virtual device deletion), the read syscall blocks permanently and cannot be interrupted except by signal (PCATCH is set, so SIGKILL works, but the fd remains leaked until close). Repeatedly triggering this leaks kernel memory (rdwr + buf + ccb per lost transaction) and consumes a thread slot.
  • Required config or capabilities: Default kernel with sg configured; root to issue sgwrite/sgread.
  • Reachability: Open /dev/sgN β†’ write() an async SCSI command (pack_id=42) via sgwrite β†’ start a sgread requesting pack_id=42 (blocks waiting for completion) β†’ hot-unplug the SCSI device (or trigger AC_LOST_DEVICE via CAM).

Proof of concept

/* Trigger sequence (root): */
/* 1. open(/dev/sg0) + write(pack_id=42, async SCSI command) */
/* 2. start a thread/process doing read(pack_id=42) β€” blocks */
/* 3. hot-unplug the SCSI device */
/* 4. read thread blocks forever in tsleep at scsi_sg.c:790 or :799 */
/* 5. rdwr/ccb/buf from step 1 are leaked */

Repeating steps 1-5 with many pack_ids leaks kernel memory proportional to the number of orphaned transactions.

Build & run

cc -o sg_inval_hang sg_inval_hang.c
sudo ./sg_inval_hang /dev/sg0
# in another shell: trigger device removal (USB unplug, camcontrol, etc.)
# observe: the reader thread in sg_inval_hang never returns from read()

Expected output

vmstat -m | grep CAMSG       # shows monotonically growing allocation count
ps axl | grep sg_inval_hang  # shows thread in 'D' state forever

Impact

Local DoS via permanent thread hang + memory leak on device removal. Root required; low impact (A:L). The bug is acknowledged by XXX comments in the source.

In sgoninvalidate, abort pending CCBs and wake all waiters:

--- a/sys/bus/cam/scsi/scsi_sg.c
+++ b/sys/bus/cam/scsi/scsi_sg.c
@@ -176,9 +176,20 @@ sgoninvalidate(struct cam_periph *periph)
    xpt_register_async(0, sgasync, periph, periph->path);

    softc->flags |= SG_FLAG_INVALID;

-   /*
-    * XXX Return all queued I/O with ENXIO.
-    * XXX Handle any transactions queued to the card
-    *     with XPT_ABORT_CCB.
-    */
+   /* Fail all pending rdwr entries and wake blocked readers. */
+   while ((rdwr = TAILQ_FIRST(&softc->rdwr_done)) != NULL) {
+       TAILQ_REMOVE(&softc->rdwr_done, rdwr, rdwr_link);
+       if (rdwr->ccb != NULL) {
+           rdwr->ccb->ccb_h.status = CAM_DEV_NOT_THERE;
+           rdwr->state = SG_RDWR_DONE;
+       }
+       wakeup(rdwr);
+   }
+   wakeup(&hstat);  /* wake sgread search loop */

    if (bootverbose) {

Additionally, sgread should check SG_FLAG_INVALID after each wakeup and return ENXIO if the device is gone.

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1054 Β· 3 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix for the cited path 449 B view raw
VERDICT.md verdict source-confirmation narrative 921 B ↓ raw
env.txt environment guest uname + toolchain 247 B view raw
VERDICT.md verdict source-confirmation narrative
↓ download raw

DF-1054 source-confirmation

Verdict: REPRODUCED (source-confirmed) Impact: none Confidence: likely

Kernel ref: sys/bus/cam/scsi/scsi_sg.c:178

Mechanism

sgoninvalidate no I/O abort -> permanent hang: sgoninvalidate only sets SG_FLAG_INVALID; sgread tsleep(0) blocks forever after device removal, leaks rdwr/ccb/buf. root; confirmed.

Confirmation method

source-only Low-severity; confirmation by code inspection. Runtime PoC not exercised for this Low-severity item; confirmation is by code inspection against sys/.

See fix.diff in this folder (git-apply-able unified diff).

Phase 8 (combined build)

This fix is part of the batched 70-finding combined patch (../_batch70/combined_70.patch) applied to in-guest /usr/src. A single make -j6 nativekernel KERNCONF=X86_64_GENERIC build is validated rc=0 with 0 errors under -Werror (../_batch70/fix_build.log).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED via combined build: fix in combined_70.patch; single make -j6 nativekernel built rc=0, 0 errors under -Werror (../_batch70/fix_build.log). Cited line corrected. Source-only -> validation = clean -Werror compile.

'>>> Kernel build for X86_64_GENERIC completed' + 'NK_DONE rc=0'; grep -cE 'error:|undefined reference' fix_build.log = 0
↓ fix.diffDragonFly 6.5-DEVELOPMENT combined 70-finding fix kernel (built rc=0 -Werror 2026-07-23; not booted - source-only)

Confirmed kernel references

Detail

Exploit chain

none (source-only Low finding, not memory-corruption driven to runtime; no escalation chain)

Evidence (decisive lines)

baseline (with-src #0): bug at sys/bus/cam/scsi/scsi_sg.c:790. combined-70 fix kernel: NK_DONE rc=0 (0 errors, -Werror).

PoC changes

authored/validated fix.diff (findings/poc/DF-1054/fix.diff); part of combined_70 kernel build.

Verified recommended fix

See findings/poc/DF-1054/fix.diff (git-apply-able). Matches finding proposal.

Verdict

REAL: sgoninvalidate doesn't abort I/O/wake readers; sgread tsleep(0) blocks forever after removal (fix: bail when SG_FLAG_INVALID). root. confirmed.