fs->backlogged never reset when pipe deletion purges child flowsets: stale counter double-subtracted from p->sum on flowset delete β sum==0 divide-by-zero panic (DF-3351 family confirmed in v3)
Summary
dn_unref_pipe_cb purges each child flowset's queues on pipe deletion but leaves fs->backlogged stale. After pipe recreation and new traffic, deleting the flowset subtracts weight x (stale+live) while the new pipe's sum only holds the live contributions, driving sum negative; the next new flow on another flowset brings it to exactly 0 and its packet move divides by zero. VERIFIED on guest with the console showing the smoking gun '++ ref to pipe 1 from fs 300' (the dn_unref kprintf) immediately before Fatal trap 18 at the same :574 sink (~1.4s after the last packet). Same code/mechanism as the fresh v1 finding DF-3351. Fixed on rebuilt kernel (backlogged reset -> subtraction exact -> no panic). Root-configured local DoS; pipe delete/recreate cycles are routine during reconfiguration, so the drift can accumulate accidentally.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3353 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 975 B | β raw | |
| VERDICT.md | β | 2.5 KB | β raw | |
| dfv3.c | β | 10.6 KB | view raw | |
| setup.sh | β | 896 B | view raw | |
| build.sh | β | 130 B | view raw | |
| run.sh | β | 116 B | view raw | |
| run.log | β | 1.3 KB | view raw | |
| panic.txt | β | 569 B | view raw | |
| fix.diff | β | 1.8 KB | view raw | |
| env.txt | β | 447 B | view raw | |
| verdict.json | β | 4.0 KB | view raw | |
| manifest.json | β | 1.0 KB | view raw |
DF-3353 β fs->backlogged not reset when pipe deletion purges child flowsets: p->sum accounting drift reaches 0 -> divide-by-zero panic (DF-3351 family in v3)
File: sys/net/dummynet3/ip_dummynet3.c (pass 2) Severity: Medium (CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H) β CWE-682 Incorrect Calculation Verdict: reproduced (panic) on stock INVARIANTS guest, see verdict.json.
Build
copy dfv3.c + setup.sh to the guest, then (root):
cc -O -o /tmp/dfv3 /tmp/dfv3.c
Run
sh /tmp/setup.sh # kldloads ipfw3 (default-accept) + dummynet3, adds queue rules /tmp/dfv3 drift
Expected
kernel panic signature in serial console (see panic.txt)
setup.sh sets net.filters_default_to_accept=1 BEFORE kldload ipfw3 β
ipfw3 otherwise boots default-deny and locks out ssh.
Fix
fix.diff (union patch for DF-3352..DF-3356; the hunk for THIS finding is:
reset fs->backlogged=0 in dn_unref_pipe_cb before purging (fix.diff hunk 6)).
DF-3353 VERDICT β reproduced
Root cause
Deleting a pipe calls dn_unref_pipe_cb() (ip_dummynet3.c:1618-1629) which purges each child flowset's queues via purge_flow_set(fs,0) but never resets fs->backlogged. After the pipe is recreated (same number) and the flowset sees traffic again, backlogged counts stale+live flows. Deleting the flowset then executes p->sum -= fs->weight * fs->backlogged (:1681) with the inflated counter, driving the NEW pipe's sum negative; the next new flow on another flowset of that pipe brings sum to exactly 0, and its packet move hits p->V += (len << MY_M) / p->sum (:574) -> integer divide fault. Reproduced 1/1 on the stock guest; the serial console shows the smoking gun '++ ref to pipe 1 from fs 300' (the dn_unref_pipe_cb kprintf at :1624) immediately before 'Fatal trap 18 ... Stopped at ready_event_wfq+0xd1: divq %rcx,%eax'. Identical code and mechanism to v1 DF-3351 (sys/net/dummynet/ip_dummynet.c), which pass-1 of this file missed.
Exploit chain
root: slow pipe 1 + fs300(mask dst-port,w=1) + fs400(w=1); 2 pkts to flow A (fs300) so one stays backlogged; DEL pipe 1 (queues purged, backlogged stays 1, sum dies with pipe); recreate pipe 1; 2 pkts to flow B on fs300 (backlogged=1stale+1live, sum=1); DEL fs300 -> sum = 1-1*2 = -1; 1 pkt to flow C on fs400 -> sum=0; the pending wfq timer fire (~1.4s) moves C's packet -> divq by zero -> panic. No uid=0.
Evidence
- panic.txt: '++ ref to pipe 1 from fs 300' then Fatal trap 18, ready_event_wfq+0xd1 divq
- run.log: six-step drift dance transcript + console panic
- dfv3.c: drift subcommand
Kernel refs
- sys/net/dummynet3/ip_dummynet3.c:1618
- sys/net/dummynet3/ip_dummynet3.c:1627
- sys/net/dummynet3/ip_dummynet3.c:1681
- sys/net/dummynet3/ip_dummynet3.c:574
Fix validation
fix.diff hunk: reset fs->backlogged=0 in dn_unref_pipe_cb before purging (fix.diff hunk 6). See verdict.json fix_* fields β a single guest kernel rebuilt with the union fix.diff (all five findings) was re-tested with the exact same PoCs: baseline panic/growth reproduced on the stock kernel, patched kernel rejects or sanitizes the bad configs and shows none of the five defects (details in fix section of verdict.json).
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.
Fix verification
fixedUnion fix.diff (all five DF-3352..DF-3356 hunks; this finding's hunk is the one noted in recommended_fix) applied to the guest /usr/src, kernel rebuilt with make -j6 nativekernel KERNCONF=X86_64_GENERIC and installed (>>> Kernel build completed 07:41:12, install completed 07:42:30 UTC). fs->backlogged=0 reset in dn_unref_pipe_cb: the full 6-step drift dance completes with no divide fault (baseline panicked ~1.4s after the last packet). All other four PoCs re-run on the same patched kernel: all clean.
['run.2.log (patched-kernel validation transcript)', 'fix.diff (git-apply-able unified diff, 6 hunks vs sys/net/dummynet3/ip_dummynet3.c)']
Confirmed kernel references
Detail
Exploit chain
root: slow pipe 1 + fs300(mask dst-port,w=1) + fs400(w=1); 2 pkts to flow A (fs300) so one stays backlogged; DEL pipe 1 (queues purged, backlogged stays 1, sum dies with pipe); recreate pipe 1; 2 pkts to flow B on fs300 (backlogged=1stale+1live, sum=1); DEL fs300 -> sum = 1-1*2 = -1; 1 pkt to flow C on fs400 -> sum=0; the pending wfq timer fire (~1.4s) moves C's packet -> divq by zero -> panic. No uid=0.
Evidence (decisive lines)
["panic.txt: '++ ref to pipe 1 from fs 300' then Fatal trap 18, ready_event_wfq+0xd1 divq", 'run.log: six-step drift dance transcript + console panic', 'dfv3.c: drift subcommand']
PoC changes
PoC authored fresh (multi-trigger dfv3.c): raw setsockopt with ABI structs verbatim from ip_dummynet3.h; ipfw3 (not ipfw) queue <fs> udp ... via lo0 rules divert lo0 UDP into the WF2Q path; setup.sh sets net.filters_default_to_accept=1 before kldload ipfw3 because ipfw3 defaults to deny-all and otherwise locks out ssh.
Verified recommended fix
reset fs->backlogged=0 in dn_unref_pipe_cb before purging (fix.diff hunk 6)
Verdict
Deleting a pipe calls dn_unref_pipe_cb() (ip_dummynet3.c:1618-1629) which purges each child flowset's queues via purge_flow_set(fs,0) but never resets fs->backlogged. After the pipe is recreated (same number) and the flowset sees traffic again, backlogged counts stale+live flows. Deleting the flowset then executes p->sum -= fs->weight * fs->backlogged (:1681) with the inflated counter, driving the NEW pipe's sum negative; the next new flow on another flowset of that pipe brings sum to exactly 0, and its packet move hits p->V += (len << MY_M) / p->sum (:574) -> integer divide fault. Reproduced 1/1 on the stock guest; the serial console shows the smoking gun '++ ref to pipe 1 from fs 300' (the dn_unref_pipe_cb kprintf at :1624) immediately before 'Fatal trap 18 ... Stopped at ready_event_wfq+0xd1: divq %rcx,%eax'. Identical code and mechanism to v1 DF-3351 (sys/net/dummynet/ip_dummynet.c), which pass-1 of this file missed.
No comments yet.