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

del_vif() leaves tbf_reprocess_q_ch armed with the dying vif β€” v_tbf NULL dereference panic in tbf_update_tokens()

Summary

tbf_control() arms the global tbf_reprocess_q_ch callout with vifp when a packet queues on a rate-limited vif. del_vif() frees the queued mbufs and bzeros the vif - v_tbf becomes NULL - but never stops the callout (X_ip_mrouter_done does). When the 10 ms callout fires, tbf_reprocess_q() only checks ip_mrouter and tbf_update_tokens() dereferences t->tbf_last_pkt_t at NULL+0x8. REPRODUCED on guest iteration 0: 'Fatal trap 12, fault VA = 0x8, Stopped at tbf_update_tokens+0x2a'. Root-only trigger (MRT_DEL_VIF), deterministic window; a routing-daemon vif reconfiguration is enough. Fix validated (callout_stop in del_vif + v_tbf NULL guard): 200 iterations clean.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3305 Β· 12 files
FileTypeDescriptionSize
tbfcallout.c β€” 3.9 KB view raw
ip_mroute_common.h β€” 2.2 KB view raw
build.sh β€” 94 B view raw
run.sh β€” 93 B view raw
build.log β€” 120 B view raw
run.log β€” 731 B view raw
panic.txt β€” 666 B view raw
fix_run.log β€” 10.3 KB view raw
env.txt β€” 181 B view raw
VERDICT.md β€” 2.7 KB ↓ raw
fix.diff β€” 2.7 KB view raw
verdict.json β€” 2.8 KB view raw
VERDICT.md
↓ download raw

DF-3305 VERDICT β€” REPRODUCED (deterministic NULL deref β†’ kernel panic)

Bottom line

del_vif() leaves the tbf_reprocess_q_ch callout armed with vifp as its argument; it then bzeros the vif (and its tbf) so vifp->v_tbf becomes NULL, and when the 10 ms callout fires it dereferences t->tbf_last_pkt_t β†’ Fatal trap 12 at virtual address 0x8. Reproduced on the guest on the first qualifying iteration (after fixing the PoC to actually reach the tbf path). Trigger is privileged (MRT_DEL_VIF), so severity is Low (root can crash the box β€” but a routing daemon restart sequence del_vif at the wrong moment is enough; no malicious intent needed).

Root cause (citations sys/net/ip_mroute/ip_mroute.c)

  • :1791-1793 tbf_control() arms the global callout with vifp when a packet is queued on a rate-limited vif: callout_reset(&tbf_reprocess_q_ch, TBF_REPROCESS, tbf_reprocess_q, vifp);
  • :873-923 del_vif() frees the queued mbufs and bzeros vifp (v_tbf becomes NULL) under mroute_token β€” but never stops the callout. (X_ip_mrouter_done does stop it, at :644 β€” del_vif forgot.)
  • :1873-1885 tbf_reprocess_q(vifp) only checks ip_mrouter == NULL, then :1961-1972 tbf_update_tokens() does struct tbf *t = vifp->v_tbf; and TV_DELTA(tp, t->tbf_last_pkt_t, tm) β†’ NULL+0x8 read.

Reproduction (guest, PIMTEST kernel #0)

MRT_INIT: 0
iter 0: sent, DEL_VIF rc=0; waiting for callout...

Fatal trap 12: page fault while in kernel mode
fault virtual address   = 0x8                    <- &tbf_last_pkt_t.tv_usec - NULL
Stopped at      tbf_update_tokens.isra.11+0x2a:  subq 0x8(%rbx),%rax  <- rbx = t = NULL

(runs: first attempt with 200 iterations did not panic because a raw socket without IP_MULTICAST_TTL gets its TTL forced to 1 by ip_output and X_ip_mforward drops TTL<=1 before reaching the vif; with the TTL option attached, the panic is immediate β€” see poc_changes.)

Fix validation (kernel #1)

Patched del_vif stops the callout and tbf_reprocess_q guards v_tbf == NULL; ./tbfcallout completes 200 iterations: no panic after 200 iterations (fix_run.log).

+    callout_stop(&tbf_reprocess_q_ch);
     bzero((caddr_t)vifp->v_tbf, sizeof(*(vifp->v_tbf)));

plus if (ip_mrouter == NULL || vifp == NULL || vifp->v_tbf == NULL) return; in tbf_reprocess_q (see fix.diff).

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

Patched kernel: tbfcallout runs 200 add/send/del cycles, 'no panic after 200 iterations' (fix_run.log). Baseline panicked on iteration 0.

fix_run.log; fix_build.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Sep 10 10:33:47 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/PIMTEST x86_64

Confirmed kernel references

Detail

Exploit chain

root: MRT_INIT -> MRT_ADD_VIF(phyint, rate_limit>0) -> MRT_ADD_MFC -> inject one matching multicast (queues in tbf, arms 10ms callout with vifp) -> MRT_DEL_VIF (bzero, v_tbf=NULL, callout not stopped) -> callout fires -> tbf_update_tokens derefs NULL -> panic.

Evidence (decisive lines)

['run.log: iter 0 sent, DEL_VIF rc=0 then Fatal trap 12', 'panic.txt: fault virtual address 0x8, Stopped at tbf_update_tokens.isra.11+0x2a subq 0x8(%rbx),%rax', 'fix_run.log: no panic after 200 iterations']

PoC changes

First version never reached the vif: a raw socket without imo gets ip_ttl forced to 1 by ip_output and X_ip_mforward drops TTL<=1. Added IP_MULTICAST_TTL=5 (+loop off) on the raw socket -> panic on iteration 0.

Verified recommended fix

del_vif(): callout_stop(&tbf_reprocess_q_ch) before bzero; tbf_reprocess_q(): return if vifp==NULL || vifp->v_tbf==NULL.

Verdict

del_vif() does not stop tbf_reprocess_q_ch which tbf_control() armed with vifp; del_vif bzeros the vif (v_tbf=NULL) and the 10ms callout fires into tbf_update_tokens() which reads t->tbf_last_pkt_t at NULL+0x8: Fatal trap 12, Stopped at tbf_update_tokens.isra.11+0x2a subq 0x8(%rbx),%rax, on iteration 0. Root-only trigger (MRT_DEL_VIF) so Low severity, but deterministic and a plain daemon-restart sequence can hit it. Fixed kernel stops the callout in del_vif and guards v_tbf==NULL: 200 iterations, no panic.