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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3518 Β· 4 files
FileTypeDescriptionSize
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
VERDICT.md verdict full code trace + vector blocker analysis
↓ download 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 on node->private.
  • sys/netgraph/pptpgre/ng_pptpgre.c:437-440 β€” ng_pptpgre_rmnode(): after ng_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.c
  • run.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_testable
per-fix-DF-3518

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