HexBrain

curl · Report #3638715

Use-After-Free race condition in url_move_hostname() via shared connection pool

N/Astatus
mediumseverity
23upvotes
31 Mar 2026disclosed

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:

  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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original