CVE-2024-6197: freeing stack buffer in utf8asn1str
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
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.
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
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.
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).
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.
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.
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)
Source code
| 342 | static CURLcode |
| C2: static CURLcode utf8asn1str(struct dynbuf *to, int type, const char *from, const char *end) | |
| 343 | utf8asn1str(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 | * |
| 338 | static CURLcode |
| 339 | utf8asn1str(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 | } |
Show 42 lines (not cited by the judge)
| 74 | while(<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 | } |