✓ProveIt
Examples / Report #2559516 Saved analysis

CVE-2024-6197: freeing stack buffer in utf8asn1str

curl · 2024-06-1814.8s analysis · $0.014 in model calls

What the report claims

The report claims that curl's utf8asn1str() function contains an invalid free() call on a stack-allocated buffer, leading to potential memory corruption and control-flow hijacking.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2024-6197 freeing stack buffer in utf8asn1str

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Code supports the claim

The code supports the reported flaw. A maintainer should review its impact and decide what to do next.

Why this finding?
  • The code at lib/vtls/x509asn1.c:378 (master) / :374 (curl-8_8_0) shows buf is a stack-allocated array: 'char buf[4];'
  • The code at lib/vtls/x509asn1.c:397 (master) / :393 (curl-8_8_0) calls free(buf) on that stack buffer when wc >= 0x00200000
  • This code path is reachable during TLS certificate parsing for non-UTF8 string types (e.g., CURL_ASN1_UNIVERSAL_STRING) with attacker-controlled certificate data
  • Calling free() on a stack address corrupts the heap allocator, enabling use-after-free and potential code execution via stack overwrite
  • The bug exists in both the reported commit and the latest release (curl-8_8_0); master still has it
How this analysis ran 5 claims · 10 citations verified

Checked against curl permalink 04739054cdac (04739054cdac). Dev 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 · 9 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 2044 tokens · $0.0002
3 · Evidence
3 code snippets
real curl source at 04739054cdac
4 · Judgment
Nemotron-3-Ultra-550b-a55b
9085 tokens · 10.7s · $0.0137
5 · Receipts
10 of 10 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 this clear and well-documented report. The code confirms the bug: utf8asn1str() allocates 'char buf[4]' on the stack at line 378 and incorrectly calls free(buf) at line 397 when wc >= 0x00200000. This is reachable via a malicious TLS certificate containing a UniversalString with crafted bytes. The fix (removing the free() call) is correct. This is a valid security vulnerability in the released version curl-8_8_0 and master.

Inspect all claims and source code 5 claims · 3 code excerpts

Report claims

C1flaw supported

Calls free() on a stack-allocated buffer (buf) in utf8asn1str() when wc >= 0x00200000

Read the reasoning

The code at lines 378 and 397 in lib/vtls/x509asn1.c (master) shows buf is a stack-allocated array of 4 chars, and free(buf) is called on it when wc >= 0x00200000. The same bug exists in the release curl-8_8_0 at lines 374 and 393.

C2location supported

Function utf8asn1str() in lib/vtls/x509asn1.c at line 397

Read the reasoning

The function utf8asn1str is defined starting at line 342, and the free(buf) call is at line 397 in the master branch (line 393 in curl-8_8_0 release).

C3trigger supported

Triggered during TLS handshake when parsing a malicious certificate with type CURL_ASN1_UTF8_STRING

Read the reasoning

The vulnerable path is in the else branch (non-UTF8 string types). When type is CURL_ASN1_UNIVERSAL_STRING (size=4), the code reads 4 bytes from the certificate data into wc. If wc >= 0x00200000, the bug triggers. This is reachable during TLS certificate parsing with attacker-controlled certificate data.

C4impact supported

Overwrites stack contents via use-after-free, potentially allowing control-flow hijacking

Read the reasoning

Calling free() on a stack address is undefined behavior that can corrupt the heap allocator's free list, causing subsequent malloc() calls to return stack addresses. This allows attacker-controlled data to overwrite stack contents (return addresses, frame pointers, local variables), enabling control-flow hijacking. This is a well-known exploitation technique.

C5reproduction supported

Set up malicious server with TLS certificate that triggers the condition wc >= 0x00200000 during certificate parsing

Read the reasoning

An attacker can craft a certificate with a UniversalString (CURL_ASN1_UNIVERSAL_STRING) containing a 4-byte sequence that decodes to wc >= 0x00200000 (e.g., any value with the high bit of the first byte set). This triggers the bug during TLS handshake certificate parsing.

Read the original report (excerpt)

Full report on HackerOne ↗

Libcurl at commit [04739054cdac5a0614fb94e3655e313c03399f35](https://github.com/curl/curl/tree/04739054cdac5a0614fb94e3655e313c03399f35) contains an invalid invocation of `free()` in the function `utf8asn1str()` at [lib/vtls/x509asn1.c:397](https://github.com/curl/curl/blob/04739054cdac5a0614fb94e3655e313c03399f35/lib/vtls/x509asn1.c#L397). The relevant code can be seen below: ```c static CURLcode utf8asn1str(struct dynbuf *to, int type, const char *from, const char *end) { // --- snip --- if(type == CURL_ASN1_UTF8_STRING) { // --- snip --- } else { while(!result && (from < end)) { char buf[4]; /* decode buffer */ // --- snip --- if(wc >= 0x00200000) { free(buf); /* Invalid char. size for target encoding. */ return CURLE_WEIRD_SERVER_REPLY; } } } } ``` `buf` is located on the stack and not the heap, which means that `buf` will be falsely treated as a heap chunk. This poses a security risk because the address of `buf` can be returned in subsequent calls to `malloc()`, which causes the stack contents at that point to be overwritten. The stack holds data that determines the control flow of the application. This can either be local variables that indirectly determine the control flow of their corresponding functions or security-critical metadata liked saved framepointers or return addresses. If an attacker manages to overwrite these kinds of stack contents he/she can gain control over the control flow of the application. # Exploit Scenario Consider the following scenario that demonstrates how to leverage the invalid `free()` to overwrite return addresses: 1. The attacker sets up a malicious server with a TLS certificate that triggers the invalid `free()` 2. An application using libcurl connects to the server and during the connect() phase parses the invalid certificate. This causes the stack address to be put into the freelist of the allocator. 3. The attacker interacts with the client in a way that causes the stack address to be returned by a `malloc()` call and used to store data from the attacker 4. Depending on the state of the stack, the attacker can now overwrite local variables, pointers or security-critical metadata. The worst case would be that a return address gets overwritte …

Source code

lib/vtls/x509asn1.clines 342–422 · definition of utf8asn1str; lines 397-397 cited by the report
342static CURLcode
C2: static CURLcode utf8asn1str(struct dynbuf *to, int type, const char *from, const char *end)
343utf8asn1str(struct dynbuf *to, int type, const char *from, const char *end)
344{
345 size_t inlength = end - from;
346 int size = 1;
347 CURLcode result = CURLE_OK;
348
349 switch(type) {
350 case CURL_ASN1_BMP_STRING:
351 size = 2;
352 break;
353 case CURL_ASN1_UNIVERSAL_STRING:
354 size = 4;
355 break;
356 case CURL_ASN1_NUMERIC_STRING:
357 case CURL_ASN1_PRINTABLE_STRING:
358 case CURL_ASN1_TELETEX_STRING:
359 case CURL_ASN1_IA5_STRING:
360 case CURL_ASN1_VISIBLE_STRING:
361 case CURL_ASN1_UTF8_STRING:
362 break;
363 default:
364 return CURLE_BAD_FUNCTION_ARGUMENT; /* Conversion not supported. */
365 }
366
367 if(inlength % size)
368 /* Length inconsistent with character size. */
369 return CURLE_BAD_FUNCTION_ARGUMENT;
370
371 if(type == CURL_ASN1_UTF8_STRING) {
372 /* Just copy. */
373 if(inlength)
374 result = Curl_dyn_addn(to, from, inlength);
375 }
376 else {
C3: else branch (type != CURL_ASN1_UTF8_STRING) contains the vulnerable code
377 while(!result && (from < end)) {
378 char buf[4]; /* decode buffer */
C1: char buf[4]; /* decode buffer */ - stack allocation
379 size_t charsize = 1;
380 unsigned int wc = 0;
381
382 switch(size) {
383 case 4:
C3: switch(size) builds wc from input bytes; for size=4 (CURL_ASN1_UNIVERSAL_STRING) reads 4 bytes
C5: wc built from 4 input bytes for CURL_ASN1_UNIVERSAL_STRING
384 wc = (wc << 8) | *(const unsigned char *) from++;
385 wc = (wc << 8) | *(const unsigned char *) from++;
386 FALLTHROUGH();
387 case 2:
388 wc = (wc << 8) | *(const unsigned char *) from++;
389 FALLTHROUGH();
390 default: /* case 1: */
391 wc = (wc << 8) | *(const unsigned char *) from++;
392 }
393 if(wc >= 0x00000080) {
394 if(wc >= 0x00000800) {
395 if(wc >= 0x00010000) {
396 if(wc >= 0x00200000) {
C3: if(wc >= 0x00200000) triggers the free(buf)
C5: Condition wc >= 0x00200000 is attacker-controllable via certificate content
397 free(buf);
C1: free(buf); - called on stack buffer when wc >= 0x00200000
C2: free(buf); at line 397 in master (line 393 in curl-8_8_0)
C4: free(buf) on stack address corrupts heap allocator metadata
398 /* Invalid char. size for target encoding. */
399 return CURLE_WEIRD_SERVER_REPLY;
400 }
401 buf[3] = (char) (0x80 | (wc & 0x3F));
402 wc = (wc >> 6) | 0x00010000;
403 charsize++;
404 }
405 buf[2] = (char) (0x80 | (wc & 0x3F));
406 wc = (wc >> 6) | 0x00000800;
407 charsize++;
408 }
409 buf[1] = (char) (0x80 | (wc & 0x3F));
410 wc = (wc >> 6) | 0x000000C0;
411 charsize++;
412 }
413 buf[0] = (char) wc;
414 result = Curl_dyn_addn(to, buf, charsize);
415 }
416 }
417 return result;
418}
419
420/*
421 * Convert an ASN.1 OID into its dotted string representation.
422 *
lib/vtls/x509asn1.c@curl-8_8_0lines 338–414 · definition of utf8asn1str as in release curl-8_8_0; differs from master; commits that changed it since: 0887297100 lib/v*: tidy up types and casts
338static CURLcode
339utf8asn1str(struct dynbuf *to, int type, const char *from, const char *end)
340{
341 size_t inlength = end - from;
342 int size = 1;
343 CURLcode result = CURLE_OK;
344
345 switch(type) {
346 case CURL_ASN1_BMP_STRING:
347 size = 2;
348 break;
349 case CURL_ASN1_UNIVERSAL_STRING:
350 size = 4;
351 break;
352 case CURL_ASN1_NUMERIC_STRING:
353 case CURL_ASN1_PRINTABLE_STRING:
354 case CURL_ASN1_TELETEX_STRING:
355 case CURL_ASN1_IA5_STRING:
356 case CURL_ASN1_VISIBLE_STRING:
357 case CURL_ASN1_UTF8_STRING:
358 break;
359 default:
360 return CURLE_BAD_FUNCTION_ARGUMENT; /* Conversion not supported. */
361 }
362
363 if(inlength % size)
364 /* Length inconsistent with character size. */
365 return CURLE_BAD_FUNCTION_ARGUMENT;
366
367 if(type == CURL_ASN1_UTF8_STRING) {
368 /* Just copy. */
369 if(inlength)
370 result = Curl_dyn_addn(to, from, inlength);
371 }
372 else {
373 while(!result && (from < end)) {
374 char buf[4]; /* decode buffer */
375 int charsize = 1;
376 unsigned int wc = 0;
377
378 switch(size) {
379 case 4:
380 wc = (wc << 8) | *(const unsigned char *) from++;
381 wc = (wc << 8) | *(const unsigned char *) from++;
382 FALLTHROUGH();
383 case 2:
384 wc = (wc << 8) | *(const unsigned char *) from++;
385 FALLTHROUGH();
386 default: /* case 1: */
387 wc = (wc << 8) | *(const unsigned char *) from++;
388 }
389 if(wc >= 0x00000080) {
390 if(wc >= 0x00000800) {
391 if(wc >= 0x00010000) {
392 if(wc >= 0x00200000) {
393 free(buf);
394 /* Invalid char. size for target encoding. */
395 return CURLE_WEIRD_SERVER_REPLY;
396 }
397 buf[3] = (char) (0x80 | (wc & 0x3F));
398 wc = (wc >> 6) | 0x00010000;
399 charsize++;
400 }
401 buf[2] = (char) (0x80 | (wc & 0x3F));
402 wc = (wc >> 6) | 0x00000800;
403 charsize++;
404 }
405 buf[1] = (char) (0x80 | (wc & 0x3F));
406 wc = (wc >> 6) | 0x000000C0;
407 charsize++;
408 }
409 buf[0] = (char) wc;
410 result = Curl_dyn_addn(to, buf, charsize);
411 }
412 }
413 return result;
414}
lib/optiontable.pllines 74–115 · definition of while
Show 42 lines (not cited by the judge)
74while(<STDIN>) {
75 my $l = $_;
76 if($fl) {
77 # continued deprecation
78 if($l =~ /(.*)\),/) {
79 $fl .= $1;
80
81 # the end
82 my @p=split(/, */, $fl);
83 add($p[0], $p[1], $p[2]);
84 undef $fl;
85 }
86 else {
87 # another line to append
88 chomp $l;
89 $fl .= $l;
90 }
91 }
92
93 if(/^ *CURLOPTDEPRECATED\((.*)/) {
94 $fl = $1;
95 chomp $fl;
96 }
97
98 if(/^ *CURLOPT\(([^,]*), ([^,]*), (\d+)\)/) {
99 my($opt, $type, $num)=($1,$2,$3);
100 add($opt, $type, $num);
101 }
102
103 # alias for an older option
104 # old = new
105 if(/^#define (CURLOPT_[^ ]*) *(CURLOPT_\S*)/) {
106 my ($o, $n)=($1, $2);
107 # skip obsolete ones
108 if($n !~ /OBSOLETE/) {
109 $o =~ s/^CURLOPT_//;
110 $n =~ s/^CURLOPT_//;
111 $alias{$o} = $n;
112 push @names, $o,
113 }
114 }
115}