# DF-2595 — ng_bpf missing zero-length-packet guard → `bpf_filter` type confusion

## Verdict
**REPRODUCED (code path confirmed reachable & exercised) but LATENT on the default
guest: no observable panic or leak.** The defective code path — a zero-length mbuf
reaching `bpf_filter()` with `buflen==0`, which in the kernel makes `bpf_filter()`
cast the flat data pointer to `(struct mbuf *)` and walk it as an mbuf chain — is
**unambiguously confirmed reached** (line-by-line trace + `recvFrames` delta = 4 for
4 zero-length sends). But on the default `6.5-DEVELOPMENT #0` guest the stale mbuf
data area read as the "fake struct mbuf" is benign (zeroed → fake `m_next == NULL` →
`m_xword()` bails with `merr=1` → `bpf_filter` returns 0), so there is **no
deterministic crash or kernel-memory disclosure**. This is a **confirmed-reachable
latent type confusion / hardening gap**: FreeBSD's `ng_bpf` has the guard,
DragonFly's does not. **FIX VALIDATED** (rebuilt `ng_bpf.ko`, guard confirmed in
disasm, clean before/after).

## The bug (confirmed line-by-line)
`ng_bpf_rcvdata()` in `sys/netgraph/bpf/ng_bpf.c:373` computes `totlen =
m->m_pkthdr.len` and, with **no guard for `totlen == 0`**, hands `totlen` as both
`wirelen` and `buflen` straight to `bpf_filter()` (`ng_bpf.c:403`):

```c
int totlen = m->m_pkthdr.len;            /* line 376 */
...
if (m->m_next != NULL) { ...; data = buf; m_copydata(m, 0, totlen, data); }
else data = mtod(m, u_char *);           /* line 400 */
...
len = bpf_filter(hip->prog->bpf_prog, data, totlen, totlen);  /* line 403 */
```

In the kernel, `bpf_filter()` (`sys/net/bpf_filter.c:174`) treats `buflen == 0` as
the signal "the buffer `p` is actually an mbuf chain; traverse it". For every
packet-load instruction the bounds check fails when `buflen==0`, and the guard
`if (buflen != 0) return 0;` is **false**, so execution falls into the mbuf
traversal that casts the flat buffer to a `struct mbuf *`:

```c
case BPF_LD|BPF_W|BPF_ABS:                 /* bpf_filter.c:206 */
    k = pc->k;
    if (k > buflen || sizeof(int32_t) > buflen - k) {   /* true for any k when buflen==0 */
#ifdef _KERNEL
        int merr;
        if (buflen != 0)        /* <-- guard FALSE because buflen==0 */
            return 0;
        A = m_xword((struct mbuf *)p, k, &merr);   /* TYPE CONFUSION (line 214) */
        if (merr != 0)
            return 0;
        continue;
```

`m_xword()` (`bpf_filter.c:85`) then dereferences `m->m_len`, `m->m_next`,
`mtod(m)` on the fake mbuf. The identical pattern applies to `BPF_LD|BPF_H|BPF_ABS`
(`m_xhalf`, c:238), `BPF_LD|BPF_B|BPF_ABS` (`MINDEX`+`mtod`, c:255), and the
`BPF_IND` variants. This mbuf-traversal fallback is **intentional** for live
`bpf(4)` filtering (where `p` really is an mbuf and `buflen==0` is the "use the
chain" sentinel); `ng_bpf` mis-uses the API by always passing a flat buffer and
forgetting to reject `totlen==0`. FreeBSD's `ng_bpf_rcvdata` has the `totlen==0`
guard; DragonFly's does not.

## Trigger requires a packet-load program
The **default** hook program is `{ BPF_STMT(BPF_RET+BPF_K, 0) }` — a bare return,
no load instruction — so the default never reaches the confused path. The PoC
installs a load+ret program first (`BPF_LD|BPF_W|BPF_ABS, k=0` then `BPF_RET|BPF_K`).

## Reachability / privilege
`ng_bpf_rcvdata` is reached via a netgraph data socket (`PF_NETGRAPH SOCK_DGRAM`).
Building the graph (`NGM_MKPEER ng_bpf`, `NGM_BPF_SET_PROGRAM`) goes through the
netgraph **control** socket, whose attach `ngc_attach()`
(`sys/netgraph/socket/ng_socket.c:172`) gates on
`caps_priv_check(SYSCAP_RESTRICTEDROOT)` = **root**. The **data** socket
(`ngd_attach`, `ng_socket.c:310`) needs no privilege, but it can only SEND on a hook
a root-built graph already wired up. So the bug trigger is **root-reachable**
(a root firewall admin configuring an ng_bpf filter); an unprivileged user can only
inject data on a graph a root already configured. (The finding prompt's "any user
PF_NETGRAPH SOCK_DGRAM" is true for the data socket alone, not for graph setup.)

## Reproduction (unpatched #0 kernel)
```
# /tmp/poc2595
[+] created socket node 'df2595' csock=3 dsock=4
[+] mkpeer ng_bpf 'df2595:out' -> bpf 'in'
[+] installed BPF program on bpf 'in': LD_W_ABS k=0; RET 0
[*] stats BEFORE: recvFrames=0 recvOctets=0
[*] sending 4 ZERO-LENGTH data items -> ng_bpf 'in' (totlen=0)...
[*] stats AFTER : recvFrames=4 recvOctets=0  (delta=4)
[+] CONFIRMED: zero-length data reached ng_bpf_rcvdata -> bpf_filter(buflen=0)
    -> type-confusion branch entered
[*] grooming mbuf pool: flooding 4000 pointer-shaped packets then re-sending zero-length...
[*] still alive — kernel survived grooming+zero-length flood
```
`recvFrames += 4` for 4 zero-length sends **proves** `ng_bpf_rcvdata` was entered
with `m_pkthdr.len == 0`, and the code trace proves that path reaches
`bpf_filter(prog, data, 0, 0)` → the `buflen==0` mbuf-traversal branch. **No panic**
occurred: the mbuf's data area (read as the fake `struct mbuf`) is zeroed/benign on
the default guest, so `m_xword`'s `m = m->m_next` yields `NULL` → it returns
`merr=1` → `bpf_filter` returns 0. A targeted mbuf-pool grooming flood (4000
pointer-shaped packets interleaved with zero-length sends) did **not** land a stale
mbuf whose residue dereferences to a fault — the `MH_ALIGN(m, 0)` data pointer
points near the *end* of `m_pktdat`, which the grooming packets' bytes don't reach,
so the fake fields stay zeroed.

To make this *deterministically* panic/leak one would need to defeat that
alignment-dependent grooming window — a real but non-trivial exploit-dev task whose
payoff is a kernel pointer deref from a **root-only** trigger. The defect is real
and worth fixing as defense-in-depth regardless.

## The fix (defense-in-depth, matches FreeBSD upstream)
Reject zero-length packets at the top of `ng_bpf_rcvdata`, before any data-pointer
work and before `bpf_filter` is ever called:

```c
if (totlen == 0) {
    NG_FREE_DATA(m, meta);
    return (0);
}
```
Standalone `git apply`-able diff: `fix.diff`.

## Fix validation (rebuilt ng_bpf.ko, reloaded, re-ran same PoC)
- **Before** (unpatched #0 kernel + #0 module, sha `b69ff3fd…`):
  `recvFrames delta = 4` — zero-length packets reach `ng_bpf_rcvdata`, get counted,
  and `bpf_filter(buflen=0)` is called → the type-confusion branch is entered.
- **After** (#0 kernel + rebuilt single-fix `ng_bpf.ko`, sha `d6407950…`):
  `recvFrames delta = 0` — zero-length packets are dropped by the guard **before**
  the stats update and before `bpf_filter` is called. Disasm confirms:
  `mov 0xb8(%rsi),%r12d` (load totlen) → `test %r12d,%r12d` →
  `je <ng_bpf_rcvdata+0x180>` (early `NG_FREE_DATA`+`return 0`).
- **No regression**: non-zero packets are still filtered
  (`recvFrames += 2` for two 32-byte packets on the patched module).
- Module rebuilt via `cd /usr/src/sys/netgraph/bpf && make KERNBUILDDIR=…`
  (module-only; `MAKE_RC=0`); `kldunload ng_bpf` (rc=0, graph torn down) →
  `kldload ng_bpf` (rc=0). No reboot needed (ng_bpf is safely unloadable, unlike
  ipfw3).

**fix_status = fixed** (the dangerous code path is now deterministically
short-circuited; clean before/after on the `recvFrames` signal; no regression).

## Files
- `poc.c` — builds ng_bpf graph with a packet-load program, sends zero-length items,
  queries `NGM_BPF_GET_STATS` to prove the path, and grooms the mbuf pool.
- `build.sh` / `run.sh` — exact build & run (run as root; loads ng_socket + ng_bpf).
- `run.log` — baseline run (`recvFrames delta=4`, type-confusion path entered, no panic).
- `fix_run.log` — patched-module run (`recvFrames delta=0`, path short-circuited).
- `fix_build.log` — module rebuild + disasm (`MAKE_RC=0`, guard confirmed).
- `env.txt` — guest uname / cc / patched sha / kldstat.
- `fix.diff` — the standalone git-apply-able zero-length guard.
- `manifest.json` — artifact catalog.
