Use-After-Free race condition in url_move_hostname() via shared connection pool
Medium
Vulnerability Details
## 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:
1. Install dependencies:
sudo pacman -S gcc curl
2. Save the two attached files race_poc.c and curl_min.h
in the same directory.
3. 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
4. Run TSAN:
TSAN_OPTIONS="halt_on_error=0:history_size=4" \
timeout 15 ./race_tsan 2>&1
Expected: libcurl.so.4+0x360d2 (curl_strequal
in url_match_destination) racing with libcurl.so.4+0x3770a
(hostname swap in url_conn_reuse_adjust).
5. 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
6. Run ASAN:
ASAN_OPTIONS="halt_on_error=0:detect_leaks=0" \
timeout 15 ./race_asan 2>&1
Expected: 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.
Actions
View on HackerOneReport Stats
- Report ID: 3638715
- State: Closed
- Substate: not-applicable
- Upvotes: 3