curl · Report #3833577
heap-use-after-free in curl_easy_cleanup() called from callback
How it was found
Summary:
Curl_close() (lib/url.c:214) calls curl_multi_remove_handle(data->multi, data) and ignores the return value. When curl_easy_cleanup() is invoked from within a write/read/progress/header callback, multi->in_callback is TRUE, so curl_multi_remove_handle() (lib/multi.c:818-819) returns CURLM_RECURSIVE_API_CALL without removing the handle. Curl_close() then proceeds to free data (curlx_free(data) at lib/url.c:309). When the callback returns, cw_out_cb_write() (lib/cw-out.c:191) calls Curl_set_in_callback(data, FALSE), dereferencing the freed data pointer — a heap-use-after-free. multi_runsingle() continues to use the freed data afterwards. ASan confirms the UAF.
Affected version
curl 8.21.1-DEV (x86_64-pc-linux-gnu) libcurl/8.21.1-DEV OpenSSL/3.5.6 zlib/1.3.1
Bug is in lib/url.c:214, reproduced on current master commit cfc7922377.
Steps To Reproduce:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <curl/curl.h>
static CURL *g_easy;
static int cleanup_done;
static size_t write_cb(char *ptr, size_t size, size_t nmemb, void *u) {
if(!cleanup_done++) {
curl_easy_cleanup(g_easy); /* frees data, multi still references it */
g_easy = NULL;
}
return size * nmemb;
}
static void *http_server(void *arg) {
int port = *(int *)arg;
int sfd, cfd, opt = 1;
struct sockaddr_in addr;
char buf[4096], body[256], hdr[256];
sfd = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_port = htons(port);
bind(sfd, (struct sockaddr *)&addr, sizeof(addr));
listen(sfd, 1);
cfd = accept(sfd, NULL, NULL);
read(cfd, buf, sizeof(buf));
memset(body, 'A', sizeof(body));
int hlen = snprintf(hdr, sizeof(hdr),
"HTTP/1.0 200 OK\r\nContent-Length: %zu\r\n\r\n", sizeof(body));
write(cfd, hdr, hlen);
write(cfd, body, sizeof(body));
usleep(200000);
close(cfd); close(sfd);
return NULL;
}
int main(void) {
signal(SIGPIPE, SIG_IGN);
int port = 18080;
pthread_t tid;
pthread_create(&tid, NULL, http_server, &port);
usleep(100000);
curl_global_init(CURL_GLOBAL_DEFAULT);
CURLM *multi = curl_multi_init();
g_easy = curl_easy_init();
curl_easy_setopt(g_easy, CURLOPT_URL, "http://127.0.0.1:18080/");
curl_easy_setopt(g_easy, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(g_easy, CURLOPT_NOSIGNAL, 1L);
curl_multi_add_handle(multi, g_easy);
int running;
do {
curl_multi_perform(multi, &running);
if(running) curl_multi_wait(multi, NULL, 0, 100, NULL);
} while(running);
curl_multi_cleanup(multi);
curl_global_cleanup();
pthread_join(tid, NULL);
return 0;
}
Self-contained Docker reproduction (ASan build):
docker build -t uaf-001 .
docker run --rm uaf-001
Expected ASan output:
==14==ERROR: AddressSanitizer: heap-use-after-free on address 0x522000001978
READ of size 8 at 0x522000001978 thread T0
#0 Curl_set_in_callback /root/curl/lib/multi.c:3912
#1 cw_out_cb_write /root/curl/lib/cw-out.c:191
...
freed by thread T0 here:
#2 Curl_close /root/curl/lib/url.c:304
#3 curl_easy_cleanup /root/curl/lib/easy.c:844
#4 write_cb /root/poc/poc.c:65
SUMMARY: AddressSanitizer: heap-use-after-free /root/curl/lib/multi.c:3912 in Curl_set_in_callback
The root cause is that Curl_close() at lib/url.c:214 ignores the return value of curl_multi_remove_handle(). When in_callback is TRUE, the removal fails silently, but Curl_close() proceeds to free data. The caller (cw_out_cb_write) then dereferences the freed pointer.
The fix should check the return value of curl_multi_remove_handle() in Curl_close(). If it returns CURLM_RECURSIVE_API_CALL, Curl_close() should not proceed with freeing data — it should defer the cleanup until the callback has returned.
Impact
Summary:
A heap-use-after-free occurs when an application calls curl_easy_cleanup() from within a write, read, progress, or header callback. The freed data pointer is subsequently dereferenced by cw_out_cb_write() and multi_runsingle(). This can lead to a process crash or, depending on heap layout, potential code execution.