# CVE-2026-47291 — Windows `HTTP.sys` Kernel Pool Overflow via 16-bit Integer Overflow of the Buffer-Reference Array Capacity in `UlpParseNextRequest`

---

## Summary

| | |
|---|---|
| **Product** | Microsoft Windows — `HTTP.sys` (kernel-mode HTTP protocol stack) |
| **CVE ID** | CVE-2026-47291 |
| **Impact** | Remote Code Execution (kernel) / Denial of Service |
| **MSRC severity** | Critical |
| **CWE** | CWE-190: Integer Overflow or Wraparound → kernel pool heap overflow |
| **Patch Date** | June 9, 2026 |
| **Pre-patch binary** | `http.sys` 10.0.26100.8521 (May 2026) — SHA256 `808491dc0a89fb5f7a9bade3d7ba680bd685e184fc3de610e047eba9c366a64f` |
| **Post-patch binary** | `http.sys` 10.0.26100.8655 (KB, June 9 2026) — SHA256 `1a34d70a649f461745c782f1f27bcea6f6603aafd59842b3ed412181ac5c2c9a` |
| **Rollback control** | `UxKirRefBufferOverflowCheck` — the fix is behind a **Known Issue Rollback (KIR)** toggle |

---

## Product Description

`HTTP.sys` is the kernel-mode driver implementing the Windows HTTP protocol
stack — request parsing, response caching and (via SChannel) TLS termination for
IIS and any application that registers a URL prefix. It listens on the configured
TCP ports (80/443) and is reachable **pre-authentication** from the network,
which is what makes a memory-corruption bug here a Critical, wormable-class RCE.

The HTTP/1.x parser keeps a per-request state object with a dynamically grown
**buffer-reference array**: one 8-byte entry per receive buffer consumed while
parsing. Three fields drive it (offsets from our diff):

- `capacity` at `+0x640` — allocated slot count, a **16-bit** field
- `count` at `+0x642` — slots in use, a **16-bit** field
- `ref_array_ptr` at `+0x648` — pointer to the entry array

---

## Vulnerability Summary

While parsing HTTP/1.x header lines, `UlpParseNextRequest` adds a buffer
reference each time it consumes a new receive buffer. When `count` reaches
`capacity`, it grows the array. The growth arithmetic increments the **16-bit**
`capacity` by 5 with **no overflow check**:

```c
// UlpParseNextRequest (http.sys 10.0.26100.8521) — PRE-PATCH, inline growth, from our diff
if (*(ushort *)(state + 0x642) < *(ushort *)(state + 0x640)) {   // count < capacity: fast path
    ref_array[count]     = new_buffer;
    *(short *)(state + 0x642) += 1;                              // count++
}
else {                                                          // grow
    _Dst = ExAllocatePool3(0x42, (ulonglong)capacity * 8 + 0x28, ...);   // 0x28 + capacity*8
    if (_Dst) {
        memmove(_Dst, ref_array_ptr, (ulonglong)count << 3);    // copy count*8 bytes
        if (1 < capacity) ExFreePoolWithTag(ref_array_ptr, 0);
        *(short *)(state + 0x640) = capacity + 5;               // *** capacity += 5, 16-bit, NO CHECK ***
        ref_array_ptr = _Dst;
    }
}
```

After **13,107** growth events `capacity` = `0xFFFB`. The next growth computes
`0xFFFB + 5 = 0x10000`, which truncates to **`0x0000`** in the 16-bit field. On
the following buffer reference, `count` (now ≥ 65,536) exceeds the zeroed
`capacity`, forcing another growth — but this time the allocation is
`0x28 + 0*8 = 40 bytes`, while the `memmove` copies `count << 3` ≈ **524,256
bytes** from the old array into the 40-byte buffer. The result is a **kernel
NonPagedPool overflow of ~500 KB** with attacker-influenced contents.

---

## Prerequisites and Constraints

- **Remote, unauthenticated** — `AV:N`, `PR:N`. Reachable by anyone who can open
  an HTTPS connection to the server.
- **HTTP/1.x over TLS only.** HTTP/2 and HTTP/3 use different parser paths that
  do not touch this array. Body parsing (Content-Length / chunked) does not grow
  it either — only **header** parsing does.
- **One header line per TLS record.** Over TLS, SChannel delivers each TLS
  application-data record to the parser as a separate buffer via
  `UlHttpBufferReceiveEvent()`, giving a 1:1 record→reference mapping. Plaintext
  HTTP coalesces segments (and `UlpMergeBuffers` merges further), so the count
  never climbs — the bug is **specific to the per-record TLS delivery path**.
- **Non-default `MaxRequestBytes`.** ~65,536 references at ~4 bytes/line ≈ 262 KB
  of request; the server must have `MaxRequestBytes` ≥ 262,144 (default 16,384,
  which caps at ~4,000 lines). Keeping `MaxRequestBytes` ≤ 65,535 is the
  configuration mitigation.
- Slow: at ~10 ms/record the overflow takes ~11 minutes of sustained connection.

---

## Vulnerability Details

### Call Chain

```
Remote attacker (no auth), HTTPS:
  many TLS application-data records, one HTTP/1.x header line each
        ↓  (SChannel decrypts per-record)
Kernel (http.sys):
  UlHttpBufferReceiveEvent -> UlpCopyIndicatedData
    -> UlpParseNextRequest            [*** 16-bit capacity overflow -> undersized alloc ***]
         ExAllocatePool3(0x28 + capacity*8)   // 40 bytes after wrap
         memmove(count << 3)                  // ~500 KB copy  -> POOL OVERFLOW
    -> UlpAdjustBuffers (non-merge path advances to next buffer)
```

### Root Cause

`capacity` is a 16-bit counter incremented by a constant 5 with no check that the
addition stays within 16 bits. Once it wraps to 0 the allocation size (derived
from `capacity`) collapses to the 40-byte base while the copy length (derived
from `count`) does not — a classic size-desync heap overflow born from an
integer-width overflow (CWE-190 → CWE-122).

### The patch

The June build **extracts the growth logic out of `UlpParseNextRequest` into a
new routine `UlpReferenceBuffers`** and adds the missing overflow check — but
ships **both** the old and corrected paths, selected at runtime by the global
`UxKirRefBufferOverflowCheck`:

```c
// UlpReferenceBuffers (http.sys 10.0.26100.8655) — PATCHED, from our diff
uVar3 = *(ushort *)(state + 0x640);                 // capacity
if (uVar3 <= *(ushort *)(state + 0x642)) {          // count >= capacity: grow
    if (UxKirRefBufferOverflowCheck == '\0') {
        /* ORIGINAL vulnerable path: ExAllocatePool3(0x28 + capacity*8),
           memmove(count<<3), capacity += 5  — no overflow check */
    }
    else {
        uVar6 = uVar3 + 5;                           // new capacity
        if (uVar6 < uVar3) goto bail;               // *** 16-bit overflow check ***
        pvVar5 = ExAllocatePool3(0x42, (ulonglong)uVar6 << 3, ...);
        if (pvVar5 == NULL) goto bail;
        memmove(pvVar5, ref_array_ptr, (ulonglong)count << 3);
        if (1 < capacity) ExFreePoolWithTag(ref_array_ptr, 0);
        *(ushort *)(state + 0x640) = uVar6;         // store the checked value
        ref_array_ptr = pvVar5;
    }
}
```

The fix is one comparison — `new = capacity + 5; if (new < capacity) fail;` —
the exact 16-bit wraparound guard that was missing.

### Patch Completeness Assessment

**The fix is behind a Known Issue Rollback (KIR) toggle.** The corrected branch
runs only when the global byte `UxKirRefBufferOverflowCheck` is non-zero; when it
is zero, the patched binary executes the **original unchecked growth** and
remains overflowable. The `Kir` in the symbol name marks this as a Known Issue
Rollback control — Microsoft's mechanism for disabling a servicing change in the
field without pulling the update. So, exactly as with the CFR feature-flag
gating seen across the CLFS, `lserver` and `tapisrv` fixes in this corpus, **file
version alone does not determine patch state**: a machine on `.8655` with the KIR
toggle rolled back still has the reachable overflow. The pattern now spans a
remote, pre-auth, Critical kernel RCE — implemented here as a KIR global rather
than a `Feature_NNNN` CFR flag, but with the same "both paths ship" consequence.

---

## Detection Guidance

**Decrypted inspection (preferred).** Count header field lines per HTTP/1.x
request; more than ~1,000 in a single request on one connection is suspicious
(ZDI's threshold). Legitimate requests rarely exceed a few dozen headers.

**Encrypted heuristic.** Without decryption, watch for a single TLS session
carrying a large number (>1,000) of small application-data records, each holding
one short payload — the per-record header delivery the exploit depends on. Higher
false-positive rate (interactive streaming can look similar).

**Duration.** The attack needs sustained connection (~11 min at 10 ms/record);
long-lived HTTPS connections emitting a steady stream of tiny records are a
supplementary signal.

**Crash signature.** Bugchecks inside `http.sys` around `UlpParseNextRequest` /
`UlpReferenceBuffers` / `memmove`, NonPagedPool corruption. Special Pool on
`http.sys` makes the overrun land on a guard page at the copy.

**Config.** Confirm `MaxRequestBytes` (under
`HKLM\SYSTEM\CurrentControlSet\Services\HTTP\Parameters`) is ≤ 65,535 on any host
where the KIR-gated fix cannot be guaranteed enabled.

---

## Mitigation

- **Patch** (MS June 2026) — and ensure the KIR toggle `UxKirRefBufferOverflowCheck`
  is enabled; the fix is inert if the Known Issue Rollback is active.
- **`MaxRequestBytes` ≤ 65,535** blocks the request size needed to reach the
  wrap, for unpatched or KIR-rolled-back hosts.
- Terminate/inspect TLS at a proxy that enforces a sane header-count limit.

---

## References

- ZDI / TrendAI Research — *CVE-2026-47291: Remote Code Execution in the Windows
  HTTP.sys* (Yazhi Wang, Jonathan Lein). `thezdi.com/blog/2026/7/9/...`
- MSRC advisory — CVE-2026-47291 (HTTP.sys Remote Code Execution)
- Full binary diff: `/data/patch_diffs/http_sys-cve-2026-47291-ghidriff.md`
