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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2281 Β· 6 files
FileTypeDescriptionSize
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
VERDICT.md verdict gate analysis + source trace + exploit-chain stop reason
↓ download 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_testable
baseline no→ patch + rebuild →patched clean

not_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).
↓ fix.diffn/a (no target HW/device on this guest)

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.