✓ProveIt
Examples / Report #2402845 Saved analysis

CVE-2024-2398: HTTP/2 push headers memory-leak

curl · 2024-03-0525.1s analysis · $0.024 in model calls

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

Needs human review

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.

1 · Grounding
grounded
no model · 10 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 2896 tokens · $0.0003
3 · Evidence
7 code snippets
real curl source at 6c632b216b48 · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
16225 tokens · 14.0s · $0.0238
5 · Receipts
0 of 0 citations verified
every citation must point at code the judge was shown
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

C1flaw not verifiable

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.

C2location not verifiable

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.

C3trigger not verifiable

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.

C4impact not verifiable

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.

C5reproduction not verifiable

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)

Full report on HackerOne ↗

## Summary: For each incoming `PUSH_PROMISE` header a new `name:value` string is allocated and the pointer to that string is stored in the `stream->push_headers` array. ``` h = aprintf("%s:%s", name, value); if(h) stream->push_headers[stream->push_headers_used++] = h; ``` Libcurl will reject `PUSH_PROMISE` frames with too many headers. When the number of headers exceeds some threshold, `on_header` returns an error. However, libcurl forgets to free the `stream->push_headers` array elements before `stream->push_headers` is freed. A malicious server may continuously send `PUSH_PROMISE` frames with over 1000 headers, which would eventually consume all available memory. The same issue exists when `Curl_saferealloc` fails. ``` if(stream->push_headers_alloc > 1000) { /* this is beyond crazy many headers, bail out */ failf(data_s, "Too many PUSH_PROMISE headers"); Curl_safefree(stream->push_headers); return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; } stream->push_headers_alloc *= 2; headp = Curl_saferealloc(stream->push_headers, stream->push_headers_alloc * sizeof(char *)); if(!headp) { stream->push_headers = NULL; return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; } ``` ## Steps To Reproduce: 1. compile `nghttp2` with {F3099659} applied 1. compile {F3099658} 1. run `nghttpd -p/=/foo.bar --no-tls 8181` 1. run `valgrind --leak-check=full http2_push_promise` for each `-p` option `nghttpd` will send 200 `PUSH_PROMISE` frames, each with 1280 headers (not counting pseudo headers) ## Supporting Material/References: `valgrind --leak-check=full http2_push_promise` output: ``` ==13928== ==13928== HEAP SUMMARY: ==13928== in use at exit: 8,285,018 bytes in 256,674 blocks ==13928== total heap usage: 261,567 allocs, 4,893 frees, 12,766,009 bytes allocated ==13928== ==13928== 64 bytes in 2 blocks are possibly lost in loss record 2 of 10 ==13928== at 0x48436C4: malloc (vg_replace_malloc.c:392) ==13928== by 0x4889F45: dyn_nappend (dynbuf.c:107) ==13928== by 0x488A2C5: Curl_dyn_addn (dynbuf.c:170) ==13928== by 0x48C393E: alloc_addbyter (mprintf.c:1065) ==13928== by 0x48C2FF9: dprintf_formatf (mprintf.c:852) ==13928== by 0x48C39FF: curl_mvaprintf (mprintf.c:1095) ==13928== by 0x48C3AF0: curl_maprintf (mprintf.c:1110) ==13928== …

Source code

src/tool_doswin.clines 108–235 · definition of stream
108alternate data stream (ADS) cannot be truncated in any case.
109
110Success: (SANITIZE_ERR_OK) *sanitized points to a sanitized copy of file_name.
111Failure: (!= SANITIZE_ERR_OK) *sanitized is NULL.
112*/
113SANITIZEcode 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}
lib/http2.clines 387–513 · definition of on_header
387static 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);
392static int error_callback(nghttp2_session *session, const char *msg,
393 size_t len, void *userp);
394
395/*
396 * Initialize the cfilter context
397 */
398static 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
509out:
510 if(cbs)
511 nghttp2_session_callbacks_del(cbs);
512 return result;
513}
lib/http2.c@curl-8_6_0lines 398–524 · definition of on_header as in release curl-8_6_0; differs from master
Show 127 lines (not cited by the judge)
398static 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);
403static int error_callback(nghttp2_session *session, const char *msg,
404 size_t len, void *userp);
405
406/*
407 * Initialize the cfilter context
408 */
409static 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
520out:
521 if(cbs)
522 nghttp2_session_callbacks_del(cbs);
523 return result;
524}
lib/curl_trc.hlines 63–63 · definition of failf
Show 1 lines (not cited by the judge)
63#define failf Curl_failf
lib/memdebug.hlines 199–200 · definition of Curl_safefree
Show 2 lines (not cited by the judge)
199#define Curl_safefree(ptr) \
200 do { free((ptr)); (ptr) = NULL;} while(0)
lib/strdup.clines 136–143 · definition of Curl_saferealloc
Show 8 lines (not cited by the judge)
136void *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}
lib/curl_printf.hlines 53–53 · definition of aprintf
Show 1 lines (not cited by the judge)
53# define aprintf curl_maprintf