sgwrite: size_t->int truncation of uio_resid into buf_len plus unchecked kmalloc/M_ZERO memset-on-NULL yields local kernel panic
Summary
sgwrite stores uio->uio_resid (size_t 64-bit) into int buf_len at scsi_sg.c:698 with no overflow check. A write() with nbytes whose residual after 37-byte header+cdb has bit 31 set (e.g. nbytes=0x80000025) produces negative buf_len; subsequent kmalloc(buf_len M_DEVBUF M_WAITOK|M_ZERO) receives sign-extended impossible size. The M_ZERO optimization in sys/malloc.h unconditionally calls __builtin_memset(_malloc_item 0 _size) whenever (M_WAITOK|M_NULLOK)==M_WAITOK so a NULL return from _kmalloc panics in memset(NULL 0 huge) before caller checks. Same defect on else-if hdr->reply_len branch (:705-707) where user-controlled int reply_len passed unsanitized to kmalloc. sys/kern/sys_generic.c:336-337 negative nbytes clamp is dead code (overwritten at :349). writev bypasses even that. Reachable by operator group (0660 root:operator).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2281 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | gate analysis + source trace + exploit-chain stop reason | 3.0 KB | β raw |
| fix.diff | suggested-fix | reject uio_resid > INT_MAX / negative buf_len and bound user hdr->reply_len to [0,MAXPHYS] before kmalloc | 1.1 KB | view raw |
| fix_build.log | build-log | nativekernel rc=0 with all fixes applied (-Werror -DINVARIANTS) | 5.6 MB | β download |
| env.txt | environment | guest uname, kldstat, camcontrol devlist, /dev perms, pciconf, maxx groups | 2.7 KB | view raw |
| build.sh | build-log | documents the HW/permission gate (no PoC binary) | 492 B | view raw |
| run.sh | run-log | prints the gate proof | 471 B | view raw |
DF-2281 β scsi_sg.c sgwrite size_t->int truncation of uio_resid
Verdict: NOT REPRODUCED (permission-gated) β source bug CONFIRMED real; fix.diff compiles.
Classification: not_reproduced / permission-gated for the unprivileged actor /
impact=none. The truncation bug is real in source; the defense-in-depth fix.diff
compiles cleanly (nativekernel rc=0, -Werror, -DINVARIANTS).
Why not reproduced (the gate)
sgwrite() is the write(2) handler of the sg(4) SCSI-passthrough peripheral.
The guest has an sg node (/dev/sg0, mode 0600 root:operator) on the QEMU DVD-ROM,
but it is root-only. The unprivileged actor maxx (uid 1001, in no privileged
group β not operator, not wheel) cannot open /dev/sg0, so the sgwrite path is
unreachable. (The finding's own threat model is the operator group via mode 0660;
this guest uses 0600, closing even that.)
Gate proof (this guest):
$ ls -la /dev/sg0 crw------- root operator /dev/sg0 # mode 0600 β root only $ id maxx -> uid=1001(maxx) ... groups=1001(maxx) [not operator] $ (as maxx) camcontrol devlist -> couldn't open /dev/xpt0: Permission denied
The source bug (real, cited path:line)
sys/bus/cam/scsi/scsi_sg.c, sgwrite():
- int error = 0, cdb_len, buf_len, dir; (scsi_sg.c:648) β buf_len is int.
- buf_len = uio->uio_resid; (scsi_sg.c:698) β size_t (64-bit) silently
truncated into int. A write() whose residual after the 37-byte header+CDB has
bit 31 set yields a negative buf_len.
- buf = kmalloc(buf_len, M_DEVBUF, M_WAITOK | M_ZERO); (:700) β kmalloc takes
size_t, so the negative int is sign-extended to a huge size. The M_ZERO
fast-path in sys/sys/malloc.h unconditionally calls __builtin_memset(_, 0, _)
when (M_WAITOK|M_NULLOK)==M_WAITOK, so a NULL/oversized allocation panics inside
memset before the caller ever checks. (The nbytes clamp in
sys/kern/sys_generic.c:336-337 is overwritten at :349; writev bypasses it
entirely.)
- Same defect on the else if (hdr->reply_len != 0) branch (:705-707): the
user-controlled int hdr->reply_len is passed unsanitized to kmalloc.
Attacker model (per finding): a member of the operator group on a system where
/dev/sgN is 0660. On this guest the node is 0600 root-only, so the unprivileged
path is closed.
Exploit chain
Not developed β /dev/sg0 is 0600; maxx cannot open it, so there is no
unprivileged trigger. A root trigger would be rootβkernel (game-over by definition),
not an escalation. Valid hard blocker for the unprivileged model: the device node is
not openable by the actor.
Fix (defense-in-depth, validated to compile)
fix.diff rejects uio_resid > INT_MAX and buf_len < 0 before the kmalloc, and
bounds the user-controlled hdr->reply_len to [0, MAXPHYS]. Validated in the
combined nativekernel build (fix_build.log).
Reproduce
No runnable PoC for the unprivileged actor (/dev/sg0 is 0600). Artifacts:
env.txt, fix.diff, fix_build.log.
Fix verification
not_testablenot_testable: PoC cannot run on this guest (HW/permission gated). fix.diff validated to APPLY cleanly and COMPILE in full nativekernel build (rc=0, -Werror, -DINVARIANTS) alongside sibling fixes (findings/poc/DF-2281/fix_build.log).
Compile validation: nativekernel build of patched tree -> 'NK_DONE rc=0'. No runtime test possible (HW/permission gated).
Confirmed kernel references
Detail
Exploit chain
none β valid hard blocker (driver code path dead / device node root-only / no malicious device present). No unprivileged->root path.
Evidence (decisive lines)
kldstat -> kernel, ehci.ko, xhci.ko (no target driver module); pciconf -l -> no SCSI/HBA HW; camcontrol devlist -> only <QEMU QEMU DVD-ROM> (sg0,pass0,cd0) on PIIX3 ata-cam; id maxx -> uid=1001(maxx) gid=1001(maxx) groups=1001(maxx) [not operator]. Source confirmed at cited lines.
PoC changes
Created findings/poc/DF-2281/{VERDICT.md, fix.diff, manifest.json, build.sh, run.sh, env.txt, fix_build.log}. No PoC source (HW/permission gated).
Verified recommended fix
fix.diff rejects uio_resid>INT_MAX / negative buf_len and bounds hdr->reply_len to [0,MAXPHYS]. Full git-apply-able diff in findings/poc/DF-2281/fix.diff.
Verdict
NOT REPRODUCED β HW/permission gated on this guest. The bug is REAL in source (traced line-by-line). scsi_sg.c sgwrite size_t->int truncation of uio_resid; /dev/sg0 0600 root-only, maxx EACCES. Gate confirmed via kldstat (only kernel+ehci+xhci), pciconf -l (no SCSI/HBA HW), camcontrol devlist (only QEMU DVD-ROM on PIIX3 ata-cam), and id maxx (uid 1001 not in operator) for the device-node findings.
No comments yet.