# DF-0032 — PoC

`fdcopy()` failure in `fork1()` permanently leaks the child `struct proc`, the
system-wide `nprocs` counter, and the per-uid proc-count → **local unprivileged
system-wide fork-DoS** (permanent until reboot). Severity Medium, CWE-401/772.

## Status

**REPRODUCED.** See `VERDICT.md` for the full narrative and `run.log` for the
decisive evidence.

> The original `fork_leak.c` (mmap-pressure trigger) does **not** actually fire
> the bug — anonymous `mmap` consumes user VM, not the kernel `M_FILEDESC`
> malloc pool. `exhaust.c` is the working trigger (fd-table amplification).

## The bug

`fork1()` charges `nprocs++` (`kern_fork.c:415`) and `chgproccnt++` (`:421`),
kmalloc's `p2` (`:444`), gives it `uidpcpu`/`ucred`/`sigacts`/`textvp` refs,
and `proc_add_allproc(p2)` (`:491`) — all **before** `fdcopy` (`:551`). `fdcopy`
is the only fd op that can fail (its `struct filedesc` kmalloc is
`M_WAITOK|M_ZERO|M_NULLOK` at `kern_descrip.c:2481-2486`; `fdinit`/`fdshare`
and the `fd_files[]` array are plain `M_WAITOK`). On failure `fork1` does
`goto done` (`:552-554`) and `done:` (`:724-732`) only releases tokens — no
teardown of `p2`, no `nprocs--`, no `chgproccnt--`, no `crfree`/`vrele`. The
`SIDL` orphan (no lwp, no parent — `lwp_fork1` is at `:674`, after fdcopy) never
reaches `exit`, so `nprocs`/`chgproccnt` are never decremented. Each failure
permanently consumes one system-wide `maxproc` slot + one per-uid
`RLIMIT_NPROC` slot.

## When fdcopy actually fails

Its `M_NULLOK` kmalloc returns NULL when `M_FILEDESC`'s per-type `ks_limit` is
exceeded (`kern_slaballoc.c:863-879`). `ks_limit = kmem_lim_size()/10 =
min(physmem, KvaSize)/10` (~195 MB on a 2 GB guest). Each `fork()` on the
`RFFDG` path charges `M_FILEDESC` for a copy of the parent's `fd_files[]` table
— so growing the parent's fd table (via `dup2` to high fds) amplifies the
charge: ~260 children with ~15000-fd tables push `M_FILEDESC` to its limit, and
the next `fdcopy` returns NULL → `fork()` returns ENOMEM → leak.

## Build & run (unprivileged; disposable VM)

```
./build.sh        # cc -o exhaust exhaust.c  (and the other PoC binaries)
./run.sh          # runs exhaust, prints before/after malloc-type counts
```

### Expected (bug present)

```
[!!!] ENOMEM from fork() -- fdcopy failure leak TRIGGERED at child 259
[*] summary: ok=259 eagain=51 enomem=705 other=0
```
After the run the `proc` (M_PROC) malloc-type Count is permanently elevated by
~705, while `lwp` and `file_desc` stay flat and `ps ax|wc -l` is unchanged
(leaked SIDL orphans are invisible). Repeating across ~4–6 unprivileged uids
exhausts `maxproc` and fork-DoSes the whole system (root included) until reboot.

### Expected (bug fixed)

`fork()` no longer returns ENOMEM (the per-type limit is never reached because
the failed allocations are rolled back, and/or `M_FILEDESC` no longer climbs
because the leak is gone); `proc` Count returns to baseline after the run.

## CAUTION

Each `./exhaust` run permanently consumes ~700 **system-wide** `maxproc` slots
until reboot. The guest is left ~18 % fork-exhausted after a single run. Run
`vm.sh reset` to clean up. Do **not** loop across many uids on a host you are
not prepared to reboot — that is the full DoS.
