1. Overview

Item Value
Unpatched binary http_unpatched.sys
Patched binary http_patched.sys
Overall similarity 0.9915 (effectively identical except for a targeted, feature-gated change)
Matched functions 4402
Changed functions 8
Identical functions 4394
Unmatched (either direction) 0 / 0

Verdict: The patch adds a new, configurable MaxHeadersCount limit to the HTTP/2 request header-processing function (UlParseRequestHeaderPairs @ 0x1400C7BD0 unpatched → 0x1400C7E20 patched). The entire new code path is gated behind the Known-Issue-Rollback / feature-staging flag UxKirMaxHeadersCountLimit, which is set at driver init from the WIL feature gate Feature_3936850235. The pre-patch code has no exploitable resource-exhaustion primitive: per-header work is constant, total work is linear in the header count, and the header count is already bounded by the existing MaxRequestBytes cap. This is feature-staged hardening / serviceability plumbing, not a fix for an exploitable vulnerability.


2. Change Summary

Field Value
Severity None (no security-relevant change)
Class Feature-gated resource-limit hardening (new configurable HTTP/2 header-count cap)
Affected function (unpatched) UlParseRequestHeaderPairs @ 0x1400C7BD0 (patched: 0x1400C7E20)
Reachability HTTP/2 header processing, reached via UlHttpReceiveHeadersEvent @ 0x1400C6CB0 (call at 0x1400C6FDE)

What the code actually does

UlParseRequestHeaderPairs receives a decoded array of HPACK header entries (arg2, with the 32-bit count at arg2+0x8) and walks it. For each entry it trims whitespace, identifies the four HTTP/2 pseudo-headers (:method, :scheme, :path, :authority) by length + strncmp, computes a name hash via HashCountedStringNoCaseA @ 0x14006D5A0, and dispatches on that hash to a small fixed set of known/sensitive header handlers with duplicate tracking held in fixed stack slots. All per-entry work is constant time; the loop is O(n) in the number of entries.

The number of entries is already bounded by the request-size cap MaxRequestBytes: each HPACK entry costs at least a few bytes, so a default 16 KB request yields on the order of a few thousand entries, and the per-entry work is cheap and constant. There is no super-linear behavior (name matching is hash-dispatched, duplicate tracking uses fixed slots), so there is no algorithmic-complexity amplification and no attacker-controlled unbounded work.

The patch

The patched build adds, before the loop:

  1. A feature-flag gate on UxKirMaxHeadersCountLimit: the new logic runs only when the flag is non-zero.
  2. A read of MaxHeadersCount from the server config at *(*(obj+0x18)+0x448)+0x1128, populated by UxReadHttp11ParserSettings (0x1400BB8B0 unpatched / 0x1400BBA58 patched).
  3. A count check: if (header_count > MaxHeadersCount + 4) { fault code = 0x14; return 0xC0000040 (STATUS_FAIL_CHECK); }.

MaxHeadersCount is read from the MaxHeadersCount registry value with default max(MaxRequestBytes >> 9, 0xC8) (default 200) and bounds min 0x32 / max 0xFFFF. The +4 tolerance covers the four mandatory HTTP/2 pseudo-headers.

Why this is not a security-relevant fix

  • The gate is a UxKir* Known-Issue-Rollback / feature-staging flag set at init via the WIL feature Feature_3936850235 (setnz cs:UxKirMaxHeadersCountLimit at 0x1400BC95E, immediately after call Feature_3936850235__private_IsEnabledDeviceUsageNoInline at 0x1400BC947). This is the standard hallmark of staged servicing behavior that Microsoft can roll back, not an unconditional security fix.
  • The pre-patch path is linear and already bounded by MaxRequestBytes; there is no exploitable resource-exhaustion primitive that the check closes.
  • No memory-safety difference exists in the diff (no OOB, no overflow, no UAF). The count check only short-circuits into an early error return.

3. Pseudocode Diff

// ============================================================
// UlParseRequestHeaderPairs  0x1400C7BD0 (unpatched) / 0x1400C7E20 (patched)
// arg1 (rcx) : request/connection object; obj = *(arg1+0x18)
// arg2 (rdx) : header array; arg2[0] = entries ptr, *(arg2+0x8) = entry count
// arg3 (r8d) : flags (bit 0x1000 captured and stored)
// ============================================================

// ---------- UNPATCHED (0x1400C7BD0) ----------
NTSTATUS ProcessH2Headers(void *reqCtx, H2HeaderArr *arg2, uint32_t flags) {
    obj = *(reqCtx + 0x18);
    flag1000 = flags & 0x1000;   // captured, stored locally

    // No header-count cap. Loop bound is the natural entry count.
    for (uint32_t i = 0; i < *(uint32_t*)(arg2 + 8); i++) {
        trim name/value whitespace;
        match :method / :scheme / :path / :authority by length + strncmp;
        hash = HashCountedStringNoCaseA(name);        // 0x14006D5A0
        dispatch on hash to known/sensitive header handlers;  // constant set
        track duplicates in fixed stack slots;
    }
    return STATUS_SUCCESS;
}

// ---------- PATCHED (0x1400C7E20) ----------
NTSTATUS ProcessH2Headers(void *reqCtx, H2HeaderArr *arg2, uint32_t flags) {
    obj = *(reqCtx + 0x18);
    flag1000 = flags & 0x1000;

    if (UxKirMaxHeadersCountLimit != 0) {                     // +NEW feature gate
        uint32_t count  = *(uint32_t*)(arg2 + 8);            // +NEW
        uint32_t maxHdr = *(uint32_t*)(*(void**)(obj + 0x448) + 0x1128); // +NEW
        if (count > maxHdr + 4) {                            // +NEW
            faultCode = 0x14;                                // +NEW
            return 0xC0000040; // STATUS_FAIL_CHECK          // +NEW
        }
    }

    for (uint32_t i = 0; i < *(uint32_t*)(arg2 + 8); i++) { /* same as before */ }
    return STATUS_SUCCESS;
}

The pre-loop block is the entire functional delta, and it is inert unless UxKirMaxHeadersCountLimit is non-zero.


4. Assembly Analysis

Patched build — added feature-gated count check (inside UlParseRequestHeaderPairs @ 0x1400C7E20)

00000001400C7ED1  mov     rcx, [rbp+4Fh+var_88]         ; rcx = obj (= *(arg1+0x18))
00000001400C7EDE  and     ebx, 1000h                    ; capture arg3 & 0x1000
00000001400C7EE4  cmp     cs:UxKirMaxHeadersCountLimit, r10b ; r10b=0; feature gate
00000001400C7EEB  mov     [rbp+4Fh+arg_10], ebx
00000001400C7EEE  mov     [rax], r10d
00000001400C7EF1  jz      loc_1400C8068                 ; flag==0 -> skip check, enter loop
00000001400C7EF7  mov     rax, [rcx+448h]               ; rax = server config
00000001400C7EFE  mov     r9d, [r12+8]                  ; r9d = header count (arg2[1])
00000001400C7F03  mov     r8d, [rax+1128h]              ; r8d = MaxHeadersCount
00000001400C7F0A  lea     eax, [r8+4]                   ; eax = MaxHeadersCount + 4
00000001400C7F0E  cmp     r9d, eax                      ; count vs limit+4
00000001400C7F11  jbe     loc_1400C8068                 ; within limit -> enter loop
; fall-through: set fault code and error status, then return
00000001400C7F48  mov     [rbp+4Fh+var_AC], 14h         ; fault code 0x14
00000001400C7F4F  mov     [rbp+4Fh+var_B0], 0C0000040h  ; STATUS_FAIL_CHECK
...
00000001400C8049  mov     eax, [rbp+4Fh+var_B0]         ; return value
00000001400C8066  retn

loc_1400C8068 is the loop-entry target (mov r15d, 1 at 0x1400C8068), i.e. both the "gate disabled" and "within limit" branches fall through to the same unchanged processing loop.

Unpatched build (UlParseRequestHeaderPairs @ 0x1400C7BD0)

The prologue captures the flag and goes straight to the loop bound — there is no feature flag read, no config read, and no count comparison:

00000001400C7C7B  and     ebx, 1000h                    ; capture arg3 & 0x1000
00000001400C7C81  mov     [rbp+4Fh+arg_10], ebx         ; store flag
00000001400C7C84  mov     [rax], r15d
00000001400C7C87  mov     eax, r15d                     ; loop counter = 0
00000001400C7C8A  mov     r15d, 1
...
00000001400C7C9C  cmp     eax, [r12+8]                  ; i < header count (loop bound only)
00000001400C7CA1  jnb     loc_1400C8336                 ; exit when done

Consequences of the absent sequence:

  • No read of [obj+0x448] (server config) in this path.
  • No read of [config+0x1128] (MaxHeadersCount).
  • No cmp count, MaxHeadersCount+4 — the only loop terminator is the natural entry count, which is itself bounded by MaxRequestBytes.

Supporting config change (UxReadHttp11ParserSettings, patched @ 0x1400BBA58)

00000001400BBB06  ...
00000001400BBB11  cmp     cs:UxKirMaxHeadersCountLimit, r12b ; gate
00000001400BBB1E  jz      short loc_1400BBB61           ; feature off -> skip
00000001400BBB20  shr     r8d, 9                        ; MaxRequestBytes >> 9
00000001400BBB24  lea     rdx, aMaxheaderscoun          ; "MaxHeadersCount"
00000001400BBB2B  mov     eax, 0C8h                     ; floor 200
00000001400BBB36  cmp     r8d, eax
00000001400BBB3C  cmovb   r8d, eax                      ; default = max(MaxReqBytes>>9, 200)
00000001400BBB40  lea     rax, [rsi+1128h]              ; write target = config+0x1128
00000001400BBB5C  call    UxReadBoundedULongSetting     ; read "MaxHeadersCount" reg value

Prior config fields are shifted (e.g. the old field is now written to [rsi+0x112C]) to make room for the new MaxHeadersCount at 0x1128.


5. Reachability

  1. HTTP/2 header processing reaches UlParseRequestHeaderPairs via UlHttpReceiveHeadersEvent @ 0x1400C6CB0 (call UlParseRequestHeaderPairs at 0x1400C6FDE). The pseudo-header set (:method, :scheme, :path, :authority) confirms this is the HTTP/2 header path.
  2. The function walks the decoded HPACK entry array; the entry count lives at arg2+0x8.
  3. In the patched build, when UxKirMaxHeadersCountLimit is non-zero, a request whose entry count exceeds MaxHeadersCount + 4 is rejected with STATUS_FAIL_CHECK (0xC0000040) before the loop runs. When the flag is zero, or in the unpatched build, the loop runs over all entries.
  4. The entry count is bounded by MaxRequestBytes, and the per-entry cost is constant, so neither build exposes an attacker-controlled unbounded workload.

6. Impact Assessment

  • Type: New configurable limit (defense-in-depth cap on HTTP/2 header count), feature-staged behind a KIR gate.
  • Attacker control before patch: The number of loop iterations equals the HPACK entry count, but that count is bounded by MaxRequestBytes and each iteration is constant time. The result is ordinary linear parsing work proportional to request size, with no amplification.
  • No memory-safety change: The diff adds only an early error return; there is no write/OOB/UAF difference between the builds.
  • Net effect: The patch lets an administrator (or the default max(MaxRequestBytes>>9, 200)) cap the HTTP/2 header count and reject over-count requests early. This is a robustness/serviceability improvement, not the closure of an exploitable flaw.

Factors that make this a non-issue as a vulnerability

Factor Effect
MaxRequestBytes Already bounds the encoded request, and therefore the entry count, before this function runs.
Per-entry cost Constant: hash-dispatched name matching + fixed-slot duplicate tracking. No O(n^2) behavior.
KIR / WIL gate Whole check is inert unless UxKirMaxHeadersCountLimit is set; classic staged-servicing pattern.
Memory safety Unchanged; the added code only short-circuits to an error return.

7. Verification Anchors

Addresses and data confirmed directly in both builds.

Function entries

Item Unpatched Patched
UlParseRequestHeaderPairs 0x1400C7BD0 0x1400C7E20
UlHttpReceiveHeadersEvent (caller) 0x1400C6CB0 (calls parser at 0x1400C6FDE unpatched)
HashCountedStringNoCaseA (per-entry name hash) 0x14006D5A0 0x14006D5A0
UxReadHttp11ParserSettings (config loader) 0x1400BB8B0 0x1400BBA58

Key instructions in the patched delta

Address (patched) Instruction Meaning
0x1400C7EE4 cmp cs:UxKirMaxHeadersCountLimit, r10b Feature gate (r10b = 0).
0x1400C7EF1 jz loc_1400C8068 Flag off -> skip check, enter loop.
0x1400C7EF7 mov rax, [rcx+448h] Load server config.
0x1400C7EFE mov r9d, [r12+8] Header count.
0x1400C7F03 mov r8d, [rax+1128h] MaxHeadersCount.
0x1400C7F0A lea eax, [r8+4] Limit = MaxHeadersCount + 4.
0x1400C7F0E cmp r9d, eax Count vs limit.
0x1400C7F11 jbe loc_1400C8068 Within limit -> enter loop.
0x1400C7F4F mov [rbp+var_B0], 0C0000040h STATUS_FAIL_CHECK on over-count.

Feature-flag origin

Address Instruction Meaning
0x1400BC947 call Feature_3936850235__private_IsEnabledDeviceUsageNoInline WIL feature-staging query.
0x1400BC95E setnz cs:UxKirMaxHeadersCountLimit Flag set from the WIL result at driver init.

The symbol UxKirMaxHeadersCountLimit does not exist in the unpatched binary (0 occurrences); it is introduced only in the patched build.

Struct / offset notes

arg1 (rcx)          : request/connection object
 +0x18   : obj (sub-object saved by both builds at prologue)

obj
 +0x448  : ServerConfig*

ServerConfig
 +0x1128 : uint32_t MaxHeadersCount   (patched only; written by UxReadHttp11ParserSettings)
                                        default max(MaxRequestBytes >> 9, 0xC8),
                                        reg value "MaxHeadersCount", bounds 0x32..0xFFFF
 +0x112C : field shifted +4 in the patched build to make room

arg2 (rdx)
 +0x00 : HeaderEntry* (decoded HPACK entries)
 +0x08 : uint32_t     entry count

Feature flag
 UxKirMaxHeadersCountLimit (byte) : 0 = check disabled, non-zero = check enabled
                                     set in UxStartEnvironmentModule @ 0x1400BC95E
                                     from WIL feature Feature_3936850235

8. Changed Functions — Full Triage

Function Sim. Change type Note
UlParseRequestHeaderPairs (0x1400C7BD0) 0.917 feature-gated behavioral Adds the MaxHeadersCount + 4 count check gated on UxKirMaxHeadersCountLimit, returning STATUS_FAIL_CHECK. Inert unless the flag is set; loop body unchanged. Not an exploitable-vulnerability fix (see sections 2 and 6).
UxReadHttp11ParserSettings (0x1400BB8B0) 0.9114 behavioral Config loader. When the flag is set, reads the MaxHeadersCount registry value (default max(MaxRequestBytes>>9, 0xC8), bounds 0x32..0xFFFF) into ServerConfig+0x1128; prior fields shift +4. Supporting infrastructure for the new limit.
UxDuoFaultCodeFromHttpReceiveFault (0x1400B9BB8) 0.819 behavioral Lookup-table update: adds fault-code case 0x14 (the new over-count fault), reads UxKirMaxHeadersCountLimit; remaining cases shift by one index. Telemetry/plumbing.
UxQuicErrorCodeFromHttpReceiveFault (0x1400FE03C) 0.849 behavioral Lookup-table update: adds case 0x14; case 4 changed from constant 0x4b to arg1+0x47; case 9 also arg1+0x47. HTTP/3 (QUIC) fault mapping plumbing.
UxpPktMonFaultToDropStatus (0x14014C4F8) 0.857 behavioral Lookup-table update: adds case 0x14 returning 0xD71 when the flag is set; case reorder. PacketMon drop-reason plumbing.
UxStartEnvironmentModule (0x1400BC5E0) 0.955 behavioral Driver init. Sets UxKirMaxHeadersCountLimit (and other UxKir* flags) from WIL feature gates; reordered init; recompiled call targets. This is where the flag is established.
UlpParseNextRequest (0x140029C20) 0.940 cosmetic Stack/variable renames only. No logic change.
UxPktMonTraceDropHttpReceiveWithFault (0x14014C1E0) 0.985 register_allocation Instruction reordering (lookup-table call hoisted). No semantic change.

The three lookup-table functions (UxDuoFaultCodeFromHttpReceiveFault, UxQuicErrorCodeFromHttpReceiveFault, UxpPktMonFaultToDropStatus) share one pattern: a new fault case 0x14 for the over-count outcome, gated on UxKirMaxHeadersCountLimit. These are telemetry / error-mapping plumbing for the new feature and are not independently security-relevant.


9. Unmatched Functions

None added, none removed. The patch is a pure in-place modification of existing functions.

  • No removed sanitizers or checkers.
  • No added mitigation helpers — the new logic lives inline in UlParseRequestHeaderPairs and its supporting config/telemetry functions.

10. Confidence & Caveats

Overall confidence: High (that this is not a security-relevant vulnerability fix).

  • The assembly delta is unambiguous: a feature-gated count comparison (count > MaxHeadersCount + 4) that is entirely absent from the unpatched build, plus supporting config parsing and fault/telemetry plumbing.
  • The whole path is gated behind the Known-Issue-Rollback / WIL feature-staging flag UxKirMaxHeadersCountLimit, set at init from Feature_3936850235. This is the standard staged-servicing pattern, not an unconditional security fix.
  • The pre-patch code has no exploitable resource-exhaustion primitive: per-entry work is constant, total work is linear in the entry count, and the entry count is already bounded by MaxRequestBytes. There is no super-linear amplification and no memory-safety difference.

Notes:

  1. Flag polarity. The check is active only when UxKirMaxHeadersCountLimit != 0. Its runtime default is determined by the WIL feature Feature_3936850235, which is not statically decidable from the binary alone.
  2. New limit semantics. With the flag enabled, over-count HTTP/2 requests are rejected early with STATUS_FAIL_CHECK; the default cap is max(MaxRequestBytes>>9, 200). This is a configurable robustness cap, not the closure of a demonstrable flaw.
  3. No corruption path. Nothing in the diff indicates memory corruption; the added code only short-circuits to an error return.