ng_pptpgre priv UAF: rmnode kfree(priv) races in-flight rcvdata/rcvmsg dispatched inline without a node reference
Summary
Legacy ng_send_data dispatches rcvdata inline on the sender's CPU holding no reference on the peer node and none on node->private. ng_pptpgre_rmnode() stops the timers and, when priv->timers==0, kfrees priv and NULLs node->private. On SMP, a peer (ng_ether per-flow input, ng_ksocket upcall) executing xmit/recv on CPU A while the last hook is disconnected / shutdown is processed on CPU B frees the ~208-byte priv under A's feet - post-free writes to priv->stats (:551-552,:576-577), recvSeq/xmitSeq, and reads of conf/callout structs. The pptpgre instantiation of DF-0492's 'node private data' half, not covered by DF-0596. Honest hammer-negative on this guest (51,962 rmnode cycles overlapped with in-flight frames, no panic - every ng_socket send serializes on CPU-0 netisr, the established blocker); the ng_ether SMP deployment vector is real. Fix: per-node lockmgr or a node ref across the inline dispatch.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3518 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| uaf_hammer.c | trigger-source | rmnode-vs-rcvdata hammer: 52k node lifecycles overlapped with in-flight frames | 8.9 KB | view raw |
| run.log | run-log | 30s run: kill_cycles=51962, no panic, guest up, Count unchanged | 471 B | view raw |
| VERDICT.md | verdict | full code trace + vector blocker analysis | 3.6 KB | β raw |
| verdict.json | verdict | machine verdict | 2.5 KB | view raw |
DF-3518 β ng_pptpgre priv UAF: rmnode free vs in-flight rcvdata
Verdict: NOT REPRODUCED on this guest β code-confirmed UAF whose trigger vector is blocked by the same CPU-0 netisr serialization that DF-0590 established empirically (millions of frames, no concurrency).
status = not_reproduced, reproduced = 0, impact = none (on this
vector), confidence = certain (code-level).
The defect (code trace)
v1 netgraph delivers data inline, with no node reference and no lock across the call:
sys/netgraph/netgraph/ng_base.c:1687βng_send_data():error = (*rcvdata)(hook->peer, m, meta);β direct call, caller keeps no ref on the peer node and none exists onnode->private.sys/netgraph/pptpgre/ng_pptpgre.c:437-440βng_pptpgre_rmnode(): afterng_pptpgre_reset()sync-stops both timers,if (priv->timers == 0) { kfree(priv, M_NETGRAPH); node->private = NULL; }.
Nothing pins priv for the duration of an in-flight rcvdata /
rcvmsg. On an SMP system where a peer node dispatches into the
pptpgre node on CPU A (e.g. ng_ether delivering from
ether_input's per-flow netisr, sys/net/if_ethersubr.c:1189), a
concurrent teardown on CPU B (last-hook disconnect β ng_rmnode β
ng_pptpgre_rmnode β kfree(priv)) frees the ~208-byte priv while
CPU A is still writing priv->stats (:576-577, :551-552),
priv->recvSeq/xmitSeq (:698, :516), priv->conf reads, etc.
β use-after-free write of controlled-ish data on the M_NETGRAPH heap.
This is the pptpgre instantiation of the "node private data" half of
DF-0492 (L2TP) β not covered by DF-0596, which is scoped to the
xmitWin/timeSent[] state race.
Aggravator (post-free, not pre-free): with DF-3521's leak,
priv->timers never reaches 0, so rmnode doesn't free β the UAF
window needs a clean-timers shutdown (e.g. node with no armed timers,
or a kernel with the kern_timeout contract fixed).
Why it cannot fire from userspace on this guest
Every ng_socket send (data and control) is processed on CPU 0's
netisr port: sys/kern/uipc_socket.c:254-259 (no PR_SYNC_PORT, no
pr_initport for ng_socket β so->so_port = netisr_cpuport(0)), and the
callouts arm on the CPU that starts them. DF-0590's pack demonstrated
1β3 M injected frames with zero concurrency β the identical blocker.
uaf_hammer here reproduces that negative for the pptpgre rmnode path:
hammer 30 s: injector floods pptp:upper while killer builds+shuts down nodes DONE inj_sent=9 (ENOBUFS backpressure) kill_cycles=51962 kill_fail=0 EXIT_OK, guest up, vmstat -m netgraph Count unchanged
52k create/config/shutdown lifecycles overlapped with in-flight frames, zero panics β consistent with full serialization on CPU 0.
Realistic exposure
A root-configured PPTP/VPN concentrator with ng_ether- or
ng_ksocket-fed lower hooks receives frames on per-flow CPUs β the
rmnode-vs-rcvdata window is genuinely open there. Fix (same as
DF-0590's): per-node serialization for v1 ng_pptpgre (lockmgr LK_*
around rcvdata/rcvmsg/shutdown, or take a node ref + flag across the
inline dispatch in ng_send_data).
Files
uaf_hammer.cβ hammer (root):cc -O2 -lpthread -o uaf_hammer uaf_hammer.crun.logβ full 30 s run output
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)
run.log (uaf_hammer 30s: kill_cycles=51962, kill_fail=0, guest up, EXIT_OK, netgraph Count unchanged); VERDICT.md path:line trace of the inline-dispatch-without-ref and the rmnode free
PoC changes
Authored from scratch. Fixed during bring-up: killer must not reuse the victim's ctl-node hook names or node names (EEXIST/EADDRINUSE storm); unnamed nodes addressed by hook-walk paths ('p0'); separate ctl nodes for victim vs killer.
Verified recommended fix
Serialize v1 ng_pptpgre with a per-node lockmgr lock across rcvdata/rcvmsg/shutdown (as recommended for DF-0590), or hold a node reference plus validity flag across the inline dispatch in ng_send_data.
Verdict
NOT REPRODUCED on this guest, code-certain: ng_pptpgre_rmnode() kfree(priv) (ng_pptpgre.c:437-440) can overlap in-flight rcvdata/rcvmsg because v1 ng_send_data dispatches inline with no peer-node reference or lock (ng_base.c:1687), giving a write-after-free on the ~208-byte priv chunk on SMP. The userspace trigger vector is blocked by the same CPU-0 netisr serialization DF-0590 established empirically (uipc_socket.c:254-259: every ng_socket data AND control send processes on netisr_cpuport(0)); the hammer ran 52k create/config/shutdown lifecycles overlapped with in-flight frames with zero panics and a clean exit, consistent with full serialization. The window is genuinely open in the ng_ether/ksocket per-flow SMP deployment (VPN concentrator). Same code-level certainty class as DF-0590's verified negative.
No comments yet.