http.sys UlCalculateFastForwardDataContextSize integer overflow

The “fast-forward” path in http.sys is the streaming data path: the in-kernel pipe between an upstream content source (a responder app, a cached body, a forwarded connection) and the response socket, used when the response is too large or too dynamic to assemble in one shot. Each stream carries a fast-forward data context — a control structure holding the per-stream state, the variable storage area for headers and content negotiated for this stream, and the tail bookkeeping (pending byte ranges, MDL chain pointers, chunk accounting) that lets the kernel push body bytes through without crossing into user mode for each chunk.

UlCalculateFastForwardDataContextSize is the sizer for that context. It takes a count of variable-storage entries, scales it by 0x10, aligns the result up to the kernel’s natural alignment with (scaled + 7) & ~7, and adds a fixed 0xf0 of overhead to produce a single byte count the caller hands to ExAllocatePool3. The variable count is request-influenced — it reflects the negotiated header set and range depth — so a many-range or many-header request drives count << 4 into the region where the 32-bit arithmetic wraps.

The vulnerable build does the align-up and the tail-add in 32-bit, with no per-operation wrap check:

// UlCalculateFastForwardDataContextSize — vulnerable build
uint64_t scaled = (uint64_t)count << 4;          // arg1 zero-extended, scaled by 0x10 per storage entry
*outSize = 0;
if (scaled > 0xffffffff)                         // guard: scaled must fit in 32 bits
    return STATUS_INTEGER_OVERFLOW;              //   0xC0000095 — passes for every scaled up to 0xfffffff0
*outSize = (((uint32_t)scaled + 7) & ~7u)        // 32-bit align-up to 8 bytes
         + 0xf0;                                 //   then +0xf0 of fixed overhead, also 32-bit
return STATUS_SUCCESS;                           //   neither add is wrap-checked

Only one of those two adds can actually fire here. count << 4 is always sixteen-aligned, so the inner +7 of the align-up never carries: the largest scaled value to clear the guard is 0xfffffff0, plus 7 lands at 0xfffffff7 still inside 32 bits, and the & ~7 mask snaps it back to 0xfffffff0. The aligned result simply equals scaled. The add that wraps is the +0xf0 tail — with scaled at 0xfffffff0 the sum is 0x100000000, which truncates to a small value in 32-bit, and for any scaled from 0xfffff110 up to 0xfffffff0 the wrapped total lands somewhere under 0xf0. The caller hands that wrapped total to the allocator; the initialisation code that fills the variable storage and the tail bookkeeping then overruns the buffer.

The mistake is to treat a rounding macro as benign bookkeeping rather than sizing arithmetic. (n + 7) & ~7 is an add followed by a mask, and the add is as capable of wrapping as any other — even when, as here, the call site’s input alignment happens to mask the wrap on the +7 while leaving the next add in the expression fully exposed. On attacker-controlled n, every add in the size expression has to be widened and tested; the same applies to ALIGN_UP_BY, ALIGN_UP, and every other rounding macro that compiles to add-and-mask.

The patch

The fix adds an explicit wrap test after each add that can carry. The align-up is recomputed in 64-bit and compared back against its input; the +0xf0 tail-add stays in 32-bit but is now followed by a total < 0xf0 test:

// UlCalculateFastForwardDataContextSize — patched
uint64_t scaled = (uint64_t)count << 4;
*outSize = 0;
if (scaled > 0xffffffff)
    return STATUS_INTEGER_OVERFLOW;                       // 0xC0000095

uint64_t alignedInput = (uint64_t)(uint32_t)scaled;
if (((alignedInput + 7) & ~7ull) < alignedInput)           // wrap check #1: the align-up add
    return STATUS_INTEGER_OVERFLOW;

uint32_t total = (((uint32_t)scaled + 7) & ~7u) + 0xf0;
if (total < 0xf0) {                                          // wrap check #2: the +0xf0 overhead add — this is the one that fires
    *outSize = 0xffffffff;                                   //   sentinel written before the error return
    return STATUS_INTEGER_OVERFLOW;
}
*outSize = total;
return STATUS_SUCCESS;

A size expression with multiple operations has to be checked at every operation that can wrap, and the right shape of check is “the result went backwards” — ((x + 7) & ~7) < x for the align-up, total < 0xf0 for the overhead add. A sum of two non-negative numbers can only be smaller than one of its addends if it wrapped, so each test is a precise wrap detector rather than a loose ceiling. Two operations mean two such tests; a single composite test at the end would be decorative when the wrap happens upstream of it, and the 0xf0 add stays 32-bit only because the < 0xf0 test now catches its carry.

Attack path

flowchart TD
    A["anonymous HTTP request, many-header or many-range stream"] --> B["UlCalculateFastForwardDataContextSize: count times 0x10 clears the 32-bit guard at scaled near 0xfffffff0"]
    B --> C["32-bit +0xf0 tail-add wraps the total past 2^32 down to a value under 0xf0"]
    C --> D["caller allocates the wrapped total"]
    D --> E["variable-storage and tail writes overrun the allocation, pool overflow, RCE or DoS"]

The streaming path is the one taken for any large or many-range response — exactly the shape an attacker can force a server to emit by request choice alone.