http.sys UlComputeMultipleKnownHeaderSize integer overflow

http.sys recognises a fixed set of HTTP response headers — Content-Type, Content-Length, Cache-Control, Expires, ETag, Last-Modified, and a handful of others — and stores them in a dedicated “known header” array on the internal response object rather than in the generic variable-header blob. The point is speed: known headers are looked up by index, serialised from a cached literal, and never re-parsed. UlComputeMultipleKnownHeaderSize is the helper that walks the list of multiple-known-header entries (headers that may repeat, such as WWW-Authenticate) and sums the per-entry byte costs. It is called by the internal response constructor and by the cache-lookup path to produce one term of the overall response buffer size, and its size outputs feed the upstream response-buffer allocation.

Both inputs are attacker-controlled in transformation-style responders: the count of known headers present on the response (driven by what the responder chooses to emit, which reflects the request) and the per-slot byte total (which includes value lengths derived from request data). The vulnerable build sums them in a 32-bit loop with no per-iteration wrap check:

// UlComputeMultipleKnownHeaderSize — vulnerable build
// response    = arg3 (UL_RESPONSE_INTERNAL*),
// entryCount  = arg5,
// entries     = arg6 (UL_MULTIPLE_KNOWN_HEADER_ENTRY*, stride 0x18)
uint16_t authIndicator = response->AuthHeaderIndicator;     /* +0x208 */
ULONG    headerSizeSum = 0;   // accumulates each entry's per-header byte cost
ULONG    auxSum        = 0;   // accumulates each entry's secondary cost

for (USHORT i = 0; i < entryCount; i++) {
    if (entries[i].HeaderToken == 0x1d) {                    /* HttpHeaderAuthorization */
        /* UlpCalculateAuthHeaderSize sizing pre-pass, elided */
        continue;
    }

    ULONG singleSize;                                         /* out: var_54 */
    ULONG singleAux;                                          /* out: var_4c  */
    UlpCalculateSizeOfSingleMultipleHeader(request, compressFlag,
        entries[i].HeaderNameLength,        /* +0x08 */
        entries[i].HeaderValueLength,       /* +0x10 */
        tokenCtx, &singleSize, &singleAux);

    headerSizeSum += singleSize;     /* 32-bit add, no wrap check  <-- overflow */
    auxSum        += singleAux;       /* 32-bit add, no wrap check */
}

/* fold a response-header-map contribution into the accumulated total */
ULONG total = responseHeaderMapBytes + headerSizeSum;       /* no wrap check */
*outTotalSize = total;                                      /* *arg11 */
*outAuxSize   = auxSum;                                     /* *arg12 */
return 0;                                                   /* STATUS_SUCCESS */

The unguarded running sum is the bug. Sums that grow in a loop are the easiest place to wrap because the running total compounds across iterations — a single iteration cannot wrap but a thousand of them can, and a responder that lets the client steer many long known-header values turns this accumulator into an attacker-driven sum. With enough long-value slots the running total passes 2^32, the carry out of bit 31 is dropped silently, and the computed total handed back through the output parameter is a small value. The caller sizes the response buffer against that small value, allocates a small buffer, and the serialisation loop writes the full known-header set past the end.

The patch

The fix adds the standard wrap test inside the loop, mirrors it on the final response-header-map combine, and propagates either failure back to the caller as STATUS_INTEGER_OVERFLOW:

// UlComputeMultipleKnownHeaderSize — patched
ULONG headerSizeSum = 0;
ULONG auxSum        = 0;

for (USHORT i = 0; i < entryCount; i++) {
    if (entries[i].HeaderToken == 0x1d) { /* auth-header pre-pass, elided */ continue; }

    ULONG singleSize;
    ULONG singleAux;
    UlpCalculateSizeOfSingleMultipleHeader(request, compressFlag,
        entries[i].HeaderNameLength, entries[i].HeaderValueLength,
        tokenCtx, &singleSize, &singleAux);

    ULONG next = singleSize + headerSizeSum;     /* candidate new total */
    if (next < headerSizeSum) {                   /* wrapped past 2^32 */
        if (authScratch != NULL)
            ExFreePoolWithTag(authScratch, 0);
        return STATUS_INTEGER_OVERFLOW;            /* 0xC0000095 */
    }
    headerSizeSum = next;
    auxSum       += singleAux;                     /* fixed overhead, still unguarded */
}

ULONG total = responseHeaderMapBytes + headerSizeSum;      /* final combine */
if (total < headerSizeSum) {                                /* wrapped */
    if (authScratch != NULL)
        ExFreePoolWithTag(authScratch, 0);
    return STATUS_INTEGER_OVERFLOW;
}
*outTotalSize = total;
*outAuxSize   = auxSum;
return 0;

next < headerSizeSum after the add is the canonical detector — a sum of two non-negative numbers can only be smaller than its left operand if the add wrapped. The check is one line, catches every wrap regardless of addend size, and lets the caller refuse the response rather than recover. The same guard is repeated on the final combine that folds the response-header-map contribution in, because that add is just as able to wrap as any per-iteration one. The two guarded adds are applied as a pair, so a wrap at the per-iteration site or at the final combine is caught either way. The secondary auxSum accumulator is left unguarded; it collects a small fixed per-entry overhead, so it is not where this overflow lives.

Attack path

flowchart TD
    A["request drives many large known-header values onto the response"] --> B["UlComputeMultipleKnownHeaderSize sums per-entry byte costs in a 32-bit accumulator with no wrap check"]
    B --> C["accumulator wraps past 2^32; a small total is written to the size output parameter"]
    C --> D["caller sizes the response buffer against the wrapped total"]
    D --> E["header serialisation overruns the allocation, pool corruption"]

Reachable from any host that exposes http.sys and drives a transformation- style responder — the request supplies the headers that the responder echoes into the known-header slots.