home intel cve-2026-40170-ngtcp2-qlog-stack-buffer-overflow
CVE Analysis 2026-04-16 · 8 min read

CVE-2026-40170: ngtcp2 qlog Stack Buffer Overflow via QUIC Transport Params

ngtcp2_qlog_parameters_set_transport_params() serializes peer transport parameters into a fixed 1024-byte stack buffer without bounds checking, enabling remote stack corruption during QUIC handshake.

#buffer-overflow#stack-overflow#quic-protocol#qlog-callback#bounds-checking
Technical mode — for security professionals
▶ Attack flow — CVE-2026-40170 · Buffer Overflow
ATTACKERRemote / unauthBUFFER OVERFLOWCVE-2026-40170Cross-platform · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2026-40170 is a stack buffer overflow in ngtcp2, a widely deployed C implementation of IETF QUIC (RFC 9000). The vulnerability exists in ngtcp2_qlog_parameters_set_transport_params(), the function responsible for serializing QUIC transport parameters into qlog JSON output during connection establishment. When qlog is enabled, a remote peer—acting as either client or server—can send crafted transport parameters during the cryptographic handshake phase that exceed the function's fixed 1024-byte stack buffer, triggering a classic stack-based buffer overflow before any authenticated session is established.

CVSS 7.5 (HIGH) is assigned under the network vector with no authentication required. The attack surface is any deployment that enables the ngtcp2_conn_set_qlog_callbacks() interface and processes connections from untrusted peers—a configuration common in debugging, observability, and performance-measurement tooling. No exploitation in the wild has been confirmed as of publication.

Root cause: ngtcp2_qlog_parameters_set_transport_params() accumulates serialized transport parameter strings into a stack-allocated 1024-byte buffer via repeated ngtcp2_qlog_write_transport_params() calls without tracking total written bytes or enforcing a bounds check, allowing a peer to overflow the buffer by supplying sufficiently large or numerous transport parameter values during the QUIC handshake.

Affected Component

The vulnerable code lives in lib/ngtcp2_qlog.c, specifically within ngtcp2_qlog_parameters_set_transport_params(). This function is invoked from the handshake processing path after peer transport parameters are decoded from the TLS extension. All versions of ngtcp2 prior to 1.22.1 are affected. The qlog feature must be explicitly enabled by the application via a callback registration, but once enabled, the attack is fully network-reachable with no further preconditions.

Root Cause Analysis

The vulnerable function allocates a fixed-size stack buffer and then writes formatted transport parameter fields into it sequentially. There is no accumulator tracking how many bytes have been written, and no guard against writing past the buffer's end.


/*
 * ngtcp2_qlog.c — vulnerable serialization path (pre-1.22.1)
 *
 * Called from ngtcp2_conn_set_remote_transport_params() after
 * peer TP decoding completes, regardless of authentication state.
 */
void ngtcp2_qlog_parameters_set_transport_params(
        ngtcp2_qlog *qlog,
        const ngtcp2_transport_params *params,
        int server,
        ngtcp2_qlog_side side) {

    /* BUG: fixed 1024-byte stack buffer, no size tracking */
    uint8_t buf[1024];
    uint8_t *p = buf;

    /* Each write_transport_params call appends formatted JSON
     * fields into buf via p without checking remaining capacity. */

    p = ngtcp2_qlog_write_transport_params(p, params);
    // BUG: p advances based on formatted output length;
    //      if params contain large values (e.g., long preferred_address,
    //      max token, or many extension parameters), p walks past
    //      buf + 1024 into adjacent stack frames.

    /* ... additional field serialization calls follow ... */

    ngtcp2_qlog_buf_write(qlog, buf, (size_t)(p - buf));
}

The individual ngtcp2_qlog_write_transport_params() helper uses ngtcp2_put_uint* and byte-copy routines that return an advanced pointer, relying entirely on the caller to ensure space. With no remaining-capacity argument threaded through, the helpers write blindly. Transport parameters such as preferred_address (variable-length IPv6 + connection ID + stateless reset token), initial_max_stream_data_bidi_local, and extension parameters can be crafted to push total serialized output well beyond 1024 bytes.

Memory Layout

The following depicts a typical 64-bit stack frame for ngtcp2_qlog_parameters_set_transport_params() and the corruption pattern a remote attacker achieves by sending an oversized preferred_address transport parameter combined with a large stateless reset token and several padded extension fields.


STACK FRAME — ngtcp2_qlog_parameters_set_transport_params() [x86-64]

  HIGH ADDRESS
  ┌──────────────────────────────────────────────┐
  │  caller saved rbp          @ frame+0x420     │
  │  saved return address      @ frame+0x428     │  <-- corruption target
  │  caller locals / canary    @ frame+0x430     │
  ├──────────────────────────────────────────────┤
  │  buf[1024]  (0x400 bytes)                    │
  │    @ frame+0x020  ..  frame+0x41f            │  <-- stack buffer
  │    p = buf (initialized)                     │
  ├──────────────────────────────────────────────┤
  │  local vars (qlog*, params*, side, server)   │
  │    @ frame+0x000  ..  frame+0x01f            │
  LOW ADDRESS

AFTER OVERFLOW (attacker sends ~1200 bytes of serialized TP):

  ┌──────────────────────────────────────────────┐
  │  saved rbp      @ frame+0x420  = 0x4141414141414141  [CORRUPTED] │
  │  ret addr       @ frame+0x428  = 0x4242424242424242  [CORRUPTED] │
  ├──────────────────────────────────────────────┤
  │  buf[0x000..0x3ff]  legitimate serialized JSON                   │
  │  buf[0x400..0x4af]  attacker overflow bytes (176 bytes past end) │
  └──────────────────────────────────────────────┘

  Overflow delta from buf end to ret addr: 0x28 bytes (8-byte rbp + padding)
  Attacker controls ~176 bytes beyond buf end before hitting ret addr.

Stack canaries, when present, sit between buf and the saved frame pointer and will catch this overflow in hardened builds. However, deployments compiled without -fstack-protector (embedded QUIC stacks, custom build systems, WASM environments) are directly exploitable for return-address overwrite.

Exploitation Mechanics

The attack is fully pre-authentication and network-reachable. A QUIC Initial packet containing a crafted transport_parameters TLS extension is sufficient to trigger the overflow on a qlog-enabled server. No valid certificate, token, or established session is required.


EXPLOIT CHAIN:

1. Attacker sends QUIC Initial packet to target server (UDP, any port).
   - ClientHello TLS extension 0x39 (transport_parameters) embedded.
   - preferred_address field: max-length IPv4 + IPv6 + 20-byte CID
     + 16-byte stateless_reset_token = ~55 bytes per field.
   - Pad with multiple large extension transport parameters
     (unknown IDs tolerated per RFC 9000 §7.4.2) to push total
     serialized JSON past 1024 bytes.

2. Server decodes transport parameters successfully (TP decoder is
   separate from qlog serializer; no length check at decode stage).

3. ngtcp2_conn_set_remote_transport_params() is called.
   → triggers ngtcp2_qlog_parameters_set_transport_params()

4. Serialization loop writes formatted JSON fields into buf[1024].
   p advances past buf+1024, overwriting stack.

5. Without stack canary:
   - rbp overwritten at buf+0x400
   - ret addr overwritten at buf+0x408
   - Function returns → control hijacked to attacker-supplied address.

6. With stack canary (common case):
   - Canary check triggers abort() / stack smashing detected.
   - Effective impact: remote crash / denial of service.
   - ASan-enabled builds report STACK-BUFFER-OVERFLOW.

7. In canary-absent embedded targets (e.g., custom QUIC middleboxes):
   - ROP chain or direct shellcode address placed at buf+0x408.
   - Full remote code execution achieved before handshake completes.

Patch Analysis

The fix in ngtcp2 1.22.1 replaces the unconstrained stack buffer with a dynamically sized approach that passes buffer capacity through the call chain, ensuring writes are bounded. The core change in lib/ngtcp2_qlog.c:


/* BEFORE (vulnerable, < 1.22.1): */
void ngtcp2_qlog_parameters_set_transport_params(
        ngtcp2_qlog *qlog,
        const ngtcp2_transport_params *params,
        int server,
        ngtcp2_qlog_side side) {

    uint8_t buf[1024];   // fixed stack allocation, no size guard
    uint8_t *p = buf;

    p = ngtcp2_qlog_write_transport_params(p, params);
    // no check: (p - buf) may exceed 1024

    ngtcp2_qlog_buf_write(qlog, buf, (size_t)(p - buf));
}


/* AFTER (patched, 1.22.1): */
void ngtcp2_qlog_parameters_set_transport_params(
        ngtcp2_qlog *qlog,
        const ngtcp2_transport_params *params,
        int server,
        ngtcp2_qlog_side side) {

    /* Compute required size before committing to any buffer. */
    size_t needed = ngtcp2_qlog_transport_params_len(params);

    /* Use heap allocation sized to actual output; fall back
     * gracefully on allocation failure rather than overflowing. */
    uint8_t *buf = ngtcp2_mem_malloc(qlog->mem, needed);
    if (buf == NULL) {
        return;  // allocation failure: silently skip qlog entry
    }

    uint8_t *p = buf;
    uint8_t *end = buf + needed;

    /* All downstream writers now receive end pointer and
     * return NULL on would-overflow, propagating failure upward. */
    p = ngtcp2_qlog_write_transport_params(p, end, params);
    if (p == NULL) {
        ngtcp2_mem_free(qlog->mem, buf);
        return;
    }

    ngtcp2_qlog_buf_write(qlog, buf, (size_t)(p - buf));
    ngtcp2_mem_free(qlog->mem, buf);
}

The secondary change is a new ngtcp2_qlog_transport_params_len() function that pre-computes the worst-case serialized size of a ngtcp2_transport_params struct, allowing the heap allocation to be exactly sized. Every internal write helper is updated to accept (uint8_t *p, uint8_t *end, ...) and performs a bounds check before each field write, returning NULL to signal overflow rather than writing past the end.

Detection and Indicators

On instrumented builds, the overflow is unambiguous:


==12483==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd3a200420
WRITE of size 8 at 0x7ffd3a200420 thread T0
    #0 ngtcp2_qlog_write_transport_params   ngtcp2_qlog.c:847
    #1 ngtcp2_qlog_parameters_set_transport_params  ngtcp2_qlog.c:901
    #2 ngtcp2_conn_set_remote_transport_params      ngtcp2_conn.c:2341
    #3 conn_recv_crypto                             ngtcp2_conn.c:5812

Address 0x7ffd3a200420 is located in stack of thread T0 at offset 1056
in frame ngtcp2_qlog_parameters_set_transport_params
  [buf] @ offset 32, size 1024       <-- buf ends at offset 1056, write at 1056

Network-level indicators: an unusually large QUIC transport_parameters TLS extension (>800 bytes) from a single-packet Initial should be treated as anomalous. RFC 9000 does not impose a hard limit on transport parameter size, making purely network-based filtering imprecise, but values exceeding 512 bytes in a ClientHello are uncommon in legitimate implementations.

For production crash analysis: look for __stack_chk_fail in crash stacks originating from ngtcp2_qlog_parameters_set_transport_params, or SIGABRT / SIGSEGV during QUIC handshake processing with qlog callbacks active.

Remediation

Immediate: Upgrade to ngtcp2 1.22.1 or later. The fix is contained in a single commit to lib/ngtcp2_qlog.c and carries no API-breaking changes.

Workaround (if upgrade is not immediately possible): Disable qlog entirely by not registering a qlog_write callback via ngtcp2_conn_set_qlog_callbacks(). The vulnerable function is only reachable when a qlog callback is registered; removing the callback eliminates the attack surface with no impact on QUIC functionality.

Build hardening (defense in depth): Ensure all ngtcp2-consuming binaries are built with -fstack-protector-strong and -D_FORTIFY_SOURCE=2. Stack canaries convert this from an RCE to a reliable remote DoS, substantially reducing exploitability while a patch is applied. In memory-safe deployment contexts (e.g., ngtcp2 wrapped by Rust via FFI), the overflow still crashes the process; no memory-safety guarantee from the wrapper applies to the C stack frame.

Verification: After upgrading, confirm the fix with: nm -D libngtcp2.so | grep qlog_transport_params_len. The presence of ngtcp2_qlog_transport_params_len in the symbol table confirms 1.22.1 or later is linked.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →