http.sys — No delivered security fix: feature-staged double-fetch mitigation in UlGenerateTrailers, vulnerable path retained behind UxKir gate
KB5075941
1. Overview
- Unpatched Binary:
http_unpatched.sys - Patched Binary:
http_patched.sys - Overall Similarity: 0.9919
- Diff Statistics:
- Matched Functions: 3584
- Changed Functions: 10
- Identical Functions: 3574
- Unmatched (Added/Removed): 0
- Verdict: The unpatched build contains a genuine double-fetch pattern in the HTTP trailer serialization routine
UlGenerateTrailers(each trailer string pointer is probed, then re-read from the same user-mode memory for the copy). The patched build does not remove this unconditionally. Instead it adds a mitigation that captures the pointer once, gated behind a Windows feature-staging flag (UxKirHttpReadingTrailersFromUm, set fromFeature_2347446586__private_IsEnabledDeviceUsage), and retains the original double-fetch path in the flag-off branch. This is a staged/rollback-gated change, not an unconditionally delivered security fix. The runtime default of the gate is controlled by external feature configuration and cannot be determined from the binary. The remaining changes in the diff are companion feature-staging plumbing and WIL feature-reporting/telemetry churn.
2. Vulnerability / Change Summary
Double-fetch pattern in UlGenerateTrailers (0x1C000CF94), CWE-367
- Affected Function:
UlGenerateTrailers@0x1C000CF94 - Nature: The function serializes an array of HTTP trailer entries. Each 24-byte (
0x18) entry holds twoint16_tlengths at+0x00/+0x02and two string pointers at+0x08/+0x10. In the unpatched build, for each string the length is read once and cached, but the pointer is read twice: first to pass to the probe helperUlProbeAnsiString(which validates user-mode readability viaUlProbeForRead→MmUserProbeAddress), and again immediately before the copy helpermemmove. Only the pointer is double-fetched; the length is single-fetched. - Patched behavior: The patched build reads each pointer once into a register, stores it to a stack slot (
Src), and branches onUxKirHttpReadingTrailersFromUm. When the flag is nonzero it validates and copies from the captured stack value (no re-read). When the flag is zero it falls back to the original behavior and re-reads the pointer from the array for thememmove. The vulnerable path is therefore still present in the patched binary. - Reachability / impact if the flag-off path runs and the race is won: The trailer array and its string pointers live in the calling process's own user-mode memory (the array is validated with
UlProbeForRead, the strings withUlProbeAnsiString, i.e. read in place, not deep-copied first). A second thread in that process can overwrite a pointer field between the probe and the re-read. If swapped to a kernel address,memmovecopies from that address into the response output buffer, producing a local, race-dependent kernel information disclosure for a process that already speaks the HTTP Server API. This is not remotely triggerable: the pointers are supplied by the local server process, not by a network client. - Entry point: HTTP Server API response send path (
HttpSendHttpResponse/HttpSendResponseEntityBody). - Call Chain:
- Response send path reaches
UlpPrepareHttpResponse@0x1C00DD810(and the fast pathUlFastSendHttpResponse@0x1C00E21F0). UlpPrepareHttpResponsecallsUlGenerateTrailers(call site at0x1C00DD9D0).UlGenerateTrailersprobes each trailer string pointer, then (flag-off path) re-reads it for thememmove.
3. Pseudocode Diff
The unpatched code reads each trailer string pointer directly from the array for both validation and the copy. The patched code captures the pointer once and, when the feature flag is set, copies from the captured value; the flag-off branch keeps the re-read.
// --- UNPATCHED UlGenerateTrailers (per entry) ---
int16_t size1 = *(word*)(r15 + entry_off); // read length once (cached)
UlProbeAnsiString(*(qword*)(r15 + entry_off + 8), ...); // VALIDATE ptr1 (1st pointer read)
memmove(dst, *(qword*)(r15 + entry_off + 8), size1); // COPY re-reads ptr1 (2nd pointer read)
int16_t size2 = *(word*)(r15 + entry_off + 2);
UlProbeAnsiString(*(qword*)(r15 + entry_off + 0x10), ...); // VALIDATE ptr2 (1st read)
memmove(dst, *(qword*)(r15 + entry_off + 0x10), size2); // COPY re-reads ptr2 (2nd read)
// --- PATCHED UlGenerateTrailers (per entry) ---
qword ptr1 = *(qword*)(r13 + entry_off + 8); // read pointer ONCE
Src = ptr1; // capture to stack slot
if (UxKirHttpReadingTrailersFromUm != 0) { // feature flag ON
UlProbeAnsiString(ptr1, ...); // validate captured pointer
memmove(dst, Src, size1); // copy uses captured value (no re-read)
} else { // feature flag OFF = original behavior
UlProbeAnsiString(ptr1, ...);
memmove(dst, *(qword*)(r13 + entry_off + 8), size1); // re-read (double-fetch retained)
}
4. Assembly Analysis
Unpatched double-fetch (UlGenerateTrailers, cold/outlined section)
The loop body is relocated into an outlined section at 0x1C0028886+.
00000001C002894E movzx eax, word ptr [r15+r12*8] ; read size1 once (cached)
00000001C0028970 mov rcx, [r15+r12*8+8] ; FIRST read of ptr1
00000001C0028975 call UlProbeAnsiString ; validate ptr1 (-> UlProbeForRead)
00000001C00289A7 mov rdx, [r15+r12*8+8] ; SECOND read of ptr1 (double-fetch)
00000001C00289B4 call memmove ; copy from re-read ptr1
00000001C00289E4 movzx ebx, word ptr [r15+r12*8+2] ; read size2 once
00000001C00289FC mov rcx, [r15+r12*8+10h] ; FIRST read of ptr2
00000001C0028A01 call UlProbeAnsiString ; validate ptr2
00000001C0028A21 mov rdx, [r15+r12*8+10h] ; SECOND read of ptr2 (double-fetch)
00000001C0028A31 call memmove ; copy from re-read ptr2
Patched, feature-gated capture path (UlGenerateTrailers)
The flag selects between capture (flag on) and re-read (flag off).
00000001C000D0CF mov rcx, [r13+rax*8+8] ; read ptr1 ONCE
00000001C000D0D4 mov [rsp+0A8h+Src], rcx ; capture to stack
00000001C000D0E5 cmp cs:UxKirHttpReadingTrailersFromUm, 0
00000001C000D0EC jz short loc_1C000D125 ; flag off -> re-read path below
; ---- flag ON: capture path ----
00000001C000D0EE call UlProbeAnsiString ; validate captured ptr1
00000001C000D111 mov rdx, [rsp+0A8h+Src] ; copy source = captured value
00000001C000D119 call memmove
; ---- flag OFF (loc_1C000D125): original double-fetch retained ----
00000001C000D125 call UlProbeAnsiString ; validate ptr1
00000001C000D14D mov rdx, [r13+r12*8+8] ; RE-READ ptr1 from array
00000001C000D155 call memmove
The value pointer at +0x10 is handled by the same flag branch at 0x1C000D1A2.
5. Trigger Conditions
For the flag-off (retained) path to disclose kernel memory, a local process would need to:
- Initialize the HTTP Server API and create a request queue (
HttpInitialize,HttpCreateHttpHandle/HttpCreateRequestQueue,HttpAddUrl). - Obtain a request to respond to.
- Build an
HTTP_RESPONSEwith at least one trailer whose 24-byte entry hasint16_tname/value lengths at+0x00/+0x02and pointer fields at+0x08/+0x10initially pointing at valid user-mode buffers. - From a second thread, overwrite a pointer field with a target kernel address after the probe but before the
memmovere-read. - Call
HttpSendHttpResponse. Entry count must be> 0and< 0x2710to pass the count check at0x1C00288DC. - Win the race for at least one entry; on success the kernel copies from the swapped address into the response buffer. An invalid kernel address faults during
memmove.
6. Impact Notes
- Primitive (flag-off path, race won): Local kernel memory read into the response buffer (information disclosure). The target address is chosen via the swapped pointer; the read length is bounded by the entry's
int16_tlength field (up to 65535 bytes per entry). - Constraints: The race window is a handful of instructions between the probe call and the pointer re-read. Success is probabilistic. The trailer pointers are the local process's own memory, so this is a same-process race against the kernel, not a remote or cross-privilege primitive on its own.
- Delivery caveat: In the patched binary this path only runs when
UxKirHttpReadingTrailersFromUmis zero; when the flag is set the pointer is captured once and the race is closed. The flag default is set by feature staging outside the binary.
7. Debugger Aids
Instruction addresses in the unpatched build for observing the double-fetch:
0x1C0028975—call UlProbeAnsiStringvalidatingptr1(source inrcx).0x1C00289A7—mov rdx, [r15+r12*8+8], the second read ofptr1before the copy. Ifrdxhere differs from the value validated at0x1C0028975, the pointer changed between probe and copy.0x1C00289B4—call memmoveusing the re-readptr1(rdx= source,rcx= destination,r8= length).0x1C0028A21/0x1C0028A31— the equivalent re-read and copy for the value pointerptr2.
Register notes at these sites: r15 = trailer array base (user-mode), r12*8 = byte offset of the current entry (index * 0x18), esi = remaining output space.
8. Changed Functions — Full Triage
Function names (the diff labels are addresses; names are recovered from the symbols in the disassembly).
UlGenerateTrailers@0x1C000CF94(change of interest): Adds theUxKirHttpReadingTrailersFromUm-gated pointer-capture path described above. The original double-fetch is retained in the flag-off branch. Copy helper ismemmove(0x1C0021200); probe helper isUlProbeAnsiString(0x1C0039148).UxQuicConnectionCopyBindings@0x1C0047B38(feature-staging): Adds aUxKirWritingAndReadingUmgate around amemmove(0x1C0047BEB). The flag-off branch executesmov ecx, ecx, truncating the destination pointer to 32 bits; the flag selects user-mode pointer-width handling. Operates under an exclusive push lock. Not a delivered security fix. (The address0x1C0047A68used elsewhere for this function is off; the function header is at0x1C0047B38.)UlpRebuildResponseHeaders@0x1C013E860(behavioral): Feature-flag branch simplification in the response-rebuild path.UlpCompleteSendToExtensionWorker@0x1C0060F10(behavioral): Restructures case handling in the send-response completion path (referencessendresponse.h). Feature-flag cleanup, not a direct security fix.UlHkeBuilderCopyFixedHeaders@0x1C015791C(behavioral): Header-copy helper; call targets updated to relocated equivalents of the samememmove/helpers. Behavioral/feature churn.UxStartEnvironmentModule@0x1C01757B0(initialization): Sets the WIL feature-staging flags.UxKirHttpReadingTrailersFromUmis set fromFeature_2347446586__private_IsEnabledDeviceUsage;UxKirWritingAndReadingUmfromFeature_1389320505__private_IsEnabledDeviceUsage; alsoUxKirChunkExtHeaderFixandUxKirSetCbtHardeningAsMedium. Emits WPP trace events for the flag states. Configuration plumbing, not a vulnerability.wil_details_FeatureReporting_IncrementOpportunityInCache@0x1C001EAD0(WIL telemetry): Feature-usage reporting cache helper. Loop/register churn.wil_details_FeatureReporting_IncrementUsageInCache@0x1C001EBB8(WIL telemetry): Feature-usage reporting cache helper. Loop/register churn.wil_details_FeatureReporting_RecordUsageInCache@0x1C001ECA4(WIL telemetry): Feature-usage reporting cache helper. Loop/register churn.
Note: 0x1C0028A91 is not a separate function; that address is inside the outlined cold section of UlGenerateTrailers.
9. Unmatched Functions
- Added: 0
- Removed: 0
No functions were added or removed. New symbols in the patched build are the WIL feature-staging entries Feature_2347446586 and Feature_1389320505 (replacing removed Feature_176206139), consistent with feature-flag rollout rather than new code paths.
10. Confidence & Caveats
- Confidence in the double-fetch pattern: High. The unpatched assembly shows the trailer string pointer read once for
UlProbeAnsiStringand re-read formemmoveat the addresses cited. - Confidence in the "delivered fix" claim: The mitigation is real code but is feature-staged (
UxKirHttpReadingTrailersFromUm) with the original double-fetch retained in the flag-off branch. It is therefore not an unconditionally delivered fix; the runtime default is set by feature configuration outside the binary. - Scope: Impact of the retained path is a local, race-dependent, same-process kernel information disclosure reachable via the HTTP Server API response path; it is not remote and not a self-contained privilege escalation.