# DF-3350 VERDICT

**status: reproduced** | impact: panic (+ permanent network wedge) | confidence: certain
**fix_status: fixed** (validated on rebuilt guest kernel)

## Root cause (line-accurate)

`sys/net/dummynet/ip_dummynet.c:1516` — `config_pipe()`:
```c
x->bandwidth = ioc_pipe->bandwidth;   /* no validation; negative accepted */
```
`bandwidth` comes verbatim from the `IP_DUMMYNET_CONFIGURE` setsockopt
(`struct dn_ioc_pipe`, copied by `soopt_to_kbuf()` in
`sys/net/dummynet/ip_dummynet_glue.c:517`). Zero ("no limit") is legal;
**negative is nonsense** and every consumer assumes `numbytes < 0 ⇒
bandwidth > 0`:

`ip_dummynet.c:618-623` — `ready_event_wfq()`:
```c
if (p->numbytes < 0) {        /* comment claims: implies bandwidth>0 */
    dn_key t = 0;
    if (p->bandwidth > 0)     /* false for bw<0 -> t stays 0 */
        t = (p->bandwidth - 1 - p->numbytes) / p->bandwidth;
    TAILQ_LAST(&p->p_queue, dn_pkt_queue)->output_time += t;  /* NULL deref */
```
With `bandwidth = -1`, the first packet for a WF2Q queue enters the drain
loop with `numbytes = 0 >= 0`, moves to the delay line, drives `numbytes`
negative, and schedules the pipe in `wfq_ready_heap` with `key ==
curr_time` (t = 0).

* **delay == 0**: `transmit_event()` (called synchronously at :638 because
  the delay line was empty) redispatches the packet immediately — the delay
  line is empty again. On the next 1 ms tick, `dummynet()` (:678) fires the
  wfq event, `p->numbytes` stays negative, `t == 0`, and `TAILQ_LAST()`
  returns NULL on the empty queue → write fault at 0x30.
* **delay > 0**: the packet is still in the delay line, so the deref is
  legal, but the pipe is reinserted with `key == curr_time`; the drain
  `while (h->elements > 0 && DN_KEY_LEQ(h->p[0].key, curr_time))` at :678
  extracts and reinserts the same pipe forever — `dummynet()` never
  returns, the netisr thread on `ip_dn_cpu` (cpu0) spins at 100%, and the
  network stack is dead with no crash (unrecoverable without hard reset).

## How it was reproduced (guest: X86_64_GENERIC stock INVARIANTS kernel)

Setup: `kldload ipfw && ipfw add 1 allow ... via vtnet0 && ipfw add 100
queue 100 ip from any to any via lo0 && kldload dummynet` (the audit-target
module, *not* dummynet3 — verified by kldstat). `/tmp/df3350 0` then
configures pipe 1 (`bw=-1, delay=0`) and WF2Q queue 100 (`parent=1,
weight=1`) via raw setsockopt and sends one UDP packet to 127.0.0.1:9.

Result (serial console, `panic.txt`):
```
Fatal trap 12: page fault while in kernel mode
cpuid = 0; lapic id = 0
fault virtual address  = 0x30
fault code             = supervisor write data, page not present
current process        = Idle                      <- netisr drain thread
Stopped at ready_event_wfq+0x274: addq %rdx,0x30(%rax)
```
`0x30 == offsetof(struct dn_pkt, output_time)`; `%rax == NULL`
(`TAILQ_LAST` of empty queue); `%rdx == t == 0`. Exactly the analyzed
instruction. 100% deterministic (2/2 runs).

Wedge variant (`run.2.log`): `/tmp/df3350 50` → ssh died instantly; qemu
still running; console idle at `login:` with **no** panic for 40+ s;
`vm.sh status` down (network dead). Deterministic (1/1 run; variant not
repeated because each run costs a guest reset).

## Exploit chain / uid=0?

None. The fault address is fixed (NULL+0x30) with a fixed addend (t=0 or a
small controlled tick count) — a clean crash/wedge primitive, not a
controllable write. Primary impact: local kernel DoS; the wedge variant is
worse than a panic (no crash dump, survives watchdog-less configs, requires
power cycle). Trigger packet itself needs no privilege once the (root-made)
pipe exists.

## Fix validation

`fix.diff` = reject `bandwidth < 0` in `config_pipe()` (:1487-1492) +
defensive NULL guard on the `TAILQ_LAST` deref (:620-627).
Guest rebuilt with the fix (`make nativekernel KERNCONF=X86_64_GENERIC`,
RC=0; `make installkernel`; uname `#1 Fri Sep 11 06:08:07 2026`):
* `/tmp/df3350 0`  → `config pipe 1: Invalid argument`, no panic
* `/tmp/df3350 50` → `config pipe 1: Invalid argument`, no wedge
* positive control `bw=1000000` → pipe+queue configure, packet traverses
  WF2Q path, guest alive.
Baseline vs patched: panic/wedge present → gone, functionality retained.

## Not re-reported (known six)

DF-0373..DF-0378 cover the delete-constant, both config_red divisors
(lines 1346-1350 incl. `max_th`), red_drops lookup_step, negative qsize,
config_red error-path kfree, SET_TICKS overflow. This finding is the
missing-bandwidth-validation family — distinct root cause and lines.

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