# DF-3350 — dummynet negative-bandwidth pipe: NULL-deref panic / netisr wedge

## What
`sys/net/dummynet/ip_dummynet.c:config_pipe()` (line 1516) stores the
user-supplied `ioc_pipe->bandwidth` into the kernel pipe with **no
validation**. A negative bandwidth is accepted and drives the WF2Q
scheduler into two deterministic failure modes:

1. **delay == 0 variant — kernel panic (NULL deref)**
   `ready_event_wfq()` line 623:
   `TAILQ_LAST(&p->p_queue, dn_pkt_queue)->output_time += t;`
   With `bandwidth < 0`, `p->numbytes` goes negative and `t` stays 0
   (the `t = (bandwidth-1-numbytes)/bandwidth` computation at :621-622
   is guarded by `if (p->bandwidth > 0)`). When the wfq event fires on
   the next 1 ms tick, the delay line is already empty (delay 0 packets
   are redispatched synchronously by `transmit_event()` at the end of
   the first invocation), so `TAILQ_LAST()` returns NULL and the kernel
   takes a write page fault at `NULL + offsetof(dn_pkt, output_time)`
   = 0x30. **Panic, 100% reproducible on the first packet.**

2. **delay > 0 variant — permanent netisr wedge (hard DoS)**
   The packet is still in the delay line, so `TAILQ_LAST()` is legal,
   but the pipe is reinserted into `wfq_ready_heap` with
   `key == curr_time` (`t == 0`), so the drain loop
   `while (h->elements > 0 && DN_KEY_LEQ(h->p[0].key, curr_time))`
   (`dummynet()`, :678) extracts and reinserts the same pipe forever.
   `dummynet()` never returns: the netisr thread on `ip_dn_cpu` (cpu0)
   spins at 100%, all packet processing on that CPU stops, the network
   stack dies with **no crash and no recovery short of a hard reset**.

## Reachability / threat
Configuring a pipe requires `setsockopt(IPPROTO_IP, IP_DUMMYNET_CONFIGURE)`
on a raw IP socket → root (or `SYSCAP_NONET_RAW`). There is no kernel-side
priv_check in `ip_dn_sockopt()` (only a `securelevel >= 3` guard), and no
userland tool can be relied on to validate (the PoC bypasses `dnctl`
entirely). Once the bad pipe exists, **any** user's traffic matched by an
`ipfw queue` rule triggers the fault — the triggering packet needs no
privilege.

## Reproduce (on the audit QEMU guest)
```
# host
cat findings/poc/DF-3350/df3350.c | ssh -F dfbsd-qemu/config dfbsd 'cat > /tmp/df3350.c'
dfbsd-qemu/vm.sh run_root 'cc -O -Wall -o /tmp/df3350 /tmp/df3350.c'
dfbsd-qemu/vm.sh run_root 'kldload ipfw && \
    ipfw add 1 allow ip from any to any via vtnet0 && \
    ipfw add 100 queue 100 ip from any to any via lo0 && \
    kldload dummynet'
# panic variant (delay=0):
dfbsd-qemu/vm.sh run_root '/tmp/df3350 0'      # => Fatal trap 12 in ready_event_wfq
# wedge variant (delay=50):
dfbsd-qemu/vm.sh run_root '/tmp/df3350 50'     # => ssh dies, guest never answers again
```
Expected (baseline, unfixed): `Fatal trap 12 ... fault virtual address = 0x30`,
`Stopped at ready_event_wfq+0x274: addq %rdx,0x30(%rax)` (see panic.txt);
for the wedge variant: qemu alive, console idle, network dead (run.2.log).

Expected (patched with fix.diff): `config pipe 1: Invalid argument` — the
setsockopt is rejected, no panic, no wedge.

## Fix
`fix.diff` — reject `bandwidth < 0` in `config_pipe()` plus a defensive
NULL guard on the `TAILQ_LAST()` deref. Validated: kernel rebuilt with the
fix, PoC cleanly rejected (see VERDICT.md).
