β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3353

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3353 Β· 12 files
FileTypeDescriptionSize
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)).

VERDICT.md
↓ download raw

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

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Union 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)']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Sep 11 07:37:10 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (dummynet3 fix.diff applied, rebuilt via make nativekernel)

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.