curl · Report #4033971
Use-after-free of the internal multi->admin easy handle via the documented CURLMOPT_NOTIFYFUNCTION callback
How it was found
Summary:
curl_multi_init() creates an internal easy handle (multi->admin) and marks it state.internal = TRUE (lib/multi.c:248). The CURLMOPT_NOTIFYFUNCTION callback is handed that internal handle on CURLMNOTIFY_INFO_READ (lib/multi_ntfy.c:109), and the option documentation invites callbacks to remove and clean up easy handles. Neither curl_multi_remove_handle() (lib/multi.c:903, only GOOD_EASY_HANDLE()) nor curl_easy_cleanup() (lib/easy.c:855, no state.internal check) rejects it, so the callback can free multi->admin while the multi handle still references it. The program then reads the freed handle inside curl_multi_cleanup() and closes multi->admin a second time (ASan: heap-use-after-free in Curl_async_thrdd_multi_destroy, lib/vdns/asyn-thrdd.c:506). I reproduced this on the official curl 8.22.0 release tarball and on current git master cdbe4eb; I can provide a patch or more details if useful.
Affected version
curl/libcurl 8.22.0, official release tarball (Release-Date: 2026-09-02, tag curl-8_22_0); also reproduced on current git master cdbe4eb. Platform: x86_64 Linux; static libcurl built from source with clang 14 and -fsanitize=address (RelWithDebInfo). curl -V output of the build used: curl 8.22.0 (Linux) libcurl/8.22.0 Release-Date: 2026-09-02 Protocols: dict file ftp gopher http imap ipfs ipns mqtt pop3 rtsp smtp telnet tftp ws Features: alt-svc AsynchDNS IPv6 Largefile threadsafe UnixSockets
Steps To Reproduce:
- Unpack the attached poc.zip (poc.c, reproduce.sh, build.md). reproduce.sh builds a static libcurl with AddressSanitizer from a curl source tree (master by default, or point SRC at the 8.22.0 source) and compiles the harness.
- Run ./poc - on CURLMNOTIFY_INFO_READ the callback calls curl_multi_remove_handle() and curl_easy_cleanup() on the handle it was given; the program then calls curl_multi_cleanup().
- Observe the ASan heap-use-after-free: the free stack shows curl_easy_cleanup() called from the callback, and the read happens in curl_multi_cleanup() -> Curl_async_thrdd_multi_destroy(). Control: ./poc control (the callback leaves the delivered handle alone) exits cleanly with no sanitizer report.
Impact
Summary:
An application that follows the CURLMOPT_NOTIFYFUNCTION documentation frees libcurl's internal easy handle. Subsequent multi API calls operate on freed heap memory (use-after-free read) and close the handle a second time, which crashes the process or corrupts state (denial of service). Only a completed transfer with notifications enabled is needed - no malicious server is required.