# 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.

## 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).
