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

config_pipe accepts negative bandwidth: TAILQ_LAST NULL-deref write in ready_event_wfq (delay=0) or t==0 wfq_ready_heap self-loop netisr wedge (delay>0) β€” DF-3350 family confirmed in v3

Summary

Bandwidth is stored with no sign check. With bw<0 the credit-tick computation is guarded by (p->bandwidth > 0) so t stays 0, and TAILQ_LAST(&p->p_queue)->output_time += t dereferences NULL once the delay line has emptied - a supervisor WRITE to NULL+0x30. With delay>0 the same defect instead reinserts the pipe into wfq_ready_heap with key==curr_time every tick, spinning the dummynet() drain loop forever (netisr wedge). VERIFIED the panic variant on stock guest (Fatal trap 12, fault VA 0x30, Stopped at ready_event_wfq+0x274). Identical code and signature to verified v1 DF-3350. Fixed on rebuilt kernel (bw<0 -> EINVAL + NULL guard).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3354 Β· 12 files
FileTypeDescriptionSize
README.md β€” 1010 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 β€” 118 B view raw
run.log β€” 923 B view raw
panic.txt β€” 403 B view raw
fix.diff β€” 1.8 KB view raw
env.txt β€” 447 B view raw
verdict.json β€” 4.2 KB view raw
manifest.json β€” 1.1 KB view raw

DF-3354 β€” negative pipe bandwidth unvalidated -> NULL-deref write at ready_event_wfq TAILQ_LAST (delay=0) or t==0 wfq_ready_heap self-loop netisr wedge (DF-3350 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-476 NULL Pointer Dereference 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 negbw 0

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: reject bandwidth<0 at config_pipe + NULL-guard the TAILQ_LAST write (fix.diff hunks 1-2)).

VERDICT.md
↓ download raw

DF-3354 VERDICT β€” reproduced

Root cause

config_pipe() stores ioc_pipe->bandwidth verbatim (ip_dummynet3.c:1519) with no sign check. With bandwidth<0 on a pipe carrying a WF2Q queue, the tick computation at :641 is guarded by (p->bandwidth > 0) so t stays 0; :642 then does TAILQ_LAST(&p->p_queue, dn_pkt_queue)->output_time += t. On the next dummynet tick the pipe is re-extracted with an empty delay line (delay=0 packets were already transmitted), TAILQ_LAST returns NULL and the kernel writes to NULL+0x30 (offset of output_time in struct dn_pkt). Reproduced 1/1 on the stock guest: 'Fatal trap 12: page fault, fault virtual address = 0x30, supervisor write data', Stopped at ready_event_wfq+0x274: addq %rdx,0x30(%rax), current process Idle. Identical code and signature to the verified v1 finding DF-3350 (sys/net/dummynet/ip_dummynet.c:1516/618-623); with delay>0 the same defect instead reinserts the pipe into wfq_ready_heap with key==curr_time forever, wedging the netisr drain loop (verified for v1; same code here).

Exploit chain

root: setsockopt pipe{bw=-1, delay=0} + queue{parent}; 1 UDP packet via lo0 ipfw3 queue rule -> WF2Q path -> ready_event_wfq -> next tick TAILQ_LAST on empty delay line -> write to NULL+0x30 -> panic. Fixed-address NULL write; crash/wedge primitive only, no uid=0.

Evidence

  • panic.txt: Fatal trap 12, fault VA 0x30 supervisor write, ready_event_wfq+0x274 addq %rdx,0x30(%rax)
  • run.log: config + single trigger packet transcript; panic ~1ms after tick
  • dfv3.c: negbw subcommand

Kernel refs

Fix validation

fix.diff hunk: reject bandwidth<0 at config_pipe + NULL-guard the TAILQ_LAST write (fix.diff hunks 1-2). 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). config_pipe returns EINVAL for bandwidth<0: PoC exits with 'config pipe: Invalid argument' rc=1 instead of configuring the pipe (baseline: config accepted, then Fatal trap 12 write to NULL+0x30). 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: setsockopt pipe{bw=-1, delay=0} + queue{parent}; 1 UDP packet via lo0 ipfw3 queue rule -> WF2Q path -> ready_event_wfq -> next tick TAILQ_LAST on empty delay line -> write to NULL+0x30 -> panic. Fixed-address NULL write; crash/wedge primitive only, no uid=0.

Evidence (decisive lines)

['panic.txt: Fatal trap 12, fault VA 0x30 supervisor write, ready_event_wfq+0x274 addq %rdx,0x30(%rax)', 'run.log: config + single trigger packet transcript; panic ~1ms after tick', 'dfv3.c: negbw 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. Second iteration added to dfv3.c (userland segv on the error path, kernel behavior unaffected).

Verified recommended fix

reject bandwidth<0 at config_pipe + NULL-guard the TAILQ_LAST write (fix.diff hunks 1-2)

Verdict

config_pipe() stores ioc_pipe->bandwidth verbatim (ip_dummynet3.c:1519) with no sign check. With bandwidth<0 on a pipe carrying a WF2Q queue, the tick computation at :641 is guarded by (p->bandwidth > 0) so t stays 0; :642 then does TAILQ_LAST(&p->p_queue, dn_pkt_queue)->output_time += t. On the next dummynet tick the pipe is re-extracted with an empty delay line (delay=0 packets were already transmitted), TAILQ_LAST returns NULL and the kernel writes to NULL+0x30 (offset of output_time in struct dn_pkt). Reproduced 1/1 on the stock guest: 'Fatal trap 12: page fault, fault virtual address = 0x30, supervisor write data', Stopped at ready_event_wfq+0x274: addq %rdx,0x30(%rax), current process Idle. Identical code and signature to the verified v1 finding DF-3350 (sys/net/dummynet/ip_dummynet.c:1516/618-623); with delay>0 the same defect instead reinserts the pipe into wfq_ready_heap with key==curr_time forever, wedging the netisr drain loop (verified for v1; same code here).