The AI believed a made-up quote.
Heap Out-of-Bounds Read in lib/http2.c via Malformed PUSH_PROMISE Headers
What the report claims
The report relies on a claim about how nghttp2 handles HTTP headers.
Compare with curl's recorded outcome
Labeled AI slop
curl's maintainer lists this report as AI slop
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The model accepted a documentation quote that the fetched source did not contain. The citation check withdrew its judgment and asked for human review.
See the withdrawn model reasoning
The following reasoning failed the citation check and does not support the final finding.
- lib/http2.c:1642 uses %s format specifiers on name/value pointers from nghttp2 without using the provided namelen/valuelen lengths
- nghttp2 documentation explicitly states name/value pointers are NOT guaranteed to be null-terminated and applications MUST use length parameters
- curl_maprintf processes %s by calling strlen, which reads past the buffer boundary when no null terminator exists
- The fix is straightforward: use %.*s with the length parameters as already done correctly for trailers at line 1650-1651
- This is reachable by a malicious HTTP/2 server sending PUSH_PROMISE headers, causing heap out-of-bounds read (information disclosure or crash)
How this analysis ran 5 claims · 12 citations verified
Evidence check: judge quoted an external source that does not say that (https://nghttp2.org/documentation/nghttp2.h.html)
Checked against curl master on 2026-01-10 (f4b16563ab74). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Inspect the withdrawn draft reply
This draft belongs to the withdrawn judgment. It may contain unsupported claims.
Thank you for the detailed report. The vulnerability is confirmed: lib/http2.c:1642 uses `%s:%s` with nghttp2 header pointers that are not guaranteed null-terminated, while the callback provides `namelen` and `valuelen`. The fix using `%.*s:%.*s` with the length parameters is correct and matches the pattern already used for trailers at line 1650. We will apply this fix.
Inspect all claims and source code 5 claims · 3 code excerpts
This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.
Report claims
curl_maprintf is called with %s format specifiers on header name and value pointers that are not null-terminated, causing strlen() to read past buffer boundaries
Read the reasoning
The code at lib/http2.c:1642 passes name and value pointers to curl_maprintf with %s format specifiers. Since nghttp2 does not guarantee null-termination (per external source), and curl_maprintf uses strlen for %s, this causes an out-of-bounds read when the buffers lack a terminating null byte.
on_header callback in lib/http2.c around line 1642
Read the reasoning
The deterministic checks confirm lib/http2.c exists and line 1642 contains the curl_maprintf call within the on_header function.
sending a PUSH_PROMISE header with a name and value that lack null termination, provided by nghttp2 via its callback parameters namelen and valuelen
Read the reasoning
The on_header callback is registered for nghttp2 header events. When a PUSH_PROMISE frame arrives (frame type HEADERS with category NGHTTP2_HCAT_PUSH_PROMISE), the code at line 1642 processes the header name/value using %s without length limits.
out-of-bounds read of heap memory adjacent to the header buffer, potentially disclosing sensitive data or causing a crash
Read the reasoning
The ASAN log in the report shows a heap-buffer-overflow in strlen called from curl_maprintf at lib/http2.c:1642. An out-of-bounds read can disclose adjacent heap memory (information disclosure) or hit unmapped memory (crash/DOS).
run an HTTP/2 server that pushes a stream with a header whose name and value are not null-terminated, then invoke curl with --http2-prior-knowledge against that server while ASAN is enabled
Read the reasoning
The reproduction steps are plausible: a malicious HTTP/2 server sends PUSH_PROMISE headers with non-null-terminated name/value buffers. The deterministic checks show the ungrounded symbols (H2Configuration, etc.) belong to the Python test script (h2 library), not curl itself, so they don't affect the curl code assessment.
Deterministic checks that failed
- symbol H2Configuration() does not exist in curl at f4b16563ab74 or in any release since 7.50
- symbol H2Connection() does not exist in curl at f4b16563ab74 or in any release since 7.50
- symbol initiate_connection() does not exist in curl at f4b16563ab74 or in any release since 7.50
- symbol update_settings() does not exist in curl at f4b16563ab74 or in any release since 7.50
- symbol data_to_send() does not exist in curl at f4b16563ab74 or in any release since 7.50
- symbol receive_data() does not exist in curl at f4b16563ab74 or in any release since 7.50
- symbol isinstance() does not exist in curl at f4b16563ab74 or in any release since 7.50
- symbol send_headers() does not exist in curl at f4b16563ab74 or in any release since 7.50
What the report says about other projects
confirmed The 'name' and 'value' pointers are NOT guaranteed to be null-terminated. Applications MUST use the provided length parameters.
“The 'name' and 'value' pointers are NOT guaranteed to be null-terminated. Applications MUST use the provided length parameters.” source
Read the original report (excerpt)
Source code
| 1617 | stream->push_headers_alloc = 10; |
| C3: Code context shows this is in the PUSH_PROMISE handling branch (push_headers allocation) | |
| 1618 | stream->push_headers = curlx_malloc(stream->push_headers_alloc * |
| 1619 | sizeof(char *)); |
| 1620 | if(!stream->push_headers) |
| 1621 | return NGHTTP2_ERR_CALLBACK_FAILURE; |
| 1622 | stream->push_headers_used = 0; |
| 1623 | } |
| 1624 | else if(stream->push_headers_used == |
| 1625 | stream->push_headers_alloc) { |
| 1626 | char **headp; |
| 1627 | if(stream->push_headers_alloc > 1000) { |
| 1628 | /* this is beyond crazy many headers, bail out */ |
| 1629 | failf(data_s, "Too many PUSH_PROMISE headers"); |
| 1630 | free_push_headers(stream); |
| 1631 | return NGHTTP2_ERR_CALLBACK_FAILURE; |
| 1632 | } |
| 1633 | stream->push_headers_alloc *= 2; |
| 1634 | headp = curlx_realloc(stream->push_headers, |
| 1635 | stream->push_headers_alloc * sizeof(char *)); |
| 1636 | if(!headp) { |
| 1637 | free_push_headers(stream); |
| 1638 | return NGHTTP2_ERR_CALLBACK_FAILURE; |
| 1639 | } |
| 1640 | stream->push_headers = headp; |
| 1641 | } |
| 1642 | h = curl_maprintf("%s:%s", name, value); |
| C1: curl_maprintf("%s:%s", name, value) uses %s format specifiers on name and value pointers without using the available namelen/valuelen parameters | |
| C2: The vulnerable curl_maprintf call is at line 1642 in the on_header function | |
| C4: curl_maprintf("%s:%s", name, value) will read past buffer end via strlen | |
| C5: Vulnerable code path is in PUSH_PROMISE header handling | |
| 1643 | if(h) |
| 1644 | stream->push_headers[stream->push_headers_used++] = h; |
| 1645 | return 0; |
| 1646 | } |
| 1647 | |
| 1648 | if(stream->bodystarted) { |
| 1649 | /* This is a trailer */ |
| 1650 | CURL_TRC_CF(data_s, cf, "[%d] trailer: %.*s: %.*s", |
| 1651 | stream->id, (int)namelen, name, (int)valuelen, value); |
| 1652 | result = Curl_dynhds_add(&stream->resp_trailers, |
| 1653 | (const char *)name, namelen, |
| 1654 | (const char *)value, valuelen); |
| 1655 | if(result) { |
| 1656 | cf_h2_header_error(cf, data_s, stream, result); |
| 1657 | return NGHTTP2_ERR_CALLBACK_FAILURE; |
| 1658 | } |
| 1659 | |
| 1660 | return 0; |
| 1661 | } |
| 1662 | |
| 1663 | if(namelen == sizeof(HTTP_PSEUDO_STATUS) - 1 && |
| 1664 | memcmp(HTTP_PSEUDO_STATUS, name, namelen) == 0) { |
| 1665 | /* nghttp2 guarantees :status is received first and only once. */ |
| 1666 | char buffer[32]; |
| 1667 | size_t hlen; |
| 1161 | char *curl_maprintf(const char *format, ...) |
| 1162 | { |
| 1163 | va_list ap_save; |
| 1164 | char *s; |
| 1165 | va_start(ap_save, format); |
| 1166 | s = curl_mvaprintf(format, ap_save); |
| C1: curl_maprintf calls curl_mvaprintf which processes %s by calling strlen on the pointer | |
| C4: curl_maprintf -> curl_mvaprintf -> strlen for %s format | |
| 1167 | va_end(ap_save); |
| 1168 | return s; |
| 1169 | } |
| 487 | static int on_header(nghttp2_session *session, const nghttp2_frame *frame, |
| C2: on_header function definition starts at line 487 | |
| 488 | const uint8_t *name, size_t namelen, |
| C1: on_header callback signature shows namelen and valuelen parameters are provided but ignored at line 1642 | |
| C3: on_header receives name, namelen, value, valuelen from nghttp2 | |
| 489 | const uint8_t *value, size_t valuelen, |
| 490 | uint8_t flags, |
| 491 | void *userp); |
| 492 | #ifndef CURL_DISABLE_VERBOSE_STRINGS |
| 493 | static int error_callback(nghttp2_session *session, const char *msg, |
| 494 | size_t len, void *userp); |
| 495 | #endif |
| 496 | static CURLcode cf_h2_ctx_open(struct Curl_cfilter *cf, |
| 497 | struct Curl_easy *data) |
| 498 | { |
| 499 | struct cf_h2_ctx *ctx = cf->ctx; |
| 500 | struct h2_stream_ctx *stream; |
| 501 | CURLcode result = CURLE_OUT_OF_MEMORY; |
| 502 | int rc; |
| 503 | nghttp2_session_callbacks *cbs = NULL; |
| 504 | |
| 505 | DEBUGASSERT(!ctx->h2); |
| 506 | DEBUGASSERT(ctx->initialized); |
| 507 | |
| 508 | rc = nghttp2_session_callbacks_new(&cbs); |
| 509 | if(rc) { |
| 510 | failf(data, "Could not initialize nghttp2 callbacks"); |
| 511 | goto out; |
| 512 | } |
| 513 | |
| 514 | nghttp2_session_callbacks_set_send_callback(cbs, send_callback); |
| 515 | nghttp2_session_callbacks_set_on_frame_recv_callback(cbs, on_frame_recv); |
| 516 | nghttp2_session_callbacks_set_on_invalid_frame_recv_callback(cbs, |
| 517 | cf_h2_on_invalid_frame_recv); |
| 518 | #ifndef CURL_DISABLE_VERBOSE_STRINGS |
| 519 | nghttp2_session_callbacks_set_on_frame_send_callback(cbs, on_frame_send); |
| 520 | #endif |
| 521 | nghttp2_session_callbacks_set_on_data_chunk_recv_callback( |
| 522 | cbs, on_data_chunk_recv); |
| 523 | nghttp2_session_callbacks_set_on_stream_close_callback(cbs, on_stream_close); |
| 524 | nghttp2_session_callbacks_set_on_begin_headers_callback( |
| 525 | cbs, on_begin_headers); |
| 526 | nghttp2_session_callbacks_set_on_header_callback(cbs, on_header); |
| C3: nghttp2_session_callbacks_set_on_header_callback(cbs, on_header) registers on_header as the header callback | |
| C5: on_header callback is active for HTTP/2 sessions | |
| 527 | #ifndef CURL_DISABLE_VERBOSE_STRINGS |
| 528 | nghttp2_session_callbacks_set_error_callback(cbs, error_callback); |
| 529 | #endif |
| 530 | |
| 531 | /* The nghttp2 session is not yet setup, do it */ |
| 532 | rc = h2_client_new(cf, cbs); |
| 533 | if(rc) { |
| 534 | failf(data, "Could not initialize nghttp2"); |
| 535 | goto out; |
| 536 | } |
| 537 | ctx->max_concurrent_streams = DEFAULT_MAX_CONCURRENT_STREAMS; |
| 538 | |
| 539 | if(ctx->via_h1_upgrade) { |
| 540 | /* HTTP/1.1 Upgrade issued. H2 Settings have already been submitted |
| 541 | * in the H1 request and we upgrade from there. This stream |
| 542 | * is opened implicitly as #1. */ |
| 543 | uint8_t binsettings[H2_BINSETTINGS_LEN]; |
| 544 | ssize_t rclen; |
| 545 | size_t binlen; /* length of the binsettings data */ |
| 546 | |
| 547 | rclen = populate_binsettings(binsettings, data); |
| 548 | |
| 549 | if(!curlx_sztouz(rclen, &binlen) || !binlen) { |
| 550 | failf(data, "nghttp2 unexpectedly failed on pack_settings_payload"); |
| 551 | result = CURLE_FAILED_INIT; |
| 552 | goto out; |
| 553 | } |
| 554 | |
| 555 | result = http2_data_setup(cf, data, &stream); |
| 556 | if(result) |
| 557 | goto out; |
| 558 | DEBUGASSERT(stream); |
| 559 | stream->id = 1; |
| 560 | /* queue SETTINGS frame (again) */ |
| 561 | rc = nghttp2_session_upgrade2(ctx->h2, binsettings, binlen, |
| 562 | data->state.httpreq == HTTPREQ_HEAD, |
| 563 | NULL); |
| 564 | if(rc) { |
| 565 | failf(data, "nghttp2_session_upgrade2() failed: %s(%d)", |
| 566 | nghttp2_strerror(rc), rc); |
| 567 | result = CURLE_HTTP2; |
| 568 | goto out; |
| 569 | } |
| 570 | |
| 571 | rc = nghttp2_session_set_stream_user_data(ctx->h2, stream->id, |
| 572 | data); |
| 573 | if(rc) { |
| 574 | infof(data, "http/2: failed to set user_data for stream %u", |
| 575 | stream->id); |
| 576 | DEBUGASSERT(0); |
| 577 | } |
| 578 | CURL_TRC_CF(data, cf, "created session via Upgrade"); |
| 579 | } |
| 580 | else { |
| 581 | nghttp2_settings_entry iv[H2_SETTINGS_IV_LEN]; |
| 582 | size_t ivlen; |
| 583 | |
| 584 | ivlen = populate_settings(iv, data, ctx); |
| 585 | rc = nghttp2_submit_settings(ctx->h2, NGHTTP2_FLAG_NONE, |
| 586 | iv, ivlen); |
| 587 | if(rc) { |
| 588 | failf(data, "nghttp2_submit_settings() failed: %s(%d)", |
| 589 | nghttp2_strerror(rc), rc); |
| 590 | result = CURLE_HTTP2; |
| 591 | goto out; |
| 592 | } |
| 593 | } |
| 594 | |
| 595 | rc = nghttp2_session_set_local_window_size(ctx->h2, NGHTTP2_FLAG_NONE, 0, |
| 596 | HTTP2_HUGE_WINDOW_SIZE); |
| 597 | if(rc) { |
| 598 | failf(data, "nghttp2_session_set_local_window_size() failed: %s(%d)", |
| 599 | nghttp2_strerror(rc), rc); |
| 600 | result = CURLE_HTTP2; |
| 601 | goto out; |
| 602 | } |
| 603 | |
| 604 | /* all set, traffic will be send on connect */ |
| 605 | result = CURLE_OK; |
| 606 | CURL_TRC_CF(data, cf, "[0] created h2 session%s", |
| 607 | ctx->via_h1_upgrade ? " (via h1 upgrade)" : ""); |
| 608 | |
| 609 | out: |
| 610 | if(cbs) |
| 611 | nghttp2_session_callbacks_del(cbs); |
| 612 | return result; |
| 613 | } |