curl · Report #4135291
Heap-use-after-free in libcurl's multi event machinery (curl_multi_remove_handle() from a socket callback nested under a notify callback)
How it was found
Summary:
The multi interface in libcurl 8.23.0 and current master has a reproducible use-after-free. Inside a CURLMOPT_NOTIFYFUNCTION callback, calling curl_easy_pause() on a second transfer that is still in flight re-enters libcurl's event handling and invokes the socket callback, where a curl_multi_remove_handle() on that transfer is allowed (it returns 0) and frees its easy_pollset while mev_pollset_diff() is still walking it - ASan points at lib/multi_ev.c:437 (read), with a write path at :497.
Affected version
curl 8.23.0 (release tarball) and current master (8.23.1-DEV, checked 2026-10-21), built static with ASan+UBSan (-fsanitize=address,undefined) on Linux x86-64; no platform-specific behaviour is involved.
Steps To Reproduce:
Build curl with -fsanitize=address,undefined, compile the program below with gcc -g -O1 -fno-omit-frame-pointer -fsanitize=address,undefined poc.c -I$SRC/include $SRC/lib/.libs/libcurl.a -lpthread -o poc, then run it - the program puts two transfers on one multi handle (a file:// transfer that finishes and raises EASY_DONE, and an HTTP transfer to a local socket that never answers), pauses the HTTP transfer inside the notify callback, and removes it from the socket callback that pause triggers.
#include <curl/curl.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <stdio.h>
#include <string.h>
static CURLM *m; static CURL *a, *b; static curl_socket_t fd[8]; static int act[8], run = 1, ntfy, gone, nf;
static size_t wr(char *p, size_t s, size_t n, void *u){ (void)p; (void)u; return s * n; }
static int sc(CURL *e, curl_socket_t s, int w, void *c, void *d){ int i; (void)c; (void)d;
for(i = 0; i < nf && fd[i] != s; ++i); if(w == CURL_POLL_REMOVE){ if(i < nf){ fd[i] = fd[--nf]; act[i] = act[nf]; } } else { if(i == nf) fd[nf++] = s; act[i] = w; }
if(ntfy && e == b && !gone){ gone = 1; printf("remove_handle rc=%d\n", (int)curl_multi_remove_handle(m, b)); } return 0; }
static int tc(CURLM *x, long t, void *u){ (void)x; (void)t; (void)u; return 0; }
static void nc(CURLM *x, unsigned t, CURL *e, void *u){ (void)x; (void)t; (void)e; (void)u; ntfy = 1; printf("pause B from notify cb\n"); curl_easy_pause(b, CURLPAUSE_RECV); ntfy = 0; }
int main(void){ setbuf(stdout, NULL); int ls, i, n, mx, ev; struct sockaddr_in ad; socklen_t al = sizeof(ad); char url[64]; fd_set r, w; struct timeval tv; curl_socket_t ff[8]; int ee[8];
memset(&ad, 0, sizeof(ad)); ad.sin_family = AF_INET; ad.sin_addr.s_addr = htonl(0x7f000001);
ls = socket(AF_INET, SOCK_STREAM, 0); bind(ls, (void *)&ad, sizeof(ad)); listen(ls, 1); getsockname(ls, (void *)&ad, &al);
curl_global_init(CURL_GLOBAL_ALL); m = curl_multi_init(); curl_multi_setopt(m, CURLMOPT_SOCKETFUNCTION, sc); curl_multi_setopt(m, CURLMOPT_TIMERFUNCTION, tc); curl_multi_setopt(m, CURLMOPT_NOTIFYFUNCTION, nc); curl_multi_notify_enable(m, CURLMNOTIFY_EASY_DONE);
b = curl_easy_init(); snprintf(url, sizeof(url), "http://127.0.0.1:%d/", ntohs(ad.sin_port)); curl_easy_setopt(b, CURLOPT_URL, url); curl_easy_setopt(b, CURLOPT_WRITEFUNCTION, wr); curl_easy_setopt(b, CURLOPT_TIMEOUT, 10L); curl_multi_add_handle(m, b); curl_multi_socket_action(m, CURL_SOCKET_TIMEOUT, 0, &run);
a = curl_easy_init(); curl_easy_setopt(a, CURLOPT_URL, "file:///etc/hosts"); curl_easy_setopt(a, CURLOPT_WRITEFUNCTION, wr); curl_multi_add_handle(m, a);
while(run){ FD_ZERO(&r); FD_ZERO(&w); tv.tv_sec = 0; tv.tv_usec = 20000; mx = -1;
for(i = 0; i < nf; ++i){ if(act[i] & CURL_POLL_IN) FD_SET(fd[i], &r); if(act[i] & CURL_POLL_OUT) FD_SET(fd[i], &w); if((int)fd[i] > mx) mx = (int)fd[i]; }
select(mx + 1, &r, &w, NULL, &tv); n = nf; memcpy(ff, fd, sizeof(ff)); memcpy(ee, act, sizeof(ee));
for(i = 0; i < n; ++i){ ev = 0; if((ee[i] & CURL_POLL_IN) && FD_ISSET(ff[i], &r)) ev |= CURL_CSELECT_IN; if((ee[i] & CURL_POLL_OUT) && FD_ISSET(ff[i], &w)) ev |= CURL_CSELECT_OUT; if(ev) curl_multi_socket_action(m, ff[i], ev, &run); }
curl_multi_socket_action(m, CURL_SOCKET_TIMEOUT, 0, &run); }
return 0; }
pause B from notify cb
remove_handle rc=0
ERROR: AddressSanitizer: heap-use-after-free ... READ of size 4
#0 mev_pollset_diff lib/multi_ev.c:437
#3 curl_easy_pause lib/easy.c:1243
#4 nc poc.c:12
freed by:
#7 Curl_multi_remove_handle lib/multi.c:821
#9 sc poc.c:10
SUMMARY: AddressSanitizer: heap-use-after-free .../lib/multi_ev.c:437 in mev_pollset_diff
Impact
An application using the notify-callback API as documented gets a use-after-free inside libcurl, which in a release build is undefined behaviour on freed memory (crash or corruption depending on the allocator) - memory corruption, not a plain NULL dereference, and it is not remotely triggerable.