Non-atomic node->refs++ / priv->timers++ in ng_pptpgre timer start vs ng_unref β lost update frees live ng_node (UAF)
Summary
The two timer-start helpers do plain, non-atomic node->refs++ and priv->timers++ with no crit/atomic, racing ng_unref()'s plain --node->refs and priv->timers-- in the callbacks and stop paths. Timer start/stop churns once per ack in a live session. A lost ++/-- update undercounts refs -> struct ng_node freed while the armed timer's callback still dereferences it (then ng_unref's LIST_REMOVE corrupts the global node/id hash lists). DF-0492's 'refcount also racy' instantiated for ng_pptpgre; outside DF-0596's scope. SMP-only instruction-level window, needs the data path on multiple CPUs (ng_ether concentrator); unreachable from userspace ng_socket (CPU-0 serialization - the 52k-lifecycle hammer re-confirmed). Fix: NG_NODE_REF (atomic) + atomic timers (or the per-node lock).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3519 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | non-atomic refs/timers analysis; double-free direction killed; undercount direction (node UAF) proven at code level | 3.8 KB | β raw |
| verdict.json | verdict | machine verdict | 2.5 KB | view raw |
DF-3519 β non-atomic node->refs++ / priv->timers++ in ng_pptpgre timer start vs ng_unref under crit-only
Verdict: NOT REPRODUCED on this guest β instruction-level SMP race with the same CPU-0 netisr vector blocker as DF-0590/DF-3518; code-confirmed non-atomic RMW pair.
status = not_reproduced, reproduced = 0, impact = none (on this
vector), confidence = certain (code-level).
The defect (code trace)
The timer-start paths do bare, non-atomic increments of two shared fields:
sys/netgraph/pptpgre/ng_pptpgre.c:768-769(ng_pptpgre_start_recv_ack_timer):c node->refs++; /* :768 β plain RMW, no atomic, no crit */ priv->timers++; /* :769 β plain RMW */sys/netgraph/pptpgre/ng_pptpgre.c:871-872(ng_pptpgre_start_send_ack_timer): same pair.
The matching decrements run in other contexts:
* ng_unref() β sys/netgraph/netgraph/ng_base.c:474-485:
crit_enter(); if (--node->refs <= 0) ... β crit sections are
per-CPU on DragonFly; they do not exclude a plain ++ executing
on another CPU.
* priv->timers-- in the two timeout callbacks
(:811, :915) and in the stop paths (:788, :891).
Two CPUs doing ++refs (timer start) and --refs (any unref, e.g. the
previous timer instance's callback or a message-dispatch release) on
the same plain int lose one update β refs undercounts β the node is
freed while the new timer's callback/item still dereferences it
(*((node_p *)arg) at :802/:906, then node->private, ->flags,
ng_unref) β UAF on struct ng_node (~120 B M_NETGRAPH chunk,
containing type pointer and LIST links β ng_unref's
LIST_REMOVE(node, nodes/idnodes) on freed memory corrupts the global
node hash lists). The same lost-update on priv->timers biases the
drain count (leak direction; a double-zero was analyzed and is NOT
reachable because ng_pptpgre_rmnode's reset uses synchronous
callout_stop before checking timers).
This is DF-0492's "Refcount also racy" finding instantiated for
ng_pptpgre β DF-0596 does not cover it (it is scoped to the
ackp/xmitWin state TOCTOU).
Trigger model
Timer start/stop churns once per ack in a live session
(:679-681 stop+restart per received ack). With the data path
dispatching on multiple CPUs (ng_ether per-flow input β the realistic
VPN-concentrator deployment; NOT reachable from ng_socket, which
serializes on CPU 0's netisr port per uipc_socket.c:254-259), the
++/-- pair executes concurrently β lost update. Window is a couple
of instructions; requires sustained SMP traffic.
Fix
Use the framework's atomic helper β replace both node->refs++ with
NG_NODE_REF(node) (atomic_add_int, netgraph.h:132) and make
priv->timers atomic (atomic_add_int(&priv->timers, 1) /
atomic_add_int(&priv->timers, -1)), or (better) serialize the node
with a per-node lock as recommended for DF-0590/DF-3518, which removes
the whole class.
Why no in-guest run
The race requires concurrent timer start/stop from multiple CPUs; from userspace every ng_socket send (data and control) is processed serially on CPU 0's netisr port (see DF-0590 VERDICT Β§2, empirically demonstrated with millions of frames; DF-3518's 52k-lifecycle hammer re-confirmed for this node type). A dedicated run would reproduce the same negative; the code-level defect is certain.
Files
VERDICT.md(this file) β the full analysis; no binary PoC for the reasons above.
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.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md code trace (ng_pptpgre.c:768-769/:871-872 vs ng_base.c:474-485, callbacks :811/:915/:802/:906); no runnable vector on this guest - documented rather than burning a run that can only reproduce the known serialization negative
PoC changes
No binary PoC authored: the only userspace vector (ng_socket) is serialized on CPU 0 by construction; DF-3518's 52k-lifecycle hammer already re-confirmed the blocker on this node type this run.
Verified recommended fix
Use NG_NODE_REF(node) (atomic_add_int, netgraph.h:132) at both start sites and atomic_add_int for priv->timers; better, add the per-node serialization lock (also fixes DF-0590/DF-3518 class).
Verdict
NOT REPRODUCED (vector-blocked), code-certain: ng_pptpgre_start_recv_ack_timer() and ng_pptpgre_start_send_ack_timer() do plain non-atomic node->refs++ (ng_pptpgre.c:768, :871) racing ng_unref()'s plain --node->refs (ng_base.c:474-485, crit-enter protects the current CPU only) and plain priv->timers++/-- across contexts. A lost RMW update undercounts refs, freeing struct ng_node while the armed timer's callback still dereferences it (recv/send_ack_timeout :802/:906) and while ng_unref's LIST_REMOVE corrupts the global node hash lists - UAF on the node object. DF-0492's 'refcount also racy' instantiated for ng_pptpgre; not covered by DF-0596 (xmitWin/timeSent scope). Instruction-level window requires concurrent timer start/stop on multiple CPUs, unreachable from userspace ng_socket (CPU-0 netisr serialization, DF-0590 empirical).
No comments yet.