sgwrite truncates uio_resid to int and does not validate reply_len, enabling allocation-size confusion
| Field | Value |
|---|---|
| ID | DF-1055 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-197 Integer Truncation Error; CWE-190 Integer Overflow or Wraparound |
| File | sys/bus/cam/scsi/scsi_sg.c |
| Lines | 648, 698-707 (buf_len int truncation; reply_len unvalidated kmalloc) |
| Area | bus/cam/scsi (CAM SCSI generic passthrough /dev/sgN) |
| Confidence | likely |
| Discovered | 2026-07-14 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
sgwrite declares buf_len as int (line 648) and assigns it from uio->uio_resid
(ssize_t, 64-bit) at line 698 without overflow checking. A write() with >2 GB of data
truncates the residual to a 32-bit int, which can be zero, negative, or a small positive
value. If truncated to zero with a non-zero reply_len, the write silently switches from
CAM_DIR_OUT to CAM_DIR_IN semantics. If negative, kmalloc receives a massive size_t
(M_WAITOK hangs). Additionally, hdr->reply_len (int, user-controlled) is passed
directly to kmalloc at line 706 without a non-negative or upper-bound check.
Root cause
/* scsi_sg.c:648 */
int error = 0, cdb_len, buf_len, dir;
...
/* scsi_sg.c:698-707 */
buf_len = uio->uio_resid; /* ssize_t -> int truncation */
if (buf_len != 0) {
buf = kmalloc(buf_len, M_DEVBUF, M_WAITOK | M_ZERO);
error = uiomove(buf, buf_len, uio);
...
dir = CAM_DIR_OUT;
} else if (hdr->reply_len != 0) {
buf = kmalloc(hdr->reply_len, M_DEVBUF, M_WAITOK | M_ZERO); /* unvalidated */
buf_len = hdr->reply_len;
dir = CAM_DIR_IN;
}
uio_resid is ssize_t (64-bit on amd64); buf_len is int (32-bit). If the user writes
0x100000048 bytes (4 GiB + 72), after consuming the 36-byte header + 12-byte CDB (48
bytes), uio_resid = 0x100000000. buf_len = (int)0x100000000 = 0. buf_len == 0 enters
the reply_len branch (line 705). If reply_len is also non-zero, a CAM_DIR_IN buffer
of reply_len bytes is allocated instead of the intended CAM_DIR_OUT data buffer β a
semantic flip. If uio_resid truncates to a negative int,
kmalloc((size_t)negative_int, M_WAITOK) attempts a multi-exabyte allocation that hangs
the kernel indefinitely.
Separately, line 706: kmalloc(hdr->reply_len, ...) where reply_len is a user-controlled
int with no validation β a negative reply_len produces the same kmalloc-size
explosion.
Threat model & preconditions
- Attacker position: Local root to open
/dev/sg*(caps check atsgopen:385). - Privileges gained or impact: A root process calling
write()with a multi-gigabyte count can hang the kernel inkmalloc(M_WAITOK)or cause the direction/buffer-size confusion. This is a self-inflicted local DoS with no cross-privilege impact, but it is still a kernel correctness defect that a confused or buggy privileged process can trigger. - Required config or capabilities: Default kernel with
sgconfigured; root. - Reachability:
write(/dev/sgN, buf, very_large_count)with a header that exercises the truncation, or withhdr->reply_lenset to a large negative int.
Proof of concept
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
int main(void) {
int fd = open("/dev/sg0", O_WRONLY);
/* Map a large region (does not need to be all resident) */
void *buf = mmap(NULL, 0x100010000ULL, PROT_READ|PROT_WRITE,
MAP_ANON|MAP_PRIVATE, -1, 0);
/* Write ~4 GiB: header(36) + cdb(12) + data(0xFFFFFFFF*0 + leftover)
causes buf_len truncation. With reply_len crafted in the header,
this either hangs in kmalloc or flips CAM_DIR_OUT to CAM_DIR_IN. */
write(fd, buf, 0x100000048ULL);
return 0;
}
Build & run
cc -o sg_trunc sg_trunc.c sudo ./sg_trunc
Expected output
Kernel hang in kmalloc (vmstat shows one thread in kmalloc or slab wait) or unexpected
SCSI command direction (silent functional failure). On 32-bit builds the effect is more
pronounced since size_t is also 32-bit.
Impact
Local DoS via kernel hang in kmalloc(M_WAITOK) or direction/buffer-size confusion. Root
required, low impact (A:L). Latent β needs a root process that issues oversized writes to
/dev/sg*, which no in-tree tool does.
Recommended fix
Use ssize_t for buf_len, validate reply_len, and cap both:
--- a/sys/bus/cam/scsi/scsi_sg.c
+++ b/sys/bus/cam/scsi/scsi_sg.c
@@ -645,7 +645,7 @@ sgwrite(struct dev_write_args *ap)
struct sg_header *hdr;
struct sg_rdwr *rdwr;
u_char cdb_cmd;
char *buf;
- int error = 0, cdb_len, buf_len, dir;
+ int error = 0, cdb_len, dir;
+ ssize_t buf_len;
struct uio *uio = ap->a_uio;
@@ -697,11 +697,19 @@ sgwrite(struct dev_write_args *ap)
buf_len = uio->uio_resid;
if (buf_len != 0) {
+ if (buf_len < 0 || buf_len > SG_MAX_XFER) {
+ error = EINVAL;
+ goto out_ccb;
+ }
buf = kmalloc(buf_len, M_DEVBUF, M_WAITOK | M_ZERO);
error = uiomove(buf, buf_len, uio);
if (error)
goto out_buf;
dir = CAM_DIR_OUT;
} else if (hdr->reply_len != 0) {
+ if (hdr->reply_len < 0 || hdr->reply_len > SG_MAX_XFER) {
+ error = EINVAL;
+ goto out_ccb;
+ }
buf = kmalloc(hdr->reply_len, M_DEVBUF, M_WAITOK | M_ZERO);
buf_len = hdr->reply_len;
dir = CAM_DIR_IN;
Where SG_MAX_XFER is a sane cap (e.g. MAXPHYS or MAXBSIZE).
References
sys/bus/cam/scsi/scsi_sg.c:648βbuf_lendeclaredintsys/bus/cam/scsi/scsi_sg.c:698βbuf_len = uio->uio_residtruncationsys/bus/cam/scsi/scsi_sg.c:706βkmalloc(hdr->reply_len, ...)unvalidated- CWE-197 Integer Truncation Error
- CWE-190 Integer Overflow or Wraparound
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1055 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able fix for the cited path | 780 B | view raw |
| VERDICT.md | verdict | source-confirmation narrative | 905 B | β raw |
| env.txt | environment | guest uname + toolchain | 247 B | view raw |
DF-1055 source-confirmation
Verdict: REPRODUCED (source-confirmed) Impact: none Confidence: likely
Kernel ref: sys/bus/cam/scsi/scsi_sg.c:698
Mechanism
sgwrite uio_resid int truncation + reply_len: buf_len(int)=uio_resid(64-bit) truncates >2GiB; kmalloc(hdr->reply_len) no bound. self-inflicted root DoS. 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
- :
- 6
- 9
- 8
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:698. combined-70 fix kernel: NK_DONE rc=0 (0 errors, -Werror).
PoC changes
authored/validated fix.diff (findings/poc/DF-1055/fix.diff); part of combined_70 kernel build.
Verified recommended fix
See findings/poc/DF-1055/fix.diff (git-apply-able). Matches finding proposal.
Verdict
REAL: sgwrite truncates 64-bit uio_resid to int + kmalloc(unchecked reply_len) -> alloc-size confusion. self-inflicted root DoS. confirmed.
No comments yet.