CVE-2024-2398: HTTP/2 push headers memory-leak
What the report claims
A memory leak occurs when processing PUSH_PROMISE frames with excessive headers, leading to denial of service.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2024-2398 HTTP/2 push headers memory-leak
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The available evidence does not settle the report. A maintainer should investigate.
Why this finding?
- Deterministic checks confirm the report's quoted code patterns (2/2 and 10/10 lines) exist in the target master revision (6c632b216b48), suggesting the described allocation and error-path logic is present.
- However, the complete on_header function implementation from the target master revision is not included in the provided source snippets — only an older release version (curl-8_6_0) is shown, which is explicitly noted as differing from master.
- Without the master version of the function, the claims cannot be verified or contradicted from the provided code alone. A human must examine the actual master revision's lib/http2.c to confirm the leak and whether the individual push_headers elements are freed on error paths.
- If the leak exists as described (allocating per-header strings, then freeing only the array pointer on error), it would constitute a large memory leak exploitable for DoS, which the project policy treats as a security issue (not a 'small memory leak').
What still needs checking
- The master revision (6c632b216b48) version of lib/http2.c containing the on_header function implementation with push_headers handling.
How this analysis ran 5 claims · 0 citations verified
Checked against curl master on 2024-03-05 (6c632b216b48). Test evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Review a draft reply to the reporter
Edit and verify before sending.
Thank you for the detailed report. The deterministic checks confirm your quoted code patterns exist in the target master revision. However, the provided source snippets do not include the master version of on_header in lib/http2.c (only an older release is shown). Could you confirm the exact file and line numbers in the master branch where the push_headers allocation and the two error paths (header count limit and realloc failure) are handled? This will allow us to verify whether the individual header strings are freed before the array is freed on those error paths.
Inspect all claims and source code 5 claims · 7 code excerpts
Report claims
For each incoming PUSH_PROMISE header a new name:value string is allocated and stored in stream->push_headers array without freeing before stream->push_headers is freed.
Read the reasoning
The on_header function implementation containing the push_headers allocation logic is not present in the provided source snippets. The provided lib/http2.c@curl-8_6_0 shows only a forward declaration of on_header (lines 387-402) and is explicitly noted as differing from master. The master revision (6c632b216b48) version of this function is not included in the provided code.
The allocation and storage occurs in on_header function in http2.c.
Read the reasoning
While on_header is declared in the provided lib/http2.c@curl-8_6_0 (lines 387-402), the actual implementation where allocation occurs is not shown. The provided file is from release curl-8_6_0 and differs from the target master revision.
A malicious server can send more than 1000 PUSH_PROMISE headers to exceed the threshold and cause the leak.
Read the reasoning
The threshold check code (stream->push_headers_alloc > 1000) quoted in the report is not present in the provided source snippets. The deterministic checks confirm 10/10 quoted lines appear in the actual repository at the target revision, but this code is not in the provided excerpts for analysis.
The leak consumes all available memory, leading to denial of service.
Read the reasoning
The impact claim depends on verifying the leak exists (C1-C3), which cannot be determined from the provided code alone.
Running valgrind --leak-check=full http2_push_promise reproduces the leak as shown in the provided output.
Read the reasoning
The valgrind output is provided in the report, but the reproduction cannot be verified against the provided source code because the relevant HTTP/2 push promise handling code from the target master revision is not included in the provided snippets.
Read the original report (excerpt)
Source code
| 108 | alternate data stream (ADS) cannot be truncated in any case. |
| 109 | |
| 110 | Success: (SANITIZE_ERR_OK) *sanitized points to a sanitized copy of file_name. |
| 111 | Failure: (!= SANITIZE_ERR_OK) *sanitized is NULL. |
| 112 | */ |
| 113 | SANITIZEcode sanitize_file_name(char **const sanitized, const char *file_name, |
| 114 | int flags) |
| 115 | { |
| 116 | char *p, *target; |
| 117 | size_t len; |
| 118 | SANITIZEcode sc; |
| 119 | size_t max_sanitized_len; |
| 120 | |
| 121 | if(!sanitized) |
| 122 | return SANITIZE_ERR_BAD_ARGUMENT; |
| 123 | |
| 124 | *sanitized = NULL; |
| 125 | |
| 126 | if(!file_name) |
| 127 | return SANITIZE_ERR_BAD_ARGUMENT; |
| 128 | |
| 129 | if((flags & SANITIZE_ALLOW_PATH)) { |
| 130 | #ifndef MSDOS |
| 131 | if(file_name[0] == '\\' && file_name[1] == '\\') |
| 132 | /* UNC prefixed path \\ (eg \\?\C:\foo) */ |
| 133 | max_sanitized_len = 32767-1; |
| 134 | else |
| 135 | #endif |
| 136 | max_sanitized_len = PATH_MAX-1; |
| 137 | } |
| 138 | else |
| 139 | /* The maximum length of a filename. |
| 140 | FILENAME_MAX is often the same as PATH_MAX, in other words it is 260 and |
| 141 | does not discount the path information therefore we shouldn't use it. */ |
| 142 | max_sanitized_len = (PATH_MAX-1 > 255) ? 255 : PATH_MAX-1; |
| 143 | |
| 144 | len = strlen(file_name); |
| 145 | if(len > max_sanitized_len) { |
| 146 | if(!(flags & SANITIZE_ALLOW_TRUNCATE) || |
| 147 | truncate_dryrun(file_name, max_sanitized_len)) |
| 148 | return SANITIZE_ERR_INVALID_PATH; |
| 149 | |
| 150 | len = max_sanitized_len; |
| 151 | } |
| 152 | |
| 153 | target = malloc(len + 1); |
| 154 | if(!target) |
| 155 | return SANITIZE_ERR_OUT_OF_MEMORY; |
| 156 | |
| 157 | strncpy(target, file_name, len); |
| 158 | target[len] = '\0'; |
| 159 | |
| 160 | #ifndef MSDOS |
| 161 | if((flags & SANITIZE_ALLOW_PATH) && !strncmp(target, "\\\\?\\", 4)) |
| 162 | /* Skip the literal path prefix \\?\ */ |
| 163 | p = target + 4; |
| 164 | else |
| 165 | #endif |
| 166 | p = target; |
| 167 | |
| 168 | /* replace control characters and other banned characters */ |
| 169 | for(; *p; ++p) { |
| 170 | const char *banned; |
| 171 | |
| 172 | if((1 <= *p && *p <= 31) || |
| 173 | (!(flags & (SANITIZE_ALLOW_COLONS|SANITIZE_ALLOW_PATH)) && *p == ':') || |
| 174 | (!(flags & SANITIZE_ALLOW_PATH) && (*p == '/' || *p == '\\'))) { |
| 175 | *p = '_'; |
| 176 | continue; |
| 177 | } |
| 178 | |
| 179 | for(banned = "|<>\"?*"; *banned; ++banned) { |
| 180 | if(*p == *banned) { |
| 181 | *p = '_'; |
| 182 | break; |
| 183 | } |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | /* remove trailing spaces and periods if not allowing paths */ |
| 188 | if(!(flags & SANITIZE_ALLOW_PATH) && len) { |
| 189 | char *clip = NULL; |
| 190 | |
| 191 | p = &target[len]; |
| 192 | do { |
| 193 | --p; |
| 194 | if(*p != ' ' && *p != '.') |
| 195 | break; |
| 196 | clip = p; |
| 197 | } while(p != target); |
| 198 | |
| 199 | if(clip) { |
| 200 | *clip = '\0'; |
| 201 | len = clip - target; |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | #ifdef MSDOS |
| 206 | sc = msdosify(&p, target, flags); |
| 207 | free(target); |
| 208 | if(sc) |
| 209 | return sc; |
| 210 | target = p; |
| 211 | len = strlen(target); |
| 212 | |
| 213 | if(len > max_sanitized_len) { |
| 214 | free(target); |
| 215 | return SANITIZE_ERR_INVALID_PATH; |
| 216 | } |
| 217 | #endif |
| 218 | |
| 219 | if(!(flags & SANITIZE_ALLOW_RESERVED)) { |
| 220 | sc = rename_if_reserved_dos_device_name(&p, target, flags); |
| 221 | free(target); |
| 222 | if(sc) |
| 223 | return sc; |
| 224 | target = p; |
| 225 | len = strlen(target); |
| 226 | |
| 227 | if(len > max_sanitized_len) { |
| 228 | free(target); |
| 229 | return SANITIZE_ERR_INVALID_PATH; |
| 230 | } |
| 231 | } |
| 232 | |
| 233 | *sanitized = target; |
| 234 | return SANITIZE_ERR_OK; |
| 235 | } |
| 387 | static int on_header(nghttp2_session *session, const nghttp2_frame *frame, |
| 388 | const uint8_t *name, size_t namelen, |
| 389 | const uint8_t *value, size_t valuelen, |
| 390 | uint8_t flags, |
| 391 | void *userp); |
| 392 | static int error_callback(nghttp2_session *session, const char *msg, |
| 393 | size_t len, void *userp); |
| 394 | |
| 395 | /* |
| 396 | * Initialize the cfilter context |
| 397 | */ |
| 398 | static CURLcode cf_h2_ctx_init(struct Curl_cfilter *cf, |
| 399 | struct Curl_easy *data, |
| 400 | bool via_h1_upgrade) |
| 401 | { |
| 402 | struct cf_h2_ctx *ctx = cf->ctx; |
| 403 | struct h2_stream_ctx *stream; |
| 404 | CURLcode result = CURLE_OUT_OF_MEMORY; |
| 405 | int rc; |
| 406 | nghttp2_session_callbacks *cbs = NULL; |
| 407 | |
| 408 | DEBUGASSERT(!ctx->h2); |
| 409 | Curl_bufcp_init(&ctx->stream_bufcp, H2_CHUNK_SIZE, H2_STREAM_POOL_SPARES); |
| 410 | Curl_bufq_initp(&ctx->inbufq, &ctx->stream_bufcp, H2_NW_RECV_CHUNKS, 0); |
| 411 | Curl_bufq_initp(&ctx->outbufq, &ctx->stream_bufcp, H2_NW_SEND_CHUNKS, 0); |
| 412 | ctx->last_stream_id = 2147483647; |
| 413 | |
| 414 | rc = nghttp2_session_callbacks_new(&cbs); |
| 415 | if(rc) { |
| 416 | failf(data, "Couldn't initialize nghttp2 callbacks"); |
| 417 | goto out; |
| 418 | } |
| 419 | |
| 420 | nghttp2_session_callbacks_set_send_callback(cbs, send_callback); |
| 421 | nghttp2_session_callbacks_set_on_frame_recv_callback(cbs, on_frame_recv); |
| 422 | #ifndef CURL_DISABLE_VERBOSE_STRINGS |
| 423 | nghttp2_session_callbacks_set_on_frame_send_callback(cbs, on_frame_send); |
| 424 | #endif |
| 425 | nghttp2_session_callbacks_set_on_data_chunk_recv_callback( |
| 426 | cbs, on_data_chunk_recv); |
| 427 | nghttp2_session_callbacks_set_on_stream_close_callback(cbs, on_stream_close); |
| 428 | nghttp2_session_callbacks_set_on_begin_headers_callback( |
| 429 | cbs, on_begin_headers); |
| 430 | nghttp2_session_callbacks_set_on_header_callback(cbs, on_header); |
| 431 | nghttp2_session_callbacks_set_error_callback(cbs, error_callback); |
| 432 | |
| 433 | /* The nghttp2 session is not yet setup, do it */ |
| 434 | rc = h2_client_new(cf, cbs); |
| 435 | if(rc) { |
| 436 | failf(data, "Couldn't initialize nghttp2"); |
| 437 | goto out; |
| 438 | } |
| 439 | ctx->max_concurrent_streams = DEFAULT_MAX_CONCURRENT_STREAMS; |
| 440 | |
| 441 | if(via_h1_upgrade) { |
| 442 | /* HTTP/1.1 Upgrade issued. H2 Settings have already been submitted |
| 443 | * in the H1 request and we upgrade from there. This stream |
| 444 | * is opened implicitly as #1. */ |
| 445 | uint8_t binsettings[H2_BINSETTINGS_LEN]; |
| 446 | ssize_t binlen; /* length of the binsettings data */ |
| 447 | |
| 448 | binlen = populate_binsettings(binsettings, data); |
| 449 | if(binlen <= 0) { |
| 450 | failf(data, "nghttp2 unexpectedly failed on pack_settings_payload"); |
| 451 | result = CURLE_FAILED_INIT; |
| 452 | goto out; |
| 453 | } |
| 454 | |
| 455 | result = http2_data_setup(cf, data, &stream); |
| 456 | if(result) |
| 457 | goto out; |
| 458 | DEBUGASSERT(stream); |
| 459 | stream->id = 1; |
| 460 | /* queue SETTINGS frame (again) */ |
| 461 | rc = nghttp2_session_upgrade2(ctx->h2, binsettings, binlen, |
| 462 | data->state.httpreq == HTTPREQ_HEAD, |
| 463 | NULL); |
| 464 | if(rc) { |
| 465 | failf(data, "nghttp2_session_upgrade2() failed: %s(%d)", |
| 466 | nghttp2_strerror(rc), rc); |
| 467 | result = CURLE_HTTP2; |
| 468 | goto out; |
| 469 | } |
| 470 | |
| 471 | rc = nghttp2_session_set_stream_user_data(ctx->h2, stream->id, |
| 472 | data); |
| 473 | if(rc) { |
| 474 | infof(data, "http/2: failed to set user_data for stream %u", |
| 475 | stream->id); |
| 476 | DEBUGASSERT(0); |
| 477 | } |
| 478 | CURL_TRC_CF(data, cf, "created session via Upgrade"); |
| 479 | } |
| 480 | else { |
| 481 | nghttp2_settings_entry iv[H2_SETTINGS_IV_LEN]; |
| 482 | int ivlen; |
| 483 | |
| 484 | ivlen = populate_settings(iv, data); |
| 485 | rc = nghttp2_submit_settings(ctx->h2, NGHTTP2_FLAG_NONE, |
| 486 | iv, ivlen); |
| 487 | if(rc) { |
| 488 | failf(data, "nghttp2_submit_settings() failed: %s(%d)", |
| 489 | nghttp2_strerror(rc), rc); |
| 490 | result = CURLE_HTTP2; |
| 491 | goto out; |
| 492 | } |
| 493 | } |
| 494 | |
| 495 | rc = nghttp2_session_set_local_window_size(ctx->h2, NGHTTP2_FLAG_NONE, 0, |
| 496 | HTTP2_HUGE_WINDOW_SIZE); |
| 497 | if(rc) { |
| 498 | failf(data, "nghttp2_session_set_local_window_size() failed: %s(%d)", |
| 499 | nghttp2_strerror(rc), rc); |
| 500 | result = CURLE_HTTP2; |
| 501 | goto out; |
| 502 | } |
| 503 | |
| 504 | /* all set, traffic will be send on connect */ |
| 505 | result = CURLE_OK; |
| 506 | CURL_TRC_CF(data, cf, "[0] created h2 session%s", |
| 507 | via_h1_upgrade? " (via h1 upgrade)" : ""); |
| 508 | |
| 509 | out: |
| 510 | if(cbs) |
| 511 | nghttp2_session_callbacks_del(cbs); |
| 512 | return result; |
| 513 | } |
Show 127 lines (not cited by the judge)
| 398 | static int on_header(nghttp2_session *session, const nghttp2_frame *frame, |
| 399 | const uint8_t *name, size_t namelen, |
| 400 | const uint8_t *value, size_t valuelen, |
| 401 | uint8_t flags, |
| 402 | void *userp); |
| 403 | static int error_callback(nghttp2_session *session, const char *msg, |
| 404 | size_t len, void *userp); |
| 405 | |
| 406 | /* |
| 407 | * Initialize the cfilter context |
| 408 | */ |
| 409 | static CURLcode cf_h2_ctx_init(struct Curl_cfilter *cf, |
| 410 | struct Curl_easy *data, |
| 411 | bool via_h1_upgrade) |
| 412 | { |
| 413 | struct cf_h2_ctx *ctx = cf->ctx; |
| 414 | struct stream_ctx *stream; |
| 415 | CURLcode result = CURLE_OUT_OF_MEMORY; |
| 416 | int rc; |
| 417 | nghttp2_session_callbacks *cbs = NULL; |
| 418 | |
| 419 | DEBUGASSERT(!ctx->h2); |
| 420 | Curl_bufcp_init(&ctx->stream_bufcp, H2_CHUNK_SIZE, H2_STREAM_POOL_SPARES); |
| 421 | Curl_bufq_initp(&ctx->inbufq, &ctx->stream_bufcp, H2_NW_RECV_CHUNKS, 0); |
| 422 | Curl_bufq_initp(&ctx->outbufq, &ctx->stream_bufcp, H2_NW_SEND_CHUNKS, 0); |
| 423 | ctx->last_stream_id = 2147483647; |
| 424 | |
| 425 | rc = nghttp2_session_callbacks_new(&cbs); |
| 426 | if(rc) { |
| 427 | failf(data, "Couldn't initialize nghttp2 callbacks"); |
| 428 | goto out; |
| 429 | } |
| 430 | |
| 431 | nghttp2_session_callbacks_set_send_callback(cbs, send_callback); |
| 432 | nghttp2_session_callbacks_set_on_frame_recv_callback(cbs, on_frame_recv); |
| 433 | #ifndef CURL_DISABLE_VERBOSE_STRINGS |
| 434 | nghttp2_session_callbacks_set_on_frame_send_callback(cbs, on_frame_send); |
| 435 | #endif |
| 436 | nghttp2_session_callbacks_set_on_data_chunk_recv_callback( |
| 437 | cbs, on_data_chunk_recv); |
| 438 | nghttp2_session_callbacks_set_on_stream_close_callback(cbs, on_stream_close); |
| 439 | nghttp2_session_callbacks_set_on_begin_headers_callback( |
| 440 | cbs, on_begin_headers); |
| 441 | nghttp2_session_callbacks_set_on_header_callback(cbs, on_header); |
| 442 | nghttp2_session_callbacks_set_error_callback(cbs, error_callback); |
| 443 | |
| 444 | /* The nghttp2 session is not yet setup, do it */ |
| 445 | rc = h2_client_new(cf, cbs); |
| 446 | if(rc) { |
| 447 | failf(data, "Couldn't initialize nghttp2"); |
| 448 | goto out; |
| 449 | } |
| 450 | ctx->max_concurrent_streams = DEFAULT_MAX_CONCURRENT_STREAMS; |
| 451 | |
| 452 | if(via_h1_upgrade) { |
| 453 | /* HTTP/1.1 Upgrade issued. H2 Settings have already been submitted |
| 454 | * in the H1 request and we upgrade from there. This stream |
| 455 | * is opened implicitly as #1. */ |
| 456 | uint8_t binsettings[H2_BINSETTINGS_LEN]; |
| 457 | ssize_t binlen; /* length of the binsettings data */ |
| 458 | |
| 459 | binlen = populate_binsettings(binsettings, data); |
| 460 | if(binlen <= 0) { |
| 461 | failf(data, "nghttp2 unexpectedly failed on pack_settings_payload"); |
| 462 | result = CURLE_FAILED_INIT; |
| 463 | goto out; |
| 464 | } |
| 465 | |
| 466 | result = http2_data_setup(cf, data, &stream); |
| 467 | if(result) |
| 468 | goto out; |
| 469 | DEBUGASSERT(stream); |
| 470 | stream->id = 1; |
| 471 | /* queue SETTINGS frame (again) */ |
| 472 | rc = nghttp2_session_upgrade2(ctx->h2, binsettings, binlen, |
| 473 | data->state.httpreq == HTTPREQ_HEAD, |
| 474 | NULL); |
| 475 | if(rc) { |
| 476 | failf(data, "nghttp2_session_upgrade2() failed: %s(%d)", |
| 477 | nghttp2_strerror(rc), rc); |
| 478 | result = CURLE_HTTP2; |
| 479 | goto out; |
| 480 | } |
| 481 | |
| 482 | rc = nghttp2_session_set_stream_user_data(ctx->h2, stream->id, |
| 483 | data); |
| 484 | if(rc) { |
| 485 | infof(data, "http/2: failed to set user_data for stream %u", |
| 486 | stream->id); |
| 487 | DEBUGASSERT(0); |
| 488 | } |
| 489 | CURL_TRC_CF(data, cf, "created session via Upgrade"); |
| 490 | } |
| 491 | else { |
| 492 | nghttp2_settings_entry iv[H2_SETTINGS_IV_LEN]; |
| 493 | int ivlen; |
| 494 | |
| 495 | ivlen = populate_settings(iv, data); |
| 496 | rc = nghttp2_submit_settings(ctx->h2, NGHTTP2_FLAG_NONE, |
| 497 | iv, ivlen); |
| 498 | if(rc) { |
| 499 | failf(data, "nghttp2_submit_settings() failed: %s(%d)", |
| 500 | nghttp2_strerror(rc), rc); |
| 501 | result = CURLE_HTTP2; |
| 502 | goto out; |
| 503 | } |
| 504 | } |
| 505 | |
| 506 | rc = nghttp2_session_set_local_window_size(ctx->h2, NGHTTP2_FLAG_NONE, 0, |
| 507 | HTTP2_HUGE_WINDOW_SIZE); |
| 508 | if(rc) { |
| 509 | failf(data, "nghttp2_session_set_local_window_size() failed: %s(%d)", |
| 510 | nghttp2_strerror(rc), rc); |
| 511 | result = CURLE_HTTP2; |
| 512 | goto out; |
| 513 | } |
| 514 | |
| 515 | /* all set, traffic will be send on connect */ |
| 516 | result = CURLE_OK; |
| 517 | CURL_TRC_CF(data, cf, "[0] created h2 session%s", |
| 518 | via_h1_upgrade? " (via h1 upgrade)" : ""); |
| 519 | |
| 520 | out: |
| 521 | if(cbs) |
| 522 | nghttp2_session_callbacks_del(cbs); |
| 523 | return result; |
| 524 | } |
Show 1 lines (not cited by the judge)
| 63 | #define failf Curl_failf |
Show 2 lines (not cited by the judge)
| 199 | #define Curl_safefree(ptr) \ |
| 200 | do { free((ptr)); (ptr) = NULL;} while(0) |
Show 8 lines (not cited by the judge)
| 136 | void *Curl_saferealloc(void *ptr, size_t size) |
| 137 | { |
| 138 | void *datap = realloc(ptr, size); |
| 139 | if(size && !datap) |
| 140 | /* only free 'ptr' if size was non-zero */ |
| 141 | free(ptr); |
| 142 | return datap; |
| 143 | } |
Show 1 lines (not cited by the judge)
| 53 | # define aprintf curl_maprintf |