curl · Report #3832393
libcurl upload read callbacks miss recursive API guard, allowing prohibited multi API reentry and ASAN-confirmed UAF
How it was found
Summary:
Several libcurl upload read callback paths invoke the application-provided CURLOPT_READFUNCTION without marking the easy handle as being inside a callback. As a result, recursive multi APIs that are correctly rejected from ordinary callbacks are accepted from these upload read callback paths. I confirmed this in the TELNET read callback path and in the FTP upload resume fallback path. In both cases, calling curl_multi_remove_handle() from the affected read callback returns CURLM_OK, frees active transfer state, and libcurl later dereferences freed objects, producing AddressSanitizer-confirmed heap-use-after-free crashes. A normal HTTP CURLOPT_WRITEFUNCTION control blocks the same call with CURLM_RECURSIVE_API_CALL, showing that the expected recursive callback guard exists but is not consistently applied.
This report does not claim a standalone remote-only exploit where a malicious TELNET or FTP server can crash curl by sending bytes alone. The issue is that libcurl fails to activate its recursive API guard in some upload read callback paths, so otherwise-prohibited lifecycle/multi API calls are accepted and can corrupt active transfer state.
Affected version
curl/libcurl version used for reproduction:
curl 8.21.0 (x86_64-pc-linux-gnu) libcurl/8.21.0 OpenSSL/3.5.0 zlib/1.2.11 Release-Date: 2026-06-24 Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns mqtt mqtts pop3 pop3s rtsp smtp smtps telnet tftp ws wss Features: alt-svc AsynchDNS HSTS HTTPS-proxy IPv6 Largefile libz SSL threadsafe TLS-SRP UnixSockets
Platform: Platform: Linux x86_64 Build: local curl 8.21.0 build with OpenSSL 3.5.0 ASAN/UBSAN PoCs were linked against a local static libcurl 8.21.0 build: /tmp/curl-8.21.0-asan/lib/.libs/libcurl.a
Root cause
libcurl normally marks user callbacks as active callbacks before invoking application code. While that flag is set, recursive multi APIs such as curl_multi_remove_handle(), curl_multi_cleanup(), curl_multi_perform(), and curl_multi_socket_action() are rejected with CURLM_RECURSIVE_API_CALL.
That protection works for normal callbacks. For example, calling curl_multi_remove_handle() from an HTTP CURLOPT_WRITEFUNCTION returns:
CURLM_RECURSIVE_API_CALL / API function called from within callback
However, some upload read callback paths call data->state.fread_func directly without wrapping the call with the same callback guard. I confirmed this in:
lib/telnet.c TELNET CURLOPT_READFUNCTION path
lib/ftp.c FTP upload resume fallback read path
Because these paths do not mark the easy handle as being inside a callback, prohibited recursive multi APIs are accepted and can remove or mutate the active transfer while the outer protocol state machine continues executing.
Vulnerable behavior
Control case: HTTP WRITEFUNCTION
When the same prohibited API call is attempted from a normal HTTP write callback, libcurl blocks it correctly.
Observed result:
curl_multi_remove_handle() returned 8
API function called from within callback
No ASAN crash
This proves that libcurl has an intended recursive API guard for this class of callback misuse.
TELNET positive case
When the same call is made from a TELNET CURLOPT_READFUNCTION, libcurl does not block it.
Observed result:
curl_multi_remove_handle() returned 0
No error
After the active handle/connection is removed, libcurl returns to the TELNET state machine and dereferences freed connection state. AddressSanitizer reports a heap-use-after-free in the TELNET path, including a read involving conn->created through curlx_ptimediff_ms() from telnet_do().
FTP positive case
The same missing-guard pattern was also confirmed in the FTP upload resume fallback path.
Conditions used:
CURLOPT_RESUME_FROM_LARGE = 1
SEEKFUNCTION returns CURL_SEEKFUNC_CANTSEEK
FTP upload resume fallback invokes CURLOPT_READFUNCTION
From that read callback, curl_multi_remove_handle() returned CURLM_OK. Afterward, ASAN reported a use-after-free in ftp_statemach().
This supports treating the issue as a missing recursive callback guard family around upload read callbacks, not as a TELNET-only one-off.
Steps To Reproduce
- Build curl/libcurl with ASAN
From the curl source tree:
./configure --with-openssl --disable-shared --disable-debug \
CFLAGS="-fsanitize=address,undefined -g -O1" \
LDFLAGS="-fsanitize=address,undefined"
make -j"$(nproc)"
A release-style ASAN build is useful because debug assertions can stop execution before the UAF path is reached.
- Compile the control PoC
Compile the HTTP control PoC that calls curl_multi_remove_handle() from CURLOPT_WRITEFUNCTION.
cc -fsanitize=address,undefined -g write_api_matrix.c \
-o write_api_matrix \
/path/to/curl/lib/.libs/libcurl.a \
-lssl -lcrypto -lz -lpthread
Run it:
./write_api_matrix
Expected result:
curl_multi_remove_handle() returned 8
API function called from within callback
No ASAN crash
This is the expected safe behavior.
- Compile the TELNET PoC
Compile the TELNET PoC that calls curl_multi_remove_handle() from CURLOPT_READFUNCTION.
cc -fsanitize=address,undefined -g telnet_api_matrix.c \
-o telnet_api_matrix \
/path/to/curl/lib/.libs/libcurl.a \
-lssl -lcrypto -lz -lpthread
Run it with the included local TELNET test server, or use the standalone harness if it starts its own loopback listener.
python3 attacker_telnet_server.py &
./telnet_api_matrix
Expected result before fix:
curl_multi_remove_handle() returned 0
No error
AddressSanitizer: heap-use-after-free
...
READ of size ...
...
telnet_do()
curlx_ptimediff_ms()
The important part is that the same API call that is rejected from the HTTP callback is accepted from the TELNET read callback and then leads to UAF.
- Compile the FTP resume fallback PoC
Compile the FTP PoC that reaches the upload resume fallback read path.
cc -fsanitize=address,undefined -g ftp_resume_read_remove.c \
-o ftp_resume_read_remove \
/path/to/curl/lib/.libs/libcurl.a \
-lssl -lcrypto -lz -lpthread
Run it with the included minimal FTP server:
python3 mini_ftp_server.py &
./ftp_resume_read_remove
Expected result before fix:
curl_multi_remove_handle() returned 0
No error
AddressSanitizer: heap-use-after-free
...
ftp_statemach()
This confirms that the issue is not limited to TELNET.
Additional observations
Additional lifecycle/reentrant APIs were tested from the affected TELNET read callback path:
curl_multi_cleanup() accepted, ASAN UAF
curl_easy_cleanup() ASAN UAF
curl_multi_perform() accepted, ASAN UAF
curl_multi_socket_action() accepted in the harness, no ASAN crash observed
curl_easy_reset() ASAN UAF in telrcv()
curl_easy_pause() CURLE_OK, expected for most callbacks
Documented CURLOPT_READFUNCTION return values alone did not produce a UAF in my testing:
return 0
return CURL_READFUNC_ABORT
return CURL_READFUNC_PAUSE
short read
normal data
This means the current confirmed impact depends on a callback invoking a prohibited or risky libcurl lifecycle/multi API. The bug is that affected read callback paths fail to engage the guard that normally rejects those calls.
I attached a small repro package: curl-h1-rare03-repro.zip.
It contains:
- README.md with build/run instructions
- VERSION.txt with the curl -V output used for reproduction
- 3 PoCs:
- HTTP WRITEFUNCTION negative control
- TELNET READFUNCTION positive case
- FTP resume fallback positive case
- local TELNET/FTP test servers
- sample ASAN logs from my curl/libcurl 8.21.0 ASAN build
The intended flow is:
- Build curl/libcurl 8.21.0 with ASAN/UBSAN.
- Set CURL_SRC to that ASAN-built source tree.
- Run ./run_all.sh.
Expected results:
- HTTP control: curl_multi_remove_handle() is rejected with CURLM_RECURSIVE_API_CALL.
- TELNET case: the same call returns CURLM_OK and then ASAN reports UAF.
- FTP resume fallback case: the same call returns CURLM_OK and then ASAN reports UAF.
This is meant to demonstrate that the recursive callback guard exists and works for normal callbacks, but is not engaged in these upload read callback paths.
Impact
Summary:
Applications embedding libcurl with TELNET or FTP upload-resume flows can crash or corrupt heap state if application callback logic, plugin code, scripting bindings, FFI layers, or extension code invoke libcurl lifecycle/multi APIs from affected CURLOPT_READFUNCTION paths.
The demonstrated impact is:
ASAN-confirmed heap-use-after-free Reliable process crash Active transfer state freed and then used by libcurl
I did not demonstrate code execution, information disclosure, controlled write-after-free, double-free, or a remote-server-only trigger. A remote TELNET or FTP server can influence timing and application-level conditions that lead a callback to cancel a transfer, but the UAF still requires the application callback to invoke a prohibited or risky libcurl API.
The security-relevant point is that libcurl already enforces a recursive callback guard in ordinary callbacks. The affected upload read callback paths fail to activate that guard, so calls that should return CURLM_RECURSIVE_API_CALL instead return CURLM_OK and corrupt active transfer state.