HexBrain

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

Informativestatus
mediumseverity
23upvotes
4 Apr 2026disclosed

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:

FunctionLine of dnscache_lock()Locked?
Curl_dnscache_prune()181Yes
Curl_dnscache_clear()205Yes
Curl_dnscache_get()304Yes
Curl_dnscache_add()583Yes
Curl_dns_entry_link()625Yes
Curl_dns_entry_unlink()646Yes
CURLOPT_RESOLVE (internal)716, 821Yes
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:

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.

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