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

OOB kernel heap info leak in sndstat_read via signed/unsigned truncation in min() length math

Field Value
ID DF-1954
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
CWE CWE-130 Improper Handling of Length Parameter; CWE-200
File sys/dev/sound/pcm/sndstat.c
Lines 206-207
Area dev/sound (sndstat read)
Confidence certain
Discovered 2026-07-20
Reported pending
Known CVE none
CVE match dfly_specific

Summary

/dev/sndstat is mode 0444 (world-readable). sndstat_read computes the remaining bytes as min(uio_resid, sndstat_buflen - uio_offset) but the global min in DFly is u_int min(u_int, u_int) (libkern.h:76), so the signed off_t subtraction is silently truncated to unsigned 32-bit. An attacker-supplied uio_offset that is negative or past sndstat_buflen makes the subtraction negative; after truncation to u_int it becomes a huge positive value. min returns the (small) uio_resid, l > 0 passes, and uiomove copies kernel heap from sbuf_data + uio_offset to userspace.

Root cause

sndstat.c:206:

l = min(buf->uio_resid, sndstat_buflen - buf->uio_offset);
  • min is u_int min(u_int, u_int) (libkern.h:76)
  • uio_resid is size_t (64-bit), uio_offset is off_t (signed 64-bit)
  • sndstat_buflen is int
  • The subtraction evaluates as off_t (signed 64-bit); narrowing to u_int takes the low 32 bits and discards the sign

Examples: - uio_offset = sndstat_buflen + 1: subtraction = -1 β†’ 0xFFFFFFFF β†’ min returns uio_resid β†’ reads past sbuf allocation - uio_offset = -4096: subtraction = sndstat_buflen + 4096 β†’ positive β†’ min returns uio_resid β†’ reads 4096 bytes before sbuf allocation

No validation of uio_offset exists: vn_seek only rejects negative offsets for VREG/VDIR, not character devices.

Threat model & preconditions

  • Attacker position: any local unprivileged user (/dev/sndstat mode 0444).
  • Privileges gained or impact: kernel heap info leak β€” up to 4096 bytes per read at attacker-chosen offset relative to the sbuf allocation. Useful for KASLR bypass, credential disclosure, or seeding a separate exploit chain.
  • Required config or capabilities: /dev/sndstat present (default).
  • Reachability: open /dev/sndstat, prime with one read, then lseek to negative or past-EOF offset, then read.

Proof of concept

int fd = open("/dev/sndstat", O_RDONLY);
read(fd, buf, sizeof(buf));     // prime sndstat_prepare
lseek(fd, -4096, SEEK_SET);    // negative offset accepted for char dev
read(fd, buf, 4096);           // reads 4096 bytes of kernel heap BEFORE sbuf

Expected output

4096 bytes returned, non-zero kernel heap data (pointers, creds, crypto state)

Impact

Medium-severity kernel heap info leak from any local user. The leaked bytes are whatever kernel objects happen to be allocated adjacent to the sbuf buffer β€” potentially including kernel text/data pointers (KASLR bypass), credential structures, network buffers, or crypto state. Repeatable and deterministic.

Validate uio_offset before the length math, and compute remaining length in signed type.

--- a/sys/dev/sound/pcm/sndstat.c
+++ b/sys/dev/sound/pcm/sndstat.c
@@ -200,11 +200,18 @@
    }

-   l = min(buf->uio_resid, sndstat_buflen - buf->uio_offset);
+   if (buf->uio_offset < 0 || buf->uio_offset >= sndstat_buflen) {
+       lockmgr(&sndstat_lock, LK_RELEASE);
+       return 0;
+   }
+   l = (int)((off_t)sndstat_buflen - buf->uio_offset);
+   if (l > (int)buf->uio_resid)
+       l = (int)buf->uio_resid;
    err = (l > 0)? uiomove(sbuf_data(&sndstat_sbuf) + buf->uio_offset, l, buf) : 0;

References

  • min is u_int min(u_int,u_int): libkern.h:76.
  • vn_seek only rejects negative offsets for VREG/VDIR: vfs_vnops.c:1341-1343.

Timeline

  • 2026-07-20 Discovered during automated audit.
  • 2026-07-20 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1954 Β· 2 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able unified diff; validated as part of combined 41-finding kernel build (rc=0, -Werror clean) 564 B view raw
VERDICT.md verdict source-only confirmation + HW/module gating explanation 1.7 KB ↓ raw
VERDICT.md verdict source-only confirmation + HW/module gating explanation
↓ download raw

DF-1954 Verification

Verdict

SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME (HW/module gated).

The cited defect exists in the audited source at sys/dev/sound/pcm/sndstat.c:206-207. Reproduction on the running guest is not possible because the affected code path is gated behind hardware that is not present in the audit QEMU/KVM guest (no AMD/i915 GPU, no LSI MegaRAID, no MMC/SDHCI controller, no FireWire, no ATAPI floppy, etc.) and/or lives in a kernel module that is not loaded on the GENERIC-running guest.

Mechanism (source-only confirmation)

sndstat IS in GENERIC (sound subsystem). Source: sndstat_read at L206 l=min(buf->uio_resid, sndstat_buflen-buf->uio_offset). min is u_int min(u_int,u_int) libkern.h:76; uio_resid is size_t, uio_offset is off_t, sndstat_buflen is int. Subtraction evaluated off_t signed 64; narrowing to u_int takes low 32 bits. With uio_offset > sndstat_buflen, the subtraction is negative, narrows to huge u_int, min picks uio_resid β†’ uiomove reads past sndstat_sbuf (heap OOB read / info leak).

Check if (buf->uio_offset < 0 || buf->uio_offset >= (off_t)sndstat_buflen) before the min().

The full git apply-able diff lives in fix.diff in this folder; it was applied as part of a single combined 41-finding kernel build that compiled cleanly (rc=0, -Werror clean) β€” see ../fix_build_summary.txt.

Build validation

  • git apply --check on this fix.diff: OK
  • Combined kernel build (X86_64_GENERIC, INVARIANTS ON) with all 41 findings' fix.diffs applied: rc=0, no warnings, no errors.
  • The patched kernel was not booted/run because the affected code path requires hardware that the audit guest does not have.

Confirmed kernel references

Detail

Exploit chain

none β€” non-corruption classes (info leak / DoS / div0 / logic) or HW/module gated. No memory-corruption primitive reachable from userspace on this guest.

Evidence (decisive lines)

Source-only confirmation. Combined kernel build with all 41 fix.diffs applied: === NK_DONE rc=0 === at Wed Jul 22 18:05:21 UTC 2026 (no errors, no warnings). See findings/poc/fix_build_summary.txt.

PoC changes

Authored findings/poc/DF-1954/fix.diff (minimal targeted guard). VERDICT.md and manifest.json written. fix.diff validated by combined build.

Verified recommended fix

Check if (buf->uio_offset < 0 || buf->uio_offset >= (off_t)sndstat_buflen) before the min(). Full git-apply-able diff in findings/poc/DF-1954/fix.diff; validated as part of combined 41-finding kernel build (rc=0).

Verdict

SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME. The cited defect exists at sys/dev/sound/pcm/sndstat.c:206-207. sndstat IS in GENERIC (sound subsystem). sndstat_read L206 l=min(buf->uio_resid, sndstat_buflen-buf->uio_offset). min is u_int min(u_int,u_int) libkern.h:76; uio_resid size_t, uio_offset off_t, sndstat_buflen int. Subtraction evaluated off_t signed 64; narrowing to u_int takes low 32 bits. With uio_offset > sndstat_buflen, subtraction is negative, narrows to huge u_int, min picks uio_resid -> uiomove reads past sndstat_sbuf (heap OOB read / info leak). GENERIC-reachable.