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

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_socket is 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 hpriv memory (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 / rackTimer armed.
  • 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 between lwkt_sendmsg calls 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_pptpgre module 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.

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 using priv->timers refcount.
  • sys/netgraph7/netgraph/ng_base.c:3256 (ng_callout queues NGQF_FN | NGQF_WRITER items), :3281-3284 (ng_uncallout does not free the item if callout_stop() returned 0), :2082 (ng_apply_item NG_NODE_NOT_VALID check β€” passes here because the node is not invalid).
  • DF-0596 β€” the legacy ng_pptpgre TOCTOU race on xmitWin (not exploitable in netgraph7 thanks to per-session mutex hpriv->mtx).

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0597 Β· 16 files
FileTypeDescriptionSize
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
README.md readme original PoC README (claim + sketch)
↓ download 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 race ngctl rmhook session_XXXX against the in-flight timer trampoline).
  • (added by per-PoC verifier) full race.c rewrite, 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_sendmsg calls 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 in VERDICT.md if escalation is developed.
  • Verify the fix with git apply findings/poc/DF-0597/fix.diff (the deferred-free via ng_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.c deferred-free pattern (lines 800-850) β€” the reference correct implementation.
VERDICT.md verdict full narrative: source-level proof, valid hard blocker (root-only reachability), fix validation
↓ download raw

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:

  1. kldload ng_pptpgre β€” only root can load kernel modules.
  2. kldload ng_socket, kldload ng_ksocket β€” only root.
  3. 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).

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.diff succeeded on the in-guest /usr/src (all 3 hunks).
  • Compiles cleanly with -Werror: standalone module build of /usr/src/sys/netgraph7/pptpgre succeeds; new symbol ng_pptpgre_free_session is present in the resulting /usr/obj/usr/src/sys/netgraph7/pptpgre/ng_pptpgre.ko.
  • Full nativekernel build 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 verifier
  • race_thr.c β€” multi-topology threaded variant
  • connect_test.c β€” minimal topology-build harness
  • setup.sh β€” ngctl command sequence for manual topology build
  • build.sh / run.sh β€” exact repro scripts
  • build.log β€” final successful standalone build
  • fix_build.log β€” full nativekernel build log with the fix applied
  • fix.diff β€” git-apply-able unified diff (authored by verifier)
  • manifest.json β€” artifact catalog

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Jul 13 05:00:04 UTC 2026 (ng_pptpgre_free_session symbol verified in rebuilt ng_pptpgre.ko)

Confirmed kernel references

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