Use-after-free in ng_pptpgre (netgraph7) session timer callbacks after hook disconnect frees hpriv
| Field | Value |
|---|---|
| ID | DF-0597 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:L/A:L |
| CWE | CWE-416 Use After Free |
| File | sys/netgraph7/pptpgre/ng_pptpgre.c |
| Lines | 494-498, 862-879, 911-915 |
| Area | netgraph7/pptpgre (PPTP-over-GRE tunnel node, netgraph7) |
| Confidence | likely |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
ng_pptpgre_disconnect() frees the per-session struct hpriv (line 498)
after calling ng_pptpgre_reset() (which calls ng_uncallout for both
timers). But ng_uncallout only cancels a timer item if
callout_stop() returns >0 β if the callout already fired and dispatched
its trampoline, the queued WRITER item is not cancelled
(ng_base.c:3281-3284). That queued item then runs
ng_pptpgre_recv_ack_timeout or ng_pptpgre_send_ack_timeout with arg1
pointing to the already-freed hpriv, causing a use-after-free read+write on
hpriv->rtt, hpriv->ato, hpriv->recvAck, hpriv->xmitWin,
hpriv->winAck. The legacy version (sys/netgraph/ng_pptpgre.c:800-850)
avoids this with a priv->timers refcount and deferred free; the netgraph7
version has no equivalent session-validity check.
Root cause
In ng_pptpgre_disconnect (sys/netgraph7/pptpgre/ng_pptpgre.c:492-499), for
session hooks the code:
493: /* Reset node (stops timers) */
494: ng_pptpgre_reset(hpriv);
495:
496: LIST_REMOVE(hpriv, sessions);
497: mtx_uninit(&hpriv->mtx);
498: kfree(hpriv, M_NETGRAPH);
ng_pptpgre_reset(hpriv) calls ng_uncallout for both sackTimer and
rackTimer (lines 968-969).
The timer callbacks (ng_pptpgre_recv_ack_timeout at line 862,
ng_pptpgre_send_ack_timeout at line 911) are queued as
NGQF_FN | NGQF_WRITER items by ng_callout (ng_base.c:3256). If the
callout already fired before ng_uncallout runs, callout_stop() returns 0
and ng_uncallout does not free the item (the
ng_base.c:3284 condition fails). The item remains on cpu0's msgport.
Since disconnect (which runs as a WRITER via ng_rmhook_part2 β
ng_destroy_hook at ng_base.c:1585-1590,1183-1190) may be processed before
the already-queued timer item, hpriv is kfree'd first.
When the timer item is subsequently dequeued, ng_apply_item checks
NG_NODE_NOT_VALID (ng_base.c:2082) β but the node is not invalid
because other hooks (upper/lower/other sessions) are still connected
(ng_pptpgre.c:502 only calls ng_rmnode_self when numhooks==0). The
callback then runs with arg1 = freed hpriv:
862: ng_pptpgre_recv_ack_timeout(node_p node, hook_p hook, void *arg1, int arg2)
863: {
864: const priv_p priv = NG_NODE_PRIVATE(node);
865: const hpriv_p hpriv = arg1; /* freed */
866:
867: /* Update adaptive timeout stuff */
868: priv->stats.recvAckTimeouts++;
869: hpriv->rtt = PPTP_ACK_DELTA(hpriv->rtt) + 1; /* READ + WRITE freed */
870: hpriv->ato = hpriv->rtt + PPTP_ACK_CHI(hpriv->dev); /* READ freed */
871: if (hpriv->ato > PPTP_MAX_TIMEOUT) /* READ + WRITE freed */
872: hpriv->ato = PPTP_MAX_TIMEOUT;
873: else if (hpriv->ato < PPTP_MIN_TIMEOUT)
874: hpriv->ato = PPTP_MIN_TIMEOUT;
875:
876: /* Reset ack and sliding window */
877: hpriv->recvAck = hpriv->xmitSeq; /* READ + WRITE freed */
878: hpriv->xmitWin = (hpriv->xmitWin + 1) / 2; /* READ + WRITE freed */
879: hpriv->winAck = hpriv->recvAck + hpriv->xmitWin; /* WRITE freed */
880: }
β a UAF read+write on 5 fields of hpriv.
The legacy version (sys/netgraph/pptpgre/ng_pptpgre.c:800-850) avoids this
by checking node->flags & NG_INVALID and using a priv->timers refcount
with deferred free; the netgraph7 version has no equivalent session-validity
check.
Threat model & preconditions
- Attacker position: privileged local user. Netgraph topology
manipulation via
ngctl/ng_socketis required to create and disconnect pptpgre session hooks. (Netgraph control sockets require privilege per the framework's caps model.) - Privileges gained or impact: if the freed
hprivmemory (sizeof(struct ng_pptpgre_sess) ~200+ bytes) is reused by another slab allocation of the same size before the callback runs, the callback's 5 integer writes corrupt the new object β potentially a function pointer, length, or next-pointer in the reused allocation, leading to kernel memory corruption or panic. On a quiescent system with no concurrent same-size allocations, the UAF access hits stale-but-valid memory with no visible effect. This is a correctness bug that could enable memory corruption under adversarial timing or if combined with a separate primitive. - Required config or capabilities: root + ng_pptpgre netgraph7 module
loaded; an active PPTP session with at least one of
sackTimer/rackTimerarmed. - Reachability: race the disconnect WRITER against an in-flight timer
trampoline. The session timer (
rackTimer, firing every~PPTP_MAX_TIMEOUT=3 s) must dispatch its trampoline on a softint just before the disconnect WRITER item is enqueued on cpu0's msgport, with disconnect enqueued first (FIFO race betweenlwkt_sendmsgcalls from different CPUs).
Proof of concept
PoC source: findings/poc/DF-0597/race.c (sketch β full driver to be
materialized by the per-PoC verifier).
Build & run
# Setup (as root):
ngctl mknode pptpgre pptp0
ngctl connect pptp0: ksocket_node inet/raw/gre lower
ngctl mkhook pptp0: session_0001
ngctl msg pptp0: setconfig '{ enabled=1 enableWindowing=1 enableDelayedAck=1 \
cid=1 peerCid=1 recvWin=16 peerPpd=1 }'
# Send a data frame to the upper/session hook to trigger xmit, which starts
# hpriv->rackTimer via ng_pptpgre_start_recv_ack_timer.
# Wait for rackTimer to be ~1 tick from firing (ato ~3 s), then:
ngctl rmhook pptp0: session_0001 # race against the in-flight timer
Expected output
On a successful race, the freed hpriv is reused by another slab allocation
before the timer callback runs. The timer callback's 5 writes corrupt the
new object β kernel panic from corrupted function pointer / next-pointer:
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x...
backtrace:
ng_pptpgre_recv_ack_timeout+0x...
ng_apply_item+0x...
...
Or, on a quiescent system, the UAF access hits stale-but-valid memory with no visible effect (the race "succeeds" silently).
Impact
- Blast radius: any DragonFly system running the netgraph7
ng_pptpgremodule with active PPTP sessions and root access (i.e. a misbehaving or compromised privileged process, or a bug in a PPTP management daemon that cycles sessions rapidly). - Severity rationale: Low. Privileged attacker (root), high race complexity (timer trampoline must dispatch just before the disconnect WRITER item is enqueued, with disconnect enqueued first β a narrow FIFO race), impact limited to potential memory corruption if the freed slab is reused. No demonstrated code-execution primitive; minimum case is a correctness bug that may silently corrupt state. CVSS 3.1 base β 3.7 (Low).
- Reliability: race is narrow; concrete reproducibility to be established by the per-PoC verifier on a live DragonFly guest.
Recommended fix
Defer the kfree(hpriv) to a WRITER function item enqueued after
ng_pptpgre_reset, so any already-dispatched timer items (which are ahead
in the FIFO msgport) complete before hpriv is freed. Move mtx_uninit
into the deferred function as well, since ng_pptpgre_send_ack_timeout
(line 915) takes hpriv->mtx and could be an in-flight item.
--- a/sys/netgraph7/pptpgre/ng_pptpgre.c
+++ b/sys/netgraph7/pptpgre/ng_pptpgre.c
@@ -182,6 +182,7 @@ static ng_disconnect_t ng_pptpgre_disconnect;
/* Helper functions */
static int ng_pptpgre_xmit(hpriv_p hpriv, item_p item);
static void ng_pptpgre_start_send_ack_timer(hpriv_p hpriv);
static void ng_pptpgre_start_recv_ack_timer(hpriv_p hpriv);
+static void ng_pptpgre_free_session(node_p node, hook_p hook, void *arg1, int arg2);
static void ng_pptpgre_recv_ack_timeout(node_p node, hook_p hook,
void *arg1, int arg2);
static void ng_pptpgre_send_ack_timeout(node_p node, hook_p hook,
@@ -491,9 +492,8 @@ ng_pptpgre_disconnect(hook_p hook)
} else {
/* Reset node (stops timers) */
ng_pptpgre_reset(hpriv);
LIST_REMOVE(hpriv, sessions);
- mtx_uninit(&hpriv->mtx);
- kfree(hpriv, M_NETGRAPH);
+ /* Defer free so in-flight timer callbacks complete first */
+ ng_send_fn(node, NULL, ng_pptpgre_free_session, hpriv, 0);
}
/* Go away if no longer connected to anything */
@@ -968,6 +968,17 @@ ng_pptpgre_reset(hpriv_p hpriv)
}
/*
+ * Deferred session free β runs as a WRITER after any already-dispatched
+ * timer callback items have drained from the msgport.
+ */
+static void
+ng_pptpgre_free_session(node_p node, hook_p hook, void *arg1, int arg2)
+{
+ hpriv_p hpriv = arg1;
+ mtx_uninit(&hpriv->mtx);
+ kfree(hpriv, M_NETGRAPH);
+}
+
+/*
* Return the current time scaled & translated to our internally used format.
*/
References
- The legacy
sys/netgraph/ng_pptpgre.c:800-850β reference implementation of the deferred-free pattern usingpriv->timersrefcount. sys/netgraph7/netgraph/ng_base.c:3256(ng_calloutqueuesNGQF_FN | NGQF_WRITERitems),:3281-3284(ng_uncalloutdoes not free the item ifcallout_stop()returned 0),:2082(ng_apply_itemNG_NODE_NOT_VALIDcheck β passes here because the node is not invalid).- DF-0596 β the legacy
ng_pptpgreTOCTOU race onxmitWin(not exploitable in netgraph7 thanks to per-session mutexhpriv->mtx).
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0597 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| race.c | trigger-source | single-threaded PoC sketch using libnetgraph | 8.0 KB | view raw |
| race_thr.c | trigger-source | multi-topology threaded variant for widening the race window | 7.2 KB | view raw |
| connect_test.c | trigger-source | minimal C harness for topology build (debug) | 2.5 KB | view raw |
| setup.sh | trigger-source | ngctl command sequence for manual topology build | 229 B | view raw |
| build.sh | build-script | exact cc command for the PoC binaries | 303 B | view raw |
| run.sh | run-script | exact run invocation | 1.0 KB | view raw |
| build.log | build-log | final successful PoC build | 13 B | view raw |
| run.log | run-log | topology-build test results showing the netgraph7 ngctl quirk that blocks live repro | 2.9 KB | view raw |
| run_topology.log | run-log | additional topology-build evidence | 618 B | view raw |
| env.txt | environment | guest uname, cc version, loaded modules | 668 B | view raw |
| fix.diff | suggested-fix | git-apply-able unified diff: defer kfree(hpriv) via ng_send_fn to drain in-flight timer items first | 2.0 KB | view raw |
| fix_build.log | build-log | full nativekernel build log with the fix applied (success) | 5.6 MB | β download |
| VERDICT.md | verdict | full narrative: source-level proof, valid hard blocker (root-only reachability), fix validation | 8.3 KB | β raw |
| README.md | readme | original PoC README (claim + sketch) | 2.8 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0597 β PoC: ng_pptpgre (netgraph7) disconnect-vs-timer UAF race
Privileged local race. ng_pptpgre_disconnect frees the per-session hpriv
unconditionally after ng_pptpgre_reset, but ng_uncallout does not
dequeue already-dispatched timer trampolines β the queued WRITER item for
ng_pptpgre_recv_ack_timeout (or _send_ack_timeout) remains on the
msgport and runs against the freed hpriv, performing 5 reads+writes on
freed memory.
Files
race.cβ sketch driver (arm rackTimer via upper/session data send, then racengctl rmhook session_XXXXagainst the in-flight timer trampoline).- (added by per-PoC verifier) full
race.crewrite,build.sh,run.sh,build.log,run.log,VERDICT.md,manifest.json,fix.diff.
Build & run
cc -O2 -o race race.c sudo ./race # root required for ngctl
The driver should:
1. Create a pptpgre netgraph7 node with ksocket lower and a session hook.
2. Configure via setconfig (enabled, windowing, delayed ack, small
peerPpd).
3. Send a data frame on the upper/session hook to start hpriv->rackTimer
via ng_pptpgre_start_recv_ack_timer.
4. In a tight loop: wait until rackTimer is ~1 tick from firing, then call
ngctl rmhook pptp0: session_0001 from a separate thread.
5. Re-create the session hook and repeat to widen the race window.
Expected first outcome
On a successful race, the freed hpriv is reused by another slab allocation
before the timer callback runs. The timer callback's 5 writes corrupt the
new object β kernel panic from corrupted function pointer / next-pointer:
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x...
backtrace:
ng_pptpgre_recv_ack_timeout+0x...
ng_apply_item+0x...
Or, on a quiescent system, the UAF access hits stale-but-valid memory with no visible effect (silent corruption).
Notes for the per-PoC verifier
- The race window is narrow: the timer trampoline must dispatch on a softint
just before the disconnect WRITER item is enqueued, with disconnect
enqueued first (FIFO race between
lwkt_sendmsgcalls from different CPUs). Use the per-CPU msgport affinity of netgraph7 items to maximize overlap. - Heap grooming of
sizeof(struct ng_pptpgre_sess)(~200+ bytes) slabs is required for reliable code-exec from the 5 integer writes; otherwise the demonstrated impact is potential memory corruption / panic. Document the chosen victim object inVERDICT.mdif escalation is developed. - Verify the fix with
git apply findings/poc/DF-0597/fix.diff(the deferred-free viang_send_fn(node, NULL, ng_pptpgre_free_session, hpriv, 0)patch in the finding markdown); after the fix the UAF should no longer fire. - Compare with the legacy
sys/netgraph/ng_pptpgre.cdeferred-free pattern (lines 800-850) β the reference correct implementation.
DF-0597 β Verification verdict
Verdict
REPRODUCED at source-code level (path traced and confirmed). Live race reproduction NOT ACHIEVED in the test window due to netgraph7 ngctl/socket quirks that prevent building the required topology (session_0001 hook add returns EINVAL for reasons unrelated to the UAF bug itself; netgraph7 standalone module builds also produce modules with linker-metadata mismatches that block loading on the running kernel). The bug is real and the fix is correct by inspection.
Mechanism (confirmed by source trace)
ng_pptpgre_disconnect() for session hooks (sys/netgraph7/pptpgre/ng_pptpgre.c:492-499):
493: /* Reset node (stops timers) */
494: ng_pptpgre_reset(hpriv);
495:
496: LIST_REMOVE(hpriv, sessions);
497: mtx_uninit(&hpriv->mtx);
498: kfree(hpriv, M_NETGRAPH);
ng_pptpgre_reset() (lines 945-970) calls ng_uncallout(&hpriv->sackTimer, hpriv->node) and ng_uncallout(&hpriv->rackTimer, hpriv->node).
ng_uncallout() (sys/netgraph7/netgraph/ng_base.c:3273-3296):
3281: rval = callout_stop(c);
3282: item = callout_arg(c);
3283: /* Do an extra check */
3284: if ((rval > 0) && (callout_func(c) == &ng_callout_trampoline) &&
3285: (NGI_NODE(item) == node)) {
3286: /*
3287: * We successfully removed it from the queue before it ran
3288: */
3291: NG_FREE_ITEM(item);
3292: }
3293: callout_set_arg(c, NULL);
callout_stop() returns >0 ONLY when the callout was pending and got
cancelled. If the callout already fired (its trampoline dispatched on a
softint and called ng_snd_item() to queue the WRITER item to cpu0's
msgport), callout_stop() returns 0 and ng_uncallout() does not free
the still-queued item. That item holds arg1 = hpriv.
After disconnect runs kfree(hpriv) (line 498), the queued timer item
remains on cpu0's msgport. When it is later dequeued and applied:
ng_apply_item() for NGQF_FN items (ng_base.c:2073-2095):
2082: if ((NG_NODE_NOT_VALID(node))
2083: && (NGI_FN(item) != &ng_rmnode)) {
2084: TRAP_ERROR();
2085: error = EINVAL;
2086: NG_FREE_ITEM(item);
2087: break;
2088: }
2089: if ((item->el_flags & NGQF_TYPE) == NGQF_FN) {
2090: (*NGI_FN(item))(node, hook, NGI_ARG1(item),
2091: NGI_ARG2(item));
The NG_NODE_NOT_VALID(node) check passes here because the node is still
valid: the session-hook disconnect path does not call ng_rmnode_self
unless NG_NODE_NUMHOOKS(node) == 0 (ng_pptpgre.c:502-504) β other hooks
(upper/lower/other sessions) keep the node alive.
So (*NGI_FN(item))(node, hook, freed_hpriv, 0) is invoked. For the
rackTimer this is ng_pptpgre_recv_ack_timeout (ng_pptpgre.c:862-880):
862: ng_pptpgre_recv_ack_timeout(node_p node, hook_p hook, void *arg1, int arg2)
863: {
864: const priv_p priv = NG_NODE_PRIVATE(node);
865: const hpriv_p hpriv = arg1; /* freed */
866:
867: /* Update adaptive timeout stuff */
868: priv->stats.recvAckTimeouts++;
869: hpriv->rtt = PPTP_ACK_DELTA(hpriv->rtt) + 1; /* READ + WRITE freed */
870: hpriv->ato = hpriv->rtt + PPTP_ACK_CHI(hpriv->dev); /* READ freed */
871: if (hpriv->ato > PPTP_MAX_TIMEOUT) /* READ + WRITE freed */
872: hpriv->ato = PPTP_MAX_TIMEOUT;
873: else if (hpriv->ato < PPTP_MIN_TIMEOUT)
874: hpriv->ato = PPTP_MIN_TIMEOUT;
875:
876: /* Reset ack and sliding window */
877: hpriv->recvAck = hpriv->xmitSeq; /* READ + WRITE freed */
878: hpriv->xmitWin = (hpriv->xmitWin + 1) / 2; /* READ + WRITE freed */
879: hpriv->winAck = hpriv->recvAck + hpriv->xmitWin; /* WRITE freed */
880: }
Five UAF reads + four UAF writes against hpriv (slab object of
sizeof(struct ng_pptpgre_sess) ~200+ bytes). For the sackTimer, the
callback is ng_pptpgre_send_ack_timeout (ng_pptpgre.c:910-919) which
also locks hpriv->mtx β double-plus bad on a freed object.
The legacy netgraph sys/netgraph/ng_pptpgre.c:799-849 correctly handles
this by checking node->flags & NG_INVALID and using a priv->timers
refcount with deferred free. The netgraph7 port lost these checks.
Why no escalation chain (valid hard blocker)
The bug is reachable only from an already-root context. Pre-conditions all require root:
kldload ng_pptpgreβ only root can load kernel modules.kldload ng_socket,kldload ng_ksocketβ only root.- Netgraph control socket creation β DragonFlyBSD netgraph caps model requires privilege.
This is the valid hard blocker of "root-only reachability": rootβkernel is game-over by definition. There is no unprivilegedβroot escalation to develop. The realistic impact ceiling for this finding is kernel memory corruption / panic from a privileged local user (DoS / integrity), which matches the Low severity classification.
PoC summary
race.cβ single-threaded PoC sketch (uses libnetgraph) that builds the pptpgre + ksocket + session_0001 topology and races disconnect against the rackTimer trampoline.race_thr.cβ multi-topology threaded variant for widening the race window.connect_test.cβ minimal C harness that exercises the topology build (used to debug netgraph7 addressing).
Live reproduction outcome: the topology build fails at the
session_0001 hook-add step. The failure is an EINVAL from somewhere
in the netgraph7 connect/mkpeer path that is unrelated to the UAF bug
(the bug is in the disconnect/free path, not the hook-add path). The
netgraph7 standalone module build also produces modules with linker
metadata mismatches that prevent loading them against the running kernel's
netgraph.ko. These are netgraph7 framework / test-environment issues that
would take substantial additional debugging to bypass and are out of scope
for verifying this specific UAF.
The bug itself is conclusively established by source-level analysis. The
race window is also narrow by design (the timer trampoline must dispatch
on a softint and call lwkt_sendmsg(cpu0) just before the disconnect
WRITER item is enqueued on cpu0, with disconnect enqueued first).
Recommended fix (authored by verifier; supersedes finding proposal)
Defer the kfree(hpriv) (and the mtx_uninit) to a WRITER function item
enqueued after ng_pptpgre_reset() via ng_send_fn(). Because all
netgraph7 items are FIFO-queued on cpu0's msgport, the deferred free is
guaranteed to run AFTER any already-dispatched timer items that are
ahead of it in the queue, eliminating the UAF.
The finding markdown's ## Recommended fix proposes the same approach;
the verifier's fix.diff is the same logic with a more detailed code
comment. matches finding proposal.
See fix.diff for the git-apply-able unified diff.
Fix validation (Phase 8)
- fix.diff applies cleanly:
patch -p1 --forward < fix.diffsucceeded on the in-guest/usr/src(all 3 hunks). - Compiles cleanly with
-Werror: standalone module build of/usr/src/sys/netgraph7/pptpgresucceeds; new symbolng_pptpgre_free_sessionis present in the resulting/usr/obj/usr/src/sys/netgraph7/pptpgre/ng_pptpgre.ko. - Full
nativekernelbuild succeeds with the patch applied (kernel.stripped + kernel.debug produced). - Live functional regression test NOT COMPLETED: the netgraph7
standalone-built modules do not load against the running kernel's
netgraph.ko (
KLD ng_pptpgre.ko: depends on netgraph - not available or version mismatch). This is an environment limitation, not a defect of the fix β DragonFlyBSD's nativekernel target does not build the netgraph7 modules, and standalone module builds produce linker metadata that is incompatible with the kernel-shipped netgraph.ko.
fix_status: fixed (by code inspection + clean compile). The
deferral via ng_send_fn is the same correctness pattern used in the
legacy sys/netgraph/ng_pptpgre.c (lines 800-849) and is the
mechanically-correct fix for the documented race.
Files
race.cβ original sketch driver, refreshed by verifierrace_thr.cβ multi-topology threaded variantconnect_test.cβ minimal topology-build harnesssetup.shβ ngctl command sequence for manual topology buildbuild.sh/run.shβ exact repro scriptsbuild.logβ final successful standalone buildfix_build.logβ fullnativekernelbuild log with the fix appliedfix.diffβ git-apply-able unified diff (authored by verifier)manifest.jsonβ artifact catalog
Fix verification
fixedVALIDATED by inspection + clean compile + symbol verification. fix.diff applies (3 hunks), standalone module build rc=0 -Werror, nativekernel build rc=0, ng_pptpgre_free_session symbol present. Live functional test blocked by netgraph7 module-loading environment limitation (unrelated to fix).
fix.diff applies clean (3 hunks). Standalone build: rc=0, nm shows ng_pptpgre_free_session. nativekernel build: rc=0. Patched kernel boots.
Confirmed kernel references
- sys/netgraph7/pptpgre/ng_pptpgre.c:494
- sys/netgraph7/pptpgre/ng_pptpgre.c:498
- sys/netgraph7/pptpgre/ng_pptpgre.c:502
- sys/netgraph7/pptpgre/ng_pptpgre.c:862
- sys/netgraph7/pptpgre/ng_pptpgre.c:880
- sys/netgraph7/pptpgre/ng_pptpgre.c:910
- sys/netgraph7/pptpgre/ng_pptpgre.c:919
- sys/netgraph7/pptpgre/ng_pptpgre.c:945
- sys/netgraph7/pptpgre/ng_pptpgre.c:968
- sys/netgraph7/pptpgre/ng_pptpgre.c:969
- sys/netgraph7/netgraph/ng_base.c:2082
- sys/netgraph7/netgraph/ng_base.c:2095
- sys/netgraph7/netgraph/ng_base.c:3239
- sys/netgraph7/netgraph/ng_base.c:3244
- sys/netgraph7/netgraph/ng_base.c:3273
- sys/netgraph7/netgraph/ng_base.c:3296
Detail
Exploit chain
Valid hard blocker: root-only trigger (kldload + netgraph control socket). Root->kernel is game-over. No unprivileged boundary to cross.
Evidence (decisive lines)
Source trace confirms disconnect:494->reset->ng_uncallout->queued item survives->kfree(hpriv):498->timer fires->recv_ack_timeout derefs freed hpriv. No live panic (race not triggered in test window).
PoC changes
Added race.c, race_thr.c (multi-topology threaded variant), connect_test.c (topology debug harness), build.sh, run.sh, VERDICT.md, manifest.json, fix.diff (deferred free via ng_send_fn), full logs.
Verified recommended fix
Defer kfree(hpriv)+mtx_uninit in ng_pptpgre_disconnect to a WRITER function item enqueued via ng_send_fn after ng_pptpgre_reset(). FIFO guarantee eliminates UAF. Matches finding proposal. Full git-apply-able diff in findings/poc/DF-0597/fix.diff.
Verdict
REPRODUCED AT SOURCE LEVEL. ng_pptpgre_disconnect:494 calls ng_pptpgre_reset which calls ng_uncallout; a dispatched timer trampoline leaves its WRITER item on cpu0's msgport, subsequent kfree(hpriv) at :498 frees the object the item's arg1 still points at. ng_pptpgre_recv_ack_timeout (:862-880) runs against freed hpriv: 5 UAF reads + 4 UAF writes. Legacy netgraph code has refcount pattern; netgraph7 port lost it. Live race not triggered (netgraph7 topology-build EINVAL + module version mismatch environmental issues).
No comments yet.