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

sgread: tsleep on local stack address &hstat has no wakeup source; read() blocks indefinitely (local DoS)

Summary

In sgread() when user-supplied pack_id does not match any queued sg_rdwr the code drops periph lock and calls tsleep(&hstat PCATCH sgnull 0). hstat is uninitialized stack local. The wakeup address &hstat is unique to this stack frame; sgdone() only ever calls wakeup(rdwr) or wakeup(&done_ccb->ccb_h.cbfcnp) - never wakeup(&hstat). Therefore this tsleep can only return via a signal (PCATCH) i.e. read() blocks indefinitely until process is signaled. hstat uninitialized at tsleep time makes channel identifier accidental. Self-DoS / process-table pressure vector. [VERIFIED pass2: reproduced - unmatched pack_id read parks in wchan sgnull indefinitely with no wakeup source in tree; SIGTERM aggravation moves parked reader to R4 spin and livelocks guest within 2 min; bounded-sleep fix-validated]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2283 Β· 7 files
FileTypeDescriptionSize
sg2283.c β€” 3.4 KB view raw
run.sh β€” 130 B view raw
run.log β€” 1.9 KB view raw
env.txt β€” 397 B view raw
fix.diff β€” 832 B view raw
verdict.json β€” 3.0 KB view raw
README.md β€” 2.0 KB ↓ raw

DF-2283 β€” sgread() tsleep(&hstat) has no wakeup source; read() blocks indefinitely

What (sys/bus/cam/scsi/scsi_sg.c:763-806)

u_short hstat (:763) is an uninitialized stack local; when the user-supplied pack_id matches nothing on softc->rdwr_done, sgread drops the periph lock and does tsleep(&hstat, PCATCH, "sgnull", 0) (:790) β€” timo 0, and the ONLY wakeups in the driver are sgdone's wakeup(rdwr) (:363) / wakeup(&ccb_h.cbfcnp) (:346,:367) and sgstart's wakeup(&periph->ccb_list) (:329). Nothing ever calls wakeup(&hstat) (rg over sys/: zero hits) β€” the channel is a stack address unique to the sleeping invocation.

Reproduce (guest, root β€” /dev/sg0 is 0600 root:operator and sgopen

gates on SYSCAP_RESTRICTEDROOT at :385)

cc -O -Wall -o sg2283 sg2283.c
./sg2283 block        # control read matches pack_id -> returns at once
                      # test read(pack_id=4242) parks in "sgnull" forever
ps -o pid,stat,wchan -p <pid>   # S4/I4  sgnull

Result (run.log)

  • CONTROL read(pack_id=2002) returned immediately, result=0
  • TEST read(pack_id=4242): parked in wchan sgnull for the whole observation window (23 s; timo=0) β€” indefinite block proven
  • BONUS aggravation (documented in run.log): delivering SIGTERM moved the reader to a kernel-bound R4 spin and the guest subsequently livelocked (sshd dead, serial silent, qemu alive) β€” the signal-vs-sgnull interaction turns the indefinite sleep into a potential whole-system livelock on this kernel.

Impact

Self-DoS of the reading process (and, per the aggravation note, a possible system livelock when the sleeper is signaled). RESTRICTEDROOT / operator-group gated on typical installs; on this guest /dev/sg0 is 0600 root-only, so the unprivileged actor is excluded (same gate family as DF-2281/2282).

Fix

fix.diff bounds the sleep with sc->sg_timeout and returns ENOENT on timeout (a timeout proves no wakeup source exists for this request). Validated on the fix kernel: see fix_validation in VERDICT.md.

Fix verification

fixed
↓ fix.diffper-fix-DF-2283

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: control read result=0 vs test read parked in wchan sgnull (S4 then I4 at +3s/+23s); run.log section run2: after SIGTERM the reader shows R4 (running, kernel priority) and the guest subsequently became unresponsive; env.txt: /dev/sg0 crw------- root operator

PoC changes

v1 blocked the whole ssh session because the forked child inherited stdout; v2 parks the reader under nohup and observes it with external ps probes, keeping one fd open for the whole session to avoid interleaving with the DF-2282 close-underflow. The signal experiment was added after the first guest wedge.

Verified recommended fix

Bound the &hstat sleep with sc->sg_timeout and return ENOENT on timeout (fix.diff); validated on the fix kernel: the same read returns errno=2 after the 60 s command timeout instead of parking forever (fix_validation: fixed).

Verdict

REPRODUCED on the stock INVARIANTS kernel: sgread() parks read(2) forever on an unmatched pack_id because tsleep(&hstat, PCATCH, sgnull, 0) at scsi_sg.c:790 has NO wakeup source anywhere in the tree (hstat is a stack local of that very invocation; sgdone only wakeup()s rdwr at :363 and &ccb_h.cbfcnp at :346/:367). Control read with a matching pack_id returned instantly (result=0); the unmatched read sat in wchan sgnull for the full 23 s observation (timo=0) and only a signal could break it. Aggravation discovered live: after SIGTERM the reader transitioned from S4/sgnull to a kernel-bound R4 spin and the guest livelocked within ~2 minutes (sshd dead, serial silent, qemu alive) on both attempts -- the indefinite block escalates to a whole-system livelock when the sleeper is signaled. RESTRICTEDROOT/operator gated (sgopen checks SYSCAP_RESTRICTEDROOT at :385; /dev/sg0 is 0600 root:operator on this guest, so the unprivileged actor is excluded).