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

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 at sgopen:385).
  • Privileges gained or impact: A root process calling write() with a multi-gigabyte count can hang the kernel in kmalloc(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 sg configured; root.
  • Reachability: write(/dev/sgN, buf, very_large_count) with a header that exercises the truncation, or with hdr->reply_len set 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.

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

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1055 Β· 3 files
FileTypeDescriptionSize
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
VERDICT.md verdict source-confirmation narrative
↓ download 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/.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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
↓ fix.diffDragonFly 6.5-DEVELOPMENT combined 70-finding fix kernel (built rc=0 -Werror 2026-07-23; not booted - source-only)

Confirmed kernel references

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.