# DF-3361 VERDICT

## Reproduced? Yes — remote memory-exhaustion DoS, guest wedged and had to be hard-reset.

### Chain of evidence (all from the guest session, full numbers in run.log)

1. **The limits are dead code.**
   - `pf_pool_limits[PF_LIMIT_FRAGS] = { &pf_frent_pl, PFFRAG_FRENT_HIWAT }`
     (pf.c:281) and its init (pf_ioctl.c:240-241) — grep shows the only
     other consumers are the pfctl limit get/set ioctls. Nothing in the
     packet path consults it.
   - `pf_normalize_init()` carries the old enforcement commented out
     with `XXX` (pf_norm.c:118-123): pool_sethardlimit for
     PFFRAG_FRAG_HIWAT (1000), PFFRAG_FRENT_HIWAT (5000),
     PFFRAG_FRCACHE_HIWAT (10000), PFFRAG_FRCENT_HIWAT (50000).
   - The allocations (pf_norm.c:355-363 pf_fragment, :966-971 pf_frent,
     :568-583 cache flavor) are plain `kmalloc(M_NOWAIT)`; the only
     mitigation is `pf_flush_fragments()` *after* an allocation already
     failed.

2. **Observed growth past every intended cap.** With
   `scrub in on lo0 all fragment reassemble` (the default scrub mode —
   pfctl echoes even a plain `max-mss` scrub rule back with
   `fragment reassemble` appended):
   - baseline: pffrag=1, pffrent=1, 257 clusters.
   - 40,000 first-fragments, distinct ip_id, 1024B payload each
     (34s at ~1.2k pps; PFTM_FRAG=30s expiry ate the head of the run):
     **pffrag=6.32K, pffrent=6.32K concurrently**, 6727/33296 clusters,
     13.4MB network memory, `0 requests for memory denied`.
     → 6.3× the intended 1000-descriptor cap, 1.26× the 5000-frent cap,
     with zero enforcement.

3. **Terminal exhaustion.** Two further 30k bursts (dst 127.0.0.3 /
   127.0.0.4) at 45,988 pps: the send stream itself died mid-burst,
   ssh stopped responding (my control session timed out), and the serial
   console printed `Warning: objcache(mbuf pkthdr+cl) exhausted on cpu1!`
   repeatedly before going silent. `vm.sh status` → down; no panic, no
   core — the machine was wedged with every packet-header mbuf/cluster
   pinned inside pf fragment queues. Hard reset required.

### Threat model
Unauthenticated remote: any host that can route a fragment to a
scrub-enabled interface (in production the packet arrives from the
wire; the local raw socket in the PoC is only the test harness). Each
fragment key (src,dst,id,proto) holds up to 64KB of payload per set;
64K ids × arbitrary sources give effectively unlimited concurrent sets.
`pf_find_fragment()` refreshes `fr_timeout` on every hit
(pf_norm.c:299), so an attacker keeping each set fed never lets it
expire.

### Impact
dos — system-wide kernel memory (mbuf/cluster) exhaustion; network
service loss including management access; requires a sustained but
trivially cheap stream (46k pps sustained locally; wire attackers scale
the same).

### Fix
fix.diff enforces `pf_pool_limits[PF_LIMIT_FRAGS].limit` in the
buffering branch and `PFFRAG_FRCACHE_HIWAT` in the cache branch, with a
flush-then-drop policy mirroring the historical pool hard limits.
Not kernel-rebuilt for this finding (the single-fix validation budget
went to DF-3362); the hunks are self-contained guard clauses around the
allocations demonstrated above.

### Fix status of related known findings
- DF-0391 (m_dup NULL deref, pf_norm.c:661-665): **unfixed in tree**
  (code unchanged); DF-3362's validated fix.diff reorders the NULL
  check ahead of `m_adj` and thereby also closes DF-0391.
- DF-0392 (non-8-aligned overlap shifts, pf_norm.c:412-415, 434-436):
  **unfixed in tree** (code unchanged).

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