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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3484 Β· 8 files
FileTypeDescriptionSize
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

VERDICT.md
↓ download raw

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

Evidence

run.log (ngtrace of association attempts + control run), ngrace.c, source excerpt

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_testable
↓ fix.diffper-fix-DF-3484

Confirmed 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.