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

ng7 ng_hub: two cross-wired hubs plus one entry hook form a config-free forwarding cycle β€” with the entry hook created last, one injected byte makes the hub's single synchronous NG_FWD_ITEM_HOOK recurse unbounded until the LWKT token array overflows: deterministic kernel panic (DF-3540 class, purest instance; natural hook order instead yields a 10.5k pkt/s packet-multiplication storm from one byte)

Summary

ng_hub_rcvdata forwards every item to all hooks except the arrival one: m_dup copies via NG_SEND_DATA_ONLY and the last hook in LIST_FOREACH order gets the single NG_FWD_ITEM_HOOK. ng_hub registers no newhook method so ng_add_hook accepts arbitrary hook names/counts (LIST_INSERT_HEAD -> iteration = reverse creation order), and neither node nor framework detects cycles. Two cross-wired hubs plus one socket entry hook are an injectable cycle built purely from generic MKPEER/CONNECT/RMHOOK messages - no node-local configuration, unlike DF-3540 (ng_bpf SET_PROGRAM) and DF-3553 (ng_tee direction hooks). When the FWD lands on a cycle hook (entry hook created LAST via RMHOOK+CONNECT) the SAME item recurses with no hop bound on stock - each hop takes the destination node token recursively until the token-array KKASSERT panics. With the natural construction the cycle instead runs on the ASYNC m_dup successors: one injected byte produced 159,022 packets in 15.1s (~10.5k pkt/s). VERIFIED live both modes; DF-3540's framework hop-bound fix validated in-guest (-Werror clean, PoC survives with exactly 16 packets drained then ELOOP, vs stock panic). Residual async storm documented (self-limiting, no panic). Root-gated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3559 Β· 19 files
FileTypeDescriptionSize
hub_cycle.c β€” 9.9 KB view raw
v1_neg.c β€” 9.2 KB view raw
camtest.c β€” 7.8 KB view raw
build.sh β€” 179 B view raw
run.sh β€” 217 B view raw
run.log β€” 35.9 KB view raw
run.2.log β€” 1.1 KB view raw
camtest.log β€” 957 B view raw
v1_riders.log β€” 554 B view raw
storm_stock.log β€” 664 B view raw
run_panic.log β€” 234 B view raw
run_panic_attempt.log β€” 350 B view raw
panic.txt β€” 637 B view raw
console_full.txt β€” 2.3 KB view raw
unpriv_check.txt β€” 202 B view raw
fix.diff β€” 1.8 KB view raw
fix_run.log β€” 821 B view raw
env.txt β€” 650 B view raw
code_hashes.txt β€” 251 B view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fixed

fix_run.log: after vm.sh reset with-src + patch -p1 of fix.diff into /usr/src + -Werror-clean rebuild of netgraph.ko/ng_socket.ko/ng_hub.ko, the exact 'hub_cycle sync' PoC survives 40s and drains exactly 16 packets (= NG_HOPS_MAX) with delta=0 (cycle bounded, item dropped ELOOP) vs the stock baseline panic (run_panic.log/panic.txt). Storm mode re-run post-fix still multiplies packets (fresh async items reset el_hops) but never panics and self-terminates when draining stops.
↓ fix.diffper-fix-DF-3559

Confirmed kernel references

Detail

Evidence (decisive lines)

panic.txt + console_full.txt (stock panic trace with ng_hub_rcvdata in the ng_apply_item recursion); run_panic.log (ssh died at injection); storm_stock.log (159022 pkts/15.1s from one byte); fix_run.log (patched: survives, exactly 16=NG_HOPS_MAX packets, delta=0); unpriv_check.txt (EPERM); v1_riders.log (8/8: v1 echo reflect-only+ring-inert+control-bounce-no-relay; v1 hole EINVAL incl. exact DF-3558 under-sent shape -> twin ABSENT); camtest.log (RFC3713 tables+KATs PASS, 96/96 vs OpenSSL)

PoC changes

Seed lead assumed a hub pair alone was injectable; live runs showed the natural construction only storms asynchronously (ng_snd_item queues first sends, refs==1). Root-caused the send semantics, then used ng_add_hook's LIST_INSERT_HEAD ordering to place the entry hook LAST (RMHOOK birth hook + NGM_CONNECT by node name 'hecyc:') so the hub's single NG_FWD_ITEM_HOOK stays on a cycle hook and the SAME item recurses synchronously -> panic. Also measured the storm mode (159k pkts/15s) instead of hand-waving it. Riders: v1 harness needed the trailing ':' on absolute node names (bare 'vneg' parses as a relative hook path -> ENXIO at connect); camtest's first draft extracted the wrong byte from the S+P-combined sp tables (documented in run.log) and wrongly expected the unused kw2 subkey words to be written.

Verified recommended fix

Apply the DF-3540 framework fix (el_hops / NG_HOPS_MAX=16 bound in ng_snd_item, ELOOP) -- see fix.diff; optionally document that hub graphs must not contain back-edges.

Verdict

REPRODUCED (deterministic, two independent boots, stock INVARIANTS kernel). ng_hub_rcvdata (sys/netgraph7/hub/ng_hub.c:61-91) forwards to all hooks except the arrival one: m_dup copies via NG_SEND_DATA_ONLY and the last hook in LIST_FOREACH order gets the single NG_FWD_ITEM_HOOK. ng_hub has no newhook filter (ng_hub.c:44-50; ng_base.c:1068-1071 accepts any hook) and neither node nor framework detects cycles, so two cross-wired hubs (HE.y<->HL.a, HE.b<->HL.c) plus one socket entry hook form an injectable cycle built purely from generic MKPEER/CONNECT/RMHOOK messages -- NO node-local configuration, the purest DF-3540-class instance. Because ng_snd_item only queues an item's FIRST send (refs==1 -> lwkt_sendmsg, ng_base.c:1955-1965) and forwards synchronously afterwards, the panic needs the hub's FWD to land on a cycle hook; ng_add_hook's LIST_INSERT_HEAD (ng_base.c:1077) makes iteration reverse-creation order, so the PoC creates the entry hook LAST (RMHOOK the birth hook, NGM_CONNECT a fresh one). One injected byte then recurses ng_apply_item <-> ng_hub_rcvdata unbounded until the LWKT token array overflows: 'panic: assertion "ref < &td->td_toks_end" failed in lwkt_gettoken_shared at sys/kern/lwkt_token.c:732' with ng_hub_rcvdata twice in the trace (panic.txt, run_panic.log; ssh died at injection, vm.sh status=down). The natural construction (entry hook first-created) instead yields an ASYNC packet-multiplication storm: 159,022 packets in 15.1s (~10.5k pkt/s) from ONE byte, worker-thread + mbuf-objcache saturation ('objcache(mbuf pkthdr) exhausted'), self-terminating once the entry socket stops draining (storm_stock.log). Root-gated (uid 1001: EPERM at socket(), caps_priv_check -- unpriv_check.txt), so Low severity like DF-3540/3553/3557. FIX VALIDATED: fix.diff = framework subset of DF-3540's validated fix (netgraph.h el_hops + ng_base.c NG_HOPS_MAX=16 -> ELOOP); on a fresh reset with-src guest, -Werror-clean module rebuild, the exact PoC survives with exactly 16 packets drained then full termination (delta=0); the storm mode still storms (fresh async items reset el_hops) -- documented residual, no panic, self-limiting.