curl · Report #3638715
Use-After-Free race condition in url_move_hostname() via shared connection pool
How it was found
Summary:
In lib/url.c, url_conn_reuse_adjust() calls url_move_hostname() which frees conn->host.rawalloc and conn->host.encalloc via Curl_safefree() and Curl_free_idnconverted_hostname() after Curl_cpool_find() has already released the connection pool lock. A second thread doing a concurrent pool lookup still holds that lock and reads conn->host.name inside url_match_destination() via curl_strequal() — racing with the free. This is not triggered by a malicious server — it is triggered by normal concurrent use of the documented CURLSH API.
Affected version:
curl 8.19.0 (x86_64-pc-linux-gnu) libcurl/8.19.0 OpenSSL/3.6.1 Release-Date: 2026-03-11 Platform: Arch Linux x86_64 BuildId: ab8272972e08f105716dc8989cb8d3afa27753a6
Steps To Reproduce:
-
Install dependencies: sudo pacman -S gcc curl
-
Save the two attached files race_poc.c and curl_min.h in the same directory.
-
Compile with ThreadSanitizer: gcc -g -O1 -pthread -fsanitize=thread
-I. race_poc.c -o race_tsan
/usr/lib/libcurl.so.4 -Wl,-rpath,/usr/lib -
Run TSAN: TSAN_OPTIONS="halt_on_error=0:history_size=4"
timeout 15 ./race_tsan 2>&1Expected: libcurl.so.4+0x360d2 (curl_strequal in url_match_destination) racing with libcurl.so.4+0x3770a (hostname swap in url_conn_reuse_adjust).
-
Compile with AddressSanitizer: gcc -g -O1 -pthread -fsanitize=address
-I. race_poc.c -o race_asan
/usr/lib/libcurl.so.4 -Wl,-rpath,/usr/lib -
Run ASAN: ASAN_OPTIONS="halt_on_error=0:detect_leaks=0"
timeout 15 ./race_asan 2>&1Expected: SIGSEGV crash inside libcurl.so.4 on most runs.
Root cause in lib/url.c:
url_move_hostname() frees the old hostname without any lock:
Curl_safefree(dest->rawalloc); // frees conn->host.rawalloc Curl_free_idnconverted_hostname(dest); // frees conn->host.encalloc *dest = *src; memset(src, 0, sizeof(*src));
This runs inside url_conn_reuse_adjust() AFTER Curl_cpool_find() released the pool lock. A concurrent thread inside Curl_cpool_find() holds the lock and reads conn->host.name (pointing into the freed rawalloc block) inside url_match_destination() via curl_strequal().
url_conn_reuse_adjust() also does this without the lock:
curlx_free(conn->user); curlx_free(conn->passwd); conn->user = needle->user; conn->passwd = needle->passwd;
On a multiplexed HTTP/2 connection where a concurrent transfer is mid-authentication reading conn->user, this is a second race on credentials.
CURLOPT_CONNECT_TO is NOT required to trigger this. url_move_hostname() is called unconditionally on every connection reuse. Any two threads sharing a CURLSH handle with CURL_LOCK_DATA_CONNECT hitting the connection reuse path simultaneously will trigger the race.
Impact
Any multi-threaded application using CURLSH with CURL_LOCK_DATA_CONNECT is affected. This is a standard documented libcurl pattern.
- Thread T3: memcmp read at libcurl.so.4+0x360d2 (curl_strequal inside url_match_destination)
- Thread T2: memcpy write at libcurl.so.4+0x3770a (hostname swap inside url_conn_reuse_adjust)
- Both on the same 58-byte heap block allocated inside libcurl
ASAN produced a hard SIGSEGV on most runs
The PoC only calls curl_easy_perform() via the standard public API. It never calls free() directly. The non-deterministic crash addresses across runs confirm real unpredictable heap corruption different memory layouts produce different crash manifestations from the same root cause.
Confirmed impact: reliable crash in any application using the affected pattern.
Secondary potential impact: conn->user and conn->passwd are freed and replaced in the same function without the lock. On a multiplexed connection where authentication is in progress on a concurrent thread, wrong or stale credentials could be used for that auth exchange.