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

Attacker-controlled kernel heap allocation size via read() resid

Field Value
ID DF-0924
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
CWE CWE-789 Uncontrolled Memory Allocation
File sys/vfs/procfs/procfs_map.c
Lines 61-77
Area vfs
Confidence likely
Discovered 2026-07-05
Reported pending
Known CVE none
CVE match dfly_specific

Summary

procfs_domap() sizes its scratch sbuf from uio_offset+uio_resid (i.e. from the read() length the caller supplies). The only upper bound is buflen < INT_MAX (~2 GiB), so a single read(/proc/<pid>/map, ..., 0x7fffffff) forces the kernel to malloc up to ~2 GiB per call. Looping across many fds/processes is a straightforward local kernel-memory-exhaustion vector.

Root cause

At sys/vfs/procfs/procfs_map.c:61:

ssize_t buflen = uio->uio_offset + uio->uio_resid;

The validation at procfs_map.c:75 (buflen >= INT_MAX) caps buflen at ~2 GiB but does nothing to keep it reasonable for a human-readable pseudo-file. procfs_map.c:77 then sbuf_new(sb, NULL, buflen+1, 0) β€” with flag 0 (no SBUF_AUTOEXTEND) sbuf_newbuf() at sys/kern/subr_sbuf.c:178-205 unconditionally does s->s_buf = SBMALLOC(s->s_size), i.e. a single kmalloc of the requested (up to ~2 GiB) size up front. The reader fully controls uio_resid via the length of read().

Threat model & preconditions

  • Attacker position: Local, unprivileged, default config, procfs mounted (and via DF-0921 against any visible pid).
  • Privileges gained or impact: Kernel memory pressure / OOM / failure to service other allocations (denial of service). On a system that actually has the memory the allocation succeeds and is retained; on a tighter system the malloc fails and procfs_map.c:78-79 returns EIO (no corruption, but still resource cost).
  • Required config or capabilities: procfs mounted.
  • Reachability: pread(fd, buf, 0x7ffffff0, 0) in a loop, optionally from many processes/fds.

Proof of concept

PoC source: findings/poc/DF-0924/alloc_dos.c

Build & run

cc -o alloc_dos alloc_dos.c
./alloc_dos           # in several terminals in parallel
vmstat -w 1           # observe freemem collapse

Expected output

freemem collapses; the system becomes unresponsive / OOM-killer activity / procfs_map.c:78 EIO storms in dmesg. On a 2 GiB-RAM VM the panic / OOM kill comes within seconds.

Impact

Local denial of service via kernel memory exhaustion. No escalation, no corruption.

Cap the sbuf at a sane bound regardless of the caller's requested read length (the output is already positionally truncated by uiomove_frombuf at procfs_map.c:245, so a smaller buffer simply means the reader re-issues read() with a higher offset β€” the normal procfs pattern):

--- a/sys/vfs/procfs/procfs_map.c
+++ b/sys/vfs/procfs/procfs_map.c
@@ -59,7 +59,9 @@
    struct proc *p = lp->lwp_proc;
    ssize_t buflen = uio->uio_offset + uio->uio_resid;
+#define PROCFS_MAP_MAXBUF  (1U << 20)   /* 1 MiB, ample for one read */
+   if (buflen > PROCFS_MAP_MAXBUF)
+       buflen = PROCFS_MAP_MAXBUF;
    struct vnode *vp;

References

Timeline

  • 2026-07-05 Discovered during automated audit.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0924 Β· 21 files
FileTypeDescriptionSize
alloc_dos.c trigger-source rewritten PoC: --time (timing proof) and --starve N S (concurrent-allocation panic) modes 6.0 KB view raw
README.md readme original PoC README (build/run/expected) 1.0 KB ↓ raw
build.sh build-script cc -O2 -o alloc_dos alloc_dos.c 132 B view raw
run.sh run-script ./alloc_dos --time (default) or ./alloc_dos --starve 4 8 (with 'starve' arg) 839 B view raw
build.log build-log final successful build, full output 68 B view raw
run.log run-log decisive baseline timing run (2.5s huge resid) 411 B view raw
run.baseline.log run-log baseline --time on unpatched #0 (2.75s huge resid) 400 B view raw
run.patched.log run-log patched #1 --time (1.45ms huge resid) 400 B view raw
run.patched.2.log run-log patched #1 --time repeat (4.86ms) 400 B view raw
run.patched.3.log run-log patched #1 --time repeat (3.25ms) 400 B view raw
run.patched.starve.log run-log patched #1 --starve 4 8 (no panic, freemem flat) 2.0 KB view raw
starve.log run-log baseline --starve attempt (ssh died as guest panicked) 0 B ↓ download
panic.txt panic-signature panic: sbuf: malloc limit exceeded; stack procfs_domap->sbuf_new->kmalloc 2.8 KB view raw
boot_panic.log boot-log full serial boot.log captured at panic 13.5 KB view raw
env.txt environment uname, kern.version, cc version, procfs mount, patched-kernel sha256 629 B view raw
fix.diff suggested-fix git-apply-able: cap buflen at 1MiB (PROCFS_MAP_MAXBUF) before sbuf_new 938 B view raw
fix_build.log build-log first single-fix kernel build (rc=0) 5.6 MB ↓ download
VERDICT.md verdict full narrative: mechanism, before/after evidence, fix validation 7.3 KB ↓ raw
manifest.json manifest this file 3.9 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme original PoC README (build/run/expected)
↓ download raw

DF-0924 β€” PoC: kernel memory exhaustion via large read() resid

Goal

Demonstrate that an unprivileged local user can force the kernel to kmalloc up to ~2 GiB per read() call against /proc/<pid>/map, by passing a huge resid.

Build & run

cc -o alloc_dos alloc_dos.c
./alloc_dos        # in several terminals in parallel
vmstat -w 1        # observe freemem collapse

Expected output

vmstat shows free memory collapsing toward zero; the system becomes unresponsive; OOM-killer activity may appear; or procfs_map.c:78 EIO storms appear in dmesg when the kernel finally fails the allocation.

On a 2 GiB-RAM VM, expect the panic / hang within seconds.

Notes

  • The kernel kmalloc is up-front (sbuf_new with flag 0; see subr_sbuf.c:178-205).
  • The fix is to cap buflen at a sane bound (e.g. 1 MiB) regardless of the caller's requested read length; uiomove_frombuf already handles positional truncation, so capping only changes how many syscalls the reader issues, not the visible output.
VERDICT.md verdict full narrative: mechanism, before/after evidence, fix validation
↓ download raw

DF-0924 β€” Attacker-controlled kernel heap allocation size via read() resid in /proc/<pid>/map

Verdict

REPRODUCED + FIX VALIDATED. The bug is real and the authored fix.diff closes it on a built-and-booted single-fix kernel.

Summary

procfs_domap() sizes its scratch sbuf from uio->uio_offset + uio->uio_resid (sys/vfs/procfs/procfs_map.c:61). The only upper bound is buflen >= INT_MAX (:75), so the caller's read() length directly controls the size of the up-front kernel allocation: sbuf_new(sb, NULL, buflen+1, 0) at :77 with flag 0 (no SBUF_AUTOEXTEND) makes sbuf_newbuf() at sys/kern/subr_sbuf.c:197 do an unconditional kmalloc(buflen+1, M_SBUF, M_WAITOK|M_ZERO) of up to ~2 GiB up front, regardless of how little data the pseudo-file actually produces. uio_resid is set by the length of read(), so the unprivileged reader alone decides how many bytes the kernel allocates.

This is CWE-789 (Uncontrolled Memory Allocation) β€” a local denial-of-service vector. It is not a write/corruption primitive: writes into the sbuf are bounded by the actual formatted map output, and uiomove_frombuf at :245 truncates the copyout to sbuf_len(sb). So there is no escalation path.

Mechanism (trigger -> primitive -> effect)

  1. Trigger β€” unprivileged maxx (uid 1001) opens /proc/self/map and calls pread(fd, buf, 0x7ffffff0, 0). uio_resid = 0x7ffffff0.
  2. Primitive β€” in procfs_domap: - buflen = uio->uio_offset + uio->uio_resid = 0x7ffffff0 (procfs_map.c:61) - guard buflen >= INT_MAX does not fire (0x7ffffff0 < 0x7fffffff) (:75) - sbuf_new(sb, NULL, buflen+1, 0) (:77) -> sbuf_newbuf (subr_sbuf.c:178) -> s->s_buf = SBMALLOC(s->s_size) = kmalloc(0x7ffffff1, M_SBUF, M_WAITOK|M_ZERO) (subr_sbuf.c:197, subr_sbuf.c:53) - the kernel synchronously allocates and zero-fills ~2 GiB before formatting a few KB of map entries.
  3. Effect (DoS) β€” two observable consequences: - CPU/memory waste per call: the up-front kmalloc+zero of ~2 GiB takes ~2.5 s on this VM, vs ~60 Β΅s for a normal-sized read returning the same few KB. (alloc_dos --time) - Kernel panic via per-type limit exhaustion: when several processes drive concurrent ~2 GiB M_SBUF allocations, the type's ks_limit is exceeded and kern_kmalloc.c:706 fires panic("sbuf: malloc limit exceeded") from within procfs_domap -> sbuf_new -> kmalloc. (alloc_dos --starve 4 8)

Evidence (before / after)

Baseline β€” unpatched 6.5-DEVELOPMENT #0 (Thu Jul 2 06:02:54 UTC 2026)

Timing (alloc_dos --time) β€” same ~1.5 KB of map output, ~44,000Γ— slower:

[time] resid=4096        pread=1421    kernel-side elapsed=0.000057 s
[time] resid=0x7ffffff0 pread=1615    kernel-side elapsed=2.752359 s

Repeated runs: 2.66 s, 2.53 s, 2.59 s, 2.75 s β€” deterministic.

Panic (alloc_dos --starve 4 8 as uid 1001) β€” dfbsd-qemu/boot.log:

panic: sbuf: malloc limit exceeded
cpuid = 0
Trace beginning at frame 0xfffff801183fb598
_kmalloc() at _kmalloc+0xb09 0xffffffff806578c9
_kmalloc() at _kmalloc+0xb09 0xffffffff806578c9
sbuf_new() at sbuf_new+0x66 0xffffffff806a0dc6
procfs_domap() at procfs_domap+0x75 0xffffffff807114a5
procfs_rw() at procfs_rw+0x235 0xffffffff80713065
vop_read() at vop_read+0x57 0xffffffff8070a487
Debugger("panic")
Stopped at      Debugger+0x7c:  movb    $0,0xbdaf09(%rip)
db>

The stack names the exact buggy function and sink cited in the finding. Guest dies; vm.sh status β‡’ down.

Patched β€” single-fix kernel 6.5-DEVELOPMENT #1 (Tue Jul 14 13:23:50 UTC 2026)

fix.diff applied to /usr/src, make -j6 nativekernel KERNCONF=X86_64_GENERIC, installed stripped kernel + debug to /boot/kernel/, rebooted.

kern.version: DragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 13:23:50 UTC 2026 sha256(/boot/kernel/kernel) = 483c43b88368f134ab29389441d674e88267129eaa5485547494ef4242c474f2

Timing β€” ~1-5 ms (cap = 1 MiB):

[time] resid=4096        pread=1421    kernel-side elapsed=0.000062 s
[time] resid=0x7ffffff0 pread=1615    kernel-side elapsed=0.001451 s   (run 1)
[time] resid=0x7ffffff0 pread=1615    kernel-side elapsed=0.004864 s   (run 2)
[time] resid=0x7ffffff0 pread=1615    kernel-side elapsed=0.003246 s   (run 3)

~1.5 ms vs 2.75 s baseline = ~1,900Γ— faster.

Starve (--starve 4 8) β€” no panic, freemem barely moves:

[starve] peak transient kernel alloc target β‰ˆ 4 Γ— 2047 MiB = 8191 MiB
  t=0.0s  free_pages=849808  (3319 MiB)
  ... (8 s of samples, all in 3315-3318 MiB range) ...
  t=8.0s  free_pages=848919  (3316 MiB)
[starve] free_pages start=849808  min_observed=848662  end=849739

Min free = 3315 MiB (only ~4 MiB delta = 4 Γ— 1 MiB cap, transiently in-flight). Guest stays UP. Compare baseline: same input β†’ kernel panic.

Impact

Local denial of service (kernel memory exhaustion β†’ panic) by an unprivileged user, default config (procfs is mounted by default), no special privileges. No escalation, no corruption β€” the sbuf write surface is bounded by actual map output and uiomove_frombuf truncates the copyout. Severity Low is correct (CVSS A:L). The finding's prose said "OOM / EIO storms / unresponsive"; in practice on this 4 GiB VM the dominant observed effect is the hard kernel panic from per-type kmalloc limit exhaustion, which is more severe than the finding's "freemem collapse" wording suggests but still within the DoS class.

Why no escalation (Phase 6 analysis)

The primitive is read-only in the corruption sense: the attacker controls the size of a transient M_WAITOK|M_ZERO allocation but not its contents (the buffer is zeroed, then filled only with sbuf_printf map output). There is no write to a victim object, no UAF, no type confusion, no function-pointer or refcount corruption. The "primitive" is purely resource consumption. There is therefore no chain to develop β€” the realistic impact ceiling is DoS, which is demonstrated.

PoC changes

The original alloc_dos.c allocated a 2 GiB userland buffer with malloc + memset, which conflates userland memory pressure with the kernel allocation under test and made the demonstration noisy on small VMs. Rewritten to: - --time mode: time pread() with small vs huge resid on the same fd. The userland buffer is a fixed 8 KB (only the first min(sbuf_len, resid) bytes are ever copied out). The 2.5 s vs 60 Β΅s contrast cleanly isolates the kernel-side up-front kmalloc+zero as the only variable. - --starve N S mode: fork N children each looping the huge-resid pread(), parent samples vm.stats.vm.v_free_count every 0.25 s for S seconds. On the unpatched kernel this drives the per-type M_SBUF limit over the edge and panics; on the patched kernel freemem barely moves.

fix.diff in this folder β€” cap buflen at 1 MiB (PROCFS_MAP_MAXBUF) before the sbuf_new call. The output is positionally truncated by uiomove_frombuf at :245, so a smaller buffer only changes how many syscalls a reader issues to consume the same total output β€” the visible content is unchanged. Supersedes the finding markdown's ## Recommended fix proposal (which was identical in spirit but sketched as a single-line clamp without the explanatory comment or #define placement used here).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline 2.75s+panic; patched 1.45ms+no panic. ~1900x faster, 4MiB cap.

BEFORE: 2.75s, panic sbuf malloc limit exceeded. AFTER: 1.45ms, no panic, 4MiB delta.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 13:23:50 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none -- CWE-789 read-only allocation size control. Buffer zeroed+filled by sbuf_printf. No write primitive. Ceiling: kernel memory exhaustion -> panic.

Evidence (decisive lines)

BEFORE: --time 2.75s for 2GiB resid; --starve 4 8 -> panic sbuf malloc limit exceeded. AFTER: --time 1.45ms (~1900x faster); --starve no panic, ~4MiB delta.

PoC changes

Rewrote alloc_dos.c (--time mode + --starve mode, fixed 8KB userland buffer), fix.diff (clamp buflen to 1MiB PROCFS_MAP_MAXBUF), VERDICT.md, manifest.json.

Verified recommended fix

Clamp buflen to PROCFS_MAP_MAXBUF (1MiB) after existing INT_MAX guard at procfs_map.c:75 before sbuf_new. Content truncated by uiomove_frombuf anyway. Full diff in findings/poc/DF-0924/fix.diff.

Verdict

REPRODUCED. procfs_domap procfs_map.c:61 buflen=uio_offset+uio_resid attacker-controlled up to ~2GiB -> sbuf_new kmalloc+zero of full size. Timing: 2.75s vs 60us. Starve: 4 procs x 2GiB -> panic 'sbuf: malloc limit exceeded'. CWE-789 unprivileged local DoS.