1. Overview

Field Value
Unpatched binary http_unpatched.sys
Patched binary http_patched.sys
Overall similarity 0.9843
Matched functions 4399
Changed functions 83
Identical functions 4316
Unmatched (unpatched / patched) 0 / 0

Verdict: The patch adds a missing destination-size cap in UlpCopyNetworkAddress (unpatched 0x14009BE80, patched 0x14009B990). This routine copies the connection's stored network addresses (two SOCKADDR fields on the connection object) into the marshalled HTTP_REQUEST output buffer. The copy length is taken from the address-family–indexed sockaddr_size table. The unpatched build validates only that the two fixed 0x20-byte destination slots fit inside the output buffer; it never checks that the per-address length is <= 0x20. The patch adds that check and returns STATUS_INVALID_PARAMETER when the length exceeds 0x20. Only two address families in the table (AF_UNIX = 0x6e bytes, Hyper-V/HV = 0x24 bytes) exceed 0x20; standard TCP/IP connections (AF_INET = 0x10, AF_INET6 = 0x1c) do not. The stored address family is set by the transport at accept time and is not selected by HTTP request content, so an oversized copy is not demonstrably reachable from a crafted request. This is a genuine bounds-check hardening of an out-of-bounds write path; severity is Low on demonstrable reachability.


2. Vulnerability Summary

Finding 1 — Missing size cap in UlpCopyNetworkAddress (SOCKADDR copy into the request buffer)

  • Severity: Low
  • Class: Out-of-bounds Write / Buffer Copy Without Checking Size of Input (CWE-787 / CWE-120)
  • Affected function: UlpCopyNetworkAddress — unpatched 0x14009BE80, patched 0x14009B990

Root cause:

UlpCopyNetworkAddress copies two SOCKADDR structures from the connection object into a caller-supplied output buffer (a6), laid out as two adjacent 0x20-byte slots: bytes 0..0x1f and 0x20..0x3f. The connection object pointer is a1; the address block is *(a1 + 0x18); the two source addresses are at block offsets +0x180 and +0x19c.

The copy length comes from the sockaddr_size table (unpatched at 0x14018a040, patched at 0x140187000; the table bytes are identical between builds). It is indexed by the stored address family, a 16-bit field read from *(uint16*)(*(a1 + 0x18) + 0x180) (the sa_family of the first stored address), with indices >= 0x23 folded to entry 0.

The unpatched code checks only that the destination window fits the output buffer: a6 + 0x20 <= (a2 + a4) and a6 + 0x40 <= (a2 + a4), where a2 + a4 is the end of the output buffer. It never checks that the copy length is <= 0x20. Both copies use that length, so if the length exceeds 0x20 the first copy (to a6 + 0x20) writes past the second slot and the second copy (to a6) writes past a6 + 0x40, into whatever follows in the output buffer.

The copies are performed by RtlCopyToUser (when the copy-to-user flag a5 is set) or by RtlCopyVolatileMemory (kernel-to-kernel copy) — not by a general memcpy shim. The two callers, UlpDeepCopyRequest (0x140116354) and UlpDeepCopyRequest32 (0x1401166d8), propagate the copy-to-user flag, so the destination is the HTTP_REQUEST receive buffer, which may be a user-mode buffer or a kernel copy of it.

The patch adds the length check size > 0x20 to the reject condition and returns STATUS_INVALID_PARAMETER (0xC000000D) when it fails. The length is also handled as a 16-bit value.

Reachability:

The copy length only exceeds 0x20 for two table entries: index 1 (AF_UNIX, 0x6e = 110 bytes) and index 0x22 (Hyper-V socket, 0x24 = 36 bytes). Every other entry is <= 0x1c. The index is the address family of the connection's stored transport address, established when the connection is accepted; it is not derived from HTTP headers, method, URL, content-type, or any request content. For a standard TCP/IP HTTP listener the family is AF_INET or AF_INET6, whose sizes (0x10 / 0x1c) are already within a slot, so the guarded overflow path is not exercised. Triggering an oversized length would require http.sys to service a connection over an AF_UNIX or Hyper-V-socket transport; that is not demonstrable from these binaries. The change is therefore a defensive size cap (defense-in-depth against an unexpected address family) rather than a demonstrably remotely-reachable overflow.


3. Pseudocode Diff

// === UNPATCHED UlpCopyNetworkAddress @ 0x14009BE80 ===
// af = sa_family of the first stored address
v13 = *(unsigned __int16 *)(*(_QWORD *)(a1 + 0x18) + 0x180);
if (v13 >= 0x23)
    v14 = sockaddr_size[0];
else
    v14 = sockaddr_size[v13];          // copy length, NO upper bound
v15 = a2 + a4;                          // end of output buffer
// Only the destination-window fit is checked; length is not capped:
if ((unsigned __int64)(a6 + 0x20) > v15 || (unsigned __int64)(a6 + 0x40) > v15)
    return 0xC000000D;                  // STATUS_INVALID_PARAMETER
...
// slot 1 (a6+0x20) <- address at block+0x19c, length v14  (OOB if v14 > 0x20)
if (a5) RtlCopyToUser(a6 + 0x20, *v12 + 0x19c, v14);
else    RtlCopyVolatileMemory(a6 + 0x20, *v12 + 0x19c, v14);
// slot 0 (a6) <- address at block+0x180, length v14        (OOB if v14 > 0x20)
if (a5) RtlCopyToUser(a6, *v12 + 0x180, v14);
else    RtlCopyVolatileMemory(a6, *v12 + 0x180, v14);

// === PATCHED UlpCopyNetworkAddress @ 0x14009B990 ===
v12 = (v11 >= 0x23) ? sockaddr_size[0] : sockaddr_size[v11];   // v12 is 16-bit
v13 = a2 + a4;
// FIX: length now capped to 0x20 alongside the window-fit checks:
if ((unsigned __int64)(a6 + 0x20) > v13 || v12 > 0x20u || (unsigned __int64)(a6 + 0x40) > v13)
    return 0xC000000D;                  // STATUS_INVALID_PARAMETER
...
// same two copies, but v12 is now guaranteed <= 0x20

Key deltas: - New guard v12 > 0x20 folded into the existing reject condition — the security fix. - Copy length handled as a 16-bit value (si) rather than a 64-bit value. - No other behavioral change; the two SOCKADDR copies and the copy-to-user/kernel selection are unchanged. - The sockaddr_size table was relocated (0x14018a0400x140187000) with identical contents; the relocation is build churn, not part of the fix.


4. Assembly Analysis

Instructions below are copied from the disassembly of the named functions at the stated addresses.

Unpatched vulnerable path (UlpCopyNetworkAddress @ 0x14009BE80)

000000014009BEB9  mov     rax, [r12]                     ; r12 = rcx+0x18 (address block)
000000014009BEBD  movzx   ecx, word ptr [rax+180h]       ; sa_family of first stored address
000000014009BEC4  cmp     ecx, 23h
000000014009BEC7  jnb     short loc_14009BED8
000000014009BEC9  mov     eax, ecx
000000014009BECB  lea     rcx, sockaddr_size
000000014009BED2  movzx   esi, byte ptr [rax+rcx]        ; esi = sockaddr_size[af]  (NO size cap)
000000014009BED6  jmp     short loc_14009BEDF
000000014009BED8  movzx   esi, cs:sockaddr_size          ; default sockaddr_size[0]
000000014009BEDF  mov     [rsp+arg_18], esi
000000014009BEE6  mov     ecx, r9d
000000014009BEE9  add     rcx, r13                       ; rcx = end = arg2 + arg4
000000014009BEEC  lea     rdi, [r14+20h]
000000014009BEF0  cmp     rdi, rcx
000000014009BEF3  jbe     short loc_14009BEFF            ; require r14+0x20 <= end
000000014009BEF5  mov     ebx, 0C000000Dh                ; STATUS_INVALID_PARAMETER
...
000000014009BF07  lea     rax, [rdi+20h]                 ; r14+0x40
000000014009BF0B  cmp     rax, rcx
000000014009BF0E  ja      short loc_14009BEF5            ; require r14+0x40 <= end
; ... no 'cmp esi, 0x20' exists between the lookup and the copies ...
000000014009BFB9  mov     rdx, [r12]
000000014009BFBD  add     rdx, 19Ch                      ; Src = block+0x19c
000000014009BFCD  mov     r8, rsi                        ; Size = sockaddr_size[af] (uncapped)
000000014009BFD0  mov     rcx, rdi                       ; dst = r14+0x20
000000014009BFD3  test    r15b, r15b
000000014009BFD8  call    RtlCopyToUser                  ; else RtlCopyVolatileMemory
000000014009BFE4  mov     rdx, [r12]
000000014009BFE8  add     rdx, 180h                      ; Src = block+0x180
000000014009BFEF  mov     r8, rsi                        ; Size (uncapped)
000000014009BFF2  mov     rcx, r14                       ; dst = r14
000000014009BFFA  call    RtlCopyToUser                  ; else RtlCopyVolatileMemory

Patched equivalent (UlpCopyNetworkAddress @ 0x14009B990)

000000014009B9CD  movzx   ecx, word ptr [rax+180h]       ; sa_family
000000014009B9D4  cmp     ecx, 23h
000000014009B9D7  jnb     short loc_14009B9E8
000000014009B9E2  movzx   esi, byte ptr [rax+rcx]        ; esi = sockaddr_size[af]
000000014009B9EF  mov     [rsp+arg_18], si               ; length kept as 16-bit
000000014009B9F7  mov     ecx, r9d
000000014009B9FA  add     rcx, rdx                       ; rcx = end = arg2 + arg4
000000014009B9FD  lea     rdi, [r14+20h]
000000014009BA01  cmp     rdi, rcx
000000014009BA04  jbe     short loc_14009BA1C
000000014009BA06  mov     ebx, 0C000000Dh                ; STATUS_INVALID_PARAMETER
...
000000014009BA24  cmp     si, 20h                        ; <<< NEW: cap length to 0x20
000000014009BA28  ja      short loc_14009BA06            ; <<< reject if length > 0x20
000000014009BA2A  lea     rax, [rdi+20h]
000000014009BA2E  cmp     rax, rcx
000000014009BA31  ja      short loc_14009BA06
...
000000014009BAFA  call    RtlCopyToUser                  ; slot 1 copy (length now <= 0x20)
...
000000014009BB1C  call    RtlCopyToUser                  ; slot 0 copy (length now <= 0x20)

Annotations: - Missing check (unpatched): no cmp esi, 0x20 between the table lookup at 0x14009BED2 and the copies at 0x14009BFD8 / 0x14009BFFA. - Added check (patched): cmp si, 20h / ja at 0x14009BA240x14009BA28 short-circuits to STATUS_INVALID_PARAMETER before the copies.


5. Trigger Conditions

The guarded overflow requires the connection's stored address family to select a sockaddr_size entry greater than 0x20.

  1. The index is *(uint16*)(*(connection + 0x18) + 0x180) — the sa_family of the connection's stored transport address. It is fixed when the connection is accepted and is not influenced by HTTP method, URL, headers, content-type, charset, or transfer encoding.
  2. Table entries greater than 0x20 exist only for AF_UNIX (index 1, 0x6e = 110 bytes) and the Hyper-V socket family (index 0x22, 0x24 = 36 bytes). AF_INET (0x10) and AF_INET6 (0x1c) are within a slot.
  3. http.sys HTTP listeners bind over TCP/IP, so in normal operation the family is AF_INET/AF_INET6 and the copy length is <= 0x1c. Servicing a connection over an AF_UNIX or Hyper-V-socket transport would be required to select an oversized length, and that is not demonstrable from these binaries.
  4. If such a connection existed, the source addresses at block offsets +0x180 and +0x19c must contain at least length bytes for the copy to proceed.

There is no request-content-controlled path to an oversized length in either build.


6. Impact & Primitive

Primitive (if an oversized address family were reachable): the two copies write length bytes each into 0x20-byte slots at a6 and a6 + 0x20. With length up to 0x6e, the writes extend past a6 + 0x40 into the remainder of the HTTP_REQUEST output buffer.

  • The destination is the request-marshalling buffer selected by the callers UlpDeepCopyRequest / UlpDeepCopyRequest32. When the copy-to-user flag is set the copy goes through RtlCopyToUser into the user-mode receive buffer; otherwise RtlCopyVolatileMemory copies into the kernel-side buffer.
  • The overflow bytes are the connection's own stored SOCKADDR bytes (kernel-held address data), not attacker-chosen content, and the caller reports a fixed consumed size of 0x40 regardless, so subsequent marshalling offsets would be computed from 0x40 rather than the larger amount actually written.

No exploit chain is demonstrated here: the oversized-length precondition is not shown to be reachable, the overflow magnitude is bounded by the table entry (0x6e - 0x20 at most), and the overwritten bytes are not attacker-controlled. The value of the patch is removing the possibility that an unexpected stored address family drives a copy past the fixed slot width.


7. Verification Notes

  • Function identity: unpatched UlpCopyNetworkAddress @ 0x14009BE80, patched UlpCopyNetworkAddress @ 0x14009B990. Both start with the same prologue and read the sa_family at [*(rcx+0x18)+0x180].
  • Direction: the unpatched build lacks the cmp si, 0x20 / ja reject; the patched build adds it. The patched build is the stricter one.
  • Table contents: sockaddr_size is identical in both builds. Non-zero entries: index 1 = 0x6e, index 2 = 0x10, index 0x17 = 0x1c, index 0x22 = 0x24; all others 0. Only indices 1 and 0x22 exceed 0x20.
  • Copy mechanism: RtlCopyToUser / RtlCopyVolatileMemory, selected by the copy-to-user flag; the source pointers are block+0x180 and block+0x19c.
  • Callers: UlpDeepCopyRequest (0x140116354) and UlpDeepCopyRequest32 (0x1401166d8).

8. Changed Functions — Full Triage

Security-relevant

Function Change Note
UlpCopyNetworkAddress (0x14009BE800x14009B990) Security (hardening) Adds length <= 0x20 cap before the two SOCKADDR copies; returns STATUS_INVALID_PARAMETER on failure. Overflow only for AF_UNIX/HV address families, which are not shown to be request-reachable. Severity Low.

Note: the addresses 0x14009BF77 (referenced elsewhere as a distinct entry) is an internal error-handling label inside UlpCopyNetworkAddress, not a separate function.

Behavioral (non-security)

The remaining changed functions are servicing/feature-staging churn: WIL feature-staging gates (Feature_HttpBugFix25Q1 / Feature_HttpBugFix2605 / Feature_Servicing_*), removal of duplicated *Old code paths, WPP/ETW tracing, and HTTP-3/QUIC rebuild churn. Representative items confirmed as non-security:

Function Note
UlpSetRequestFlags (0x14009E8E0) Request-flag computation; feature-gate consolidation.
UcCaptureCredentialParameters (0x140164274) Client-credential capture; SSL-config flag handling consolidation.
UcpCreateClientConfiguration (0x14016491C) Client config; same flag-consolidation pattern.
UlCopyChannelBindConfigFromPropertyInfo (0x1400DACE4) Channel-binding config copy; array element-width churn (<<3 vs <<2). The addresses 0x1400DAFD6 and 0x1400DAED2 are internal labels of this function, not separate functions.
UlHttpReceiveHeadersEvent (0x1400C71F0) Receive-headers dispatch; feature-gated dual path consolidated.
UxSslValidateErrorHeaders (0x1400876CC) SSL error-header validation; feature-gate consolidation, validation preserved.
UlFastForwardReceiveEvent (0x1401190D8) Fast-forward receive dispatch; feature-gate consolidation.
UlpPcwEnumerateInstance (0x14014BC50) Performance-counter enumeration; error-handling tidy-up, functionally equivalent.
UlCheckProtocolCompliance (0x140070E10) Larger refactor; feature-gate consolidation pattern.

None of these introduces or removes an attacker-reachable bounds/validation boundary.


9. Unmatched Functions

Removed (in unpatched, absent in patched):  none
Added   (in patched, absent in unpatched):  none

At the whole-function level the diff tooling reports no additions or deletions. The observable *Old-suffixed duplicates removed and WIL feature-staging helpers added between builds are the servicing/feature-staging churn noted in section 8; none is a delivered security boundary change beyond Finding 1.


10. Confidence & Caveats

Confidence: HIGH for the mechanism and direction. The patched build adds a length <= 0x20 cap (cmp si, 0x20 / ja at 0x14009BA24) to UlpCopyNetworkAddress before two SOCKADDR copies whose length is the sockaddr_size table entry; the unpatched build has no such cap. The guarded operation is an out-of-bounds write (CWE-787 / CWE-120).

Caveats: - The copy length exceeds 0x20 only for the AF_UNIX and Hyper-V-socket table entries. The address family is set by the transport at accept time and is not selectable from HTTP request content, so an oversized copy is not demonstrably reachable on a standard TCP/IP listener. Severity is rated Low on that basis. - The overflow, if reached, writes the connection's own stored address bytes (not attacker-chosen data) into the request-marshalling buffer, bounded by the table entry. - No exploit chain, pool/stack grooming, or mitigation-bypass claim is made; none is supported by the binaries.