# DF-3406 — VERDICT

**Status: `reproduced` (baseline, stock INVARIANTS kernel #0).**
**Fix: `fixed` (one-line `m_freem(m)` in the validated combined kernel).**
**Impact: deterministic kernel-memory (mbuf) exhaustion — privileged
local DoS; `Low`.**

## Root cause (path:line)

`tapwrite()` (sys/net/tap/if_tap.c:917) allocates the *next* chain
mbuf at the bottom of the copy loop **before re-checking the loop
condition**:

```c
955:  while (error == 0 && uio->uio_resid > 0) {
956:          m->m_len = (int)szmin(mlen, uio->uio_resid);
957:          error = uiomove(mtod(m, caddr_t), (size_t)m->m_len, uio);
958:          *mp = m;                          /* linked into chain */
959:          mp = &m->m_next;
960:          if (uio->uio_resid > 0) {
961:                  MGET(m, M_WAITOK, MT_DATA);   /* spare -- never
962:                  mlen = MLEN;                     linked on error */
963:          }
964:  }
965:  if (error) {
966:          IFNET_STAT_INC(ifp, ierrors, 1);
967:          if (top)
968:                  m_freem(top);              /* frees only the chain */
969:          return (error);                    /* <-- spare m leaked */
970:  }
```

`uiomove` faults leave `uio_resid > 0` (only successfully-moved bytes
are deducted), so a part-way EFAULT write always exits the loop one
fresh `MGET` ahead of the chain — exactly one mbuf leaked per failed
write.  The leaked mbufs are never reclaimed (the kernel holds the
only reference).

## How it was reproduced (baseline numbers, stock kernel #0)

`tapleak.c` (run as root): create tap, open it, then 100,000
`write(fd, buf, page+128)` where `buf` is 2 pages with the second
`PROT_NONE` — every write faults part-way (EFAULT).

```
before: 7/146632 mbufs in use ... 263 data mbufs ... 515 Kbytes
after 100000 writes (ok=0 efault=100000):
        100007/146632 mbufs ... 100263 data mbufs ... 50515 Kbytes
        (36% of mb_map in use)
+1 more EFAULT write: 100008  (perfectly linear; persists over idle)
```

**Exactly +1 leaked `data` mbuf per EFAULT write; permanent until
reboot.**  ~0.5 KB kernel memory per write → unbounded exhaustion
with ~293k writes to fill `mb_map` on this guest (network-wide DoS:
mbuf exhaustion blocks all networking).

Privilege bar: writing to `/dev/tapN` requires an open fd, which
requires root (0600 node + `tapuopen=0` SYSCAP gate at if_tap.c:323).
Low severity for that reason; no memory *corruption* — pure leak.

## Fix validation

`fix.diff` adds `m_freem(m);` to the error path (the spare is always
the unlinked `m` on that path: a faulting `uiomove` never exhausts
`uio_resid`, so the spare `MGET` always happened; a normal exit never
takes the path).  Built into the same patched kernel #1 as DF-3405's
fix; re-ran the identical 100,000-EFAULT storm:

```
before: 7/146632 mbufs in use
after 100000 EFAULT writes: 7/146632 mbufs in use   <- zero growth
```

## Artifacts

- `tapleak.c` — trigger (faulting write storm)
- `build.sh` / `run.log` — build + baseline numbers
- `fix.diff` — one-line validated fix

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
