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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3519 Β· 2 files
FileTypeDescriptionSize
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
VERDICT.md verdict non-atomic refs/timers analysis; double-free direction killed; undercount direction (node UAF) proven at code level
↓ download 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:

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_testable
per-fix-DF-3519

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