Unbounded DNS cache growth allows memory exhaustion
medium swesweep-fasthttp · tcpdialer.go · getTCPAddrs / dial · run #4
Description
The tcpAddrsMap (sync.Map) has no maximum entry limit. Each unique addr string triggers a DNS lookup and a Store into the map. An attacker supplying many unique host:port values (e.g., via user-controlled hostnames in a proxy) can create an unbounded number of cache entries within the 2-minute expiry window (2 x DefaultDNSCacheDuration). Each entry holds a []net.TCPAddr. While the background cleaner (tcpAddrsClean) removes entries older than 2*DNSCacheDuration, entries created in a burst persist until then. Combined with the absence of a concurrency limit on DNS resolution (see next finding), an attacker can rapidly fill the map and exhaust process memory.
Triage
- Verdict
- INVALID confidence 0.17
- Model
- qwen/qwen3.8-27b
- Rounds
- 6
- Cost
- $0.0251 · 280.5 s
[ARBITER] The vulnerability report describes a resource exhaustion issue (unbounded memory growth) caused by providing an unbounded number of unique hostnames to the public API. Under the provided threat model for a 'library', the attacker is defined as code calling the public API with adversarial data, but 'misuse that violates a documented precondition' is explicitly excluded. The library's public API (Dial/DialTimeout) is a dialer, not a protocol parser or decoder; its documented purpose is to connect to addresses provided by the host application. The host application is responsible for validating or limiting the cardinality of the addresses it intends to dial. The library provides a `Concurrency` limit for simultaneous connections and time-based cache expiration, but it is not the responsibility of a low-level dialer library to impose an arbitrary global cap on the number of unique destinations to prevent the host application from misusing the API by dialing to millions of unique, unvalidated, or maliciously crafted hostnames. Therefore, this falls under host application misuse/configuration error rather than a vulnerability in the library itself.
All rounds
[
{
"finding_title": "Unbounded DNS cache growth allows memory exhaustion",
"verdict": "VALID",
"reasoning": "The report claims that the `tcpAddrsMap` allows unbounded growth, leading to memory exhaustion. The code confirms that `getTCPAddrs` stores entries in `d.tcpAddrsMap` keyed by the `addr` string without checking the current size of the map or enforcing a maximum limit. The background cleaner `tcpAddrsClean` only removes entries older than `2 * DefaultDNSCacheDuration` (2 minutes). While the `Concurrency` field (default 1000) limits the number of *simultaneous* active dials, it does not limit the total number of unique addresses cached in memory. An attacker using fasthttp as a client (e.g., in a proxy or crawler scenario) can supply an arbitrary number of unique `host:port` strings. Each unique string triggers a DNS lookup and a store operation in the map. Since the map has no size cap, an attacker can generate millions of unique hostnames, causing the map to grow indefinitely until the process runs out of memory. The 2-minute expiry window is insufficient to prevent this accumulation if the rate of new unique addresses exceeds the rate of cleanup (which is only 1 per second, but even if it were faster, the burst could exhaust memory before cleanup catches up, or simply require a massive number of entries). This fits the definition of a resource exhaustion vulnerability reachable via public API (`Dial`, `Client.Do`, etc.).\n\nCRUX: Whether the tcpAddrsMap has a size limit or eviction policy other than time-based expiry that prevents unbounded growth.\nGREP: tcpAddrsMap.Store",
"elapsed": 40.8,
"tokens": 6733,
"tokens_in": 6389,
"tokens_out": 344,
"cost_usd": 0.00371538,
"file": "tcpdialer.go",
"round": 1,
"grep_used": true,
"grep_results": "GREP `tcpAddrsMap.Store`:\n```\ntcpdialer.go:472:\t\td.tcpAddrsMap.Store(addr, e)\n```"
},
{
"finding_title": "Unbounded DNS cache growth allows memory exhaustion",
"verdict": "INVALID",
"reasoning": "The vulnerability report claims that an attacker can exhaust memory by supplying an unbounded number of unique `host:port` strings, causing the `tcpAddrsMap` to grow indefinitely. The code confirms that `getTCPAddrs` stores entries in `tcpAddrsMap` without a size limit, and the background cleaner only removes entries older than 2 minutes. However, the threat model for a reusable library defines the attacker as \"Code that calls the public API... parsers, decoders... receive adversarial payloads that the host application obtained from untrusted sources.\" In this context, the `addr` string passed to `Dial` is typically configured by the application developer (e.g., upstream server addresses) or derived from trusted configuration, not directly from untrusted user input in a way that would allow an external attacker to inject millions of unique hostnames to trigger DNS lookups. Even in a proxy scenario where the host is user-controlled, the primary security concern would be SSRF or DNS rebinding, not memory exhaustion of the DNS cache itself, which is a resource management issue that application developers are expected to manage (e.g., via rate limiting or connection pooling). Furthermore, the library provides `FlushDNSCache` for manual management. Since the ability to trigger this requires an attacker to control the destination addresses of outgoing connections (which is typically a trust boundary violation handled by the host application, not the library), and the impact is memory exhaustion of the cache rather than a direct crash or exploit of the library's core logic, this is considered a code quality/resource management issue rather than a security vulnerability in the library itself under the given threat model.\n\nCRUX: Whether an external attacker can directly control the `addr` parameter passed to `Dial` to inject an unbounded number of unique hostnames, or if this is a host application configuration issue.\nGREP: Dial\\(",
"elapsed": 59.6,
"tokens": 7300,
"tokens_in": 6902,
"tokens_out": 398,
"cost_usd": 0.0040928399999999995,
"file": "tcpdialer.go",
"round": 2,
"grep_used": true,
"grep_results": "GREP `Dial(`:\n```\nfasthttpadaptor/adaptor_test.go:247:\t\tc, err := ln.Dial()\nfasthttpadaptor/adaptor_test.go:316:\t\tc, err := ln.Dial()\nfasthttpadaptor/adaptor_test.go:399:\t\tc, err := ln.Dial()\nfasthttpadaptor/adaptor_test.go:487:\t\tc, err := ln.Dial()\nfasthttpadaptor/adaptor_test.go:647:\tc, err := ln.Dial()\nfasthttpproxy/dialer.go:95:\t\t\t\treturn d.Dial(network, addr)\nfasthttpproxy/dialer.go:107:\t\t\t\treturn httpProxyDial(d, network, addr, proxyAddr, auth)\nfasthttpproxy/dialer.go:113:\t\t\treturn proxyDialer.Dial(network, addr)\nfasthttpproxy/dialer.go:132:\t\t\treturn d.Dial(network, addr)\nfasthttpproxy/dialer.go:143:\t\t\t\treturn httpProxyDial(d, network, addr, proxyAddr, auth)\nfasthttpproxy/dialer.go:148:\t\treturn proxyDialer.Dial(network, addr)\nfasthttpproxy/dialer.go:153:func (d *Dialer) Dial(network, addr string) (net.Conn, error) {\nfasthttpproxy/dialer.go:158:\t\treturn d.TCPDialer.Dial(addr)\nfasthttpproxy/dialer.go:185:func (d DialerFunc) Dial(network, addr string) (net.Conn, error) {\nfasthttpproxy/dialer.go:190:func httpProxyDial(dialer proxy.Dialer, network, addr, proxyAddr, auth string) (net.Conn, error) {\nfasthttpproxy/dialer.go:195:\tconn, err := dialer.Dial(network, proxyAddr)\nfasthttpproxy/dialer_test.go:246:\tconn, err := httpProxyDial(DialerFunc(func(network, addr string) (net.Conn, error) {\nstreaming_test.go:60:\tconn, err := ln.Dial()\nstreaming_test.go:195:\tconn, err := ln.Dial()\nstreaming_test.go:223:\tc, err := ln.Dial()\nstreaming_test.go:246:\t\t\t\tc, err := ln.Dial()\nhttp_timing_test.go:187:\tconn, err := net.Dial(\"tcp\", addr)\nhttp_timing_test.go:258:\tconn, err := net.Dial(\"tcp\", addr)\nhttp_timing_test.go:278:\t\tconn, err = net.Dial(\"tcp\", addr)\ntcplisten/tcplisten_test.go:80:\t\tc, err := net.Dial(network, addr)\ntcpdialer.go:37:func Dial(addr string) (net.Conn, error) {\ntcpdialer.go:38:\treturn defaultDialer.Dial(addr)\ntcpdialer.go:191:func (d *TCPDialer) Dial(addr string) (net.Conn, error) {\ntcpdialer.go:310:\t\treturn d.tryDial(network, addr, deadline, d.concurrencyCh)\ntcpdialer.go:319:\t\tconn, err = d.tryDial(network, addrs[idx%n].String(), deadline, d.concurrencyCh)\n```"
},
{
"finding_title": "Unbounded DNS cache growth allows memory exhaustion",
"verdict": "INVALID",
"reasoning": "The report identifies a potential resource exhaustion vulnerability due to the `tcpAddrsMap` lacking a maximum entry limit. The code confirms that `getTCPAddrs` stores entries in `d.tcpAddrsMap` keyed by the `addr` string, and the background cleaner `tcpAddrsClean` only removes entries older than `2 * DefaultDNSCacheDuration` (2 minutes). There is no LRU or size-based eviction policy. \n\nHowever, the threat model for this library defines the attacker as \"Code that calls the public API... receive adversarial payloads that the host application obtained from untrusted sources.\" It explicitly states: \"Missing parameter validation in non-public... functions... is not a vulnerability under this model\" and \"misuse that violates a documented precondition of the public API\" is not a vulnerability. More importantly, for a client library like fasthttp, the `addr` parameter passed to `Dial` is typically determined by the application logic (e.g., based on the URL being fetched). While a web crawler or proxy *could* be configured to fetch URLs from untrusted input, the library itself does not parse external network traffic to extract hostnames to be dialed in a way that bypasses the host application's control. The host application is responsible for deciding *where* to dial. If the host application allows an untrusted input to dictate arbitrary thousands of unique hostnames to dial, that is a flaw in the host application's resource management (e.g., lack of rate limiting or allow-listing), not a flaw in the library's memory safety. The library assumes it is called by trusted code with sane inputs. The \"unbounded growth\" is a consequence of the caller's behavior, not a memory corruption bug in the library. Therefore, under the strict threat model where the library's public API inputs are controlled by the host application (which is trusted), this is considered a host application misconfiguration or lack of application-level defense, rather than a security vulnerability in the library itself.\n\nCRUX: Whether the library is responsible for validating the *number* of unique hosts dialed, or if that is the responsibility of the host application calling the public API.\nGREP: Client.Do",
"elapsed": 80.6,
"tokens": 8534,
"tokens_in": 8068,
"tokens_out": 466,
"cost_usd": 0.0047865600000000005,
"file": "tcpdialer.go",
"round": 3,
"grep_used": true,
"grep_results": "GREP `Client.Do`:\n```\nREADME.md:122:$ GOMAXPROCS=1 go test -bench='HTTPClient(Do|GetEndToEnd)' -benchmem -benchtime=10s\nREADME.md:141:$ GOMAXPROCS=1 go test -bench='kClient(Do|GetEndToEnd)' -benchmem -benchtime=10s\nREADME.md:158:$ GOMAXPROCS=4 go test -bench='HTTPClient(Do|GetEndToEnd)' -benchmem -benchtime=10s\nREADME.md:172:$ GOMAXPROCS=4 go test -bench='kClient(Do|GetEndToEnd)' -benchmem -benchtime=10s\nlbclient.go:47:\t// Timeout is the request timeout used when calling LBClient.Do.\nlbclient.go:58:// when calling LBClient.Do.\nclient.go:37:\treturn defaultClient.Do(req, resp)\nclient.go:64:\treturn defaultClient.DoTimeout(req, resp, timeout)\nclient.go:91:\treturn defaultClient.DoDeadline(req, resp, deadline)\nclient.go:2781:// ErrPipelineOverflow may be returned from PipelineClient.Do*\n```"
},
{
"finding_title": "Unbounded DNS cache growth allows memory exhaustion",
"verdict": "INVALID",
"reasoning": "The finding identifies that `tcpAddrsMap` in `TCPDialer` grows without bound for each unique `addr` string, with eviction only via time-based expiration (2 minutes). While the code confirms the map lacks a size limit or LRU eviction, the threat model for this library defines the attacker as code calling the public API and explicitly states that inputs are derived from the host application's logic. The `addr` parameter passed to `Dial` is determined by the host application (e.g., based on a URL provided by the user). If the host application allows an untrusted source to dictate an arbitrary number of unique hostnames to dial, that is a flaw in the host application's input validation and resource management (i.e., a lack of allow-listing or rate-limiting at the application level), not a vulnerability in the library's memory safety or core logic. The library cannot reasonably know the upper bound of valid hosts for every application using it. Therefore, this is a host application misconfiguration or resource management issue, not a security vulnerability in the library itself under the specified threat model.\n\nCRUX: Whether the responsibility for limiting the number of unique dialed addresses lies with the library or the host application.\nGREP: d.tcpAddrsMap.Store",
"elapsed": 48.5,
"tokens": 8376,
"tokens_in": 8101,
"tokens_out": 275,
"cost_usd": 0.00422742,
"file": "tcpdialer.go",
"round": 4,
"grep_used": true,
"grep_results": "GREP `d.tcpAddrsMap.Store`:\n```\ntcpdialer.go:472:\t\td.tcpAddrsMap.Store(addr, e)\n```"
},
{
"finding_title": "Unbounded DNS cache growth allows memory exhaustion",
"verdict": "INVALID",
"reasoning": "The report identifies a lack of size limit on the `tcpAddrsMap` in the TCPDialer. The code confirms that `getTCPAddrs` stores entries keyed by the `addr` string without any maximum size check, LRU eviction, or count limit. The background cleaner (`tcpAddrsClean`) only removes entries based on time age (`2 * DNSCacheDuration`). \n\nHowever, the provided Threat Model for a 'library' explicitly states: 'Attacker: Code that calls the public API...'. It also notes that 'misuse that violates a documented precondition... is not a vulnerability'. In the context of a client library like fasthttp, the `addr` parameter (host:port) is determined by the application developer's logic (e.g., the URL to fetch). While an application *could* be built to fetch URLs from untrusted input, the library's responsibility is to process the request given to it. The 'unbounded growth' occurs only if the host application passes an unbounded number of unique addresses, which is a failure of the host application's resource management/input validation (allow-listing, rate limiting), not a defect in the library's memory safety or core logic. The library does not parse external network traffic to autonomously generate dial targets; it merely dials what it is told to. Therefore, under the strict threat model where the public API is called by trusted code, this is considered a host application misconfiguration or resource management issue, not a security vulnerability in the library itself. The library assumes sane input regarding the cardinality of hostnames.\n\nCRUX: Whether the library is responsible for validating the cardinality of dialed hosts, or if this is a responsibility of the host application providing the input to the public API.\nGREP: tcpAddrsMap.Store",
"elapsed": 26.5,
"tokens": 8653,
"tokens_in": 8267,
"tokens_out": 386,
"cost_usd": 0.00463014,
"file": "tcpdialer.go",
"round": 5,
"grep_used": true,
"grep_results": "GREP `tcpAddrsMap.Store`:\n```\ntcpdialer.go:472:\t\td.tcpAddrsMap.Store(addr, e)\n```"
},
{
"verdict": "INVALID",
"reasoning": "[ARBITER] The vulnerability report describes a resource exhaustion issue (unbounded memory growth) caused by providing an unbounded number of unique hostnames to the public API. Under the provided threat model for a 'library', the attacker is defined as code calling the public API with adversarial data, but 'misuse that violates a documented precondition' is explicitly excluded. The library's public API (Dial/DialTimeout) is a dialer, not a protocol parser or decoder; its documented purpose is to connect to addresses provided by the host application. The host application is responsible for validating or limiting the cardinality of the addresses it intends to dial. The library provides a `Concurrency` limit for simultaneous connections and time-based cache expiration, but it is not the responsibility of a low-level dialer library to impose an arbitrary global cap on the number of unique destinations to prevent the host application from misusing the API by dialing to millions of unique, unvalidated, or maliciously crafted hostnames. Therefore, this falls under host application misuse/configuration error rather than a vulnerability in the library itself.",
"round": 6,
"file": "tcpdialer.go",
"finding_title": "Unbounded DNS cache growth allows memory exhaustion",
"elapsed": 24.5,
"tokens_in": 7078,
"tokens_out": 234,
"cost_usd": 0.0036747599999999996
}
]Raw scan report for this file
## Detailed Security Analysis
I'll trace the data flow from the public API surface through each function, looking for exploitable issues.
### Data Flow
```
Dial/DialTimeout/DialDualStack/DialDualStackTimeout
→ TCPDialer.dial(addr, dualStack, timeout)
→ TCPDialer.getTCPAddrs(addr, dualStack, deadline)
→ resolveTCPAddrs(addr, dualStack, resolver, deadline)
→ net.SplitHostPort(addr) → host, portS
→ strconv.Atoi(portS) → port
→ resolver.LookupIPAddr(ctx, host) → ipaddrs
→ build []net.TCPAddr
→ TCPDialer.tryDial(network, addr, deadline, concurrencyCh)
→ net.Dialer.DialContext(ctx, network, addr)
```
Attacker-controlled: `addr` (host:port string), `timeout`, and if they construct their own `TCPDialer`: `Resolver`, `Concurrency`, `DNSCacheDuration`, `LocalAddr`, `DisableDNSResolution`.
---
### Function-by-Function Analysis
**`Dial` / `DialTimeout` / `DialDualStack` / `DialDualStackTimeout` (package-level)**
Thin wrappers delegating to `defaultDialer`. No independent logic. Safe.
**`TCPDialer.Dial*` methods**
All funnel into `d.dial(addr, dualStack, timeout)`. No validation of `addr` or `timeout` beyond what downstream functions do.
**`TCPDialer.dial`**
- `once.Do` initializes `concurrencyCh`, `DNSCacheDuration`, and starts the background cleaner. The `sync.Once` guarantees single initialization. Correct.
- `deadline := time.Now().Add(timeout)` — if `timeout` is negative or zero, `deadline` is at or before `time.Now()`. Downstream `tryDial` checks `time.Until(deadline) <= 0` and returns `ErrDialTimeout` immediately. Safe.
- `n := uint32(len(addrs))` — `addrs` comes from `getTCPAddrs` which guarantees `len(addrs) > 0` (enforced by `resolveTCPAddrs` returning `errNoDNSEntries` when empty). So `n > 0` and `idx % n` never divides by zero. Safe.
- The round-robin loop `for range n` iterates exactly `n` times. `idx` is a `uint32` from `atomic.AddUint32`; `idx % n` is well-defined for unsigned. Safe.
- **Key observation:** `getTCPAddrs` is called *before* any `tryDial`, and it does **not** acquire a slot from `concurrencyCh`. DNS resolution is therefore **not** bounded by the concurrency limiter.
**`TCPDialer.tryDial`**
- Checks `timeout <= 0` → immediate `ErrDialTimeout`. Safe.
- Concurrency slot acquisition with non-blocking + timed fallback. If the timed select times out, it returns *before* the `defer func() { <-concurrencyCh }()` line, so no slot is leaked. Correct.
- `dialer.DialContext` with a deadline context. Errors are wrapped via `wrapDialWithUpstream`. If `ctx.Err() == context.DeadlineExceeded`, returns `ErrDialTimeout`. Otherwise wraps the actual error. Both paths pass non-nil `err` to the wrapper. Safe.
- `d.LocalAddr` is nil-checked before assignment. Safe.
**`TCPDialer.getTCPAddrs`**
- **Race condition (TOCTOU):** Two goroutines can simultaneously `Load(addr)`, both find no entry (or an expired one), both set `e = nil`, both call `resolveTCPAddrs`, and both `Store` a new entry. The second `Store` overwrites the first. The first goroutine holds a reference to the now-orphaned entry, uses its `addrs`, and the orphaned entry is eventually GC'd. Impact: redundant DNS lookups and a brief window where two different `addrsIdx` counters exist for the same address. Not a memory safety issue, but a correctness concern.
- **`pending` flag logic:** When an entry is expired, only the goroutine that successfully `SwapInt32(&e.pending, 1)` from 0→1 re-resolves. Others fall through with the stale `e`. This is intentional (single-flight re-resolution) but means expired DNS entries are served to concurrent callers until the re-resolve completes and the map is updated.
- On resolution failure, it re-loads the entry and resets `pending` to 0. If the entry was *also* overwritten by a concurrent successful store in the meantime, it resets the *new* entry's `pending` — a minor logic flaw but not exploitable.
**`resolveTCPAddrs`**
- `net.SplitHostPort(addr)` returns an error for malformed input (missing port, multiple colons, etc.). Safe.
- `strconv.Atoi(portS)` returns an error for non-numeric or out-of-`int`-range values. A port like `"99999"` parses successfully but produces `net.TCPAddr{Port: 99999}`; the `net` package rejects this at `DialContext` time. Safe.
- `resolver.LookupIPAddr(ctx, host)` — `host` is caller-controlled. With `net.DefaultResolver`, this sends a DNS query. A very long or malformed host causes the resolver to return an error. The context deadline bounds the wait. Safe from a crash perspective, but see the concurrency-limit finding below.
- `!dualStack && ip.IP.To4() == nil` filters out IPv6 when not dual-stack. If all results are IPv6, `addrs` is empty → `errNoDNSEntries`. Safe.
- `n := len(ipaddrs)` then `make([]net.TCPAddr, 0, n)` — allocation is bounded by resolver output. Safe.
**`cleanExpiredDNSEntries` / `tcpAddrsClean`**
- `sync.Map.Range` is safe for concurrent modification. The type assertion `v.(*tcpAddrEntry)` is guarded by `ok`. Safe.
- `expireDuration := 2 * d.DNSCacheDuration` — if `DNSCacheDuration` is `0` (before `once.Do` sets it), `expireDuration` is `0`, and every entry is immediately expired. But `tcpAddrsClean` is started *inside* `once.Do` after `DNSCacheDuration` is set, so this can't happen in practice. Safe.
- **Goroutine leak:** `tcpAddrsClean` loops forever with `time.Sleep(1s)`. There is no `Stop()` method, no `context.Context`, and no other cancellation mechanism. If a `TCPDialer` instance is short-lived (e.g., created per-request in a server), the goroutine leaks. For the package-level `defaultDialer` this is benign (process lifetime), but for user-created dialers it's a resource leak.
**`FlushDNSCache`**
- `sync.Map.Range` + `Delete` inside. Safe.
**`ErrDialWithUpstream.Error()`**
- `e.wrapErr.Error()` would panic if `wrapErr` is nil. All internal call sites pass non-nil errors. However, the struct is exported, so a caller *could* construct `&ErrDialWithUpstream{}` with a nil `wrapErr` and trigger a panic. This is an API robustness issue, not an externally-triggerable vulnerability.
---
### Findings
The most significant externally-triggerable issue is the **lack of any bound on DNS cache entries and on concurrent DNS resolution**. An attacker who controls the `addr` parameter (e.g., in a reverse-proxy or API-gateway that passes user-supplied hostnames to `fasthttp.Dial`) can:
1. Supply millions of unique `host:port` strings → each triggers a `tcpAddrsMap.Store` → unbounded memory growth until the 2-minute cleaner runs.
2. Make many concurrent `dial` calls with unique addrs → `getTCPAddrs` calls `resolveTCPAddrs` → each performs an unrestricted `resolver.LookupIPAddr` → the `concurrencyCh` limiter is **not** consulted during DNS resolution → unlimited concurrent DNS queries → exhaustion of the system's DNS resolver, network stack, or upstream DNS server.
The goroutine leak is lower severity but worth noting for long-lived services that create and discard `TCPDialer` instances.
```json
[
{
"severity": "medium",
"title": "Unbounded DNS cache growth allows memory exhaustion",
"function": "getTCPAddrs / dial",
"description": "The tcpAddrsMap (sync.Map) has no maximum entry limit. Each unique addr string triggers a DNS lookup and a Store into the map. An attacker supplying many unique host:port values (e.g., via user-controlled hostnames in a proxy) can create an unbounded number of cache entries within the 2-minute expiry window (2 x DefaultDNSCacheDuration). Each entry holds a []net.TCPAddr. While the background cleaner (tcpAddrsClean) removes entries older than 2*DNSCacheDuration, entries created in a burst persist until then. Combined with the absence of a concurrency limit on DNS resolution (see next finding), an attacker can rapidly fill the map and exhaust process memory."
},
{
"severity": "medium",
"title": "No concurrency limit on DNS resolution — unlimited concurrent resolver queries",
"function": "dial / getTCPAddrs",
"description": "The concurrencyCh limiter (default capacity 1000) is only enforced in tryDial for the TCP connection phase. getTCPAddrs, which calls resolveTCPAddrs and ultimately resolver.LookupIPAddr, is invoked before any tryDial call and does NOT acquire a concurrency slot. An attacker issuing many concurrent Dial/DialTimeout calls with distinct addr values triggers unbounded concurrent DNS lookups. With net.DefaultResolver this floods the system DNS resolver; with a custom in-process Resolver it can trigger unbounded CPU/memory. The Concurrency field's documented guarantee ('maximum number of concurrent Dials') does not cover the DNS resolution phase."
},
{
"severity": "low",
"title": "Background DNS cache cleaner goroutine never terminates (goroutine leak)",
"function": "tcpAddrsClean",
"description": "once.Do in dial() starts 'go d.tcpAddrsClean()' which loops forever calling time.Sleep(1s) and cleanExpiredDNSEntries(). There is no Stop() method, no context.Context parameter, and no other cancellation path. If a caller creates a TCPDialer instance for a short-lived purpose (e.g., per-request in a high-churn service) and discards it, the goroutine leaks indefinitely, accumulating one leaked goroutine per discarded dialer. For the package-level defaultDialer this is benign (process lifetime), but for user-constructed dialers it is a resource leak that grows linearly with the number of created-and-discarded instances."
},
{
"severity": "low",
"title": "TOCTOU race in getTCPAddrs causes redundant resolution and orphaned cache entries",
"function": "getTCPAddrs",
"description": "Two goroutines can simultaneously Load(addr), both find the entry missing or expired, both set e=nil, both call resolveTCPAddrs, and both Store a new *tcpAddrEntry. The second Store overwrites the first, orphaning the first entry. The orphaned entry is still referenced by the first goroutine (so it is used for that request's round-robin) but is no longer in the map, so the background cleaner will never delete it — it is only reclaimed by GC after the goroutine releases its reference. Impact: redundant DNS lookups (amplifying resolver load) and a brief window where two independent addrsIdx counters exist for the same address, slightly skewing round-robin distribution. Not a memory-safety issue but a correctness and load-amplification concern."
}
]
```