http.sys — Missing destination size cap on network-address copy in UlpCopyNetworkAddress (CWE-787) fixed
KB5089549
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— unpatched0x14009BE80, patched0x14009B990
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 (0x14018a040 → 0x140187000) 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 0x14009BA24–0x14009BA28 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.
- The index is
*(uint16*)(*(connection + 0x18) + 0x180)— thesa_familyof 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. - 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. http.sysHTTP 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.- If such a connection existed, the source addresses at block offsets
+0x180and+0x19cmust contain at leastlengthbytes 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 throughRtlCopyToUserinto the user-mode receive buffer; otherwiseRtlCopyVolatileMemorycopies into the kernel-side buffer. - The overflow bytes are the connection's own stored
SOCKADDRbytes (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, patchedUlpCopyNetworkAddress @ 0x14009B990. Both start with the same prologue and read thesa_familyat[*(rcx+0x18)+0x180]. - Direction: the unpatched build lacks the
cmp si, 0x20/jareject; the patched build adds it. The patched build is the stricter one. - Table contents:
sockaddr_sizeis 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 areblock+0x180andblock+0x19c. - Callers:
UlpDeepCopyRequest(0x140116354) andUlpDeepCopyRequest32(0x1401166d8).
8. Changed Functions — Full Triage
Security-relevant
| Function | Change | Note |
|---|---|---|
UlpCopyNetworkAddress (0x14009BE80 → 0x14009B990) |
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.