curl · Report #3645361
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
How it was found
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:
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:
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— inhostip_resolv(), no lock. Three lines below,Curl_dnscache_add()is called (line 694), which handles its own locking internally.lib/hostip.c:992— inCurl_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:
- Create a
CURLSHAREhandle withCURL_LOCK_DATA_DNSand lock callbacks - Create two
CURLMmulti handles in separate threads, both sharing the DNS cache - On each thread, add an easy handle that resolves a non-existent hostname (e.g.,
nonexistent1.invalid,nonexistent2.invalid) - Call
curl_multi_perform()on both threads simultaneously - Both DNS resolutions fail, both threads call
Curl_dnscache_add_negative(), both threads enterCurl_hash_add()andrefcount--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:
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 (commit96d5b5c) - 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.