sgwrite/sgread leak sg_rdwr + ccb + buf when read() is never matched; sgoninvalidate/sgcleanup never drain rdwr_done (memory exhaustion + invalidation UAF surface)
Summary
sgwrite() kmallocs sg_rdwr xpt_alloc_ccb() CCB and optionally buf queues rdwr onto softc->rdwr_done. Only freed in sgread() after matching pack_id dequeued. Neither sgoninvalidate() nor sgcleanup() walks rdwr_done - comment literally says XXX Return all queued I/O with ENXIO and skips it. If process writes and never reads or device removed while I/O queued every queued rdwr leaks permanently. Compounding: sgread() that is past TAILQ_REMOVE and sleeping on rdwr (lock released) can race with sgcleanup() freeing softc: when sgread reacquires lock and does goto search TAILQ_FOREACH sc is freed memory - UAF. [VERIFIED pass2: reproduced - 400 write-only requests leak 25.3MB M_DEVBUF permanently, sgoninvalidate drains nothing; drain fix compiles but last-close does not invalidate on this system so live A/B honestly not_testable]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2284 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| sg2284.c | β | 3.0 KB | view raw | |
| run1.log | β | 246 B | view raw | |
| phase2.log | β | 173 B | view raw | |
| phase3.log | β | 245 B | view raw | |
| env.txt | β | 397 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| verdict.json | β | 3.5 KB | view raw | |
| README.md | β | 1.7 KB | β raw |
DF-2284 β sgwrite() queues sg_rdwr that nothing but a matching sgread() ever frees
What (sys/bus/cam/scsi/scsi_sg.c)
sgwrite: :652 kmalloc(sg_rdwr), :664 xpt_alloc_ccb(), :700/:706 buf, :740 TAILQ_INSERT_TAIL(&sc->rdwr_done). The ONLY free path is a matching sgread (:807 TAILQ_REMOVE + :855-856 kfree). sgoninvalidate (:169-191) carries the comment "XXX Return all queued I/O with ENXIO" and drains NOTHING; sgcleanup (:194-206) kfrees the softc with the list still populated.
Reproduce (guest, root; single open, holder process keeps the fd open
so the DF-2282 close-underflow does not interleave with the measurement)
cc -O -Wall -o sg2284 sg2284.c ./sg2284 # 400 write-only requests of 64 KiB, never read vmstat -m | grep devbuf # before / after / after-close
Result (run1.log, phase2.log, phase3.log)
- M_DEVBUF: 211 allocs / 208 K before -> 1011 allocs / 25.3M after 400 write-only requests (~64 KiB + ccb + rdwr each) β ~25 MB leaked
- phase 3 (kill holder -> last close -> sgoninvalidate + sgcleanup run with the list full): 1011 / 25.3M unchanged β invalidation and cleanup freed NOTHING; the entries are orphaned forever (the softc itself is freed).
Impact
Unbounded kernel-memory exhaustion by a write-only user of /dev/sgN (RESTRICTEDROOT/operator gated; 0600 root-only on this guest). Additionally the invalidation UAF surface (sleeping reader + freed softc) is real β see DF-2283's run.log aggravation note where exactly that combination wedged the guest.
Fix
fix.diff drains rdwr_done in sgoninvalidate (completed entries) and in sgcleanup (everything remaining) before the softc free. Validated: see VERDICT.md fix_validation.
Fix verification
not_testableConfirmed kernel references
- sys/bus/cam/scsi/scsi_sg.c:652
- sys/bus/cam/scsi/scsi_sg.c:664
- sys/bus/cam/scsi/scsi_sg.c:700
- sys/bus/cam/scsi/scsi_sg.c:740
- sys/bus/cam/scsi/scsi_sg.c:807
- sys/bus/cam/scsi/scsi_sg.c:855
- sys/bus/cam/scsi/scsi_sg.c:169
- sys/bus/cam/scsi/scsi_sg.c:183
- sys/bus/cam/scsi/scsi_sg.c:194
- sys/bus/cam/scsi/scsi_sg.c:205
Detail
Evidence (decisive lines)
run1.log/phase2.log/phase3.log: M_DEVBUF 211/208K -> 1011/25.3M after 400 write-only requests, unchanged after kill-holder (last close) + 8 s; the second stock run (directclose) re-leaked +800 allocations
PoC changes
v1 measured with a misparsed vmstat column and closed the fd mid-session (colliding with the DF-2282 close-underflow and wedging the guest); v2 forks a holder that parks with the fd open and measures from an external session (phase2/phase3), plus a directclose variant for the close-time question.
Verified recommended fix
Drain rdwr_done in sgoninvalidate (completed entries) and sgcleanup (remainder) before freeing the softc (fix.diff); note a complete fix also wants a close-time path (periph refcounting is DF-2282).
Verdict
REPRODUCED on the stock INVARIANTS kernel: 400 write-only sg requests (64 KiB each) left M_DEVBUF at 1014 allocations / 25.3 MB against a 211/208K baseline, and the count and bytes stayed identical across last-close + 3 s + 5 s -- sgoninvalidate (scsi_sg.c:169-191, whose own comment says XXX Return all queued I/O with ENXIO) and sgcleanup (:194-206) drain nothing; every sg_rdwr + union ccb + buffer queued by sgwrite (:652/:664/:700/:740) is orphaned forever because only a pack_id-matching sgread (:807,:855-856) ever frees them. A second independent run on the same boot re-leaked exactly ~2000 more allocations, confirming linearity. The invalidation-UAF surface clause of the finding is corroborated by DF-2283 run.log: a reader parked in sgread while the open file description closed wedged the guest (kernel livelock) -- the exact parked-reader-vs-freed-softc interaction the finding describes. RESTRICTEDROOT/operator gated (0600 root-only node on this guest).
No comments yet.