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

Unsynchronized SMP race on xmitWin causes heap OOB write on timeSent[] in ng_pptpgre

Field Value
ID DF-0596
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:H
CWE CWE-367 Time-of-check Time-of-use (TOCTOU) Race Condition; CWE-787 Out-of-bounds Write
File sys/netgraph/pptpgre/ng_pptpgre.c
Lines 121, 146, 152, 490-491, 514, 672-676
Area netgraph/pptpgre (PPTP-over-GRE tunnel node)
Confidence speculative
Discovered 2026-07-02
Reported pending

Summary

The legacy ng_pptpgre node has no per-node serialization (no spinlock, token, or mutex) protecting its mutable state (xmitWin, timeSent[], sequence numbers). On an SMP system, two GRE packets can be processed concurrently on different CPUs, each invoking ng_pptpgre_recv on the same node. The xmitWin growth check at line 672 (a->xmitWin < PPTP_XMIT_WIN) followed by the non-atomic increment at line 674 is a classic TOCTOU: two concurrent ack handlers can both read xmitWin == 15, both pass the < 16 check, and both increment, yielding xmitWin == 17. The subsequent xmit path then permits timeSent index 16, writing 8 bytes past the end of the pptptime_t timeSent[PPTP_XMIT_WIN] array and corrupting the recvSeq/xmitSeq fields in the adjacent priv struct memory.

Root cause

The entire node operates without any concurrency protection.

  • ng_send_data() (sys/netgraph/netgraph/ng_base.c:1678) calls the peer hook's rcvdata with no lock β€” it just checks HK_INVALID and dispatches directly.
  • The ksocket upcall (sys/netgraph/ng_ksocket/ng_ksocket.c:982, ng_ksocket_incoming) uses crit_enter() which is per-CPU only on DragonFlyBSD and does not prevent concurrent execution on other CPUs.
  • sowakeup() (sys/kern/uipc_socket2.c:604) calls so->so_upcall without holding any serialization.

Therefore, two GRE packets arriving near-simultaneously on different CPUs both reach ng_pptpgre_recv concurrently.

The vulnerable sequence:

  1. Window growth (sys/netgraph/pptpgre/ng_pptpgre.c:672-676): c if (PPTP_SEQ_DIFF(ack, a->winAck) >= 0 && a->xmitWin < PPTP_XMIT_WIN) { // CHECK a->xmitWin++; // INCREMENT (non-atomic) a->winAck = ack + a->xmitWin; } This read-check-increment on a->xmitWin (u_int16_t at :146) is non-atomic. Two concurrent invocations both see xmitWin == 15, both pass 15 < 16, both increment to produce xmitWin == 17.

  2. Window check (sys/netgraph/pptpgre/ng_pptpgre.c:490-491): c if ((u_int32_t)PPTP_SEQ_DIFF(priv->xmitSeq, priv->recvAck) >= a->xmitWin) With xmitWin == 17, this allows xmitSeq - recvAck up to 16.

  3. OOB write (sys/netgraph/pptpgre/ng_pptpgre.c:514): c a->timeSent[priv->xmitSeq - priv->recvAck] = ng_pptpgre_time(node); Index 16 writes to timeSent[16], one past the end of pptptime_t timeSent[PPTP_XMIT_WIN] (PPTP_XMIT_WIN = 16, valid indices 0..15). This is an 8-byte (sizeof u_int64_t) heap overflow within the priv allocation, overwriting recvSeq/xmitSeq β€” the fields immediately following ackp in struct ng_pptpgre_private (lines 165-168).

The timeSent array is the last field of struct ng_pptpgre_ackp (:152), and ackp is followed by recvSeq/xmitSeq/recvAck/xmitAck in struct ng_pptpgre_private (:165-168). The overflow corrupts these sequence-number fields with a 64-bit timestamp value.

Threat model & preconditions

  • Attacker position: remote peer who can send GRE packets to the PPTP VPN concentrator's WAN interface. The GRE CID check at :632 requires the correct 16-bit call ID β€” this can be obtained by establishing a legitimate PPTP session (TCP 1723 control channel) or brute-forced (only 65536 possibilities).
  • Privileges gained or impact: 8-byte kernel heap overflow corrupting recvSeq/xmitSeq, causing all subsequent sequence checks to fail (PPTP_SEQ_DIFF with a large timestamp value is always negative for normal seq numbers) β†’ reliable PPTP session DoS. The overflow stays within the single priv kmalloc allocation (~200 bytes remaining after the write point), so it does not directly corrupt adjacent slab objects, limiting code-execution potential. The corrupted xmitSeq could in theory interact with other logic to cause further corruption if the attacker carefully times subsequent packets.
  • Required config or capabilities: SMP DragonFly system (default on multi-core) with ng_pptpgre and ng_ksocket loaded, an active PPTP session with xmitWin grown to PPTP_XMIT_WIN - 1 = 15 (occurs naturally after ~15 successful ack round-trips during PPP negotiation), and two GRE ack packets arriving within the same few-CPU-instruction window on different CPUs.
  • Reachability: send a burst of two raw GRE ack packets with the correct CID and an ack value at or beyond the current winAck threshold, simultaneously from two threads or via two raw sockets so they land on different RX queues / CPUs.

Proof of concept

PoC source: findings/poc/DF-0596/race.c (sketch β€” Linux attacker side, sending raw GRE via two threads; full driver to be materialized by the per-PoC verifier).

Build & run

# attacker (Linux + raw sockets, on the PPTP WAN-facing network):
cc -O2 -lpthread -o race race.c
./race <server_wan_ip> <cid> <ack_value>

# target (DragonFlyBSD PPTP concentrator, SMP guest with >= 2 vCPUs):
#   load ng_socket, ng_ksocket, ng_pptpgre, ng_iface, ng_ether (or ng_eiface)
#   configure a PPTP node graph per the standard mpd/netgraph PPTP recipe

Expected output

After the race succeeds (xmitWin now 17), subsequent server xmit trips the OOB write at :514. The server's recvSeq is overwritten with a large timestamp. All subsequent received GRE data packets fail the sequence check at :691 and are silently dropped (recvOutOfOrder++). The PPTP session is dead. On a DEBUG kernel, the corrupted sequence numbers may trigger KASSERT failures.

# server-side dmesg (DEBUG kernel):
panic: ... KASSERT in ng_pptpgre_xmit / ng_pptpgre_recv
backtrace:
    ng_pptpgre_xmit+0x...
    ng_pptpgre_recv+0x...
    ng_send_data+0x...

Impact

  • Blast radius: any SMP DragonFly system acting as a PPTP VPN concentrator/server using the legacy netgraph ng_pptpgre node (mpd, custom netgraph scripts). PPTP is deprecated in favor of IPsec/L2TP but remains in active use on legacy networks and embedded VPN boxes.
  • Severity rationale: Medium. Remote attacker, high race complexity (the two ack handlers must execute the check-increment window :672-676 within the same ~10-instruction window on different CPUs β€” expect O(1000-10000) attempts on a 2-vCPU guest under moderate load), impact limited to PPTP session DoS via the corrupted sequence numbers. No demonstrated code-execution primitive (overflow stays within the priv allocation, corrupting only sequence-number fields). CVSS 3.1 base β‰ˆ 6.6 (Medium).
  • Reliability: speculative β€” race is probabilistic; concrete reproducibility to be established by the per-PoC verifier on a live DragonFly guest.

Add a per-node spinlock to serialize access to the mutable ackp/sequence state. The lock must be acquired in ng_pptpgre_recv, ng_pptpgre_xmit, ng_pptpgre_send_ack_timeout, and ng_pptpgre_recv_ack_timeout. Since ng_pptpgre_xmit is called from ng_pptpgre_recv (:709) and from ng_pptpgre_send_ack_timeout (:933), use a recursive lock or restructure to avoid self-deadlock. Alternatively, use a spinlock with careful lock-ordering (release before calling NG_SEND_DATA which may re-enter the node).

Minimal targeted fix using a spinlock:

--- a/sys/netgraph/pptpgre/ng_pptpgre.c
+++ b/sys/netgraph/pptpgre/ng_pptpgre.c
@@ -159,6 +159,7 @@ typedef u_int64_t       pptptime_t;
 struct ng_pptpgre_private {
    hook_p          upper;      /* hook to upper layers */
    hook_p          lower;      /* hook to lower layers */
+   struct spinlock     lock;       /* protects ackp/seq/window state */
    struct ng_pptpgre_conf  conf;       /* configuration info */
    struct ng_pptpgre_ackp  ackp;       /* packet transmit ack state */
    u_int32_t       recvSeq;    /* last seq # we rcv'd */
@@ -285,6 +286,7 @@ ng_pptpgre_constructor(node_p *nodep)
    if (priv == NULL)
        return (ENOMEM);

+   spin_init(&priv->lock, "ng_pptpgre");
    /* Call generic node constructor */
    if ((error = ng_make_node_common(&ng_pptpgre_typestruct, nodep))) {
        kfree(priv, M_NETGRAPH);

Then in ng_pptpgre_recv, acquire priv->lock after the enabled check and before touching any ackp/seq state; release before NG_SEND_DATA calls to avoid reentrancy deadlock. In ng_pptpgre_xmit, acquire priv->lock around the window check and timeSent write (lines 490-518). In the timer callbacks, acquire priv->lock around the state modifications (:828-847 and :932-933). Since the timer callbacks also call ng_pptpgre_xmit which would try to acquire the same lock, either make the spinlock recursive or extract the xmit logic into a lock-free helper (ng_pptpgre_xmit_locked) called with the lock already held.

A simpler but less complete mitigation is to make the xmitWin read-check-increment atomic:

-       if (PPTP_SEQ_DIFF(ack, a->winAck) >= 0
-           && a->xmitWin < PPTP_XMIT_WIN) {
-           a->xmitWin++;
-           a->winAck = ack + a->xmitWin;
+       if (PPTP_SEQ_DIFF(ack, a->winAck) >= 0) {
+           int old, new;
+           do {
+               old = a->xmitWin;
+               new = (old < PPTP_XMIT_WIN) ? old + 1 : old;
+           } while (atomic_cmpset_int(&a->xmitWin, old, new) == 0);
+           if (new != old)
+               a->winAck = ack + new;
        }

Note: this atomic fix only addresses the xmitWin bound; the other shared state (recvAck, xmitSeq, timeSent shift, rtt/dev/ato) still has data races that could cause logic corruption. The full spinlock fix is recommended.

References

  • sys/netgraph/netgraph/ng_base.c:1678 (ng_send_data) β€” dispatches rcvdata inline on the caller's CPU with no serialization.
  • RFC 2637 Β§3.2.7 (PPTP GRE windowing) and Β§4.4 (RTT/RTO estimator) β€” context for the window-grow sequence the race exploits.
  • Same class as DF-0590 (legacy ng_bridge no-serialization races): the entire legacy netgraph tree lacks per-node locking for SMP.

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-0596 Β· 16 files
FileTypeDescriptionSize
race_harness.c trigger-source Deterministic pthread harness replicating the xmitWin TOCTOU race and OOB logic 4.3 KB view raw
race_harness_fixed.c exploit-chain Same harness with fix applied (clamp + bounds check) β€” shows zero OOB 4.0 KB view raw
race.c trigger-source Original PoC sketch (Linux raw-socket GRE sender, not runnable on guest) 3.9 KB view raw
fix.diff suggested-fix Bounds check on timeSent index + defensive clamp on xmitWin 804 B view raw
build.sh build-script Compiles all three harness variants 312 B view raw
run.sh run-script Runs baseline -O2, baseline -O0, and fixed -O0 harnesses 389 B view raw
run_all.log run-log Full output of all three harness runs 1.9 KB view raw
fix_build.log build-log Full kernel build log with fix applied (rc=0) 5.6 MB ↓ download
fix_run.log run-log Fixed harness output showing 0 OOB 520 B view raw
fixed_xmit_disasm.txt disassembly Disassembly of fixed ng_pptpgre_xmit showing bounds check (cmp $0xf; jbe) 566 B view raw
env.txt environment Guest uname, compiler version, loaded modules 246 B view raw
VERDICT.md verdict Full analysis: mechanism, codegen dependency, fix validation 7.3 KB ↓ raw
ng_setup.sh setup Netgraph topology setup script (for live testing reference) 851 B view raw
README.md readme human reproduce doc 3.7 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 human reproduce doc
↓ download raw

DF-0596 β€” PoC: SMP race on ng_pptpgre xmitWin -> timeSent[] OOB write

Remote (PPTP-WAN-reachable) race PoC. The legacy ng_pptpgre node has no per-node serialization; ng_send_data dispatches rcvdata inline on the caller's CPU. Two GRE ack packets arriving on different CPUs execute ng_pptpgre_recv concurrently. The xmitWin read-check-increment at ng_pptpgre.c:672-676 is a TOCTOU; two concurrent ack handlers can both see xmitWin == 15, both pass the < 16 check, both increment β†’ xmitWin == 17. Subsequent xmit then permits timeSent index 16 (one past the end of pptptime_t timeSent[16]) β†’ 8-byte heap overflow into recvSeq/xmitSeq.

Files

  • race.c β€” sketch driver (4 threads hammering the same GRE ack to maximize TOCTOU overlap on the xmitWin growth window).
  • (added by per-PoC verifier) full race.c rewrite with proper PPTP session bring-up (TCP 1723 control channel for CID discovery, then the burst), build.sh, run.sh, build.log, run.log, VERDICT.md, manifest.json, fix.diff.

Target setup (DragonFlyBSD)

# load netgraph modules
kldload ng_socket ng_ksocket ng_pptpgre ng_iface ng_eiface

# configure a PPTP concentrator (mpd5 or a custom ngctl script). Ensure:
#   - the guest has >= 2 vCPUs
#   - ng_pptpgre node is reachable on the WAN
#   - xmitWin has grown to PPTP_XMIT_WIN-1 = 15 (~15 successful ack
#     round-trips during PPP negotiation; happens naturally)

Build & run (Linux attacker)

cc -O2 -lpthread -o race race.c
./race <server_wan_ip> <cid> <ack_value>

ack_value must be at or beyond the current winAck threshold for the server to accept the ack for window growth (i.e. reflect the next window- grow point). On a fresh session this advances naturally; for a targeted trigger, the verifier should sniff the live session's last-seen ack sequence and use ack = winAck + delta values that pass the PPTP_SEQ_DIFF(ack, a->winAck) >= 0 check at :672.

Expected first outcome

After the race succeeds (xmitWin == 17), subsequent server xmit trips the OOB write at ng_pptpgre.c:514. The server's recvSeq is overwritten with a large timestamp value. All subsequent received GRE data packets fail the sequence check at :691 and are silently dropped (recvOutOfOrder++). The PPTP session is dead.

On DEBUG kernels, the corrupted sequence numbers may trigger KASSERT failures:

panic: ... KASSERT in ng_pptpgre_xmit / ng_pptpgre_recv
backtrace: ng_pptpgre_xmit+0x... ng_pptpgre_recv+0x... ng_send_data+0x...

Notes for the per-PoC verifier

  • The race window is narrow (~10 instructions, lines :672-676); expect O(1000-10000) attempts on a 2-vCPU guest under moderate load. The 4-thread burst maximizes overlap probability.
  • The CID can be obtained by establishing a legitimate PPTP session (TCP 1723 control channel) or brute-forced (only 65536 possibilities, but each wrong guess wastes server resources and may reveal the wrong-CID error).
  • The overflow stays within the single priv kmalloc allocation (~200 bytes remaining after the write point), so it does not directly corrupt adjacent slab objects β€” the demonstrated impact is PPTP session DoS, not code execution. If heap grooming of a same-sized slab yields a controlled victim object, document it in VERDICT.md; otherwise the verdict should reflect session_DoS_confirmed / escalation_unverified and the finding stays Medium.
  • Verify the fix with git apply findings/poc/DF-0596/fix.diff (the per-node spinlock patch in the finding markdown); after the fix the race should no longer fire.
  • Same class as DF-0590 (legacy ng_bridge no-serialization races): the entire legacy netgraph tree lacks per-node SMP locking. Flagged for the maintainer as a systemic issue.
VERDICT.md verdict Full analysis: mechanism, codegen dependency, fix validation
↓ download raw

DF-0596 β€” VERDICT: Unsynchronized SMP race on xmitWin in ng_pptpgre

Verdict: REPRODUCED (code-level) β€” TOCTOU race confirmed; OOB write contingent on codegen

Finding: The legacy ng_pptpgre netgraph node has no per-node serialization (no spinlock, token, or mutex) protecting its mutable state (xmitWin, timeSent[], winAck, recvAck, rtt, dev, ato, recvSeq, xmitSeq). On an SMP system, two GRE packets can be processed concurrently on different CPUs, each invoking ng_pptpgre_recv on the same node without synchronization.

Root Cause Confirmation (source trace)

  1. No serialization β€” ng_pptpgre_recv (sys/netgraph/pptpgre/ng_pptpgre.c:566) accesses all shared state without any lock. ng_send_data (sys/netgraph/netgraph/ng_base.c:1678) dispatches rcvdata inline on the caller's CPU. The ksocket upcall uses crit_enter() which is per-CPU only. Confirmed: no serialization exists.

  2. TOCTOU on xmitWin growth (ng_pptpgre.c:672-676): c if (PPTP_SEQ_DIFF(ack, a->winAck) >= 0 && a->xmitWin < PPTP_XMIT_WIN) { // CHECK (read) a->xmitWin++; // INCREMENT (non-atomic RMW) a->winAck = ack + a->xmitWin; } xmitWin is u_int16_t at struct offset 0x2c in ng_pptpgre_ackp (ng_pptpgre.c:146). The read-check-increment is non-atomic. Two concurrent ack handlers can both see xmitWin == 15, both pass < 16, and both increment.

  3. OOB write target (ng_pptpgre.c:514): c a->timeSent[priv->xmitSeq - priv->recvAck] = ng_pptpgre_time(node); The index is xmitSeq - recvAck, bounded by xmitWin. With xmitWin > 16, the index can reach 16, one past the end of pptptime_t timeSent[PPTP_XMIT_WIN] (16 elements, valid indices 0..15). timeSent is the last field of struct ng_pptpgre_ackp (ng_pptpgre.c:152), followed immediately by recvSeq in struct ng_pptpgre_private (ng_pptpgre.c:165).

  4. Struct layout verified β€” harness confirms &timeSent[16] and &recvSeq are both at offset 200 in struct ng_pptpgre_private. An 8-byte write at timeSent[16] would overwrite recvSeq (4 bytes) and xmitSeq (4 bytes).

Harness Results

Baseline β€” TOCTOU is winnable

=== -O0 (separate load/add/store codegen β€” maximizes race window) ===
[threads=2] xmitWin>16 in 31 rounds, max_xw=17
[threads=4] xmitWin>16 in 70 rounds, max_xw=17
Total over-16 rounds: 104/30000, max_xw=17
*** CONFIRMED: xmitWin can exceed PPTP_XMIT_WIN -> timeSent[] OOB ***

Baseline β€” default kernel codegen (-O2)

=== -O2 (single load reused for check+increment β€” compiler prevents exceeding 16) ===
[threads=2/4/8] xmitWin>16 in 0 rounds, max_xw=16
Race not won (xmitWin stayed <= 16). Codegen prevents exceeding the bound.

Kernel disassembly (unpatched module, -O2)

The compiler generates a single load for both check and increment:

1179: movzwl 0x2c(%rbx),%eax   ; LOAD xmitWin once
117d: cmp    $0xf,%ax           ; CHECK (reuses register)
1183: add    $0x1,%eax          ; INCREMENT (reuses register, NOT reload)
1186: mov    %ax,0x2c(%rbx)     ; STORE

With this codegen, two concurrent threads both compute 15+1=16 from their stale register copy. xmitWin cannot reach 17 with -O2.

Impact Assessment

What IS confirmed:

  • Missing serialization is a genuine bug β€” all shared state in ng_pptpgre_ackp and ng_pptpgre_private is accessed without any lock from concurrent CPU contexts.
  • TOCTOU on xmitWin is real β€” the read-check-increment race is winnable (proven with -O0 harness: 104/30000 rounds reaching xmitWin=17).
  • Struct layout confirms OOB target β€” timeSent[16] overlaps recvSeq/xmitSeq at offset 200 in the priv struct.
  • Other unsynchronized data races are real and dangerous:
  • bcopy(a->timeSent + index + 1, a->timeSent, ...) at line 668 β€” two concurrent overlapping bcopy operations on the same buffer is undefined behavior.
  • priv->recvAck = ack at line 652 β€” last writer wins.
  • a->rtt += ... at line 657, a->dev += ... at line 660 β€” lost updates.
  • priv->recvSeq = seq at line 698 β€” last writer wins.

What is NOT confirmed on the default kernel:

  • The specific heap OOB write at timeSent[16] is not achievable with the default -O2 kernel build. The compiler reuses the register value from the check, so xmitWin stays at 16 (valid maximum). The OOB IS achievable with -O0 codegen (harness proof), demonstrating the code is inherently unsafe.

Realistic impact:

  • PPTP session DoS from corrupted state (recvSeq, rtt, ato, timeSent bcopy races) β€” severity Medium (deprecated protocol, legacy systems only).
  • Heap OOB write is latent β€” not reachable with current -O2 codegen, but any compiler change, LTO configuration, or future code modification could expose it. The bounds check fix is appropriate defense-in-depth.

PoC Changes

  • Rewrote race.c β†’ race_harness.c: The original PoC was a Linux raw-socket sketch that couldn't run on the guest (no PPTP concentrator, no external GRE path). Replaced with a deterministic pthread-based harness that replicates the exact kernel growth logic, struct layout, and race conditions. The harness proves: 1. Struct layout: timeSent[16] overlaps recvSeq (OOB target). 2. TOCTOU is winnable with -O0 (xmitWin reaches 17 in ~0.3% of rounds). 3. TOCTOU is NOT winnable with -O2 (compiler prevents exceeding 16).
  • Added race_harness_fixed.c: Same harness with the fix applied (clamp + bounds check), showing zero OOB even with -O0 codegen.

Fix (fix.diff)

Two-part defense-in-depth fix:

  1. Bounds check on timeSent index (ng_pptpgre.c:514): Wrap the array access in an explicit bounds check: c u_int32_t _ts_idx = priv->xmitSeq - priv->recvAck; if (_ts_idx < PPTP_XMIT_WIN) a->timeSent[_ts_idx] = ng_pptpgre_time(node); This prevents the OOB write regardless of what xmitWin does.

  2. Defensive clamp on xmitWin (ng_pptpgre.c:674): After the increment, clamp to PPTP_XMIT_WIN: c if (a->xmitWin > PPTP_XMIT_WIN) a->xmitWin = PPTP_XMIT_WIN; (Note: gcc -O2 optimizes this away as dead code since the preceding check already prevents exceeding 16; it's included for defense-in-depth and future-proofing.)

A more complete fix would add a per-node spinlock to serialize all ack-processing state modifications (the finding markdown's recommendation), but this is complex due to reentrancy (ng_pptpgre_xmit is called from ng_pptpgre_recv). The bounds check + clamp is the minimal targeted fix that eliminates the specific OOB write vulnerability.

Fix Validation

Test Result
fix.diff applies cleanly patch -p1 --dry-run rc=0, both hunks succeeded
Kernel builds with fix make -j6 nativekernel rc=0
Fixed module has bounds check Disassembly: cmp $0xf,%r15d; jbe 970 at ng_pptpgre_xmit+0x6d
Harness -O0 baseline (unfixed) 104/30000 rounds: xmitWin=17 (OOB achievable)
Harness -O0 with fix 0/30000 rounds: xmitWin<=16 (no OOB)
Patched kernel boots 6.5-DEVELOPMENT #1: Wed Jul 8 17:57:09 UTC 2026
Module loads on patched kernel kldload ng_pptpgre rc=0

Fix status: FIXED β€” the bounds check prevents the OOB write even under adversarial codegen conditions (-O0). The defense-in-depth clamp prevents xmitWin from exceeding PPTP_XMIT_WIN.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: The baseline (unpatched -O0 codegen) shows xmitWin reaching 17 in 104/30000 rounds (TOCTOU confirmed), while the fixed harness (same -O0 codegen with clamp+bounds check) shows ZERO OOB rounds (0/30000). The fixed kernel module's disassembly confirms the bounds check is compiled in: 'cmp $0xf,%r15d; jbe 970' at ng_pptpgre_xmit+0x6d prevents writing timeSent when index >= 16. The kernel builds cleanly with the fix (make -j6 nativekernel rc=0), boots (kernel #1), and the module loads (kldload rc=0). Fix closes the specific OOB write vulnerability.

BEFORE (baseline -O0 harness): [threads=4] xmitWin>16 in 70 rounds, max_xw=17 -> TOCTOU can push xmitWin past PPTP_XMIT_WIN
AFTER (fixed -O0 harness): [threads=4] xmitWin>16 in 0 rounds, max_xw=16 -> clamp prevents exceeding PPTP_XMIT_WIN
Module disassembly confirms bounds check: cmp $0xf,%r15d; jbe 970 (only writes timeSent if index < 16)
Kernel build: rc=0; boot: kernel #1 Wed Jul 8 17:57:09 UTC 2026; module load: rc=0
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Jul 8 17:57:09 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none (non-corruption-confirmed on default kernel). The missing serialization causes data races on shared state (xmitWin, winAck, recvAck, rtt, dev, ato, timeSent bcopy, recvSeq) leading to PPTP session corruption and DoS. The specific timeSent[16] OOB write requires non-default compiler codegen (-O0); on the default -O2 kernel xmitWin stays at 16 (valid max). No escalation chain applicable -- this is a remote DoS finding on a deprecated protocol (PPTP). The bcopy race at line 668 (two concurrent overlapping memmove on timeSent[]) is the most dangerous corruption vector in practice.

Evidence (decisive lines)

Harness -O0 baseline: [threads=4] xmitWin>16 in 70 rounds, max_xw=17 -- TOCTOU confirmed
Harness -O2 (default kernel codegen): xmitWin>16 in 0 rounds -- OOB NOT reachable
Kernel disassembly (unpatched): movzwl 0x2c(%rbx),%eax; cmp $0xf,%ax; add $0x1,%eax -- single load reused, prevents exceeding 16
Struct layout: &timeSent[16]=offset 200, &recvSeq=offset 200 -> OVERLAP (OOB target confirmed)
Fixed harness -O0: xmitWin>16 in 0 rounds, max_xw=16 -- FIX WORKS
Fixed module disassembly: cmp $0xf,%r15d; jbe 970 -- bounds check present in compiled module

PoC changes

Rewrote race.c (Linux raw-socket sketch, not runnable on guest) into race_harness.c: a deterministic pthread-based harness replicating the exact kernel growth logic, struct layout, and TOCTOU race conditions. The harness proves (1) struct layout: timeSent[16] overlaps recvSeq at offset 200, (2) TOCTOU is winnable with -O0 (xmitWin reaches 17 in ~0.3% of rounds), (3) TOCTOU is NOT winnable with -O2 (compiler prevents exceeding 16). Added race_harness_fixed.c: same harness with fix applied (clamp + bounds check), showing zero OOB even with -O0 codegen. Also disassembled both the unpatched and fixed ng_pptpgre.ko modules to confirm the codegen analysis.

Verified recommended fix

Two-part defense-in-depth fix in fix.diff: (1) Bounds check on the timeSent index at ng_pptpgre.c:514 -- wrap the array access in 'if (_ts_idx < PPTP_XMIT_WIN)' to prevent OOB write regardless of xmitWin value; (2) Defensive clamp on xmitWin after increment at ng_pptpgre.c:674 -- 'if (a->xmitWin > PPTP_XMIT_WIN) a->xmitWin = PPTP_XMIT_WIN'. Note: gcc -O2 optimizes the clamp away as dead code (the preceding check already prevents exceeding 16), but it's included for defense-in-depth. The finding markdown's more complete spinlock fix would address ALL data races (bcopy, recvAck, rtt, etc.) but is complex due to reentrancy (ng_pptpgre_xmit called from ng_pptpgre_recv). This fix supersedes the finding proposal with a simpler, targeted approach.

Verdict

REPRODUCED (code-level). The ng_pptpgre node has NO per-node serialization (confirmed: no spinlock/token/mutex in ng_pptpgre_recv at sys/netgraph/pptpgre/ng_pptpgre.c:566; ng_send_data dispatches rcvdata inline on caller CPU). The xmitWin growth check at line 672-676 is a genuine TOCTOU: read-check-increment without atomicity. Harness confirms xmitWin reaches 17 with -O0 codegen (104/30000 rounds). The struct layout is confirmed: &timeSent[16] and &recvSeq are both at offset 200 in struct ng_pptpgre_private, so an 8-byte write at timeSent[16] would overwrite recvSeq/xmitSeq. HOWEVER, with the default kernel's -O2 codegen, the compiler reuses the register value from the check for the increment (disassembly: movzwl 0x2c(%rbx),%eax; cmp $0xf,%ax; add $0x1,%eax; mov %ax,0x2c(%rbx)), so xmitWin CANNOT exceed 16 (PPTP_XMIT_WIN) -- the specific OOB write at timeSent[16] is NOT achievable on the default kernel. The race is real but the OOB is latent. The other unsynchronized data races (bcopy on timeSent at line 668, recvAck at line 652, rtt/dev/ato at 657-661, recvSeq at 698) ARE real and cause PPTP session state corruption -> DoS. Impact: PPTP session DoS (Medium severity, deprecated protocol). The finding's specific '8-byte heap OOB write' claim is overstated for the default kernel but the code is inherently unsafe (proven with -O0).