# DF-0079 PoC — /dev/null (and /dev/zero) infinite kernel loop DoS

## Status: REPRODUCED (trivial unprivileged local full-system DoS)

A single `write(fd, buf, (size_t)1<<32)` to world-writable `/dev/null` (mode
0666) by an **unprivileged** user pegs one CPU at 100% in kernel context
**forever**. The `write()` syscall never returns; the process is unkillable
from userspace; only a reboot recovers. Forking N copies (one per CPU) wedges
all cores.

## Root cause (verified in audited master DEV `sys/kern/kern_memio.c`)

In `mmrw()`, the per-iteration byte count `c` is declared **`u_int`** (32-bit)
at `kern_memio.c:225`, while `iov->iov_len` is `size_t` (64-bit). The
`/dev/null` write path assigns `c = iov->iov_len;` directly with no clamp
(`:298`); `/dev/zero` write does the same (`:364`).

When the caller passes a length whose low 32 bits are zero (e.g. exactly
2³² = `0x100000000`), `c` truncates to **0**. After the switch, the bookkeeping
at `:379-382` performs `iov->iov_len -= c` (subtracting 0) and
`uio->uio_resid -= c` (subtracting 0), leaving the loop state unchanged. The
`while (uio->uio_resid > 0 && error == 0)` predicate at `:232` is still true,
so `mmrw` spins **forever** in kernel context.

The early `if (iov->iov_len == 0) { ... continue; }` guard at `:234` does NOT
trip, because it compares the full 64-bit `iov_len` (= 2³², not 0).

The upper-layer `sys_write` does **not** clamp `nbyte` below 2³²: it only
rejects `(ssize_t)nbyte < 0` (`sys_generic.c:336`), so `iov_len = 2³²` passes
through. Worse, on the `/dev/null` write path **no `uiomove`/`copyin` is ever
issued**, so the user buffer pointer is never validated — the attacker can pass
an arbitrary (even unmapped) address; the PoC passes `(void *)0x1`.

## Build & run

```sh
cc -o df0079 df0079.c          # or: ./build.sh
./df0079                       # pegs 1 CPU forever; or ./run.sh
./df0079 4                     # fork 4 copies to wedge 4 CPUs
```

Run as **any local user** — `/dev/null` and `/dev/zero` are mode `0666`
(`kern_memio.c:842`/`:847`). No privilege required.

## Expected result (on a vulnerable kernel)

Each invocation calls `write(/dev/null, buf, 0x100000000)` and **never
returns**. The kernel thread spins in `mmrw` at `:232`. Observation (serial
console, since ssh itself gets starved) shows the wedged process in state
**`R0`** (running on CPU, not blocked) with `cputime` climbing ~1.18 s per wall
second (= 100% of one core) **indefinitely**, and `top` reporting one CPU fully
in `sys`. On a 2-CPU guest a single wedge typically makes the box
unresponsive to ssh within ~1 s (the wedged CPU also services the network IRQ).
Recovery is a hard reset only.

## Verified evidence (in this folder)

- `run.log`                 — host-side DoS timeline (ssh unreachable t+1 s) + ps excerpt.
- `serial_wedge_capture.txt`— per-iteration `ps`/`top` from a serial-console watcher
                               (pid 852, UID 1001, STAT `R0`, cputime 0.50 s → 20.56 s).
- `build.log`               — clean build + `/dev/null`/`/dev/zero` perms + source excerpt.
- `env.txt`                 — guest uname, cc, ncpu, device perms, vulnerable lines.
- `VERDICT.md`              — full narrative + path:line mechanism + fix.
- `fix.diff`                — one-line root-cause fix: widen `u_int c` → `size_t c`.
- `watch_df0079.sh`         — serial-console observer (writes ps/top to `/dev/ttyd0`).
- `manifest.json`           — artifact catalog.

## Notes

- The same bug affects `/dev/zero` write (`:364`) and `/dev/kmem` (`:264`,
  root-only). The fix in `fix.diff` (widen `c` to `size_t`) closes all three.
- The wedged process cannot be killed (SIGKILL/SIGTERM are never delivered:
  the thread is in an unyielding kernel loop with no signal-check point), so
  the only recovery is a reboot.
