# DF-3351 — dummynet `fs->backlogged` accounting drift across pipe delete → `p->sum` miscalibration → divide-by-zero in WF2Q drain

## Analysis (not guest-verified — Low severity, panic-only class)

`sys/net/dummynet/ip_dummynet.c` maintains the invariant that a pipe's
`sum` equals the total weight of all flows in its three heaps
(scheduler/not_eligible/idle). Two operations violate it:

1. **`dn_unref_pipe_cb()` (:1627-1638) — pipe delete purges every child
   flow_set's queues via `purge_flow_set(fs, 0)` but never resets
   `fs->backlogged`** (purge_flow_set :1189-1239 touches only queues and
   counters of *packets*, not `backlogged`). The stale counter survives
   the pipe's deletion and any recreation.

2. **`delete_pipe()` flowset path (:1694) trusts it:
   `p->sum -= fs->weight * fs->backlogged;`** — subtracting
   `weight × (live + stale)` flows, while the correct amount is
   `weight × (live)` plus the weights of the fs's flows still sitting in
   `pipe->idle_heap` (their weights stay in `sum` until
   `dn_expire_pipe_cb` :650 removes them; the fs-delete path removes the
   *heap entries* at :1695-1698 but never subtracts their weights).

Net drift per flowset-delete = `weight × (stale − idle_count)` — attacker
steerable in both directions by choosing when to delete the pipe (creates
staleness) vs. when to delete the fs (applies it). Since
`ready_event_wfq()` computes

```c
p->V += (len << MY_M) / p->sum;      /* :555 */
```

driving `p->sum` to exactly 0 while any flow is in `scheduler_heap`
yields a **divide-by-zero panic**; negative `sum` silently corrupts WF2Q
fairness (sign-extended to uint64, division yields ~0).

Construction sketch (all root-gated config, same surface as the known
div0 family): pipe 1 with two child queues fs A and fs B, equal weights;
traffic so both have exactly one backlogged flow; delete pipe 1 (both
fs keep `backlogged == 1`); recreate pipe 1; push one packet through
fs B (re-attaches it, `sum += wB`, `backlogged` 2→1 as it drains to
idle) while fs A has one live flow (`sum += wA`); delete fs B —
`sum -= wB × 1` instead of the owed `wB × 0`, hitting 0 exactly when
`wA == wB`; the next drain tick executes :555 with a live flow in the
scheduler heap → #DE → panic.

## Fix
On fs purge/reset, keep the counters coherent with the weights:
```diff
--- a/sys/net/dummynet/ip_dummynet.c
+++ b/sys/net/dummynet/ip_dummynet.c
@@ -1633,6 +1633,8 @@ dn_unref_pipe_cb(struct dn_flow_set *fs, void *pipe0)
 	if (fs->pipe == pipe) {
 	    kprintf("++ ref to pipe %d from fs %d\n",
 		pipe->pipe_nr, fs->fs_nr);
 	    fs->pipe = NULL;
+	    /* keep weight accounting coherent: flows are being purged */
+	    fs->backlogged = 0;
 	    purge_flow_set(fs, 0);
 	}
```
and, in the fs-delete path, subtract the idle-heap weights that
`fs_remove_from_heap(&p->idle_heap, fs)` drops, e.g. by counting
`p->idle_heap` entries of `fs` before removal and adding
`fs->weight * idle_cnt` to the subtracted amount.
