ng_bpf SET_PROGRAM accepts cyclic match/nomatch forwarding destinations (incl. ifMatch == thisHook); ng7's unbounded synchronous forwarding recurses until the LWKT token array overflows β deterministic kernel panic
Summary
ng_bpf_setprog resolves ifMatch/ifNotMatch via ng_findhook with no check that the target differs from thisHook and no cycle detection; a cyclic configuration makes each arriving packet recurse synchronously through NG_FWD_ITEM_HOOK -> ng_snd_item -> ng_apply_item -> rcvdata, each hop recursively acquiring the destination node's LWKT token until td_toks_array[LWKT_MAXTOKENS=32] overflows: 'panic: assertion ref < &td->td_toks_end failed in lwkt_gettoken_shared' (KKASSERT compiled into production kernels). Same framework property reachable via other forwarding node types (ng_echo pair). VERIFIED on stock INVARIANTS guest: 1-byte inject -> instant panic, both 'self' (ifMatch==thisHook) and 'cross' (multi-hook cycle, no self-reference) modes. Root-gated config; privileged DoS. Fix validated in two stages: ng_bpf guard alone stops 'self' with EINVAL but 'cross' still panics; full fix (+ el_hops NG_HOPS_MAX=16 bound in ng_snd_item) stops both. LEAD (a) resolved: SET_PROGRAM DOES call bpf_validate (cap 512 insns, MEM bounds, jump validation, terminal RET) - DF-3444's unvalidated-filter primitive NOT reachable via ng_bpf.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3540 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 4.1 KB | β raw | |
| bpf_cycle.c | β | 7.5 KB | view raw | |
| build.sh | β | 181 B | view raw | |
| run.sh | β | 163 B | view raw | |
| run.log | β | 1.7 KB | view raw | |
| unpriv_check.txt | β | 174 B | view raw | |
| panic.txt | β | 611 B | view raw | |
| console_full.txt | β | 2.3 KB | view raw | |
| console_cross.txt | β | 2.3 KB | view raw | |
| stock_cross_run.log | β | 28 B | view raw | |
| fix1_self.log | β | 309 B | view raw | |
| fix1_cross.log | β | 28 B | view raw | |
| fix2_self.log | β | 314 B | view raw | |
| fix2_cross.log | β | 278 B | view raw | |
| fix.diff | β | 2.4 KB | view raw | |
| verdict.json | β | 4.6 KB | view raw | |
| manifest.json | β | 1.0 KB | view raw |
DF-3540 β netgraph7 ng_bpf: cyclic ifMatch/ifNotMatch forwarding β unbounded synchronous recursion β kernel panic
What
ng_bpf_setprog() (sys/netgraph7/bpf/ng_bpf.c:584-585) resolves
ifMatch/ifNotMatch with ng_findhook() and stores the result as a
direct forwarding target, with no validation that the targets differ
from thisHook and no cycle detection. netgraph7 forwards items whose
refcount is > 1 synchronously β NG_FWD_ITEM_HOOK β
ng_snd_item() β ng_apply_item() β the peer's rcvdata() β
NG_FWD_ITEM_HOOK β β¦ β with no hop bound (sys/netgraph7/netgraph/ng_base.c:1908-1965).
Each hop also takes the destination node's LWKT token
(ng_acquire_read, ng_base.c:1998) recursively.
A cyclic configuration β two ng_bpf nodes cross-connected, each with
ifMatch == thisHook ("self" mode), or a pure multi-hook ifMatch cycle
with no self-reference at all ("cross" mode) β makes one injected packet
recurse until the kernel thread's LWKT token reference array
(td_toks_array[LWKT_MAXTOKENS == 32], sys/sys/thread.h:331) overflows:
panic: assertion "ref < &td->td_toks_end" failed in lwkt_gettoken_shared
at /usr/src/sys/kern/lwkt_token.c:732 (KKASSERT: all builds)
lwkt_gettoken_shared() at lwkt_gettoken_shared+0x34c
lwkt_gettoken_shared() at lwkt_gettoken_shared+0x34c
ng_apply_item() at ng_apply_item+0xa9
ng_apply_item() at ng_apply_item+0x105 (recursive)
...
The KKASSERT is compiled into production (non-INVARIANTS) kernels too, so the panic is deterministic everywhere. Without the token-array tripwire the recursion would instead exhaust the ngthread kernel stack.
Trigger privilege
ROOT ONLY (verified live): ng7 control sockets require
caps_priv_check(SYSCAP_RESTRICTEDROOT) (sys/netgraph7/socket/ng_socket.c:182-185);
uid 1001 gets EPERM (unpriv_check.txt). Root must build the cyclic graph;
one packet (sent by root, or by any traffic source once configured)
panics the machine. Severity Low (privileged DoS) β same class as
DF-3486/DF-0679.
Build / Run (in-guest, root, clean /usr/src)
sh build.sh # ng7 netgraph.ko + ng_socket.ko + ng_bpf.ko + trigger sh run.sh self # expect panic (console: token-array assertion) # after reboot: sh run.sh cross # expect same panic (cycle class, no self-reference)
Fix validation (fix.diff)
stock self β panic cross β panic ng_bpf guard only self β EINVAL cross β panic (fix1_.log) full fix self β EINVAL cross β survives (fix2_.log)
fix.diff = (1) ng_bpf.c rejects ifMatch/ifNotMatch == thisHook
(kills the trivially-configurable self cycle); (2) ng_base.c bounds
synchronous forwards per item (el_hops, NG_HOPS_MAX = 16, ELOOP) β
the general guard covering cycles built from any node types.
DF-3540 β VERDICT
Status: reproduced. Impact: panic (kernel DoS, root-gated). Confidence: certain.
How it was reproduced
Guest: DragonFly 6.5-DEVELOPMENT #0 (stock INVARIANTS kernel), ng7 module
stack (netgraph.ko, ng_socket.ko, ng_bpf.ko) built from the guest's clean
/usr/src β build.sh / run.sh. Trigger bpf_cycle.c uses the AF_NETGRAPH
control+data socket API (NG_VERSION 8; sockaddr_ng data at offset 2 β
DFly sa_family_t is __uint8_t):
- control socket node named "bpcyc" (NGM_NAME), data socket connected to it ("bpcyc:" absolute-node addressing).
- mkpeer bpf (B1), mkpeer bpf from B1 (B2), three NGM_BPF_SET_PROGRAMs:
- self mode:
B1.x2: ifMatch=x1,B1.x1: ifMatch=x1(self),B2.y1: ifMatch=y1(self). Steady loop B1.x1 β B2.y1. - cross mode: B1 hooks e/p/r, B2 hooks q/s (NGM_CONNECT wires B1.rβB2.s); programsB1.eβp,B1.rβp,B2.qβs. Loop B1.r β B2.q β B1.r. No ifMatch equals its own thisHook anywhere. - one 1-byte packet injected via the data socket.
Both modes: kernel dies instantly (ssh hangs, guest down). Serial console:
panic: assertion "ref < &td->td_toks_end" failed in lwkt_gettoken_shared
at /usr/src/sys/kern/lwkt_token.c:732
lwkt_gettoken_shared() x2, ng_apply_item() x3+ recursive
Debugger("panic")
(panic.txt, console_full.txt, console_cross.txt)
Why it happens (source trace)
- ng_bpf.c:584-585 β
hip->match = ng_findhook(node, prog->ifMatch)β any hook name accepted, including thisHook itself; no cycle check. - ng_bpf.c:500 β
NG_FWD_ITEM_HOOK(error, item, dest). - netgraph.h:861-872 β ng_base.c:1908
ng_snd_item(): items with refs>1 are applied synchronously (line 1961) β each forward is a stack frame chain, no hop limit. - ng_base.c:1998
ng_acquire_read()βlwkt_gettoken_shared()pushes a tokref intotd_toks_array[LWKT_MAXTOKENS=32](sys/sys/thread.h:331); recursive acquisition per hop; 32 acquisitions β KKASSERT at lwkt_token.c:732 (always compiled β KKASSERT, not KASSERT). - netgraph7 dispatches everything on the cpu0 ngthread (ng_base.c:1948-1956), so the recursion is on a kernel thread stack.
The framework property (no cycle guard on synchronous forwarding) is the root enabler; ng_bpf's SET_PROGRAM is the stock node type that makes a cycle expressible with two/three control messages. The same framework property is reachable through other forwarding node types (e.g. an ng_echo pair), which is why fix.diff fixes both layers.
Exploit chain
None beyond denial of service: root-gated configuration, deterministic panic. No memory-safety violation (the overflow tripwire is an always-on KKASSERT; no controlled overwrite occurs before it).
Fix validation (fix.diff)
Applied as module rebuilds on the same stock kernel:
| build | self mode | cross mode |
|---|---|---|
| stock | panic (run 1) | panic (run 2) |
| ng_bpf guard only | SET_PROGRAM β EINVAL (sendto errno=22, apply-error propagation), no panic (fix1_self.log) | still panics (fix1_cross.log) β proves the framework part is required |
| full fix (ng_bpf + ng_base hop bound 16) | EINVAL, survives (fix2_self.log) | bounded: cycle unwinds with ELOOP, packet dropped, survives 30s+ (fix2_cross.log) |
fix_status: fixed.
Residual notes
- NG_HOPS_MAX=16 chosen so worst-case recursion stays well inside a 2-page kernel thread stack; legitimate netgraph paths are far shallower. Upstream may prefer queueing instead of recursion (FreeBSD-style), which removes the need for the bound.
- v1 netgraph (sys/netgraph/bpf/ng_bpf.c) forwards synchronously too (v1 ng_send_data calls rcvdata directly, ng_base.c:1687) β the v1 twin of this bug exists but v1 ng_bpf is a separate file with its own audit record (DF-2595 et al.); not re-filed here.
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
fixedBoth panic modes stopped: self mode now fails SET_PROGRAM with EINVAL at config time (error visible in sendto errno via the ng7 apply mechanism) and cross mode survives 30s+ with the cycle bounded (packet dropped with ELOOP, guest up). Two-stage matrix proves the ng_bpf-local guard alone is insufficient (cross still panicked, fix1_cross.log) and the framework hop bound completes the fix (fix2_cross.log).
fix1_self.log; fix1_cross.log; fix2_self.log; fix2_cross.log; fix.diff; panic.txt vs fix2_*.log
Confirmed kernel references
Detail
Evidence (decisive lines)
panic.txt + console_full.txt (stock self-mode panic trace); console_cross.txt (stock cross-mode panic β cycle without any self-reference); unpriv_check.txt (EPERM); fix1_self.log / fix1_cross.log (partial-fix matrix); fix2_self.log / fix2_cross.log (full fix, both modes survive); run.sh / build.sh
PoC changes
Seed sketch rewritten entirely: correct ng7 user ABI (NG_VERSION 8, header field order arglen/cmd/flags/token/typecookie), DFly sockaddr_ng layout (sa_family_t is __uint8_t, sg_data at offset 2, sg_len = 2 + strlen + 1), absolute-node addressing needs the trailing colon ('name:'), hook paths are DOT-separated ('in.x1' not 'in:x1'), cross-cycle needs NGM_CONNECT (each NGM_MKPEER creates a NEW node), data socket attach via connect() to the control node's '[id]:'/name.
Verified recommended fix
Reject ifMatch/ifNotMatch == thisHook in ng_bpf_setprog AND bound synchronous forwards per item in ng_snd_item (el_hops, NG_HOPS_MAX=16, ELOOP) β see fix.diff.
Verdict
Reproduced on the stock INVARIANTS guest with ng7 modules built from clean /usr/src: a cyclic ng_bpf configuration (two bpf nodes cross-connected, each with ifMatch == thisHook β or a pure multi-hook ifMatch cycle with no self-reference) makes ONE injected packet recurse synchronously through NG_FWD_ITEM_HOOK -> ng_snd_item -> ng_apply_item -> rcvdata with no hop bound, and each hop takes the destination node token recursively, overflowing the LWKT token array: 'panic: assertion "ref < &td->td_toks_end" failed in lwkt_gettoken_shared at sys/kern/lwkt_token.c:732' with the ng_apply_item recursion visible in the trace (panic.txt). The KKASSERT is compiled into production kernels, so the panic is deterministic on all configs; without the token tripwire the recursion would exhaust the ngthread kernel stack instead. Root-only trigger (ngc_attach caps_priv_check, EPERM verified for uid 1001), so impact is a privileged kernel DoS (Low, same class as DF-3486). fix.diff validated in two stages: the ng_bpf-local guard (reject ifMatch/ifNotMatch == thisHook) stops the self mode with EINVAL but the self-reference-free cross cycle still panics (fix1_cross.log), proving the framework part is required; the full fix (plus ng_snd_item hop bound el_hops/NG_HOPS_MAX=16 -> ELOOP) makes both modes survive with the packet dropped (fix2_*.log).
No comments yet.