Fix CVE-2026-27135 (Closes: #1131369) - bookworm/oldstable
Fix missing iframe->state validations to avoid assertion failure. Backported from https://github.com/nghttp2/nghttp2/commit/5c7df8f
Also, backported the corresponding upstream test from https://github.com/nghttp2/nghttp2/commit/c619c7be0737ac78051b1cacf4b1ce5467eb838d
Test/Validation:
- Debusine workflow: https://debusine.debian.net/debian/developers/work-request/606112/
In addition to that, a manual testcase run-nhgttp2-memrecv-test can be executed, using the following dependencies: libnghttp2-dev build-essential pkg-config:
#!/bin/sh
# autopkgtest for CVE-2026-27135:
# Missing iframe->state == NGHTTP2_IB_IGN_ALL checks after callbacks in
# nghttp2_session_mem_recv allow processing to continue on a terminated
# session, leading to assertion failures.
#
# The fix adds checks after every callback that could set IGN_ALL.
# This test verifies that calling nghttp2_session_terminate_session()
# from within on_data_chunk_recv_callback causes immediate return from
# nghttp2_session_mem_recv, without processing the DATA frame further.
set -e
echo "=== Building CVE-2026-27135 reproducer ==="
cat >/tmp/test_cve_2026_27135.c <<'TESTEOF'
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <nghttp2/nghttp2.h>
/*
* CVE-2026-27135 reproducer — targets on_data_chunk_recv_callback path
*
* The vulnerability: when on_data_chunk_recv_callback calls
* nghttp2_session_terminate_session(), iframe->state is set to IGN_ALL.
* But nghttp2_session_mem_recv() did not check for this state after the
* callback (at the IB_READ_DATA case), so it fell through to
* session_process_data_frame() which calls on_frame_recv_callback(DATA),
* and then session_inbound_frame_reset() which overwrites IGN_ALL with
* IB_READ_HEAD. This caused the session to continue processing
* subsequent frames instead of ignoring them.
*
* The fix adds iframe->state == NGHTTP2_IB_IGN_ALL checks after every
* callback invocation that could trigger session termination.
*
* Test approach:
* 1. Client sends: preface + SETTINGS + HEADERS (POST, no END_STREAM)
* + DATA (with body, END_STREAM) + PING
* 2. Server's on_data_chunk_recv_callback terminates the session.
* 3. With the fix: IGN_ALL is checked after on_data_chunk_recv_callback,
* mem_recv returns immediately, on_frame_recv_callback(DATA) never fires.
* 4. Without the fix: processing continues to session_process_data_frame
* which calls on_frame_recv_callback(DATA), then
* session_inbound_frame_reset overwrites IGN_ALL.
*/
/* ---------- capture buffer for client output ---------- */
typedef struct {
uint8_t data[65536];
size_t len;
} buffer_t;
static ssize_t capture_send_cb(nghttp2_session *session, const uint8_t *data,
size_t length, int flags, void *user_data) {
buffer_t *buf = (buffer_t *)user_data;
(void)session;
(void)flags;
if (buf->len + length > sizeof(buf->data)) {
return NGHTTP2_ERR_WOULDBLOCK;
}
memcpy(buf->data + buf->len, data, length);
buf->len += length;
return (ssize_t)length;
}
/* ---------- client data provider ---------- */
static ssize_t data_source_read_cb(nghttp2_session *session,
int32_t stream_id, uint8_t *buf,
size_t length, uint32_t *data_flags,
nghttp2_data_source *source,
void *user_data) {
(void)session; (void)stream_id; (void)source; (void)user_data;
/* Send 5 bytes of body data, then signal EOF (→ END_STREAM) */
size_t len = length < 5 ? length : 5;
memset(buf, 'A', len);
*data_flags = NGHTTP2_DATA_FLAG_EOF;
return (ssize_t)len;
}
/* ---------- server-side state ---------- */
static int terminate_called; /* on_data_chunk_recv_callback terminated */
static int data_frame_recv_after; /* on_frame_recv_callback(DATA) fired after terminate */
static ssize_t null_send_cb(nghttp2_session *session, const uint8_t *data,
size_t length, int flags, void *user_data) {
(void)session; (void)data; (void)flags; (void)user_data;
return (ssize_t)length;
}
/* Terminate the session when DATA chunk is received.
Without the fix, session_process_data_frame() still runs afterward,
calling on_frame_recv_callback(DATA) and then session_inbound_frame_reset()
which overwrites IGN_ALL. */
static int terminate_on_data_chunk_cb(nghttp2_session *session, uint8_t flags,
int32_t stream_id,
const uint8_t *data, size_t len,
void *user_data) {
(void)flags; (void)stream_id; (void)data; (void)len; (void)user_data;
if (!terminate_called) {
nghttp2_session_terminate_session(session, NGHTTP2_NO_ERROR);
terminate_called = 1;
}
return 0;
}
/* Track whether on_frame_recv_callback fires for DATA after termination.
With the fix, this must NOT fire for DATA. */
static int track_frame_recv_cb(nghttp2_session *session,
const nghttp2_frame *frame,
void *user_data) {
(void)session; (void)user_data;
if (frame->hd.type == NGHTTP2_DATA && terminate_called) {
data_frame_recv_after = 1;
}
return 0;
}
/* Required for header inflation to succeed on the server */
static int on_begin_headers_cb(nghttp2_session *session,
const nghttp2_frame *frame,
void *user_data) {
(void)session; (void)frame; (void)user_data;
return 0;
}
/* ---------- generate valid client HTTP/2 data ---------- */
static int generate_client_data(buffer_t *buf) {
nghttp2_session *client;
nghttp2_session_callbacks *cb;
nghttp2_data_provider data_prd;
int rv;
nghttp2_session_callbacks_new(&cb);
nghttp2_session_callbacks_set_send_callback(cb, capture_send_cb);
rv = nghttp2_session_client_new(&client, cb, buf);
nghttp2_session_callbacks_del(cb);
if (rv != 0) return rv;
/* Client connection preface: SETTINGS (magic is sent automatically) */
rv = nghttp2_submit_settings(client, NGHTTP2_FLAG_NONE, NULL, 0);
if (rv != 0) { nghttp2_session_del(client); return rv; }
/* HEADERS frame for stream 1 — POST with body (no END_STREAM) */
nghttp2_nv nva[] = {
{(uint8_t *)":method", (uint8_t *)"POST", 7, 4, NGHTTP2_NV_FLAG_NONE},
{(uint8_t *)":path", (uint8_t *)"/", 5, 1, NGHTTP2_NV_FLAG_NONE},
{(uint8_t *)":scheme", (uint8_t *)"https", 7, 5, NGHTTP2_NV_FLAG_NONE},
{(uint8_t *)":authority", (uint8_t *)"localhost", 10, 9, NGHTTP2_NV_FLAG_NONE},
};
/* Provide a data source so HEADERS is sent without END_STREAM,
followed by a DATA frame with the body */
data_prd.source.ptr = NULL;
data_prd.read_callback = data_source_read_cb;
rv = nghttp2_submit_request(client, NULL, nva, 4, &data_prd, NULL);
if (rv < 0) { nghttp2_session_del(client); return rv; }
/* PING frame — should NOT be processed if the fix works */
rv = nghttp2_submit_ping(client, NGHTTP2_FLAG_NONE, NULL);
if (rv != 0) { nghttp2_session_del(client); return rv; }
rv = nghttp2_session_send(client);
nghttp2_session_del(client);
return rv;
}
/* ---------- main ---------- */
int main(void) {
buffer_t client_data;
nghttp2_session *server;
nghttp2_session_callbacks *cb;
nghttp2_option *option;
ssize_t consumed;
int failures = 0;
printf("nghttp2 version: %s\n\n", nghttp2_version(0)->version_str);
/* --- Step 1: generate valid HTTP/2 client frames --- */
memset(&client_data, 0, sizeof(client_data));
if (generate_client_data(&client_data) != 0) {
fprintf(stderr, "FAIL: could not generate client data\n");
return 1;
}
printf("Generated %zu bytes of client HTTP/2 data\n", client_data.len);
/* --- Step 2: create server session with no HTTP messaging enforcement --- */
nghttp2_session_callbacks_new(&cb);
nghttp2_session_callbacks_set_send_callback(cb, null_send_cb);
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(cb,
terminate_on_data_chunk_cb);
nghttp2_session_callbacks_set_on_frame_recv_callback(cb,
track_frame_recv_cb);
nghttp2_session_callbacks_set_on_begin_headers_callback(cb,
on_begin_headers_cb);
/* Disable HTTP messaging enforcement to avoid interfering with test */
nghttp2_option_new(&option);
nghttp2_option_set_no_http_messaging(option, 1);
nghttp2_session_server_new2(&server, cb, NULL, option);
nghttp2_session_callbacks_del(cb);
nghttp2_option_del(option);
/* --- Step 3: feed client data to server --- */
terminate_called = 0;
data_frame_recv_after = 0;
consumed = nghttp2_session_mem_recv(server, client_data.data, client_data.len);
if (consumed < 0) {
fprintf(stderr, "FAIL: nghttp2_session_mem_recv returned error %zd: %s\n",
consumed, nghttp2_strerror((int)consumed));
nghttp2_session_del(server);
return 1;
}
printf("Server consumed %zd / %zu bytes\n", consumed, client_data.len);
printf("terminate_called = %d\n", terminate_called);
printf("data_frame_recv_after = %d\n", data_frame_recv_after);
/* --- Step 4: sanity — confirm terminate_session was called --- */
if (!terminate_called) {
fprintf(stderr,
"FAIL: on_data_chunk_recv_callback never called terminate_session.\n"
"Test infrastructure broken — cannot validate the CVE fix.\n");
nghttp2_session_del(server);
return 1;
}
printf("PASS: on_data_chunk_recv_callback fired and terminated the session.\n");
/* --- Step 5: verify fix --- */
/*
* With the fix (CVE-2026-27135 patched):
* on_data_chunk_recv_callback terminates session (→ IGN_ALL).
* The new check after the callback (hunk 5 of the patch) catches
* IGN_ALL and returns inlen immediately.
* → session_process_data_frame is never reached
* → on_frame_recv_callback(DATA) never fires
* → data_frame_recv_after == 0
*
* Without the fix:
* No IGN_ALL check after on_data_chunk_recv_callback.
* Processing falls through to session_process_data_frame() which
* calls on_frame_recv_callback(DATA).
* → data_frame_recv_after == 1
*/
if (data_frame_recv_after) {
fprintf(stderr,
"FAIL: on_frame_recv_callback(DATA) was invoked after session "
"termination from on_data_chunk_recv_callback.\n"
"This indicates CVE-2026-27135 is NOT fixed.\n");
failures++;
} else {
printf("PASS: DATA frame processing correctly stopped after session "
"termination.\n");
}
nghttp2_session_del(server);
if (failures) {
fprintf(stderr, "\n%d test(s) FAILED — CVE-2026-27135 not fixed\n",
failures);
return 1;
}
return 0;
}
TESTEOF
# Build
if ! pkg-config --exists libnghttp2; then
echo "ERROR: libnghttp2-dev not found" >&2
exit 1
fi
gcc -O2 -Wall -Wextra -std=c99 -o /tmp/test_cve_2026_27135 \
/tmp/test_cve_2026_27135.c \
$(pkg-config --cflags --libs libnghttp2)
echo "=== Running CVE-2026-27135 reproducer ==="
/tmp/test_cve_2026_27135
See also
- Backport for trixie: !12
- LTS backport for bullseye: lts-team/packages/nghttp2!1
Edited by Lukas Märdian