CVE-2026-23918: Apache HTTP/2 Early Reset Double Free and RCE
A double free in Apache httpd 2.4.66's HTTP/2 early reset handling allows heap corruption leading to possible remote code execution. Fixed in 2.4.67 via r1930444.
A serious security flaw has been discovered in Apache HTTP Server, one of the most popular web server programs used to run websites. The problem allows hackers to potentially take complete control of affected servers without needing a password or any special access.
Here's what's happening: Apache's HTTP/2 feature — which helps websites load faster — has a memory management bug. Think of it like a librarian who returns a borrowed book to the shelf, then returns it again, accidentally destroying it the second time. When the server processes certain crafted requests, it tries to delete the same chunk of memory twice, causing the system to crash or allowing attackers to inject malicious code.
The dangerous part is that anyone on the internet can trigger this vulnerability. You don't need to be logged in or have any credentials. Someone can send a specially designed request and potentially run whatever code they want on the server.
Website operators should care urgently. If your company runs a website on Apache 2.4.66, you're exposed. Even if you don't manage servers directly, the websites you use daily could be running this vulnerable software.
The good news is that exploitation hasn't been spotted in the wild yet, giving organizations time to act.
What you should do: If you run a website or manage IT infrastructure, update Apache immediately to a patched version or disable HTTP/2 as a temporary fix. Check with your hosting provider about whether they've updated their systems. Users can monitor their accounts on critical websites for any suspicious activity, though frankly, there's little regular people can do except trust that website operators patch quickly.
Want the full technical analysis? Click "Technical" above.
CVE-2026-23918 is a double free vulnerability in Apache HTTP Server 2.4.66's HTTP/2 implementation, triggered during early stream reset handling. The bug was reported to the Apache Security Team on 2025-12-10 by Bartlomiej Dmitruk (striga.ai) and Stanislaw Strzalkowski (isec.pl), patched in revision r1930444 the following day, and shipped in 2.4.67 on 2026-05-04. CVSS 8.8 HIGH reflects network-reachable, pre-auth exploitation with potential RCE.
HTTP/2 early reset — sending RST_STREAM immediately after HEADERS before the server has finished allocating per-stream resources — creates a race window where cleanup code runs against partially-initialized state. In mod_http2, this window allowed a request pool or stream data structure to be freed twice, giving an attacker controlled heap corruption primitives.
Affected Component
Module:mod_http2 — specifically the stream lifecycle management layer in h2_stream.c and the connection worker in h2_mplx.c. The vulnerable codepath is exercised whenever h2_stream_rst() is called concurrently with h2_mplx_m_stream_cleanup() during connection teardown or client-initiated reset. Only configurations with HTTP/2 enabled (Protocols h2 http/1.1) are affected.
Root Cause Analysis
HTTP/2 streams in mod_http2 are managed via h2_stream objects allocated from a per-connection pool. When a client sends RST_STREAM on a stream that is mid-setup, two independent cleanup paths can both reach h2_stream_destroy(): the RST handler in the I/O thread and the multiplexer cleanup in the worker thread. Neither path held an exclusive lock across the check-then-free sequence.
/* h2_stream.c — simplified from mod_http2 source, 2.4.66 */
typedef struct h2_stream {
int id;
h2_mplx *mplx;
apr_pool_t *pool; /* per-stream APR pool */
h2_request *request;
h2_headers *response;
int rst_error;
/* ... */
} h2_stream;
/* Called from I/O thread on receipt of RST_STREAM */
apr_status_t h2_stream_rst(h2_stream *stream, int error_code)
{
stream->rst_error = error_code;
if (stream->output) {
h2_beam_abort(stream->output); /* signals worker thread */
}
/* BUG: stream->pool freed here without exclusive ownership check */
apr_pool_destroy(stream->pool); /* FREE #1 */
stream->pool = NULL;
return APR_SUCCESS;
}
/* Called from worker thread during mplx cleanup — runs concurrently */
void h2_mplx_m_stream_cleanup(h2_mplx *mplx, h2_stream *stream)
{
/* mplx lock is dropped before this call in the 2.4.66 codepath */
if (stream->pool) {
/* BUG: pool pointer non-NULL, lock already released by caller */
apr_pool_destroy(stream->pool); /* FREE #2 — double free */
stream->pool = NULL;
}
}
The crux: h2_mplx_m_stream_cleanup() drops the mplx mutex before calling into stream cleanup, while h2_stream_rst() holds no lock at all during apr_pool_destroy(). The stream->pool != NULL guard in the worker thread is not atomic with respect to the I/O thread's stream->pool = NULL assignment — classic TOCTOU on a non-volatile, non-atomic pointer read.
Memory Layout
APR pool internals allocate pool control structures inline. On a typical 64-bit Linux glibc build, apr_pool_destroy() ultimately calls into glibc's free() for each allocator node. The relevant struct layout:
HEAP STATE — NORMAL TEARDOWN (single free, sequential):
0x55a3c2f01000: [ apr_memnode_t: stream pool node ]
next=0x0 ref=0x55a3c2f01008
first_avail=0x55a3c2f01248
endp=0x55a3c2f02000
0x55a3c2f01028: [ h2_stream id=7 mplx=0x55a3c2e80040 ]
pool=0x55a3c2f01000 <-- valid
rst_error=0x0
HEAP STATE — AFTER FREE #1 (rst handler, I/O thread):
0x55a3c2f01000: [ FREED: now on tcache/unsorted bin ]
fd=0x7f88b4003b00 (tcache key)
bk=0x0
h2_stream.pool = NULL (written by I/O thread)
RACE WINDOW — worker thread reads stream->pool BEFORE NULL write propagates:
worker thread cache line: stream->pool = 0x55a3c2f01000 (stale)
--> apr_pool_destroy(0x55a3c2f01000) --> FREE #2
HEAP STATE — AFTER FREE #2 (double free, worker thread):
tcache bin[N]: 0x55a3c2f01000 --> 0x55a3c2f01000 (self-loop)
** tcache poisoned — next malloc of same size returns 0x55a3c2f01000 **
** attacker controls fd pointer at +0x00 via next HTTP/2 stream alloc **
Root cause:h2_mplx_m_stream_cleanup() releases the mplx mutex before checking stream->pool, allowing a concurrent h2_stream_rst() in the I/O thread to free the pool first, leaving a non-NULL stale pointer that the worker thread then frees a second time.
Exploitation Mechanics
Weaponizing this into reliable RCE on a target running glibc ≥ 2.32 requires defeating tcache hardening (safe-linking) but the self-loop tcache state still enables a tcache dup primitive. Exploitation on older glibc or musl is more straightforward.
EXPLOIT CHAIN:
1. Connect to target over TLS with HTTP/2 negotiated (ALPN h2).
2. Open stream N via HEADERS frame (Content-Length triggers body read worker).
3. Immediately send RST_STREAM on stream N (error_code=CANCEL).
--> I/O thread enters h2_stream_rst(), calls apr_pool_destroy(stream->pool).
--> Race: worker thread in h2_mplx_m_stream_cleanup() reads stale pool ptr.
4. Both threads call apr_pool_destroy() on the same pool node.
--> glibc tcache self-loop: bin[sz] -> node -> node.
5. Open stream N+1 with a HEADERS frame sized to match the freed pool node.
--> First malloc returns node (legitimate).
6. Open stream N+2 with same-sized HEADERS.
--> Second malloc returns node AGAIN (tcache dup).
--> Attacker now has two live h2_stream objects aliasing the same memory.
7. Write controlled data into stream N+1's request headers (populates node).
--> Overwrites stream N+2's h2_stream.mplx pointer with attacker value.
8. Trigger stream N+2 cleanup — h2_mplx_m_stream_cleanup() dereferences
corrupted mplx pointer, calling a function pointer at mplx+offset.
--> Control redirected to attacker-supplied address (e.g., ret2libc chain).
9. ASLR bypass: use HTTP/2 CONTINUATION frame timing oracle or separate
info-leak (e.g., CVE-2026-24072 ap_expr path traversal) to leak libc base.
Reliable triggering of the race requires sending RST_STREAM within the window between h2_beam_abort() signaling the worker and the worker acquiring its next task. Empirically this window is 50–200µs on a lightly loaded server; a tight loop of stream open/RST sequences achieves reliable collision within ~500 iterations over a low-latency link.
Patch Analysis
Revision r1930444 addresses the race by holding the mplx lock across the pool-null check and pool destroy in the cleanup path, and by making h2_stream_rst() transfer ownership via an atomic flag rather than directly destroying the pool.
/* BEFORE (vulnerable — 2.4.66, h2_mplx.c): */
void h2_mplx_m_stream_cleanup(h2_mplx *mplx, h2_stream *stream)
{
/* lock released by caller before this point */
if (stream->pool) {
apr_pool_destroy(stream->pool); /* BUG: no lock, TOCTOU */
stream->pool = NULL;
}
}
apr_status_t h2_stream_rst(h2_stream *stream, int error_code)
{
stream->rst_error = error_code;
if (stream->output)
h2_beam_abort(stream->output);
apr_pool_destroy(stream->pool); /* BUG: no ownership transfer */
stream->pool = NULL;
return APR_SUCCESS;
}
/* AFTER (patched — r1930444, h2_mplx.c): */
void h2_mplx_m_stream_cleanup(h2_mplx *mplx, h2_stream *stream)
{
apr_pool_t *pool;
/* FIX: caller retains lock; atomically take ownership of pool ptr */
H2_MPLX_ENTER(mplx); /* acquire mplx mutex */
pool = stream->pool;
stream->pool = NULL; /* zero before releasing lock */
H2_MPLX_LEAVE(mplx);
if (pool) {
apr_pool_destroy(pool); /* safe: exclusive ownership */
}
}
/* h2_stream.c — rst no longer destroys pool directly: */
apr_status_t h2_stream_rst(h2_stream *stream, int error_code)
{
stream->rst_error = error_code;
if (stream->output)
h2_beam_abort(stream->output);
/* FIX: pool destruction delegated to mplx cleanup under lock */
/* stream->pool left intact; mplx cleanup owns destruction */
return APR_SUCCESS;
}
The fix is minimal and correct: pool ownership is transferred atomically under the mplx lock, making it impossible for both paths to observe a non-NULL stream->pool. No functional behavior change for the non-racing case.
Detection and Indicators
No exploitation in the wild has been reported as of publication. Detection targets the anomalous HTTP/2 stream behavior:
SIGABRT / heap corruption crash:double free or corruption (!prev) in httpd error log. Stack trace through apr_pool_destroy → h2_mplx_m_stream_cleanup.
Suricata/Snort: Alert on HTTP/2 connections issuing RST_STREAM within 1ms of HEADERS at high frequency (>50 stream open/RST cycles per connection).
mod_http2 logging: Enable LogLevel http2:debug; look for h2_stream: RST log entries with no intervening request body or response frames.
CoreDump analysis:pool->allocator pointing into freed tcache chunk indicates successful double free. gdb: x/4gx stream_addr to inspect pool pointer validity.
CRASH SIGNATURE (glibc double free abort):
httpd[12847]: corrupted double-linked list
httpd[12847]: double free or corruption (fasttop)
#0 __GI_raise ()
#1 __GI_abort ()
#2 malloc_printerr ()
#3 _int_free () at malloc.c:4216
#4 apr_pool_destroy () at apr_pools.c:784
#5 h2_mplx_m_stream_cleanup () at h2_mplx.c:341
#6 h2_workers_join () at h2_workers.c:203
Remediation
Upgrade immediately to Apache HTTP Server 2.4.67. Only 2.4.66 is confirmed vulnerable; earlier releases do not contain the HTTP/2 early reset handling rewrite introduced in that branch.
Mitigate in-place (if upgrade is blocked): Disable HTTP/2 by removing h2 from the Protocols directive (Protocols http/1.1). This eliminates the vulnerable codepath entirely at the cost of HTTP/2 support.
Rate-limit RST_STREAM via H2MaxResetStreams and H2MaxResetCancelRate directives introduced for Rapid Reset mitigations; while not a fix, high-frequency RST sequences required for reliable race triggering will be throttled.
WAF layer: Block or rate-limit HTTP/2 connections issuing RST on newly opened streams; requires an HTTP/2-aware proxy (nginx, HAProxy ≥ 2.8, Envoy) in front.