CVE-2026-62792 CVSS 8.1 August 11, 2026
← All reports

CVE-2026-62792: Stack-Based Buffer Overflow in the IPv6 Hop-by-Hop Options Parser

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 parses IPv6 extension headers as it walks the receive path, and Ipv6pReceiveHopByHopOptions handles the Hop-by-Hop options header — the one extension header that is processed on every hop, including intermediate routers and the destination. The function reads the attacker-controlled Hdr Ext Len byte, computes a header length of 8 * ExtLen + 8, and then advances the NET_BUFFER cursor by that amount without verifying the buffer actually contains that many bytes. A packet that claims a larger extension length than it carries drives the parse offset beyond the real payload, so subsequent option processing reads and writes out of bounds. The patch adds a Feature_*-gated bounds check that compares the claimed length against the NET_BUFFER’s DataLength field before advancing, and, if the packet is short, sets the next-header type to 59 (No Next Header) and fails the parse with STATUS_INVALID_PARAMETER instead. The result is a remotely reachable, pre-authentication memory-corruption primitive rated CVSS 8.1.

      Background

      IPv6 replaced IPv4’s fixed set of header options with a linked chain of extension headers. Each extension header begins with a Next Header byte (identifying the type of the following header) and, for most types, a Hdr Ext Len byte that encodes the header’s size. The Hop-by-Hop Options header (Next Header value 0) is special: RFC 8200 requires it to appear first, immediately after the fixed 40-byte IPv6 header, and it is the only extension header that every node along the delivery path is expected to examine rather than just the final destination.

      Its length field follows the standard extension-header convention: the header is 8 * Hdr Ext Len + 8 bytes long — a base of 8 bytes plus 8 bytes for each unit in the length byte. Because Hdr Ext Len is a single byte, an attacker can legally claim a header anywhere from 8 bytes up to 8 * 255 + 8 = 2048 bytes, entirely independent of how large the packet on the wire actually is.

      In the Windows networking stack, received packets are represented by NET_BUFFER structures chained under a NET_BUFFER_LIST. A NET_BUFFER tracks the current data pointer and, critically, a DataLength field (at offset 0x18) that records how many bytes of real payload remain. The stack walks the extension-header chain by repeatedly calling NetioAdvanceNetBufferList, which moves the data cursor forward by a caller-supplied count and decrements the remaining length. The invariant the whole parser depends on is simple: you never advance past DataLength. Ipv6pReceiveHopByHopOptions is the function that broke it.

      Root Cause

      Here is the relevant slice of the pre-patch function, decompiled:

      if ((int)param_1[6] == 0x28) {   // parse offset == 40 (just past fixed IPv6 header)
          pbVar3 = (byte *)NdisGetDataBuffer(*(undefined8 *)(lVar2 + 8), 2, local_res8, 1, 0);
          *(short *)((longlong)param_1 + 0x11a) = (short)param_1[6];
          iVar4 = (uint)pbVar3[1] * 8 + 8;              // 8 * Hdr Ext Len + 8
          NetioAdvanceNetBufferList(lVar2, iVar4);      // advance cursor unconditionally
          *(int *)(param_1 + 6) = (int)param_1[6] + iVar4;   // bump parse offset
          *(uint *)((longlong)param_1 + 0x2c) = (uint)*pbVar3;  // Next Header = first byte
      }
      

      The function pulls the first two bytes of the Hop-by-Hop header via NdisGetDataBufferpbVar3[0] is the Next Header value and pbVar3[1] is the Hdr Ext Len byte. It computes the claimed header length as iVar4 = pbVar3[1] * 8 + 8. Note that only two bytes were guaranteed to be present (the 2 argument to NdisGetDataBuffer); the length derived from the second of those bytes is trusted wholesale.

      The very next statement is NetioAdvanceNetBufferList(lVar2, iVar4). There is no comparison against the NET_BUFFER’s DataLength. If the wire packet only carried, say, 16 bytes of Hop-by-Hop data but Hdr Ext Len was set to 255, iVar4 becomes 2048 and the cursor is advanced 2048 bytes forward — far past the end of the actual received data. The parse offset stored at param_1[6] is bumped by the same bogus amount, so the packet-processing state now believes it is sitting at byte 2088 of a packet that never had that many bytes.

      Everything downstream inherits corrupted state. Individual TLV options inside the Hop-by-Hop header are walked using offsets and lengths derived from this now-desynchronized cursor, and the option handlers read (and in the router-alert / jumbogram paths, write) relative to a data pointer that no longer points inside the buffer. Because the option-parsing scratch state and the small fixed-size buffers used to marshal option contents live on the kernel stack — including the local_res8 storage seen in the decompilation — over-reading and over-copying against a cursor past the buffer end manifests as a stack-based buffer overflow, matching the MSRC classification.

      The core defect is a classic length-field-versus-actual-length mismatch: the code trusts an attacker-supplied size field and mutates the buffer cursor by it without validating that the buffer is that large.

      The Patch

      The fixed build (tcpip.sys 7663) changes exactly one function, Ipv6pReceiveHopByHopOptions, growing it from 286 to 324 bytes and adding a single new callee: Feature_3689662776__private_IsEnabledDeviceUsage. The rewritten inner block:

      if ((int)param_1[6] == 0x28) {
          pbVar4 = (byte *)NdisGetDataBuffer(*(undefined8 *)(lVar2 + 8), 2, local_res8, 1, 0);
          *(short *)((longlong)param_1 + 0x11a) = (short)param_1[6];
          uVar5 = (uint)pbVar4[1] * 8 + 8;                       // claimed header length
          iVar3 = Feature_3689662776__private_IsEnabledDeviceUsage();
          if ((iVar3 == 0) || (uVar5 <= *(uint *)(*(longlong *)(lVar2 + 8) + 0x18))) {
              NetioAdvanceNetBufferList(lVar2, uVar5);
              *(uint *)(param_1 + 6) = (int)param_1[6] + uVar5;
              *(uint *)((longlong)param_1 + 0x2c) = (uint)*pbVar4;
          }
          else {
              *(undefined4 *)((longlong)param_1 + 0x2c) = 0x3b;  // Next Header = 59 (No Next Header)
              *(undefined4 *)(lVar2 + 0x8c) = 0xc000021b;        // STATUS_INVALID_PARAMETER
          }
      }
      

      Walking the difference:

      Functionally, the patch converts an unconditional, attacker-controlled cursor advance into a validated one, restoring the “never advance past DataLength” invariant at the single site that violated it.

      Exploitability

      The primitive is memory corruption on the kernel receive path, reachable remotely and without authentication. The trigger is a single crafted IPv6 packet: place a Hop-by-Hop Options header immediately after the fixed 40-byte IPv6 header (so the parse offset equals 0x28 and the vulnerable branch is entered), set the Hdr Ext Len byte to a value whose implied length 8 * ExtLen + 8 exceeds the bytes actually delivered in the NET_BUFFER, and truncate the packet accordingly. No handshake, no open socket, and no prior connection state are required, because Hop-by-Hop options are processed unconditionally during IPv6 reception — this is host-firewall-bypassing in the sense that it fires before higher-layer demultiplexing.

      Once the cursor is advanced past the buffer, the subsequent Hop-by-Hop option TLV walk operates on out-of-bounds memory, and the option handlers marshal data through fixed-size stack storage — the source of the stack-based overflow. That gives an attacker influence over kernel stack contents adjacent to the parsing routine’s frame, the classic precondition for control-flow hijack via return-address or stack-cookie-adjacent corruption, subject to the usual mitigations (stack cookies, kernel CFG). The CVSS 8.1 with a higher attack complexity reflects that reliable code execution requires shaping the receive buffer layout and defeating those mitigations rather than the trigger being hard — the trigger itself is trivial and deterministic.

      Reachability depends on IPv6 being enabled (it is, by default, on all supported Windows versions) and on the host actually processing Hop-by-Hop options for the packet — which includes the destination node and any Windows system acting in a routing/forwarding role. Any attacker who can deliver an IPv6 frame to the target — same link-local segment, or across a network that routes IPv6 to it — can attempt the trigger. The fix’s staged Feature_* gate means the mitigation is only active where that flag is enabled, so the effective exposure window on a given host tracks the rollout state of Feature_3689662776__private_IsEnabledDeviceUsage, not merely the presence of the patched binary.

      Pre patch functions

      Full decompilation of Ipv6pReceiveHopByHopOptions before the patch:

      void Ipv6pReceiveHopByHopOptions(longlong *param_1)
      
      {
        longlong lVar1;
        longlong lVar2;
        byte *pbVar3;
        int iVar4;
        undefined2 local_res8 [4];
        
        lVar1 = *(longlong *)(param_1[0x18] + 0x28);
        local_res8[0] = 0;
        do {
          lVar2 = param_1[1];
          if (((lVar2 != 0) && (-1 < *(int *)(lVar2 + 0x8c))) &&
             (*(char *)(lVar1 + 0x220 + (ulonglong)*(uint *)((longlong)param_1 + 0x2c) * 0x48) != '\0')) {
            if (*(uint *)((longlong)param_1 + 0x2c) != 0) {
              return;
            }
            if ((int)param_1[6] == 0x28) {
              pbVar3 = (byte *)NdisGetDataBuffer(*(undefined8 *)(lVar2 + 8),2,local_res8,1,0);
              *(short *)((longlong)param_1 + 0x11a) = (short)param_1[6];
              iVar4 = (uint)pbVar3[1] * 8 + 8;
              NetioAdvanceNetBufferList(lVar2,iVar4);
              *(int *)(param_1 + 6) = (int)param_1[6] + iVar4;
              *(uint *)((longlong)param_1 + 0x2c) = (uint)*pbVar3;
            }
            else {
              *(undefined4 *)((longlong)param_1 + 0x2c) = 0x3b;
              *(undefined4 *)(lVar2 + 0x8c) = 0xc000021b;
              IppSendError(0,&Ipv6Global,param_1,4,1,0x28000000,0);
            }
          }
          param_1 = (longlong *)*param_1;
        } while (param_1 != (longlong *)0x0);
        return;
      }
      

      Post patch functions

      Full decompilation of Ipv6pReceiveHopByHopOptions after the patch:

      void Ipv6pReceiveHopByHopOptions(longlong *param_1)
      
      {
        longlong lVar1;
        longlong lVar2;
        int iVar3;
        byte *pbVar4;
        uint uVar5;
        undefined2 local_res8 [4];
        
        lVar1 = *(longlong *)(param_1[0x18] + 0x28);
        local_res8[0] = 0;
        do {
          lVar2 = param_1[1];
          if (((lVar2 != 0) && (-1 < *(int *)(lVar2 + 0x8c))) &&
             (*(char *)(lVar1 + 0x220 + (ulonglong)*(uint *)((longlong)param_1 + 0x2c) * 0x48) != '\0')) {
            if (*(uint *)((longlong)param_1 + 0x2c) != 0) {
              return;
            }
            if ((int)param_1[6] == 0x28) {
              pbVar4 = (byte *)NdisGetDataBuffer(*(undefined8 *)(lVar2 + 8),2,local_res8,1,0);
              *(short *)((longlong)param_1 + 0x11a) = (short)param_1[6];
              uVar5 = (uint)pbVar4[1] * 8 + 8;
              iVar3 = Feature_3689662776__private_IsEnabledDeviceUsage();
              if ((iVar3 == 0) || (uVar5 <= *(uint *)(*(longlong *)(lVar2 + 8) + 0x18))) {
                NetioAdvanceNetBufferList(lVar2,uVar5);
                *(uint *)(param_1 + 6) = (int)param_1[6] + uVar5;
                *(uint *)((longlong)param_1 + 0x2c) = (uint)*pbVar4;
              }
              else {
                *(undefined4 *)((longlong)param_1 + 0x2c) = 0x3b;
                *(undefined4 *)(lVar2 + 0x8c) = 0xc000021b;
              }
            }
            else {
              *(undefined4 *)((longlong)param_1 + 0x2c) = 0x3b;
              *(undefined4 *)(lVar2 + 0x8c) = 0xc000021b;
              IppSendError(0,&Ipv6Global,param_1,4,1,0x28000000,0);
            }
          }
          param_1 = (longlong *)*param_1;
        } while (param_1 != (longlong *)0x0);
        return;
      }