# DF-3362 VERDICT

## Reproduced? Yes — deterministic kernel panic on the second injected frame.

### Reproduction narrative

Setup (guest, root):
```
kldload pf
ifconfig tap create && ifconfig tap0 up
printf 'scrub in on tap0 all fragment crop\n' > fragcrop.conf
pfctl -f fragcrop.conf && pfctl -e
./fragcache_kassert
```
`fragcache_kassert` writes two Ethernet frames to /dev/tap0:
frame 1 = MF fragment, offset 0, payload 8000 (creates the cache range
[0,8000)); frame 2 = MF fragment, offset 496, payload 8000 (overlaps:
precut = 8000-496 = 7504 < 8000 → the chop path).

`tapwrite()` builds every frame as an MGETHDR head + MGET chain of
plain internal mbufs (sys/net/tap/if_tap.c), so both frames arrive at
pf as ~35-mbuf chains. `pf_fragcache()` then does
`m_dup()` of the chain and tail-trims the dup to the 20-byte IP header
with `m_adj(*m0, hlen - pkthdr.len)`; `m_adj(mp, negative)` zeroes the
lengths of trailing mbufs **without unlinking them**
(sys/kern/uipc_mbuf.c:1859 "toss data from remaining mbufs on chain"),
so `(*m0)->m_next != NULL` and the KASSERT at pf_norm.c:667 fires:

```
panic: (*m0)->m_next != NULL: pf_fragcache
cpuid = 3
pf_fragcache() at pf_fragcache+0xcef
pf_normalize_ip() at pf_normalize_ip+0x49d
pf_test() at pf_test+0x114
pf_check_in() at pf_check_in+0x2e
pfil_run_hooks() at pfil_run_hooks+0x6f
Debugger("panic")
```
(see panic.txt — captured from the serial console; guest died in DDB,
required `vm.sh reset`.)

The second KASSERT (pf_norm.c:684, `(int)m->m_len == ip_len - precut`)
rests on the same single-mbuf assumption and would fire instead for any
payload that does not fit the header mbuf after `m_cat()`.

### Threat model
- Precondition: pf scrub in `fragcrop` mode on an interface whose input
  path delivers multi-mbuf packets (tap/VM-facing and several
  tunnel/virtual drivers do exactly this).
- Trigger: two packets, unauthenticated, remote on that segment.
- Effect: instant panic on INVARIANTS kernels (DFly
  development/debug kernels; the audited guest's stock kernel is
  INVARIANTS). Production non-INVARIANTS kernels compile the KASSERTs
  out and proceed with zero-length mbufs chained into the packet —
  benign for the packet but violating the invariants the author relied
  on (and wasting mbufs).
- Impact class: panic (DoS), debug-kernel-gated → severity Low.

### Fix validation (see fix_validation.md)
fix.diff: (1) move the `m_dup` NULL check ahead of the `m_adj`
(also closes DF-0391's deref), (2) unlink zero-length trailing mbufs
after the tail-trim, (3) assert on the recomputed `m_pkthdr.len`
instead of the head mbuf's `m_len`.

Baseline: panic above (reproduced pre-fix, stock kernel #0).
Patched: kernel rebuilt in-guest with fix.diff applied
(`cd /usr/src && patch -p1 < fix.diff && make nativekernel &&
make installkernel`, rebooted into kernel #1 of Fri Sep 11 10:20:27
UTC 2026), same trigger re-run → **no panic**: both frames written and
processed, EXIT=0, guest alive (see fix_validation.md). Regression
controls passed: scrub max-mss TCP path (5000 pkts) and the reassemble
path (6000-fragment burst, DF-3361 deliberately still present in this
single-fix build) both behave as before.

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