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 (
PCATCHis set, soSIGKILLworks, but the fd remains leaked until close). Repeatedly triggering this leaks kernel memory (rdwr+buf+ccbper lost transaction) and consumes a thread slot. - Required config or capabilities: Default kernel with
sgconfigured; root to issuesgwrite/sgread. - Reachability: Open
/dev/sgNβwrite()an async SCSI command (pack_id=42) viasgwriteβ start asgreadrequestingpack_id=42(blocks waiting for completion) β hot-unplug the SCSI device (or triggerAC_LOST_DEVICEvia 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.
Recommended fix
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
sys/bus/cam/scsi/scsi_sg.c:169-191βsgoninvalidate(missing cleanup, XXX-acknowledged)sys/bus/cam/scsi/scsi_sg.c:783-799βsgreadsleep paths that block foreversys/bus/cam/scsi/scsi_sg.c:880-900βsgdoneand the CCB completion path- CWE-664 Improper Control of a System Through its Lifecycle
- CWE-772 Missing Release of Resource after Effective Lifetime
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1054 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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/.
Recommended fix
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
fixedVALIDATED 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
Confirmed kernel references
- s
- y
- s
- /
- b
- u
- s
- /
- c
- a
- m
- /
- s
- c
- s
- i
- /
- s
- c
- s
- i
- _
- s
- g
- .
- c
- :
- 7
- 9
- 0
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.
No comments yet.