Global netgraph queue (ngqbase/ngqlast/ngqfree) manipulated under crit_enter only: cross-CPU producer races silently orphan queue entries, permanently leaking their mbufs/messages and pinning node/hook references
Summary
The single global ngqbase/ngqlast list, the ngqfree cache, and ngqsize are all mutated under crit_enter() only - a per-CPU critical section that does not exclude other CPUs. Two producers racing the tail link lose one entry; producer-vs-consumer interleavings orphan queued entries. An orphaned entry is never RETURN_QBLK'd, so its mbuf chain or control message and the hook->refs/node->refs taken at enqueue leak permanently (pinned nodes, unloadable types). The code comments acknowledge the races ('don't worry about races') but the lost-entry consequence was not accounted for. Sustained multi-CPU netgraph traffic (PPPoE/L2TP concentrator with ngintr active on several CPUs plus ksocket/tty producers) eventually interleaves the ~10-instruction windows; each hit permanently leaks an mbuf chain (or up to ~20KB control message) plus a node/hook reference. Leak-only (no UAF - entries are unlinked before free). Not verified (narrow statistical window, Low). Fix: spinlock around the queue/free-list/ngqsize ops (or per-CPU queues). [VERIFIED pass2: global crit-enter-only queue confirmed; multi-CPU producers not drivable through v1 ng_socket userland; 300k single-producer control clean]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3484 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 547 B | β raw | |
| VERDICT.md | β | 1.8 KB | β raw | |
| env.txt | β | 309 B | view raw | |
| fix.diff | β | 634 B | view raw | |
| manifest.json | β | 727 B | view raw | |
| ngrace.c | β | 4.3 KB | view raw | |
| run.log | β | 427 B | view raw | |
| verdict.json | β | 2.2 KB | view raw |
DF-3484 - v1 netgraph global queue race
Build: gcc -O -o ngrace ngrace.c Control: ./ngrace 300000 (single producer; no mbuf drift). Multi-producer race could not be armed: v1 ng_socket data-socket association (ng_connect_data) returns ENXIO in this environment even for live named nodes. Code-certain: ng_base.c:1894-1947 mutates ngqbase/ngqlast/ngqfree under crit_enter() only ('don't worry about races' comments).
Kernel: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64
DF-3484 - VERDICT
status: not_reproduced | reproduced: 0 | impact: none | confidence: likely
Not reproduced (statistical race, producers could not be driven at rate through v1 userland on this guest). Code-confirmed by source: the single global ngqbase/ngqlast/ngqfree/ngqsize are mutated under crit_enter() only (ng_base.c:1894-1929, 1936-1947) - a per-CPU critical section that does not exclude other CPUs; the code comments themselves say 'don't worry about races'. Two producers on different CPUs executing the ~10-instruction tail-link window lose one entry permanently (its mbuf/message plus hook/node refs leak). Producer attempts: ng_socket DATA-socket flood path (ngrace.c) - blocked by a v1 ng_socket quirk (ng_connect_data on a named live node returns ENXIO from ng_path2node even though ngctl list shows the node; data sockets cannot be associated from a second process in this environment). Single-producer control 300k sends: no mbuf drift; guest healthy. Leak remains code-certain; window is instructions wide; realistic trigger is a multi-CPU PPPoE/L2TP concentrator under sustained ngintr load, not reproducible in a bounded session.
Kernel references
- sys/netgraph/netgraph/ng_base.c:1894
- sys/netgraph/netgraph/ng_base.c:1927
- sys/netgraph/netgraph/ng_base.c:1936
Evidence
run.log (ngtrace of association attempts + control run), ngrace.c, source excerpt
Recommended fix
replace crit_enter-only protection with a spinlock (or per-CPU queues) around ngqbase/ngqlast/ngqfree/ngqsize.
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
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
run.log (ngtrace of association attempts + control run), ngrace.c, source excerpt
PoC changes
ngrace.c written fresh; v1 data-socket association (ng_connect_data) failed ENXIO in this environment - flood never launched
Verified recommended fix
replace crit_enter-only protection with a spinlock (or per-CPU queues) around ngqbase/ngqlast/ngqfree/ngqsize.
Verdict
Not reproduced (statistical race, producers could not be driven at rate through v1 userland on this guest). Code-confirmed by source: the single global ngqbase/ngqlast/ngqfree/ngqsize are mutated under crit_enter() only (ng_base.c:1894-1929, 1936-1947) - a per-CPU critical section that does not exclude other CPUs; the code comments themselves say 'don't worry about races'. Two producers on different CPUs executing the ~10-instruction tail-link window lose one entry permanently (its mbuf/message plus hook/node refs leak). Producer attempts: ng_socket DATA-socket flood path (ngrace.c) - blocked by a v1 ng_socket quirk (ng_connect_data on a named live node returns ENXIO from ng_path2node even though ngctl list shows the node; data sockets cannot be associated from a second process in this environment). Single-producer control 300k sends: no mbuf drift; guest healthy. Leak remains code-certain; window is instructions wide; realistic trigger is a multi-CPU PPPoE/L2TP concentrator under sustained ngintr load, not reproducible in a bounded session.
No comments yet.