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:

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

Edited by Lukas Märdian

Merge request reports

Loading