CVE-2026-78449 CVSS 8.1 September 8, 2026
← All reports

CVE-2026-78449: Out-of-Bounds Write in the RMCAST Interface-Enumeration Path

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

      rmcast.sys, the Windows Reliable Multicast Transport driver (PGM), maintains a per-session object that embeds a fixed 20-entry array of interface indices at offset 0x150, with the live entry count stored at offset 0x14E. StopListeningOnAllInterfacesExcept walks the global list of listening interfaces and copies each matching interface’s index into that array — but the pre-patch copy loop has no upper bound on how many entries it writes. When more than 20 interfaces are enumerated for the session, the loop writes past the end of the array and clobbers adjacent session-object fields, including pointers that are dereferenced later. MSRC classifies the resulting corruption as a use-after-free, but at the source level it is a straightforward out-of-bounds write driven by the *((_WORD *)P + 167) counter. The patch adds a < 0x14 (20) cap to the loop condition, gated behind Feature_1006786872, so the array can never be overrun.

      Background

      Reliable Multicast Transport (RMCAST) is Windows’ kernel implementation of PGM (Pragmatic General Multicast), exposed through the rmcast.sys driver and reachable via the MSAFD multicast socket path. A PGM session — a sender or receiver — is tracked by a session object that RMCAST refers to throughout the decompilation as P. That object carries a signature field (*((_DWORD *)P + 4) == 1380205633, i.e. the ASCII tag 'ADDR' / 0x52444441), a state flags word, a spinlock at (PKSPIN_LOCK)P + 53, and — relevant here — an inline interface table.

      The interface table is a fixed-size array of 20 DWORD interface indices starting at byte offset 0x150 of the session object, with a 16-bit count at offset 0x14E (*((_WORD *)P + 167)). This array records which network interfaces the session is currently listening on for its multicast group. When the session’s listening set changes — for instance when a data path is being torn down or re-homed — RMCAST reconciles the recorded set against the live global interface list.

      That reconciliation is StopListeningOnAllInterfacesExcept(P, a2). Its job: keep listening on interface a2, stop on everything else, and for each interface it actually stops, push a PgmSetTcpInfo(..., 7, ...) call down the stack to update the IGMP/multicast membership. The global interface list is a doubly linked list hanging off WPP_MAIN_CB.DeviceQueue, and each node stores its interface index at node-offset +0x20 (*(_DWORD *)(node + 32)) and a sub-list of address entries at +0x10.

      Root Cause

      The function runs in two phases under two spinlocks (the global SpinLock and the per-session lock at P + 53).

      Phase one snapshots the currently recorded interface indices out of the inline array into a stack scratch buffer v23[24]:

      v7 = *((_WORD *)P + 167);            // current count
      if ( v7 ) {
        v9 = P + 336;                      // 336 == 0x150, the array base
        do {
          v10 = *(_DWORD *)v9;
          v9 += 4;
          v11 = (unsigned __int16)v8;
          LOWORD(v8) = v8 + 1;
          v23[v11] = v10;                  // copy out to stack
        } while ( (unsigned __int16)v8 < v7 );
      }
      *((_WORD *)P + 167) = 0;             // reset count to 0
      

      So far so good — v7 can be at most 20 because the array only ever held that many. The count at 0x14E is then zeroed, and the function rebuilds the array from scratch by walking the global device list.

      Phase two is where it breaks. Having found the list node whose interface index matches a2, the inner loop iterates the node’s address sub-list at +0x10 and writes each entry’s index (*((_DWORD *)j + 4)) back into the inline array — incrementing the count at 0x14E each time:

      for ( j = *(__int64 **)(*(_QWORD *)&i + 16LL);
            j != (__int64 *)(*(_QWORD *)&i + 16LL);
            j = (__int64 *)*j )
      {
        v14 = 0;
        *(_DWORD *)&P[4 * (unsigned __int16)(*((_WORD *)P + 167))++ + 336] = *((_DWORD *)j + 4);
        ...
      }
      

      The write target is P + 4 * count + 0x150, and count (*((_WORD *)P + 167)) is post-incremented on every iteration with no ceiling check. The loop’s only termination condition is reaching the end of the address sub-list (j != &list_head). Nothing ties the number of list entries to the 20-slot capacity of the array.

      Once count reaches 20, the next write lands at P + 0x150 + 80 = P + 0x1A0, which is no longer the interface array — it is whatever session-object fields follow it. Subsequent iterations keep walking forward through the session object, 4 bytes at a time, writing attacker-influenced interface-index values over live state. Critically, fields that the function itself consumes shortly afterward live in this region: *((_QWORD *)P + 8) is dereferenced as the transport handle passed to PgmSetTcpInfo, and *((_DWORD *)P + 82) feeds the byte-swapped group address. Corrupting pointer-width fields here converts the linear overflow into a controlled pointer corruption that is dereferenced inside the same call — which is the memory-safety violation MSRC reports under CWE-416. The 16-bit width of the counter also means it never saturates; it simply keeps climbing and the writes march arbitrarily far past the object.

      The rest of phase two (the inner v23 de-duplication loop that removes already-known indices) is just bookkeeping against the stack snapshot and is not where the corruption happens — it operates on v23[], which is correctly bounded by v8.

      The Patch

      The fix is surgical: it adds a capacity guard to the phase-two loop and leaves everything else semantically identical. The decompiler even re-rolls the for into a while, but the structural change is the new compound condition:

      v13 = *(__int64 **)(*(_QWORD *)&i + 16LL);
      v14 = *(_QWORD *)&i + 16LL;
      while ( v13 != (__int64 *)v14
           && (!(unsigned int)EvaluateCurrentState(&g_Feature_1006786872_63594065_FeatureDescriptorDetails)
            || *((_WORD *)P + 167) < 0x14u) )
      {
        v15 = 0;
        *(_DWORD *)&P[4 * (unsigned __int16)(*((_WORD *)P + 167))++ + 336] = *((_DWORD *)v13 + 4);
        ...
        v13 = (__int64 *)*v13;
      }
      

      The new clause *((_WORD *)P + 167) < 0x14u is the actual fix: the loop now stops writing the moment the count reaches 0x14 (20), exactly the array’s capacity. Even if the global list presents more than 20 matching address entries, the array can no longer be overrun — the remaining entries are simply dropped.

      The guard is wrapped in the standard Controlled Feature Rollout short-circuit:

      !EvaluateCurrentState(&g_Feature_1006786872_...) || (*((_WORD *)P + 167) < 0x14u)
      

      Because EvaluateCurrentState returns non-zero (enabled) by default, the || collapses to enforcing the bound: when the feature is on, the count check gates the loop; the !Enabled branch that would let the loop run unbounded is the disabled-state fallback. This matches the CFR pattern where new safety logic is added behind an on-by-default gate — the bound is live in shipped builds. This is the real fix, not a gate removal cleanup.

      Note what did not change. Phase one still copies out at most the old count; the inner de-duplication loop against v23[] is byte-for-byte the same apart from register renaming; and the trailing PgmSetTcpInfo reconciliation loop is unchanged. The only other diffs are cosmetic — a new WPP trace GUID (WPP_e47d6f73... replacing WPP_54222e8b...) and decompiler variable renumbering — both typical of a recompile and irrelevant to behavior. This tightly scoped change is what lets the same fix ship identically across every servicing branch, consistent with the CVE’s all-platform affected-build list including Server 2012/2012R2/2016.

      Exploitability

      The primitive is a kernel out-of-bounds write that escalates to a pointer-controlled dereference. Starting at the 21st enumerated interface, each loop iteration writes a 32-bit interface index — a value derived from the global interface list’s node data — into successively higher offsets of the session object. Because the write stride is 4 bytes and the count is a 16-bit post-increment with no saturation, an attacker who can present a sufficiently long address sub-list for the target interface controls both how far the overflow reaches and (through the interface indices themselves) the dwords written. The corrupted region includes the transport handle at *((_QWORD *)P + 8) that is immediately consumed by PgmSetTcpInfo in the same function, giving a short, same-call path from the overflow to a dereference of attacker-influenced data — the behavior MSRC characterizes as use-after-free and the basis for remote code execution.

      Reachability is the notable part. RMCAST processes PGM multicast traffic, and the interface-listening state this function reconciles is driven by network-visible multicast group and interface activity rather than by a privileged local configuration API. MSRC rates this as network-exploitable by an unauthenticated attacker: an actor who can influence the set of interfaces the session associates with its group — and push that set past 20 entries — drives StopListeningOnAllInterfacesExcept into the unbounded region without holding any credentials on the target. The trigger is not a tight race; it is a deterministic state condition (more than 20 live interface/address entries reconciled in a single call), which is why the “high complexity” of reaching a multi-homed, heavily-subscribed RMCAST configuration does not make the bug impractical — it only narrows the environments where the vulnerable path is naturally exercised. The patch removes the condition entirely by capping the writer at the array’s true capacity.

      Pre patch functions

      Full decompilation of StopListeningOnAllInterfacesExcept before the patch:

      /* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
      
      void StopListeningOnAllInterfacesExcept
                     (longlong *param_1,uint *param_2,longlong *param_3,longlong *param_4)
      
      {
        longlong *plVar1;
        undefined1 uVar2;
        undefined1 uVar3;
        uint uVar4;
        undefined8 *puVar5;
        uint *puVar6;
        undefined8 uVar7;
        ushort uVar8;
        ushort uVar9;
        ulonglong uVar10;
        ulonglong uVar11;
        longlong *plVar12;
        undefined1 auStack_d8 [32];
        longlong *local_b8;
        uint local_b0;
        uint local_a8;
        uint local_a4;
        uint local_98 [24];
        ulonglong local_38;
        
        local_38 = __security_cookie ^ (ulonglong)auStack_d8;
        puVar6 = param_2;
        uVar2 = KeAcquireSpinLockRaiseToDpc(&DAT_0);
        plVar1 = param_1 + 0x35;
        uVar3 = KeAcquireSpinLockRaiseToDpc(plVar1);
        if (((param_1 == (longlong *)0x0) || ((int)param_1[2] != 0x52444441)) ||
           ((*(uint *)(param_1 + 7) & 8) != 0)) {
          uVar7 = CONCAT71((int7)((ulonglong)puVar6 >> 8),uVar3);
          KeReleaseSpinLock(plVar1,uVar7);
          uVar7 = CONCAT71((int7)((ulonglong)uVar7 >> 8),uVar2);
          KeReleaseSpinLock(&DAT_0);
      LAB_1:
          PgmDereferenceAddress(param_1,uVar7,param_3,param_4);
          return;
        }
        uVar9 = *(ushort *)((longlong)param_1 + 0x14e);
        param_3 = (longlong *)(ulonglong)uVar9;
        uVar11 = 0;
        if (uVar9 != 0) {
          puVar6 = (uint *)(param_1 + 0x2a);
          uVar10 = uVar11;
          do {
            uVar4 = *puVar6;
            puVar6 = puVar6 + 1;
            uVar8 = (short)uVar10 + 1;
            uVar11 = (ulonglong)uVar8;
            local_98[uVar10] = uVar4;
            uVar10 = uVar11;
          } while (uVar8 < uVar9);
        }
        uVar9 = (ushort)uVar11;
        *(undefined2 *)((longlong)param_1 + 0x14e) = 0;
        puVar5 = DAT_2;
        while( true ) {
          if ((undefined8 **)puVar5 == &DAT_2) goto LAB_1c000b4ef;
          if ((int)param_2 == *(int *)(puVar5 + 4)) break;
          puVar5 = (undefined8 *)*puVar5;
        }
        for (puVar6 = (uint *)puVar5[2]; uVar9 = (ushort)uVar11, puVar6 != (uint *)(puVar5 + 2);
            puVar6 = *(uint **)puVar6) {
          param_3 = (longlong *)0x0;
          *(uint *)((longlong)param_1 + (ulonglong)*(ushort *)((longlong)param_1 + 0x14e) * 4 + 0x150) =
               puVar6[4];
          *(short *)((longlong)param_1 + 0x14e) = *(short *)((longlong)param_1 + 0x14e) + 1;
          if (uVar9 != 0) {
            uVar4 = puVar6[4];
            do {
              if (local_98[(longlong)param_3] == uVar4) {
                uVar9 = (short)uVar11 - 1;
                uVar11 = (ulonglong)uVar9;
                plVar12 = param_3;
                param_4 = param_3;
                if (uVar9 <= (ushort)param_3) break;
                do {
                  uVar8 = (short)plVar12 + 1;
                  param_4 = (longlong *)(ulonglong)uVar8;
                  local_98[(longlong)plVar12] = local_98[(longlong)((longlong)plVar12 + 1)];
                  plVar12 = param_4;
                } while (uVar8 < uVar9);
              }
              else {
                param_3 = (longlong *)(ulonglong)(ushort)((ushort)param_3 + 1);
              }
            } while ((ushort)param_3 < (ushort)uVar11);
          }
        }
      LAB_3:
        uVar7 = CONCAT71((int7)((ulonglong)puVar6 >> 8),uVar3);
        KeReleaseSpinLock(plVar1,uVar7);
        uVar7 = CONCAT71((int7)((ulonglong)uVar7 >> 8),uVar2);
        KeReleaseSpinLock(&DAT_0);
        uVar4 = *(uint *)(param_1 + 0x29);
        local_a8 = uVar4 >> 0x18 | (uVar4 & 0xff0000) >> 8 | (uVar4 & 0xff00) << 8 | uVar4 << 0x18;
        if (uVar9 != 0) {
          puVar6 = local_98;
          uVar11 = (ulonglong)uVar9;
          do {
            uVar4 = *puVar6;
            param_4 = (longlong *)0x8;
            local_a4 = uVar4 >> 0x18 | (uVar4 & 0xff0000) >> 8 | (uVar4 & 0xff00) << 8 | uVar4 << 0x18;
            uVar7 = 7;
            uVar4 = PgmSetTcpInfo(param_1[8],7,&local_a8,8);
            param_3 = (longlong *)(ulonglong)uVar4;
            if ((int)uVar4 < 0) {
              if (((undefined8 **)WPP_GLOBAL_Control != &WPP_GLOBAL_Control) &&
                 ((*(uint *)((longlong)WPP_GLOBAL_Control + 0x2c) & 2) != 0)) {
                param_4 = (longlong *)(ulonglong)local_a4;
                uVar7 = 0x13;
                param_3 = (longlong *)&WPP_54222e8b327d3fce0a999c41b777a933_Traceguids;
                local_b8 = param_1;
                local_b0 = uVar4;
                WPP_SF_DqD(WPP_GLOBAL_Control[3],0x13,&WPP_54222e8b327d3fce0a999c41b777a933_Traceguids,
                           local_a4);
              }
            }
            else if (((undefined8 **)WPP_GLOBAL_Control != &WPP_GLOBAL_Control) &&
                    ((*(uint *)((longlong)WPP_GLOBAL_Control + 0x2c) & 4) != 0)) {
              param_3 = (longlong *)&WPP_54222e8b327d3fce0a999c41b777a933_Traceguids;
              uVar7 = 0x12;
              local_b8 = (longlong *)CONCAT44(local_b8._4_4_,local_a4);
              param_4 = param_1;
              WPP_SF_qD(WPP_GLOBAL_Control[3],0x12,&WPP_54222e8b327d3fce0a999c41b777a933_Traceguids,
                        param_1);
            }
            puVar6 = puVar6 + 1;
            uVar11 = uVar11 - 1;
          } while (uVar11 != 0);
        }
        goto LAB_1;
      }
      

      Post patch functions

      Full decompilation of StopListeningOnAllInterfacesExcept after the patch:

      /* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
      
      void StopListeningOnAllInterfacesExcept
                     (longlong *param_1,uint *param_2,uint *param_3,longlong *param_4)
      
      {
        longlong *plVar1;
        undefined1 uVar2;
        undefined1 uVar3;
        bool bVar4;
        uint uVar5;
        undefined8 *puVar6;
        undefined7 extraout_var;
        uint *puVar7;
        undefined8 uVar8;
        undefined8 *puVar9;
        ushort uVar10;
        ushort uVar11;
        ulonglong uVar12;
        ulonglong uVar13;
        uint *puVar14;
        undefined1 auStack_d8 [32];
        longlong *local_b8;
        uint local_b0;
        uint local_a8;
        uint local_a4;
        uint local_98 [24];
        ulonglong local_38;
        
        local_38 = __security_cookie ^ (ulonglong)auStack_d8;
        puVar7 = param_2;
        uVar2 = KeAcquireSpinLockRaiseToDpc(&DAT_0);
        plVar1 = param_1 + 0x35;
        uVar3 = KeAcquireSpinLockRaiseToDpc(plVar1);
        if (((param_1 == (longlong *)0x0) || ((int)param_1[2] != 0x52444441)) ||
           ((*(uint *)(param_1 + 7) & 8) != 0)) {
          uVar8 = CONCAT71((int7)((ulonglong)puVar7 >> 8),uVar3);
          KeReleaseSpinLock(plVar1,uVar8);
          uVar8 = CONCAT71((int7)((ulonglong)uVar8 >> 8),uVar2);
          KeReleaseSpinLock(&DAT_0);
      LAB_1:
          PgmDereferenceAddress(param_1,uVar8,param_3,param_4);
          return;
        }
        uVar11 = *(ushort *)((longlong)param_1 + 0x14e);
        param_3 = (uint *)(ulonglong)uVar11;
        param_4 = (longlong *)0x1;
        uVar13 = 0;
        if (uVar11 != 0) {
          puVar7 = (uint *)(param_1 + 0x2a);
          uVar12 = uVar13;
          do {
            uVar5 = *puVar7;
            puVar7 = puVar7 + 1;
            uVar10 = (short)uVar12 + 1;
            uVar13 = (ulonglong)uVar10;
            local_98[uVar12] = uVar5;
            uVar12 = uVar13;
          } while (uVar10 < uVar11);
        }
        uVar11 = (ushort)uVar13;
        *(undefined2 *)((longlong)param_1 + 0x14e) = 0;
        puVar6 = DAT_2;
        while( true ) {
          if ((undefined8 **)puVar6 == &DAT_2) goto LAB_1c000b515;
          if ((int)param_2 == *(int *)(puVar6 + 4)) break;
          puVar6 = (undefined8 *)*puVar6;
        }
        puVar9 = (undefined8 *)puVar6[2];
        while ((uVar11 = (ushort)uVar13, puVar9 != puVar6 + 2 &&
               ((bVar4 = EvaluateCurrentState(&g_Feature_1006786872_63594065_FeatureDescriptorDetails),
                (int)CONCAT71(extraout_var,bVar4) == 0 || (*(ushort *)((longlong)param_1 + 0x14e) < 0x14))
               ))) {
          puVar7 = (uint *)0x0;
          *(undefined4 *)
           ((longlong)param_1 + (ulonglong)*(ushort *)((longlong)param_1 + 0x14e) * 4 + 0x150) =
               *(undefined4 *)(puVar9 + 2);
          *(short *)((longlong)param_1 + 0x14e) = *(short *)((longlong)param_1 + 0x14e) + 1;
          if (uVar11 != 0) {
            uVar5 = *(uint *)(puVar9 + 2);
            param_4 = (longlong *)(ulonglong)uVar5;
            do {
              if (local_98[(longlong)puVar7] == uVar5) {
                uVar11 = (short)uVar13 - 1;
                uVar13 = (ulonglong)uVar11;
                puVar14 = puVar7;
                param_3 = puVar7;
                if (uVar11 <= (ushort)puVar7) break;
                do {
                  uVar10 = (short)puVar14 + 1;
                  param_3 = (uint *)(ulonglong)uVar10;
                  local_98[(longlong)puVar14] = local_98[(longlong)((longlong)puVar14 + 1)];
                  puVar14 = param_3;
                } while (uVar10 < uVar11);
              }
              else {
                puVar7 = (uint *)(ulonglong)(ushort)((ushort)puVar7 + 1);
              }
            } while ((ushort)puVar7 < (ushort)uVar13);
          }
          puVar9 = (undefined8 *)*puVar9;
        }
      LAB_3:
        uVar8 = CONCAT71((int7)((ulonglong)puVar7 >> 8),uVar3);
        KeReleaseSpinLock(plVar1,uVar8);
        uVar8 = CONCAT71((int7)((ulonglong)uVar8 >> 8),uVar2);
        KeReleaseSpinLock(&DAT_0);
        uVar5 = *(uint *)(param_1 + 0x29);
        local_a8 = uVar5 >> 0x18 | (uVar5 & 0xff0000) >> 8 | (uVar5 & 0xff00) << 8 | uVar5 << 0x18;
        if (uVar11 != 0) {
          puVar7 = local_98;
          uVar13 = (ulonglong)uVar11;
          do {
            uVar5 = *puVar7;
            param_4 = (longlong *)0x8;
            local_a4 = uVar5 >> 0x18 | (uVar5 & 0xff0000) >> 8 | (uVar5 & 0xff00) << 8 | uVar5 << 0x18;
            uVar8 = 7;
            uVar5 = PgmSetTcpInfo(param_1[8],7,&local_a8,8);
            param_3 = (uint *)(ulonglong)uVar5;
            if ((int)uVar5 < 0) {
              if (((undefined8 **)WPP_GLOBAL_Control != &WPP_GLOBAL_Control) &&
                 ((*(uint *)((longlong)WPP_GLOBAL_Control + 0x2c) & 2) != 0)) {
                param_4 = (longlong *)(ulonglong)local_a4;
                uVar8 = 0x13;
                param_3 = (uint *)&WPP_e47d6f73ebb63faad8cae4aa4f744dad_Traceguids;
                local_b8 = param_1;
                local_b0 = uVar5;
                WPP_SF_DqD(WPP_GLOBAL_Control[3],0x13,&WPP_e47d6f73ebb63faad8cae4aa4f744dad_Traceguids,
                           local_a4);
              }
            }
            else if (((undefined8 **)WPP_GLOBAL_Control != &WPP_GLOBAL_Control) &&
                    ((*(uint *)((longlong)WPP_GLOBAL_Control + 0x2c) & 4) != 0)) {
              param_3 = (uint *)&WPP_e47d6f73ebb63faad8cae4aa4f744dad_Traceguids;
              uVar8 = 0x12;
              local_b8 = (longlong *)CONCAT44(local_b8._4_4_,local_a4);
              param_4 = param_1;
              WPP_SF_qD(WPP_GLOBAL_Control[3],0x12,&WPP_e47d6f73ebb63faad8cae4aa4f744dad_Traceguids,
                        param_1);
            }
            puVar7 = puVar7 + 1;
            uVar13 = uVar13 - 1;
          } while (uVar13 != 0);
        }
        goto LAB_1;
      }