Data race in Curl_dnscache_add_negative() corrupts shared DNS cache — heap corruption and double-free when using CURLOPT_SHARE with CURL_LOCK_DATA_DNS

Disclosed: 2026-04-04 10:48:16 By intrax To curl
Medium
Vulnerability Details
# Data race in Curl_dnscache_add_negative() corrupts shared DNS cache — heap corruption and double-free when using CURLOPT_SHARE with CURL_LOCK_DATA_DNS **Severity:** Medium **CVSS 3.1:** 6.5 — AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:H --- ## Summary `Curl_dnscache_add_negative()` in `lib/dnscache.c` modifies the shared DNS cache hash table and manipulates a `refcount` field without acquiring the `dnscache_lock()`. Every other public function in `dnscache.c` that modifies this hash table does acquire the lock. When two threads using `CURLOPT_SHARE` with `CURL_LOCK_DATA_DNS` fail DNS resolution simultaneously, the unprotected `Curl_hash_add()` and `refcount--` operations produce a data race that corrupts the heap. The result is a crash (denial of service) or, in the double-free case, a potential path to code execution. The bug was introduced in commit `96d5b5c` (2026-03-06, "dnscache: own source file, improvements") and is present on current `master` (`e65ba1b`). --- ## Vulnerability Details **Vulnerable function** — `lib/dnscache.c:593-616`: ```c CURLcode Curl_dnscache_add_negative(struct Curl_easy *data, uint8_t dns_queries, const char *host, uint16_t port) { struct Curl_dnscache *dnscache = dnscache_get(data); // ... // BUG: no dnscache_lock() here dns = dnscache_add_addr(data, dnscache, dns_queries, NULL, host, strlen(host), port, FALSE); // ^^^^^^^^^^^^^^^^ calls Curl_hash_add() on shared hash table — no lock if(dns) { dns->refcount--; // BUG: unsynchronized refcount decrement — no lock // ... } } ``` `dnscache_add_addr()` (`lib/dnscache.c:533-568`) calls `Curl_hash_add(&dnscache->entries, ...)` at line 558, which inserts into the hash table's internal linked lists. Without the lock, two concurrent calls corrupt these linked lists. **Compare with the sister function** — `lib/dnscache.c:570-591`: ```c CURLcode Curl_dnscache_add(struct Curl_easy *data, struct Curl_dns_entry *entry) { // ... dnscache_lock(data, dnscache); // <— LOCKS if(!Curl_hash_add(&dnscache->entries, id, idlen + 1, (void *)entry)) { dnscache_unlock(data, dnscache); return CURLE_OUT_OF_MEMORY; } entry->refcount++; dnscache_unlock(data, dnscache); // <— UNLOCKS return CURLE_OK; } ``` `Curl_dnscache_add()` protects the identical operation with `dnscache_lock/unlock`. `Curl_dnscache_add_negative()` does not. **Complete list of public functions that modify the hash table and their locking status:** | Function | Line of `dnscache_lock()` | Locked? | |---|---|---| | `Curl_dnscache_prune()` | 181 | Yes | | `Curl_dnscache_clear()` | 205 | Yes | | `Curl_dnscache_get()` | 304 | Yes | | `Curl_dnscache_add()` | 583 | Yes | | `Curl_dns_entry_link()` | 625 | Yes | | `Curl_dns_entry_unlink()` | 646 | Yes | | CURLOPT_RESOLVE (internal) | 716, 821 | Yes | | **`Curl_dnscache_add_negative()`** | — | **No** | The third caller of `dnscache_add_addr()` (CURLOPT_RESOLVE processing at line 845) also wraps the call with `dnscache_lock()` at line 821 and `dnscache_unlock()` at line 853. `Curl_dnscache_add_negative()` is the only code path that reaches `dnscache_add_addr()` without holding the lock. **Neither caller of `Curl_dnscache_add_negative()` holds the lock either:** - `lib/hostip.c:686` — in `hostip_resolv()`, no lock. Three lines below, `Curl_dnscache_add()` is called (line 694), which handles its own locking internally. - `lib/hostip.c:992` — in `Curl_resolv_take_result()`, no lock. Same pattern: `Curl_dnscache_add()` is called at line 986 with internal locking. --- ## Steps to Reproduce This is a source code audit finding. The bug is verifiable by reading the code — no runtime PoC is needed because the missing lock is a factual asymmetry between `Curl_dnscache_add()` and `Curl_dnscache_add_negative()`. **To trigger at runtime:** 1. Create a `CURLSHARE` handle with `CURL_LOCK_DATA_DNS` and lock callbacks 2. Create two `CURLM` multi handles in separate threads, both sharing the DNS cache 3. On each thread, add an easy handle that resolves a non-existent hostname (e.g., `nonexistent1.invalid`, `nonexistent2.invalid`) 4. Call `curl_multi_perform()` on both threads simultaneously 5. Both DNS resolutions fail, both threads call `Curl_dnscache_add_negative()`, both threads enter `Curl_hash_add()` and `refcount--` without locking — data race A network attacker triggers this by blocking or spoofing NXDOMAIN responses for multiple hostnames while the victim application resolves them concurrently. --- ## Impact **1. Heap corruption.** `Curl_hash_add()` manipulates bucket linked lists (`Curl_llist`). Two threads inserting simultaneously corrupt the list pointers (`next`, `prev`, `head`). This leads to a crash on the next hash table operation. Any application using `CURLOPT_SHARE` with `CURL_LOCK_DATA_DNS` where two or more transfers fail DNS resolution at the same time hits this code path. **2. Double-free.** The `dns->refcount--` at line 610 is unsynchronized. The classic race: ``` Thread A reads refcount = 1 Thread B reads refcount = 1 Thread A writes refcount = 0 → triggers free Thread B writes refcount = 0 → triggers free on already-freed memory ``` A double-free on heap is a well-known exploitation primitive for arbitrary code execution. **3. Use-after-free.** If both threads add a negative entry for the same `hostname:port`, `Curl_hash_add()` replaces the existing entry (freeing it). The other thread still holds a pointer to the freed entry and proceeds to decrement its `refcount`, writing to freed memory. **Attack scenario:** A network attacker (e.g., on shared WiFi, ISP-level, or corporate network) causes DNS failures for a multi-threaded application that uses libcurl with shared DNS caching. This is not an exotic configuration — `CURLOPT_SHARE` with `CURL_LOCK_DATA_DNS` is the documented way to share DNS cache across threads, used by crawlers, proxies, and HTTP client pools. --- ## Recommended Fix Add `dnscache_lock/unlock` around the hash table modification in `Curl_dnscache_add_negative()`, matching the pattern used by every other function: ```c CURLcode Curl_dnscache_add_negative(struct Curl_easy *data, uint8_t dns_queries, const char *host, uint16_t port) { struct Curl_dnscache *dnscache = dnscache_get(data); struct Curl_dns_entry *dns; DEBUGASSERT(dnscache); if(!dnscache) return CURLE_FAILED_INIT; dnscache_lock(data, dnscache); // <— ADD dns = dnscache_add_addr(data, dnscache, dns_queries, NULL, host, strlen(host), port, FALSE); if(dns) dns->refcount--; dnscache_unlock(data, dnscache); // <— ADD if(dns) { CURL_TRC_DNS(data, "cache negative name resolve for %s:%d type=%s", host, port, Curl_resolv_query_str(dns_queries)); return CURLE_OK; } return CURLE_OUT_OF_MEMORY; } ``` --- ## Timeline - **2026-03-06** — `Curl_dnscache_add_negative()` introduced without lock (commit `96d5b5c`) - **2026-03-31** — Function signature changed (commit `2b3dfb4`), lock still missing - **2026-04-02** — Bug identified via source code review (this report) ## Impact ## Summary: Heap corruption and denial of service in multi-threaded applications using libcurl's shared DNS cache. When two threads fail DNS resolution simultaneously while sharing a DNS cache via CURLOPT_SHARE with CURL_LOCK_DATA_DNS, the function Curl_dnscache_add_negative() modifies the shared hash table and decrements a refcount without holding the required lock. This data race corrupts the hash table's internal linked list pointers, crashing the application. The unsynchronized refcount-- creates a double-free condition: two threads read refcount=1, both write refcount=0, and the entry is freed twice — a classic heap exploitation primitive. A network attacker triggers this by causing concurrent DNS failures (blocking responses or injecting NXDOMAIN) against any application that uses libcurl with shared DNS caching across threads.
Actions
View on HackerOne
Report Stats
  • Report ID: 3645361
  • State: Closed
  • Substate: informative
  • Upvotes: 2
Share this report