CVE-2026-45657 CVSS 9.8 June 9, 2026
← All reports

CVE-2026-45657: Use-After-Free in the Windows IPv4/IPv6 Forward-Path Cache (IppCreateForwardPath)

Community check
      Suggest change

      Reports here are AI-generated and may be wrong. Edit the source directly on GitHub — saving opens a pull request. Add yourself to the report's editors: list in the same PR and your name will appear at the bottom once it's merged.

      ✎ Edit on GitHub (opens a PR)

      TL;DR

      tcpip.sys maintains a per-compartment forward-path cache — a scalable hash table of IppPath entries that caches next-hop routing decisions for received packets. In IppCreateForwardPath, a freshly allocated path entry was inserted into the live hash table (RtlInsertEntryHashTable) and only had its “valid” bit (+0x50 | 1) set after an intervening call to IppRestructureHashTableUnderLock. Although the write lock is held throughout, the restructure/prune machinery and concurrent readers key off that valid bit, and the ordering left a window where an entry visible in the table could be treated inconsistently — a classic publish-before-initialize error that leads to a use-after-free of the IppPath object. The patch, gated behind CFR feature 411806010, moves the valid-bit assignment to immediately after insertion (before pruning and restructuring), so the entry is never observable in an intermediate, not-yet-valid state.

      Background

      When the IP layer receives a packet that needs to be forwarded or delivered, it must resolve a next hop. Rather than walk the route table on every packet, tcpip.sys caches resolved routes in a forward-path cache keyed by source/destination address. Each cache element is an IppPath structure allocated from a per-processor lookaside list (ExAllocateFromLookasideListEx) and linked into a scalable hash table — the RTL scalable hash table primitives (RtlInsertEntryHashTable, RtlAcquireScalableWriteLockAtDpcLevel) that Windows uses for high-contention, per-CPU-sharded lookups.

      The relevant object layout, from the decompiled allocator, is a 0x58-byte header followed by inline copies of the destination and (optionally) source address keys:

      The hash is computed with RtlCompute37Hash over the destination key (and the source key, when present), seeded with g_37HashSeed, then OR’d with 0x80000000. Insertion, capacity-based pruning (IppPrunePathSetUnderLock), and table restructuring (IppRestructureHashTableUnderLock) all happen under a single scalable write lock acquired on param_1 + 0x4c0.

      The lookup side — IppFindNextHopInFwdCacheOrRouteTable and friends, which call into this function — walks the same hash table to find an existing path before creating a new one. Reference counting on IppPath entries is what keeps them alive across that lookup/use window; when the count drops to zero the entry is freed back to the lookaside list. The valid bit at +0x50 governs whether an entry is eligible to be seen and referenced by those consumers.

      Root Cause

      Here is the tail of the pre-patch IppCreateForwardPath, from allocation-complete through publication:

          *(undefined4 *)((longlong)_Dst_00 + 0x3c) = 1;          // refcount = 1
          *(undefined4 *)((longlong)_Dst_00 + 0x40) = *(undefined4 *)(param_1 + 0x14);
          *(int **)((longlong)_Dst_00 + 0x48) = param_8;
          if (*param_8 == 0x616c7049) {                            // 'Ipla'
            CarAcquireCacheAwareReference(*(undefined8 *)(param_8 + 8),1);
          }
          else if (*param_8 == 0x656e7049) {                       // 'Ipne'
            LOCK(); param_8[1] = param_8[1] + 1; UNLOCK();
          }
          ...
          lVar1 = param_1 + 0x4c0;
          RtlAcquireScalableWriteLockAtDpcLevel(lVar1,&local_48);
          RtlInsertEntryHashTable(param_1 + 0x640,_Dst_00,uVar3 | 0x80000000,0);   // (1) PUBLISH
          if ((uint)(*(int *)(param_1 + 0x604) * 2) <= *(uint *)(param_1 + 0x654)) {
            IppPrunePathSetUnderLock(param_1,lVar1,2);                             // (2) PRUNE
          }
          IppRestructureHashTableUnderLock(param_1 + 0x640);                       // (3) RESTRUCTURE
          *(byte *)((longlong)_Dst_00 + 0x50) = *(byte *)((longlong)_Dst_00 + 0x50) | 1; // (4) MARK VALID
          KeReleaseInStackQueuedSpinLockFromDpcLevel(&local_48);
      

      The defect is the ordering of steps (1) through (4). The entry _Dst_00 becomes a member of the live hash table at step (1), but its validity flag is not set until step (4) — after two operations that walk and mutate the table’s contents:

      Although the scalable write lock is held across the whole sequence, that lock is not the only synchronization boundary that matters here. The scalable hash table is designed for lockless / reader-optimistic traversal on its read paths, and the entry’s +0x50 valid bit is precisely the flag that tells a consumer “this entry is fully constructed and safe to reference.” By making the entry reachable at step (1) with +0x50 == 0, the code creates a window in which the pruning and restructuring logic — and any reader that reaches the entry through the sharded per-CPU structures without taking the full write lock — can encounter a path entry that is linked but not yet marked valid.

      The consequence is a use-after-free: pruning/eviction logic can act on the not-yet-valid entry (or a racing consumer can grab and then release a reference on it) in a way that drives the freshly created IppPath — still owned by the refcount = 1 the constructor set at +0x3c — to premature teardown and return to the lookaside list, while IppCreateForwardPath continues to operate on _Dst_00 (it dereferences it again at step (4) and hands lVar1 to IppPathSetStartOrContinueTimer). The caller (IppDispatchReceivePacketHelper) then continues down the receive path holding a stale reference to freed lookaside memory that can be reallocated as another IppPath under attacker-influenced network load.

      The Patch

      The post-patch function introduces exactly one behavioral change, gated by a Component Feature Rollout (CFR) flag, plus the addition of the enablement helper. The valid-bit write is hoisted to run immediately after insertion and before pruning/restructuring:

          RtlInsertEntryHashTable(param_1 + 0x640,_Dst_00,uVar3 | 0x80000000,0);
          iVar4 = Feature_411806010__private_IsEnabledDeviceUsage();
          if (iVar4 != 0) {                                        // NEW code path
            *(byte *)((longlong)_Dst_00 + 0x50) = *(byte *)((longlong)_Dst_00 + 0x50) | 1;
          }
          if ((uint)(*(int *)(param_1 + 0x604) * 2) <= *(uint *)(param_1 + 0x654)) {
            IppPrunePathSetUnderLock(param_1,lVar1,2);
          }
          IppRestructureHashTableUnderLock(param_1 + 0x640);
          iVar4 = Feature_411806010__private_IsEnabledDeviceUsage();
          if (iVar4 == 0) {                                        // OLD (vulnerable) path
            *(byte *)((longlong)_Dst_00 + 0x50) = *(byte *)((longlong)_Dst_00 + 0x50) | 1;
          }
          KeReleaseInStackQueuedSpinLockFromDpcLevel(&local_48);
      

      The logic is a straight A/B fork on the feature state:

      The Feature_* gate is the standard Windows CFR pattern. The new helper:

      ulonglong Feature_411806010__private_IsEnabledDeviceUsage(void)
      {
        ulonglong uVar1;
        if ((Feature_411806010__private_featureState & 0x10) == 0) {
          uVar1 = Feature_411806010__private_IsEnabledFallback(
                      Feature_411806010__private_featureState, 3);
        }
        else {
          uVar1 = (ulonglong)(Feature_411806010__private_featureState & 1);
        }
        return uVar1;
      }
      

      reads the cached Feature_411806010__private_featureState: if bit 0x10 (the “state is decided/cached” bit) is set, it returns the low bit as the enable/disable answer; otherwise it consults Feature_411806010__private_IsEnabledFallback with default priority 3 to resolve the rollout decision. The remaining textual differences in the diff — iVar4/uVar5/lVar6 renames and the identical IppPathSetStartOrContinueTimer timer-interval arithmetic — are decompiler register-naming artifacts from the two inserted Feature_* calls, not semantic changes. The function’s length grew from 623 to 645 bytes, consistent with two small call/test/jz/or sequences replacing one unconditional or.

      Note that once the flag reaches full deployment, the “disabled” branch is dead code; keeping it is purely the CFR safety valve.

      Exploitability

      The primitive is a use-after-free of a lookaside-backed IppPath object in non-paged pool, reachable from the packet receive path. IppCreateForwardPath is invoked from IppDispatchReceivePacketHelper / IppFindNextHopInFwdCacheOrRouteTable / IpIpsProviderFindNextHop — i.e., while processing inbound IP datagrams whose next hop is not already cached. No authentication and no local access are required, which is what earns the CVSS 9.8 / network attack vector: an attacker who can send packets to the target (crafted to miss the forward-path cache and force fresh entry creation) drives the vulnerable allocation path.

      The trigger is a race, and the window is small — it exists only between RtlInsertEntryHashTable and the delayed +0x50 |= 1, spanning a prune and a restructure. Two conditions make it practical to hit despite the “high complexity” feel of a kernel race:

      1. The attacker controls the pressure that opens the window. The prune pass at step (2) fires only when occupancy reaches 2 * target (+0x654 >= +0x604 * 2). By flooding the target with packets to many distinct destinations (or spoofed source/destination pairs, since the source key participates in the hash when present), an attacker inflates the cache toward that threshold on demand, guaranteeing that new insertions land in exactly the code path where pruning runs while the just-inserted entry is still invalid.
      2. Receive processing is inherently concurrent. Inbound packets are dispatched across multiple processors, and the scalable hash table is per-CPU sharded specifically to allow concurrent access. That concurrency is the same property that lets a second CPU’s lookup observe the half-published entry, or lets pruning race the constructor’s refcount = 1, collapsing the reference to zero and freeing the object back to the per-processor lookaside list.

      Once the IppPath is freed, the lookaside list makes reallocation highly deterministic: the next IppCreateForwardPath on the same processor hands back the same block, letting an attacker groom the freed slot with a new, attacker-shaped path entry while the original caller still holds a pointer to it. Controlled fields at +0x48 (the interface/next-hop back-pointer, which is dereferenced and reference-counted) and +0x20/+0x28 (key pointers) provide the levers to convert the dangling reference into a controlled dereference and, ultimately, remote code execution in kernel context. Because the corrupted object is a routing-cache entry consulted on every subsequent forwarded packet, the attacker also gets repeated, low-noise opportunities to re-trigger and stabilize the exploit.

      The fix eliminates the window entirely rather than merely narrowing it: with the feature enabled, no code between insertion and the valid-bit write can observe an entry that is linked-but-invalid, so neither the prune pass nor a concurrent reader can act on a partially constructed IppPath.

      Detection

      Reliable exploitation of this bug manifests as an unusual inbound traffic pattern: a burst of packets to a large number of distinct destination/source pairs from a single or small set of remote peers, engineered to churn the forward-path cache past its prune threshold. On unpatched hosts, failed race attempts tend to surface as tcpip.sys bugchecks in IppPrunePathSetUnderLock, IppRestructureHashTableUnderLock, or IppPathSetStartOrContinueTimer with a corrupted or freed IppPath (tag 'Ipfp' / 0x70667049) as the faulting object — a useful signature when triaging crash telemetry. On patched systems, confirm the mitigation is live by checking that CFR feature 411806010 is in the enabled state, since the vulnerable ordering remains reachable whenever the flag is off.

      Pre patch functions

      Full decompilation of IppCreateForwardPath before the patch:

      /* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
      
      void IppCreateForwardPath
                     (longlong param_1,void *param_2,void *param_3,undefined4 param_4,undefined4 param_5,
                     undefined1 param_6,undefined4 param_7,int *param_8)
      
      {
        void *_Dst;
        longlong lVar1;
        undefined1 auVar2 [16];
        uint uVar3;
        undefined8 uVar4;
        void *_Dst_00;
        longlong lVar5;
        ulonglong _Size;
        void *_Dst_01;
        undefined8 local_48;
        undefined8 uStack_40;
        undefined8 local_38;
        
        local_48 = 0;
        uStack_40 = 0;
        local_38 = 0;
        _Dst_01 = (void *)0x0;
        _Size = (ulonglong)*(ushort *)(*(longlong *)(*(longlong *)(param_1 + 0x28) + 0x10) + 6);
        uVar4 = PplpRetrieveListIndex(*(undefined8 *)(*(longlong *)(param_1 + 0x28) + 0x4ea8));
        _Dst_00 = (void *)ExAllocateFromLookasideListEx(uVar4);
        if (_Dst_00 == (void *)0x0) {
          if (DAT_0 < '\0') {
            McTemplateK0z_EtwWriteTransfer(&MICROSOFT_TCPIP_PROVIDER_Context,&TCPIP_MEMORY_FAILURES);
          }
        }
        else {
          memset(_Dst_00,0,0x58);
          _Dst = (void *)((longlong)_Dst_00 + 0x58);
          memmove(_Dst,param_3,_Size);
          if (param_2 != (void *)0x0) {
            _Dst_01 = (void *)(_Size + (longlong)_Dst);
            memmove(_Dst_01,param_2,_Size);
          }
          *(undefined4 *)((longlong)_Dst_00 + 0x34) = param_5;
          *(void **)((longlong)_Dst_00 + 0x28) = _Dst_01;
          *(undefined1 *)((longlong)_Dst_00 + 0x38) = param_6;
          *(undefined4 *)((longlong)_Dst_00 + 0x30) = param_7;
          *(undefined4 *)((longlong)_Dst_00 + 0x18) = 0x70667049;
          *(undefined4 *)((longlong)_Dst_00 + 0x1c) = param_4;
          *(void **)((longlong)_Dst_00 + 0x20) = _Dst;
          *(undefined4 *)((longlong)_Dst_00 + 0x3c) = 1;
          *(undefined4 *)((longlong)_Dst_00 + 0x40) = *(undefined4 *)(param_1 + 0x14);
          *(int **)((longlong)_Dst_00 + 0x48) = param_8;
          if (*param_8 == 0x616c7049) {
            CarAcquireCacheAwareReference(*(undefined8 *)(param_8 + 8),1);
          }
          else if (*param_8 == 0x656e7049) {
            LOCK();
            param_8[1] = param_8[1] + 1;
            UNLOCK();
          }
          lVar1 = *(longlong *)((longlong)_Dst_00 + 0x28);
          uVar3 = RtlCompute37Hash(g_37HashSeed,*(undefined8 *)((longlong)_Dst_00 + 0x20),
                                   *(undefined2 *)
                                    (*(longlong *)(*(longlong *)(param_1 + 0x28) + 0x10) + 6));
          if (lVar1 != 0) {
            uVar3 = RtlCompute37Hash(uVar3,lVar1,
                                     *(undefined2 *)
                                      (*(longlong *)(*(longlong *)(param_1 + 0x28) + 0x10) + 6));
          }
          lVar1 = param_1 + 0x4c0;
          RtlAcquireScalableWriteLockAtDpcLevel(lVar1,&local_48);
          RtlInsertEntryHashTable(param_1 + 0x640,_Dst_00,uVar3 | 0x80000000,0);
          if ((uint)(*(int *)(param_1 + 0x604) * 2) <= *(uint *)(param_1 + 0x654)) {
            IppPrunePathSetUnderLock(param_1,lVar1,2);
          }
          IppRestructureHashTableUnderLock(param_1 + 0x640);
          *(byte *)((longlong)_Dst_00 + 0x50) = *(byte *)((longlong)_Dst_00 + 0x50) | 1;
          KeReleaseInStackQueuedSpinLockFromDpcLevel(&local_48);
          auVar2._8_8_ = 0;
          auVar2._0_8_ = _DAT_1 / 10000;
          lVar5 = SUB168(ZEXT816(0x624dd2f1a9fbe77) * auVar2,8);
          IppPathSetStartOrContinueTimer
                    (lVar1,lVar5 + (_DAT_1 / 10000 - lVar5 >> 1) >> 8,1);
        }
        return;
      }
      

      Post patch functions

      Full decompilation of IppCreateForwardPath after the patch:

      /* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
      
      void IppCreateForwardPath
                     (longlong param_1,void *param_2,void *param_3,undefined4 param_4,undefined4 param_5,
                     undefined1 param_6,undefined4 param_7,int *param_8)
      
      {
        void *_Dst;
        longlong lVar1;
        undefined1 auVar2 [16];
        uint uVar3;
        int iVar4;
        undefined8 uVar5;
        void *_Dst_00;
        longlong lVar6;
        ulonglong _Size;
        void *_Dst_01;
        undefined8 local_48;
        undefined8 uStack_40;
        undefined8 local_38;
        
        local_48 = 0;
        uStack_40 = 0;
        local_38 = 0;
        _Dst_01 = (void *)0x0;
        _Size = (ulonglong)*(ushort *)(*(longlong *)(*(longlong *)(param_1 + 0x28) + 0x10) + 6);
        uVar5 = PplpRetrieveListIndex(*(undefined8 *)(*(longlong *)(param_1 + 0x28) + 0x4ea8));
        _Dst_00 = (void *)ExAllocateFromLookasideListEx(uVar5);
        if (_Dst_00 == (void *)0x0) {
          if (DAT_0 < '\0') {
            McTemplateK0z_EtwWriteTransfer(&MICROSOFT_TCPIP_PROVIDER_Context,&TCPIP_MEMORY_FAILURES);
          }
        }
        else {
          memset(_Dst_00,0,0x58);
          _Dst = (void *)((longlong)_Dst_00 + 0x58);
          memmove(_Dst,param_3,_Size);
          if (param_2 != (void *)0x0) {
            _Dst_01 = (void *)(_Size + (longlong)_Dst);
            memmove(_Dst_01,param_2,_Size);
          }
          *(undefined4 *)((longlong)_Dst_00 + 0x34) = param_5;
          *(void **)((longlong)_Dst_00 + 0x28) = _Dst_01;
          *(undefined1 *)((longlong)_Dst_00 + 0x38) = param_6;
          *(undefined4 *)((longlong)_Dst_00 + 0x30) = param_7;
          *(undefined4 *)((longlong)_Dst_00 + 0x18) = 0x70667049;
          *(undefined4 *)((longlong)_Dst_00 + 0x1c) = param_4;
          *(void **)((longlong)_Dst_00 + 0x20) = _Dst;
          *(undefined4 *)((longlong)_Dst_00 + 0x3c) = 1;
          *(undefined4 *)((longlong)_Dst_00 + 0x40) = *(undefined4 *)(param_1 + 0x14);
          *(int **)((longlong)_Dst_00 + 0x48) = param_8;
          if (*param_8 == 0x616c7049) {
            CarAcquireCacheAwareReference(*(undefined8 *)(param_8 + 8),1);
          }
          else if (*param_8 == 0x656e7049) {
            LOCK();
            param_8[1] = param_8[1] + 1;
            UNLOCK();
          }
          lVar1 = *(longlong *)((longlong)_Dst_00 + 0x28);
          uVar3 = RtlCompute37Hash(g_37HashSeed,*(undefined8 *)((longlong)_Dst_00 + 0x20),
                                   *(undefined2 *)
                                    (*(longlong *)(*(longlong *)(param_1 + 0x28) + 0x10) + 6));
          if (lVar1 != 0) {
            uVar3 = RtlCompute37Hash(uVar3,lVar1,
                                     *(undefined2 *)
                                      (*(longlong *)(*(longlong *)(param_1 + 0x28) + 0x10) + 6));
          }
          lVar1 = param_1 + 0x4c0;
          RtlAcquireScalableWriteLockAtDpcLevel(lVar1,&local_48);
          RtlInsertEntryHashTable(param_1 + 0x640,_Dst_00,uVar3 | 0x80000000,0);
          iVar4 = Feature_411806010__private_IsEnabledDeviceUsage();
          if (iVar4 != 0) {
            *(byte *)((longlong)_Dst_00 + 0x50) = *(byte *)((longlong)_Dst_00 + 0x50) | 1;
          }
          if ((uint)(*(int *)(param_1 + 0x604) * 2) <= *(uint *)(param_1 + 0x654)) {
            IppPrunePathSetUnderLock(param_1,lVar1,2);
          }
          IppRestructureHashTableUnderLock(param_1 + 0x640);
          iVar4 = Feature_411806010__private_IsEnabledDeviceUsage();
          if (iVar4 == 0) {
            *(byte *)((longlong)_Dst_00 + 0x50) = *(byte *)((longlong)_Dst_00 + 0x50) | 1;
          }
          KeReleaseInStackQueuedSpinLockFromDpcLevel(&local_48);
          auVar2._8_8_ = 0;
          auVar2._0_8_ = _DAT_1 / 10000;
          lVar6 = SUB168(ZEXT816(0x624dd2f1a9fbe77) * auVar2,8);
          IppPathSetStartOrContinueTimer
                    (lVar1,lVar6 + (_DAT_1 / 10000 - lVar6 >> 1) >> 8,1);
        }
        return;
      }
      

      Full decompilation of Feature_411806010__private_IsEnabledDeviceUsage after the patch:

      ulonglong Feature_411806010__private_IsEnabledDeviceUsage(void)
      
      {
        ulonglong uVar1;
        
        if ((Feature_411806010__private_featureState & 0x10) == 0) {
          uVar1 = Feature_411806010__private_IsEnabledFallback(Feature_411806010__private_featureState,3);
        }
        else {
          uVar1 = (ulonglong)(Feature_411806010__private_featureState & 1);
        }
        return uVar1;
      }