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
mallocfails andprocfs_map.c:78-79returnsEIO(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.
Recommended fix
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
sys/kern/subr_sbuf.c:178-205βsbuf_newbufdoes the up-frontSBMALLOC.sys/vfs/procfs/procfs_map.c:245βuiomove_frombufpositional truncation.
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0924 Β· 21 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
kmallocis up-front (sbuf_new with flag 0; seesubr_sbuf.c:178-205). - The fix is to cap
buflenat a sane bound (e.g. 1 MiB) regardless of the caller's requested read length;uiomove_frombufalready handles positional truncation, so capping only changes how many syscalls the reader issues, not the visible output.
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)
- Trigger β unprivileged
maxx(uid 1001) opens/proc/self/mapand callspread(fd, buf, 0x7ffffff0, 0).uio_resid = 0x7ffffff0. - Primitive β in
procfs_domap: -buflen = uio->uio_offset + uio->uio_resid = 0x7ffffff0(procfs_map.c:61) - guardbuflen >= INT_MAXdoes 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. - 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 GiBM_SBUFallocations, the type'sks_limitis exceeded andkern_kmalloc.c:706firespanic("sbuf: malloc limit exceeded")from withinprocfs_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.
Recommended fix
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
fixedVALIDATED: 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.
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.
No comments yet.