June 26, 2026
When 65,536 HTTP Headers Become Zero
Reverse Engineering CVE-2026–47291 in Windows HTTP.sys

By Anthony Cihan
19 min read
Reverse Engineering CVE-2026–47291 in Windows HTTP.sys
Microsoft classified CVE-2026–47291 as a critical, pre-authentication remote code execution vulnerability in the Windows HTTP Protocol Stack.
That description immediately raised several questions.
What integer overflows?
What object contains it?
How does the wrapped value influence memory allocation?
Can an ordinary network request naturally reach the vulnerable operation?
And if memory corruption occurs, how much control does an attacker actually have over the data being written?
After patch diffing multiple versions of http.sys, tracing the HTTP/2 header-processing path, building a boundary-testing client, examining the HTTP Kernel Extension architecture, and validating the final operation with Driver Verifier, I confirmed the underlying vulnerability:
A 16-bit counter used to track unknown HTTP headers can wrap to zero while the corresponding linked list still contains 65,536 entries.
A downstream builder then allocates a buffer based on the wrapped counter but fills it by traversing the full linked list.
The result is an approximately 1.5 MB sequential overwrite of kernel pool memory.
However, the path from an HTTP/2 request to that overflow is considerably more complicated than the CVE description suggests.
On the default configurations I tested, several independent controls prevented the vulnerable builder from naturally receiving the inconsistent request state. Reproducing the corruption required controlled changes to an isolated laboratory system.
This article documents both sides of that result:
- why the memory corruption is real;
- and why that does not automatically mean every default IIS server is remotely exploitable.
Research disclaimer:_ All testing described in this article was performed against isolated, authorized laboratory systems. The specific kernel memory read/write mechanism used during controlled validation is intentionally withheld. No production systems were tested._
Why HTTP.sys Matters
http.sys is the kernel-mode HTTP protocol stack used by Windows.
Applications such as IIS can register URL namespaces with it and allow the kernel to handle significant portions of connection management, request parsing, queuing, and protocol processing before traffic reaches application code.
This provides performance advantages, but it also means that a memory-safety error in the wrong part of http.sys does not merely crash a worker process.
It occurs in kernel context.
For HTTP/2 requests, a simplified version of the relevant processing flow looks like this:
Remote client
|
v
TLS and ALPN negotiation
|
v
HTTP/2 frame processing
|
v
HPACK header-block decoding
|
v
Known and unknown header classification
|
v
Internal HTTP request object
|
v
Optional HTTP Kernel Extension callback
|
v
Header marshalling bufferRemote client
|
v
TLS and ALPN negotiation
|
v
HTTP/2 frame processing
|
v
HPACK header-block decoding
|
v
Known and unknown header classification
|
v
Internal HTTP request object
|
v
Optional HTTP Kernel Extension callback
|
v
Header marshalling bufferThe vulnerability exists near the bottom of that chain.
The attacker begins by supplying HTTP/2 header fields. http.sys converts those fields into internal request state. A later component constructs a separate marshalled representation of that request.
The dangerous condition occurs when the metadata used to size that representation no longer agrees with the data structure used to populate it.
Beginning with the Patch
My analysis began with a vulnerable and patched version of http.sys.
Rather than searching blindly through the entire driver for arithmetic involving small integers, I started by examining what Microsoft added.
One of the most significant changes was a new header-count limit named:
MaxHeadersCountMaxHeadersCountThe patched driver calculates a default value from the configured maximum request size, applies lower and upper bounds, and stores the result in the HTTP configuration structure.
The patched HTTP/2 processing path then performs a check conceptually equivalent to:
if (feature_enabled &&
decoded_header_count > MaxHeadersCount + 4)
{
return STATUS_INVALID_PARAMETER_2;
}if (feature_enabled &&
decoded_header_count > MaxHeadersCount + 4)
{
return STATUS_INVALID_PARAMETER_2;
}The additional four entries account for the standard HTTP/2 pseudo-headers commonly present in a request:
:method
:path
:scheme
:authority:method
:path
:scheme
:authorityThis check occurs before the request can accumulate enough header state to reach the vulnerable integer boundary.
That provided the first major clue.
Microsoft was not merely limiting the total byte length of individual fields. The patch specifically prevented the decoded header count from growing beyond a configured threshold.
The next question was why reaching a large number of headers was dangerous.
Unknown Headers and the 16-Bit Counter
HTTP implementations frequently treat common, recognized fields differently from extension or application-defined fields.
In the vulnerable http.sys path, unknown headers are represented using a linked structure associated with the request object.
The request also contains a counter tracking how many unknown-header entries have been created.
That counter is only 16 bits wide.
In the build I analyzed, it was stored at:
request + 0x7BCrequest + 0x7BCConceptually, the field is:
USHORT UnknownHeaderCount;USHORT UnknownHeaderCount;A 16-bit unsigned integer can represent values from zero through 65,535.
The next increment wraps it back to zero:
0xFFFF + 1 = 0x00000xFFFF + 1 = 0x0000Therefore, after inserting exactly 65,536 qualifying unknown headers:
Actual unknown-header nodes: 65,536
Recorded 16-bit count: 0Actual unknown-header nodes: 65,536
Recorded 16-bit count: 0But reaching this condition required more than repeating the same field thousands of times.
The names must be distinct
During testing, repeated unknown header names did not produce an independent linked-list node for every occurrence.
http.sys merged duplicate names into the existing representation.
A request containing this field repeatedly:
x-test: a
x-test: a
x-test: a
...x-test: a
x-test: a
x-test: a
...could exercise other header-counting or resource-consumption behavior, but it would not produce 65,536 separate unknown-header nodes.
The request needed distinct names:
x-flood-000000: a
x-flood-000001: a
x-flood-000002: a
...
x-flood-065535: ax-flood-000000: a
x-flood-000001: a
x-flood-000002: a
...
x-flood-065535: aThis distinction became important later because the patched total-header-count protection and the vulnerable unknown-header counter do not necessarily measure identical things.
The Defensive Check That Happens Too Late
The producer function responsible for creating unknown-header entries does contain a wrap check.
At first glance, that seems as though it should prevent the vulnerability.
The relevant logic is conceptually similar to:
InsertTailList(
&request->UnknownHeaderList,
new_header
);
request->UnknownHeaderCount++;
if (request->UnknownHeaderCount == 0)
{
return STATUS_INVALID_PARAMETER;
}
request->UnknownHeaderBytes += entry_size;
*output_header = new_header;
return STATUS_SUCCESS;InsertTailList(
&request->UnknownHeaderList,
new_header
);
request->UnknownHeaderCount++;
if (request->UnknownHeaderCount == 0)
{
return STATUS_INVALID_PARAMETER;
}
request->UnknownHeaderBytes += entry_size;
*output_header = new_header;
return STATUS_SUCCESS;The function increments the counter and checks whether the result became zero.
If it did, the function returns:
STATUS_INVALID_PARAMETER
0xC0000010STATUS_INVALID_PARAMETER
0xC0000010The problem is the order of operations.
The new node is linked into the request's doubly linked list before the counter is incremented and checked.
At the 65,536th insertion, the state temporarily becomes:
UnknownHeaderCount = 0
UnknownHeaderList = 65,536 nodesUnknownHeaderCount = 0
UnknownHeaderList = 65,536 nodesThe function reports an error, but the request object has already been modified.
The error path also skips some of the normal success bookkeeping, including output assignment and size accounting.
This produces an internally inconsistent object:
Counter says: no unknown headers
List says: 65,536 unknown headersCounter says: no unknown headers
List says: 65,536 unknown headersThis does not automatically prove exploitation.
If every caller immediately destroys the request object and no other component observes the inconsistent state, the error check still prevents a security boundary from being crossed.
The next step was therefore to identify whether another function trusted the wrapped counter while independently consuming the linked list.
That function existed.
The Vulnerable Builder
The vulnerable sink is a request-header builder associated with the HTTP Kernel Extension layer.
I refer to it using the symbolized name:
UlpHkeBuilderPrepareForRequestUlpHkeBuilderPrepareForRequestThe function creates a kernel pool allocation used to marshal request information for an HKE consumer.
Its allocation calculation reads the 16-bit unknown-header count directly from the request:
USHORT count =
*(USHORT *)(request + 0x7BC);
SIZE_T allocation_size =
0x380 + ((SIZE_T)count * 0x18);
buffer = ExAllocatePool3(
0x100,
allocation_size,
'UlKB'
);USHORT count =
*(USHORT *)(request + 0x7BC);
SIZE_T allocation_size =
0x380 + ((SIZE_T)count * 0x18);
buffer = ExAllocatePool3(
0x100,
allocation_size,
'UlKB'
);The pool tag is:
UlKBUlKBThe important formula is:
allocation_size = 0x380 + count × 0x18allocation_size = 0x380 + count × 0x18Each unknown-header output record occupies:
0x18 bytes0x18 bytesWhen the count contains the correct number of entries, this formula is reasonable.
When the count has wrapped to zero, the builder allocates only:
0x380 bytes0x380 bytesor:
896 bytes896 bytesThe unknown-header output region begins at offset 0x290 inside that allocation.
That leaves:
0x380 - 0x290 = 0xF0 bytes0x380 - 0x290 = 0xF0 bytesavailable for unknown-header records.
Since each entry is 0x18 bytes:
0xF0 / 0x18 = 10 entries0xF0 / 0x18 = 10 entriesThe wrapped allocation therefore has room for approximately ten unknown-header output records.
The request object, however, can contain 65,536 linked nodes.
Allocation by Counter, Copy by Linked List
The most important part of the vulnerability is that the builder does not use the count to decide how many entries to copy.
It walks the linked list until it reaches the list-head sentinel.
The relevant loop is conceptually:
node = request->UnknownHeaderList.Flink;
while (node != &request->UnknownHeaderList)
{
output->Index = index;
output->Flags = 1;
output->NameLength = node->NameLength;
output->ValueLength = node->ValueLength;
output->Name = node->Name;
output->Value = node->Value;
output++;
node = node->Flink;
}node = request->UnknownHeaderList.Flink;
while (node != &request->UnknownHeaderList)
{
output->Index = index;
output->Flags = 1;
output->NameLength = node->NameLength;
output->ValueLength = node->ValueLength;
output->Name = node->Name;
output->Value = node->Value;
output++;
node = node->Flink;
}The machine-code structure clearly demonstrates the mismatch:
mov rdx, qword ptr [rsi] ; first list node
copy_loop:
mov word ptr [r8], di ; write entry index
...
lea r8, [r8+18h] ; next output record
...
mov rdx, qword ptr [rdx] ; next linked-list node
cmp rdx, rsi ; reached sentinel?
jne copy_loopmov rdx, qword ptr [rsi] ; first list node
copy_loop:
mov word ptr [r8], di ; write entry index
...
lea r8, [r8+18h] ; next output record
...
mov rdx, qword ptr [rdx] ; next linked-list node
cmp rdx, rsi ; reached sentinel?
jne copy_loopTwo independent values therefore control the operation:
Allocation bound: 16-bit counter
Copy bound: linked-list lengthAllocation bound: 16-bit counter
Copy bound: linked-list lengthOnce those values become desynchronized, the allocation and copy operations no longer agree.
This is the root cause of CVE-2026–47291.
The Overflow Arithmetic
At the exact 16-bit boundary:
PropertyValueDistinct unknown headers65,536Counter width16 bitsCounter after wrap0Base allocation0x380 bytesPer-entry output size0x18 bytesActual entry-copy volume0x180000 bytesDecimal copy volume1,572,864 bytesApproximate overrun1.5 MB
The builder allocates for zero unknown headers but attempts to marshal the full linked list.
This is not a subtle one-byte overwrite.
It is a large, sequential kernel pool overflow.
Building the HTTP/2 Boundary Test
To reach the producer boundary, I wrote a client that constructs one HTTP/2 request containing exactly 65,536 distinct unknown fields.
The client performs the following operations:
- Establishes a TLS connection.
- Requests HTTP/2 through ALPN.
- Sends the HTTP/2 client preface.
- Completes the initial SETTINGS exchange.
- Builds the four required pseudo-headers.
- Generates 65,536 distinct unknown header names.
- HPACK-encodes the complete header block.
- Splits the block across one HEADERS frame and multiple CONTINUATION frames.
- Sends the completed request.
- Decodes any response, RST_STREAM, or GOAWAY frame.
- Probes whether the listener remains available.
A simplified construction looks like:
headers = [
(b":method", b"GET"),
(b":path", b"/"),
(b":scheme", b"https"),
(b":authority", authority),
]
for i in range(65536):
headers.append((
b"x-flood-%06d" % i,
b"a"
))headers = [
(b":method", b"GET"),
(b":path", b"/"),
(b":scheme", b"https"),
(b":authority", authority),
]
for i in range(65536):
headers.append((
b"x-flood-%06d" % i,
b"a"
))The resulting HPACK block in my test was approximately 947 KB and required dozens of HTTP/2 frames.
The transmitted size is not the only relevant measurement, however.
MaxRequestBytes enforcement is based on decoded request accounting. HPACK compression does not allow an attacker to bypass every server-side request-size restriction merely by making the wire representation smaller.
The diagnostic also includes a repeated-name mode, but that mode serves a different purpose.
Repeated names can exercise total decoded-field-count protections. They do not recreate the old unknown-header linked-list growth necessary for the 16-bit wrap.
Why a Network Error Is Not Proof
Testing this vulnerability solely from the client can produce misleading conclusions.
Several outcomes may look almost identical over the network:
- an existing request-size limit rejects the input;
- the old 16-bit wrap check rejects the request;
- the patched
MaxHeadersCountcheck rejects the request; - another HTTP/2 protocol limit is reached;
- the server emits RST_STREAM;
- the server emits GOAWAY;
- the connection closes abruptly;
- the request stalls;
- or the kernel crashes.
A connection reset does not prove memory corruption.
A GOAWAY frame does not prove the new patch blocked the request.
Even an abrupt closure after the final CONTINUATION frame does not reveal which kernel branch handled the completed header list.
Distinguishing those conditions requires kernel breakpoints, ETW or WPP evidence where available, or postmortem dump analysis.
This is why I describe the client as a boundary diagnostic, not as a universal remote exploit.
The Three Gates on a Default Configuration
The vulnerable arithmetic and copy mismatch existed in the driver, but the tested stock IIS configuration did not naturally reach the overflow.
Three independent conditions blocked the path.
Gate One: MaxRequestBytes
The default MaxRequestBytes value on the tested configuration was:
16,384 bytes16,384 bytesThe decoded size of 65,536 distinct header names greatly exceeds that value.
Without increasing the request-size limit, http.sys rejects the request before enough unknown-header entries can accumulate to wrap the counter.
For the isolated laboratory reproduction, I increased the limit to provide sufficient headroom for the decoded request.
This is an important practical distinction.
The mere presence of HTTP/2 does not mean the boundary can be reached under default request-size policy.
Gate Two: The HKE Consumer
The vulnerable builder is not part of the most direct stock IIS request path.
It is reached through the HTTP Kernel Extension layer when an appropriate extension callback consumes or modifies the request.
On the tested Windows 11 and Windows Server configurations, I did not observe an active HKE consumer populating the relevant provider slot.
With that slot null, the vulnerable builder was not invoked through ordinary stock IIS processing.
The producer could reach its 16-bit boundary and report an error, but the dangerous UlKB marshalling operation did not occur.
Gate Three: Producer Error Propagation
The producer detects the wrap and returns an error.
The normal caller responds by aborting request processing before the request reaches a state where the HKE builder would ordinarily consume it.
The link-before-check ordering leaves an inconsistent object, but standard error propagation prevents later processing on the configuration I tested.
Together, these controls produced the following result:
Default request-size policy
+
No active HKE consumer
+
Producer wrap error
=
No naturally observed overflow on stock IISDefault request-size policy
+
No active HKE consumer
+
Producer wrap error
=
No naturally observed overflow on stock IISThat does not eliminate the vulnerable code.
It means the vulnerable state and the vulnerable consumer were separated by environmental and control-flow gates.
What Is an HTTP Kernel Extension?
The HKE mechanism is exposed through the Windows Network Module Registrar, or NMR.
NMR allows kernel modules to advertise and bind to Network Programming Interfaces.
At a high level:
http.sys registers an NMR provider
|
v
A kernel client binds to a matching NPI
|
v
http.sys records the attached client
|
v
HTTP request processing can dispatch callbackshttp.sys registers an NMR provider
|
v
A kernel client binds to a matching NPI
|
v
http.sys records the attached client
|
v
HTTP request processing can dispatch callbacksThe type-2 interface relevant to this builder uses the following NPI identifier:
{9c6158b1-0c87-494e-9f6d-d372cb95976f}{9c6158b1-0c87-494e-9f6d-d372cb95976f}A client capable of binding to that interface could cause the corresponding callback path to become active.
Potential architectural use cases could include:
- kernel-mode HTTP inspection;
- security or data-loss-prevention components;
- enterprise networking middleware;
- accelerated request-processing components;
- cloud host or fabric agents;
- or internal Windows networking functionality.
I did not identify a naturally active consumer on the tested systems.
The tcpip.sys observation
During driver scanning, I found the type-2 HKE NPI identifier in tcpip.sys.
The binary also contained NMR-related imports, including functionality associated with both client and provider registration.
This is interesting, but it must be interpreted conservatively.
The presence of a GUID and related imports does not prove that tcpip.sys registers as the relevant HKE consumer during normal operation.
It may represent:
- a conditional feature path;
- an environment-specific path;
- a deprecated implementation;
- unrelated NMR functionality in the same binary;
- or code that is not reachable on current configurations.
On the tested system, the relevant HKE slot remained null.
I did not observe the registration occur.
Further work is required to determine whether a Windows role, feature flag, server workload, cloud environment, Hyper-V configuration, or networking feature activates that path.
Until such a configuration is identified, the responsible conclusion is:
The tested default configurations did not contain an active HKE consumer, but evidence in the networking stack justifies continued investigation.
Controlled Validation of the Dormant Sink
Static analysis provided a strong case for memory corruption, but I wanted to observe the original builder perform the invalid write.
To do that, I used an isolated Windows 11 virtual machine with:
- a vulnerable
http.sysbuild; - IIS and HTTP/2 enabled;
- an elevated request-size limit;
- Driver Verifier Special Pool enabled for
http.sys; - kernel debugging and crash-dump collection;
- and controlled in-memory instrumentation.
A lab-only kernel memory read/write primitive was used to modify the target VM's in-memory state.
The specific driver, device interface, control codes, mapping structure, and implementation details are intentionally withheld. Those details are not necessary to understand the http.sys root cause and would unnecessarily lower the barrier to modifying protected kernel memory.
The controlled changes served three conceptual purposes:
- Activate the HKE dispatch path in the absence of a naturally registered consumer.
- Construct enough synthetic extension state for the request to reach the original builder.
- Allow the wrapped request object to continue past the existing producer error path.
These changes did not create the vulnerable allocation formula.
They did not create the linked-list copy loop.
They did not change the 16-bit width of the counter.
They removed the gates that prevented the original vulnerable code from observing the inconsistent object state.
That distinction matters.
The experiment proves that the underlying builder corrupts memory when it receives the state produced at the 16-bit boundary.
It does not, by itself, prove that an unmodified stock IIS installation naturally delivers that state to the builder.
HVCI was disabled for this experiment because the lab instrumentation required executable kernel memory. That requirement applied to the validation mechanism, not to the existence of the underlying buffer overflow.
Driver Verifier and Special Pool
Kernel pool overflows can produce confusing secondary failures.
A corrupted object may not be used immediately. The machine may crash later in an unrelated function, during cleanup, or while unloading the modified driver.
Driver Verifier's Special Pool makes validation more deterministic.
Special Pool can place a protected guard page immediately adjacent to an allocation. When the code crosses the allocation boundary, the next invalid access faults immediately instead of silently corrupting unrelated pool objects.
For this test, Special Pool was enabled for http.sys.
The wrapped allocation had room for only ten unknown-header output records.
When the builder attempted to write the next record, the output pointer entered the guard page.
The system crashed at the precise instruction predicted by the static analysis.
The Confirmed Kernel Crash
The primary dump recorded:
PAGE_FAULT_IN_NONPAGED_AREA (0x50)
Faulting virtual address:
FFFFA3829FCC7000
Access type:
Write
Faulting instruction:
FFFFF80FF3B6EE7B
Module:
HTTP.sys
Module-relative offset:
0x11EE7BPAGE_FAULT_IN_NONPAGED_AREA (0x50)
Faulting virtual address:
FFFFA3829FCC7000
Access type:
Write
Faulting instruction:
FFFFF80FF3B6EE7B
Module:
HTTP.sys
Module-relative offset:
0x11EE7BThe faulting instruction was:
mov word ptr [r8], dimov word ptr [r8], diThis is the first write performed for each unknown-header output record.
The surrounding code advances the destination pointer by 0x18 bytes:
lea r8, [r8+18h]lea r8, [r8+18h]The loop then obtains the next linked-list node and compares it against the sentinel:
mov rdx, qword ptr [rdx]
cmp rdx, rsi
jne copy_loopmov rdx, qword ptr [rdx]
cmp rdx, rsi
jne copy_loopThe dump also preserved the allocation calculation immediately preceding the copy operation:
movzx eax, word ptr [rcx+7BCh]
mov r8d, 424B6C55h
lea rdx, [rax+rax*2]
lea rdx, [rdx*8+380h]movzx eax, word ptr [rcx+7BCh]
mov r8d, 424B6C55h
lea rdx, [rax+rax*2]
lea rdx, [rdx*8+380h]The arithmetic represented by those instructions is:
allocation_size =
0x380 + UnknownHeaderCount × 0x18allocation_size =
0x380 + UnknownHeaderCount × 0x18The evidence therefore tied the complete chain together:
- The unknown-header count was read as a 16-bit value.
- The wrapped value produced a
0x380-byteUlKBallocation. - The builder began writing 24-byte output records.
- The copy loop followed the linked list instead of the count.
- The destination pointer crossed the allocation boundary.
- The next record write entered the Special Pool guard page.
- The processor faulted at the exact instruction identified during static analysis.
The memory corruption was no longer theoretical.
It occurred at the predicted instruction, in the predicted function, using the predicted allocation and traversal mismatch.
Figure placeholder: Cropped WinDbg output showing
.bugcheck, theHTTP.sysmodule base, the faulting RVA, and disassembly aroundHTTP+0x11EE7B.
A Secondary Crash That Was Not the Vulnerability
During testing, I also captured a later 0x7E crash while HTTP.sys was being unloaded.
That dump showed an access violation in a feature-configuration cleanup path:
nt!CmFcManagerFlushFeatureUsage
nt!CmFcManagerUnregisterFeatureUsageProvider
HTTP!UxStopEnvironmentModule
HTTP!UxDriverUnloadnt!CmFcManagerFlushFeatureUsage
nt!CmFcManagerUnregisterFeatureUsageProvider
HTTP!UxStopEnvironmentModule
HTTP!UxDriverUnloadThis was not the primary overflow crash.
It occurred later, during driver teardown on a modified laboratory system.
Including this distinction is important because crash dumps can be persuasive even when they do not prove the claim being made.
The 0x7E dump is evidence of a secondary lab artifact.
The 0x50 dump at HTTP+0x11EE7B is the evidence for the UlKB boundary violation.
What the Overflow Writes
A kernel pool overflow does not automatically provide a clean arbitrary-write primitive.
Each 0x18-byte output record has a structure similar to:
OffsetSizeContentAttacker control+0x002Sequential indexNo direct control+0x022FlagsConstant or deterministic+0x042Header-name lengthPartial+0x062Header-value lengthPartial+0x088Kernel pointer to nameNot directly selected+0x108Kernel pointer to valueNot directly selected
The attacker influences the header lengths and the underlying header content.
However, the qwords copied into much of the overflow region are kernel virtual addresses assigned by the allocator.
Approximately three quarters of each record consists of values the attacker cannot freely choose.
That makes this primitive very different from a classic overwrite where an attacker can fill the destination with arbitrary bytes.
Denial of service
Once the vulnerable sink is reachable, denial of service is straightforward.
A sequential overwrite approaching 1.5 MB is inherently destructive, and Special Pool confirmed a deterministic out-of-bounds write.
Information disclosure
An information-disclosure path may be possible if pool grooming places a response-related object adjacent to the UlKB buffer and the corrupted contents can be returned before the machine crashes.
That would require:
- controlling pool placement;
- identifying an object whose corrupted contents can be remotely observed;
- and winning the timing window between corruption and failure.
The overflow itself writes kernel virtual addresses, which could be valuable if they can be reflected to the client.
Remote code execution
Reliable code execution would be significantly more difficult.
Potential approaches could involve:
- corrupting adjacent object metadata;
- modifying a size or type field using the influenced 16-bit lengths;
- corrupting a linked-list structure;
- redirecting a valid kernel pointer into an unintended use;
- or constructing a data-only privilege modification.
Each approach would be highly dependent on the target build and surrounding pool layout.
Modern Windows mitigations further complicate exploitation:
- NonPagedPoolNx prevents executing shellcode directly from ordinary pool allocations.
- Kernel Control Flow Guard restricts many indirect-call redirections.
- Encoded and validated pool metadata complicates allocator attacks.
- KASLR limits the usefulness of assumed addresses.
- HVCI protects kernel code integrity and makes traditional code-patching strategies substantially harder.
- A reliable exploit would likely require an additional information leak and extensive heap grooming.
The CVE's remote code execution classification is understandable because the flaw provides kernel memory corruption from pre-authentication network input when the necessary path is active.
But the confirmed primitive is not equivalent to a convenient, attacker-controlled arbitrary write.
My practical assessment is:
Kernel denial of service:
Confirmed once the sink is reachable
Information disclosure:
Potentially achievable with significant grooming and timing
Reliable kernel RCE without HVCI:
Theoretically possible, but high effort and build-specific
Reliable kernel RCE with HVCI and kCFG:
Considerably more difficultKernel denial of service:
Confirmed once the sink is reachable
Information disclosure:
Potentially achievable with significant grooming and timing
Reliable kernel RCE without HVCI:
Theoretically possible, but high effort and build-specific
Reliable kernel RCE with HVCI and kCFG:
Considerably more difficultWhat Microsoft Changed
Microsoft's patch prevents the request from approaching the dangerous boundary.
The new configuration value is:
HKLM\SYSTEM\CurrentControlSet\Services\HTTP\Parameters
MaxHeadersCountHKLM\SYSTEM\CurrentControlSet\Services\HTTP\Parameters
MaxHeadersCountThe patched code derives a default from MaxRequestBytes, applies minimum and maximum bounds, and rejects an excessive decoded field count before the vulnerable state can be constructed.
Conceptually:
if (decoded_header_count >
MaxHeadersCount + HTTP2_PSEUDO_HEADER_COUNT)
{
return STATUS_INVALID_PARAMETER_2;
}if (decoded_header_count >
MaxHeadersCount + HTTP2_PSEUDO_HEADER_COUNT)
{
return STATUS_INVALID_PARAMETER_2;
}This is a preemptive fix.
Rather than changing the existing USHORT to a larger integer or rewriting the builder's copy loop, the patch prevents enough headers from entering the request object to wrap the counter.
The vulnerable allocation and linked-list traversal logic remain conceptually recognizable in the patched driver.
The dangerous state is blocked earlier.
This is an effective strategy as long as the new enforcement is active.
Feature-gated enforcement
The new behavior is controlled through Windows feature-configuration infrastructure.
The patched binary queries feature state during initialization and conditionally enforces the new maximum.
This is relevant during validation.
Finding the new code in an updated binary does not, by itself, prove the code path is active at runtime.
A complete patch-verification process should confirm:
- the updated binary is loaded;
- the relevant feature configuration is enabled;
- the effective maximum is initialized;
- and excessive header lists are rejected at the new check.
CVE-2026–47291 and CVE-2026–49160
Large HTTP/2 header lists can interact with more than one security fix.
CVE-2026–49160 is a separate issue related to HTTP/2 resource consumption.
A boundary diagnostic containing tens of thousands of fields may encounter protections relevant to that vulnerability before it reaches the older unknown-header insertion logic.
As a result, the same client-visible rejection could represent:
- an HTTP/2 resource protection;
- the new
MaxHeadersCountcheck; - an existing decoded request-size restriction;
- the old 16-bit wrap error;
- or another protocol validation path.
The two CVEs should not be treated as interchangeable simply because a similar high-header-count request can exercise both sets of defenses.
Kernel debugging is required to identify which condition actually handled the request.
Practical Exposure
The most responsible way to describe CVE-2026–47291 is through conditions rather than a universal statement that all Windows HTTP servers are remotely exploitable.
Default request limits and no HKE consumer
The required unknown-header volume cannot be admitted, and the vulnerable builder is not active.
I did not reproduce the overflow naturally in this configuration.
Elevated MaxRequestBytes but no HKE consumer
The producer may approach or reach the 16-bit boundary, but the vulnerable HKE builder remains unreachable through the tested request flow.
The request is rejected through existing error handling.
Elevated MaxRequestBytes with an active HKE consumer
This is the configuration of greatest interest.
A legitimate extension consuming the request could reduce the number of remaining gates.
Whether an actual product or Windows configuration exposes the precise vulnerable timing and state remains an open research question.
Controlled laboratory activation
With sufficient request-size headroom, synthetic HKE state, and controlled continuation past the producer error, the original builder receives the wrapped request and produces a confirmed kernel pool overflow.
Patched system with active MaxHeadersCount enforcement
The request is rejected before enough header state can accumulate to wrap the 16-bit counter.
Defensive Recommendations
Apply the Windows security update
The most important action is to install the Microsoft update containing the header-count enforcement.
Audit MaxRequestBytes
Search for systems where the HTTP service request-size limit has been increased substantially above its default.
A high value is not proof of vulnerability, but it removes one of the barriers that prevented the boundary from being reached during testing.
Confirm feature enforcement
For high-assurance environments, verify that the patched runtime is actually enforcing the new maximum rather than relying only on file version.
Inventory kernel networking extensions
Review third-party kernel drivers associated with:
- HTTP inspection;
- endpoint protection;
- web application filtering;
- DLP;
- network acceleration;
- cloud host networking;
- proxying;
- and traffic interception.
Look for NMR registration activity and dependencies on HTTP extension interfaces.
Monitor anomalous HTTP/2 header behavior
A legitimate request should not contain tens of thousands of distinct unknown field names.
Useful signals include:
- extremely large decoded field counts;
- long sequences of CONTINUATION frames;
- thousands of unique header names;
- repetitive generated naming patterns;
- large discrepancies between compressed and decoded header size;
- and repeated rejection near HTTP/2 header boundaries.
Do not rely solely on wire size
HPACK compression can make a request appear smaller in transit than its expanded representation.
Monitoring and enforcement should consider decoded field count and decoded size.
What This Research Demonstrated
The following behaviors were confirmed through static analysis, controlled execution, or crash-dump evidence:
- The request object uses a 16-bit unknown-header counter.
- Distinct unknown names are required to grow the linked-list population to the wrap boundary.
- The 65,536th insertion wraps the counter to zero.
- The final node is linked before the producer checks for wraparound.
- The error path leaves the counter and list population inconsistent.
- The HKE builder sizes an
UlKBallocation using the wrapped counter. - The allocation formula is
0x380 + count × 0x18. - The builder copies entries by traversing the linked list rather than by respecting the allocation count.
- A wrapped allocation has space for approximately ten unknown-header records.
- The resulting copy operation crosses the allocation boundary.
- Driver Verifier catches the invalid write at
HTTP+0x11EE7B. - The confirmed crash is a
PAGE_FAULT_IN_NONPAGED_AREAcaused by a write into the Special Pool guard page. - Microsoft's patch prevents the dangerous boundary through an earlier maximum-header-count check.
What This Research Did Not Demonstrate
The following claims have not been established:
- That default stock IIS is naturally remotely crashable through this issue.
- That every
http.sys-backed service exposes the vulnerable HKE path. - That
tcpip.sysregisters as the relevant HKE consumer under a currently known configuration. - That a specific third-party product activates the vulnerable path.
- That the producer's transient inconsistent state is naturally observable by a legitimate HKE callback.
- That reliable remote code execution has been achieved.
- That exploitation remains practical with HVCI and kernel Control Flow Guard enabled.
- That a connection reset or GOAWAY response proves the CVE was triggered.
These limitations do not invalidate the confirmed memory corruption.
They define the difference between proving a vulnerable operation and proving a deployable exploit.
Conclusion
CVE-2026–47291 is a useful example of why vulnerability research cannot stop at the first dangerous instruction.
The integer wrap was only the beginning.
Understanding the real issue required tracing:
- how HTTP/2 fields are decoded;
- how unknown headers are classified;
- why duplicate names do not create equivalent list growth;
- how a new node is linked into the request;
- when the 16-bit counter is incremented;
- when wraparound is detected;
- which success bookkeeping is skipped;
- how an inconsistent object can survive temporarily;
- which optional kernel component consumes that object;
- how the marshalling allocation is sized;
- how the output loop is terminated;
- and which runtime controls prevent the sink from being reached.
The final result is nuanced.
The kernel pool overflow is real.
The counter wraps.
The list and count diverge.
The builder allocates 896 bytes.
The builder then attempts to marshal approximately 1.5 MB of linked-list entries.
When the original builder receives that state, Driver Verifier catches the predicted out-of-bounds write at the exact instruction identified during reverse engineering.
At the same time, the default configurations I tested contained multiple independent barriers that prevented the vulnerable state from naturally reaching the builder.
Both conclusions matter.
Overstating the natural attack surface would weaken the research.
Ignoring the confirmed memory corruption because the default path is gated would also be a mistake.
The complete story is not simply that 65,536 headers crash Windows.
It is that, deep inside http.sys, 65,536 headers can become zero—while the linked list remembers every one of them.
Disclosure and Research Notes
This research was performed against isolated laboratory systems under authorized conditions.
The kernel memory read/write mechanism used to activate the dormant HKE path is intentionally omitted. The driver identity, device interface, control codes, and implementation details will not be released as part of this article.
The HTTP/2 request generator was designed as a boundary diagnostic. Network-visible rejection alone should not be interpreted as proof of memory corruption, successful exploitation, or patch enforcement.
Research timeline
- June 24, 2026: Confirmed the 16-bit unknown-header counter and downstream allocation relationship.
- June 24, 2026: Identified the link-before-wrap-check ordering.
- June 24, 2026: Confirmed that duplicate unknown names are merged and distinct names are required.
- June 24, 2026: Traced the vulnerable sink to the HKE request builder.
- June 25, 2026: Activated the dormant path in an isolated lab.
- June 25, 2026: Confirmed the
0x50Special Pool crash atHTTP+0x11EE7B.
Author
Cihan Anthony Obviam Offensive Security