# DF-0588 — PoC evidence pack

Reproduces the mbuf-chain leak in `tunwrite()` at `sys/net/tun/if_tun.c`. A
confirmed **privileged local resource-exhaustion DoS** that leaks ~263 mbufs
per write and drives the kernel mbuf zone to exhaustion system-wide.

## Root cause (confirmed in source)

`tunwrite()` builds an mbuf chain in the loop at `sys/net/tun/if_tun.c:875-882`:

```c
869:  /* get a header mbuf */
870:  MGETHDR(m, M_WAITOK, MT_DATA);          /* m = chain head */
...
874:  mp = &top;
875:  while (error == 0 && uio->uio_resid > 0) {
876:      m->m_len = (int)szmin(MHLEN, uio->uio_resid);
877:      error = uiomove(mtod(m, caddr_t), (size_t)m->m_len, uio);
878:      *mp = m;                              /* append m */
879:      mp = &m->m_next;
880:      if (uio->uio_resid > 0)
881:          MGET(m, M_WAITOK, MT_DATA);       /* get NEXT mbuf only if more data */
882:  }
```

On the last iteration `uio->uio_resid` reaches 0, so the `MGET` at line 881 is
**not taken** and `m` is left pointing at the **last (tail) mbuf** of the chain,
while `top` is the **head**. Then, when `TUN_IFHEAD` is set and the user-supplied
4-byte address family is anything other than `AF_INET`/`AF_INET6`, the default
case at lines 952-955:

```c
952:  default:
953:      m_freem(m);              /* BUG: frees only the tail mbuf */
954:      return (EAFNOSUPPORT);
955:  }
```

calls `m_freem(m)` — freeing only the trailing mbuf — and **leaks the chain head
`top` plus all intermediate mbufs** (since `m->m_next == NULL` at the tail,
`m_freem(m)` frees exactly one mbuf). The chain is reachable only from the
local `top`, which goes out of scope on return, so the leak is permanent.

A 65539-byte write (max, `TUNMRU+4`) assembles ~264 mbufs, freeing 1 and
**leaking ~263 per write** (measured).

## Trigger

1. `open("/dev/tun", O_RDWR)` (clone → fresh tunN).
2. `ioctl(fd, TUNSIFHEAD, &one)` — enable the IFHEAD address-family prefix.
3. `write(fd, buf, 65539)` where the first 4 bytes are a family ≠ AF_INET(2)/
   AF_INET6(28) (e.g. `0xffffffff`). `write()` returns `EAFNOSUPPORT` after
   leaking ~263 mbufs.
4. Loop. The mbuf count climbs monotonically and unreclaimed until the objcache
   is exhausted system-wide.

## Reachability / privilege (verified)

- `/dev/tun` clone node is `0600` `UID_UUCP`/`GID_DIALER`
  (`if_tun.c:155-157`).
- `tunopen()` (`if_tun.c:283`) requires `caps_priv_check(SYSCAP_RESTRICTEDROOT)`
  with **no** `user_open` escape (unlike tap). So this is **host-root-only**.
- `maxx` (uid 1001) cannot open `/dev/tun` → matches CVSS **PR:H**.
- Impact is **S:C**: the mbuf zone is kernel-global, so a privileged trigger
  exhausts mbufs for *every* tenant / network stack on the box.

## Files

- `tun_leak.c` — minimal reproducer (vendored `TUNSIFINFO`/`TUNMRU`; opens the
  clone `/dev/tun`; configurable write count).
- `build.sh` / `run.sh` — build & measure-before/after harness.
- `build.log`, `run.log` (50-write demo), `run.2.log` (100-write continuation,
  proves unreclaimed), `run.stress.log` (200-write exhaustion), `exhaustion.txt`
  (kernel `objcache(mbuf) exhausted` warning).
- `env.txt`, `VERDICT.md`, `fix.diff`, `manifest.json`.

## Build & run (as host root on the DragonFly guest)

```
./build.sh
./run.sh 50            # ~16k leaked mbufs, safe demo
./run.sh 1000000       # stress: drive to mbuf exhaustion (guest will wedge)
```

## Expected result (bug present)

Controlled demo (50 writes):

```
[baseline] mbufs in use: 4
... tun_leak: 50 writes ...
[after]    mbufs in use: 13203
[delta]    leaked mbufs: 13199 over 50 writes (~263 mbufs/write)
```

Stress to exhaustion:

```
76247/72904 mbufs in use (current/max):     <-- over nominal max
12 requests for memory denied
boot.log: Warning: objcache(mbuf) exhausted on cpu1!
```

After exhaustion the guest's networking is degraded system-wide (new mbuf
allocations denied). Recovery: `vm.sh reset` (reboot).

## Notes for the per-PoC verifier (done)

- tun is **built into** `X86_64_GENERIC` (no `kldload` needed; `net.link.tun`
  sysctl present, `/dev/tun` exists on clean boot).
- The seeded PoC opened `/dev/tun3` (non-existent) and `#include`d
  `<net/if_tun.h>` (not shipped in guest `/usr/include`); both fixed.
- The leak is straight-line, no race; reproduces 100 % of the time.
- After the fix (`m_freem(top)`), the same workload keeps the mbuf count flat.
