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

fs->backlogged never reset when pipe deletion purges child flowsets: p->sum accounting drift can reach exactly 0 β†’ divide-by-zero panic in WF2Q drain

Summary

Deleting a pipe purges each child flowset's queues via dn_unref_pipe_cb->purge_flow_set without resetting fs->backlogged; the stale counter survives pipe recreation. When such a flowset is later deleted, delete_pipe() subtracts 'p->sum -= fs->weight * fs->backlogged' - weight x (live+stale) - while the weights of that fs's flows still in pipe->idle_heap are never subtracted. Net drift per fs-delete is weight x (stale - idle_count), steerable in both directions; driving p->sum to exactly 0 while any flow sits in scheduler_heap makes 'p->V += (len << MY_M) / p->sum;' a divide-by-zero panic (negative drift only skews WF2Q fairness). Root-gated config surface, deliberate multi-step config sequence + traffic - Low/likely. Not guest-verified (panic-only class, skipped per contract; construction sketch in pack README). Fix: reset fs->backlogged in dn_unref_pipe_cb before purge + count idle-heap weights in the delete_pipe subtraction.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3351 Β· 2 files
FileTypeDescriptionSize
README.md file 2.9 KB ↓ raw
verdict.json file 1.8 KB view raw
README.md file
↓ download raw

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

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:

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

Fix verification

not_testable
per-fix-DF-3351

Confirmed kernel references

Detail

Evidence (decisive lines)

['README.md: full accounting-drift derivation with construction sketch and proposed fix']

PoC changes

n/a (no PoC authored; analysis-only)

Verified recommended fix

Reset fs->backlogged when dn_unref_pipe_cb purges the flowset at pipe delete, and subtract the weights of the fs's idle-heap flows in the delete_pipe fs path.

Verdict

Not guest-verified (Low severity, panic-only class, root-gated config surface identical to the already-filed DF-0374/0375 div0 family). Static analysis is line-accurate: dn_unref_pipe_cb() purges child flowset queues at pipe delete without resetting fs->backlogged, and delete_pipe():1694 subtracts fs->weightfs->backlogged from p->sum while the fs's idle-heap flows' weights are never subtracted, so p->sum drifts by weight(stale-idle) per fs-delete and can be steered to exactly 0 with a live flow in scheduler_heap, making ready_event_wfq():555 'p->V += (len << MY_M) / p->sum' a divide-by-zero panic.