No concurrency limit on DNS resolution — unlimited concurrent resolver queries
medium swesweep-fasthttp · tcpdialer.go · dial / getTCPAddrs · run #4
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.Triage
- Verdict
- INVALID confidence 0.0
- Model
- qwen/qwen3.8-27b
- Rounds
- 5
- Cost
- $0.0249 · 256.3 s
The reported vulnerability claims that an attacker can trigger unbounded concurrent DNS lookups because the `Concurrency` limit is only applied to the TCP connection phase (`tryDial`) and not the DNS resolution phase (`getTCPAddrs`). While it is true that the code does not enforce the `Concurrency` limit during the DNS lookup step, the threat model for this library defines the attacker as the code calling the public API and the *data* it passes (adversarial payloads). The number of concurrent `Dial` calls is a property of the host application's architecture and concurrency management, not a field in a network payload or a parameter passed to the `Dial` function that an external network attacker can control. An external attacker (e.g., an HTTP client) cannot directly invoke `TCPDialer.Dial` or dictate how many goroutines the host application spawns to make these calls. The `addr` parameter is data, but it does not control the *frequency* or *concurrency* of the calls to the library. Therefore, the 'unbounded concurrency' is a resource management issue for the host application (the trusted caller), not a security vulnerability in the library exploitable via untrusted input. The library cannot prevent the host from calling it too many times; that is a responsibility of the host. This falls under 'misuse' or 'lack of feature' by the trusted caller, which is out of scope for a security vulnerability under the binding threat model. CRUX: The number of concurrent Dial calls is controlled by the trusted host application, not by untrusted input to the library, making it a host-side resource management issue rather than a library security vulnerability. GREP: func (d \*TCPDialer) dial
All rounds
[
{
"finding_title": "No concurrency limit on DNS resolution — unlimited concurrent resolver queries",
"verdict": "INVALID",
"reasoning": "The report claims that `getTCPAddrs` (which performs DNS resolution) is not subject to the `Concurrency` limit, leading to unbounded resource exhaustion if an attacker sends many unique addresses. I traced the code in `tcpdialer.go`: `dial` calls `getTCPAddrs` before acquiring any slot from `concurrencyCh`. The `Concurrency` field is documented as controlling 'the maximum number of concurrent Dials'. However, the threat model defines the attacker as the host application (the library caller) who controls the input `addr`. The `TCPDialer` is instantiated and configured by this trusted caller. The library cannot enforce a limit on the *number of goroutines* the host application creates to call `Dial`, nor can it limit the number of unique addresses the host might dial. If the host application spawns 10,000 goroutines calling `Dial` with unique domains, 10,000 concurrent DNS lookups will occur. This is a resource exhaustion issue, but it requires the library's direct user to misuse it or fail to implement their own concurrency controls. The `Concurrency` limit only applies to the TCP connection phase (`tryDial`) to prevent too many open sockets, not to the DNS phase. Since the 'attacker' in the threat model is the library caller, and the caller is responsible for managing their own concurrency and resource usage (the library provides a dialer, not a full HTTP server with a built-in request rate limit for the host), this is not a vulnerability in the library itself that an external network attacker can trigger without the complicity of the host application. The library does not accept untrusted input from a network socket to determine concurrency; it is called by code within the host process. Therefore, the library cannot be held responsible for the host failing to limit concurrent calls. This falls under 'misuse' or 'lack of feature' rather than a security flaw where an external entity bypasses a defense. The 'unlimited concurrent resolver queries' is only reachable if the host application itself initiates unlimited calls, which is outside the library's trust boundary protection scope.\n\nCRUX: The 'attacker' must be an external entity, but here the input (number of concurrent Dial calls) is controlled by the host application code, not an external network payload. The library cannot limit the number of goroutines the host spawns.\nGREP: func (d *TCPDialer) dial",
"elapsed": 40.0,
"tokens": 6902,
"tokens_in": 6378,
"tokens_out": 524,
"cost_usd": 0.004250759999999999,
"file": "tcpdialer.go",
"round": 1,
"grep_used": true,
"grep_results": "GREP `func (d *TCPDialer) dial (simplified to: TCPDialer)`:\n```\ntcpdialer.go:120:var defaultDialer = &TCPDialer{Concurrency: 1000}\ntcpdialer.go:127:// TCPDialer contains options to control a group of Dial calls.\ntcpdialer.go:128:type TCPDialer struct {\ntcpdialer.go:130:\t// var dialer = &fasthttp.TCPDialer{\ntcpdialer.go:191:func (d *TCPDialer) Dial(addr string) (net.Conn, error) {\ntcpdialer.go:216:func (d *TCPDialer) DialTimeout(addr string, timeout time.Duration) (net.Conn, error) {\ntcpdialer.go:244:func (d *TCPDialer) DialDualStack(addr string) (net.Conn, error) {\ntcpdialer.go:270:func (d *TCPDialer) DialDualStackTimeout(addr string, timeout time.Duration) (net.Conn, error) {\ntcpdialer.go:276:func (d *TCPDialer) FlushDNSCache() {\ntcpdialer.go:290:func (d *TCPDialer) dial(addr string, dualStack bool, timeout time.Duration) (net.Conn, error) {\ntcpdialer.go:331:func (d *TCPDialer) tryDial(\ntcpdialer.go:427:func (d *TCPDialer) cleanExpiredDNSEntries() {\ntcpdialer.go:439:func (d *TCPDialer) tcpAddrsClean() {\ntcpdialer.go:446:func (d *TCPDialer) getTCPAddrs(addr string, dualStack bool, deadline time.Time) ([]net.TCPAddr, uint32, error) {\nclient_test.go:3705:func TestTCPDialerFlushDNSCache(t *testing.T) {\nclient_test.go:3711:\tdialer := &TCPDialer{\nfasthttpproxy/dialer.go:24:// Dialer embeds both fasthttp.TCPDialer and httpproxy.Config, allowing it\nfasthttpproxy/dialer.go:28:\tfasthttp.TCPDialer\nfasthttpproxy/dialer.go:158:\t\treturn d.TCPDialer.Dial(addr)\nfasthttpproxy/dialer.go:164:\t\treturn d.TCPDialer.DialDualStack(addr)\nexamples/client/client.go:38:\t\tDial: (&fasthttp.TCPDialer{\n```"
},
{
"finding_title": "No concurrency limit on DNS resolution — unlimited concurrent resolver queries",
"verdict": "INVALID",
"reasoning": "The threat model for a library defines the attacker as the code calling the public API and, critically, the data passed through it: 'parsers, decoders, deserializers and protocol handlers receive adversarial payloads that the host application obtained from untrusted sources.'\n\nThe reported vulnerability is 'unlimited concurrent DNS lookups'. To trigger this, an attacker must cause a large number of concurrent `Dial` calls with distinct `addr` values. \n\n1. **Entry Point**: The `Dial` function is called by the host application. The host application decides *when* to call `Dial` and *how many* goroutines to spawn. The host application also decides *what* `addr` to pass.\n2. **Control**: An external network attacker (e.g., a client connecting to an HTTP server built with fasthttp) does not have direct control over the `Dial` function's invocation frequency or the specific addresses being dialed. In a typical server setup, the server dials *out* to upstream services. The number of concurrent dials is a property of the server's workload and architecture, not a field in a network packet.\n3. **Trust Boundary**: The library cannot protect against a host application that spawns 100,000 goroutines to call `Dial` with 100,000 unique URLs. This is a resource management responsibility of the host application. The `Concurrency` field exists to limit concurrent *TCP connections* (sockets) for a specific dialer instance, but the threat model states that 'Misuse that violates a documented precondition of the public API' is not a vulnerability. More importantly, the ability to call a function repeatedly is not controlled by the library; it is controlled by the caller. \n4. **Comparison**: If the attacker could send a single HTTP request that caused the server to internally issue 1,000,000 `Dial` calls, that would be a valid 'unbounded resource use reachable through the public API with a crafted payload'. However, standard HTTP requests do not dictate the internal concurrency of the dialer. The dialer is used by the host to manage connections. The number of concurrent dials is determined by the host's request handling logic, which is outside the scope of 'adversarial payload' in the context of the dialer's input `addr`.\n\nTherefore, the 'attacker' cannot trigger this bug via untrusted input to the `Dial` function's parameters (the `addr` string) in a way that changes the *concurrency* of the calls. The concurrency is a property of the calling context, not the data passed. This falls under 'misuse' or 'lack of feature' (the library doesn't limit total concurrent calls, only concurrent sockets per dialer), not a security vulnerability exploitable by an external attacker via the defined entry points.\n\nCRUX: The attacker cannot control the number of concurrent Dial calls; this is determined by the host application's architecture, not by the untrusted input (addr) passed to the library.\nGREP: Concurrency",
"elapsed": 62.8,
"tokens": 8334,
"tokens_in": 7670,
"tokens_out": 664,
"cost_usd": 0.0052134,
"file": "tcpdialer.go",
"round": 2,
"grep_used": true,
"grep_results": "GREP `Concurrency`:\n```\ntcpdialer.go:120:var defaultDialer = &TCPDialer{Concurrency: 1000}\ntcpdialer.go:151:\t// Concurrency controls the maximum number of concurrent Dials\ntcpdialer.go:157:\tConcurrency int\ntcpdialer.go:292:\t\tif d.Concurrency > 0 {\ntcpdialer.go:293:\t\t\td.concurrencyCh = make(chan struct{}, d.Concurrency)\nserver_timing_test.go:144:\t\tConcurrency: 16 * clientsCount,\nserver_timing_test.go:162:\t\tConcurrency: 16 * clientsCount,\nserver_timing_test.go:187:\t\tConcurrency: 16 * clientsCount,\nserver_timing_test.go:337:\t\tConcurrency: 16 * clientsCount,\nserver_timing_test.go:380:\t\tConcurrency: 16 * clientsCount,\nserver_test.go:953:func TestServerConcurrencyLimit(t *testing.T) {\nserver_test.go:960:\t\tConcurrency: 1,\nserver_test.go:1035:\t\tConcurrency: 1,\nfasthttputil/pipeconns_test.go:225:\ttestConcurrency(t, 10, testPipeConnsReadWriteSerial)\nfasthttputil/pipeconns_test.go:286:\ttestConcurrency(t, 10, testPipeConnsCloseSerial)\nfasthttputil/pipeconns_test.go:343:func testConcurrency(t *testing.T, concurrency int, f func(*testing.T)) {\nserver.go:260:\t// DefaultConcurrency is used if not set.\nserver.go:262:\t// Concurrency only works if you either call Serve once, or only ServeConn multiple times.\nserver.go:264:\tConcurrency int\nserver.go:335:\t// SleepWhenConcurrencyLimitsExceeded is a duration to be slept of if\nserver.go:338:\tSleepWhenConcurrencyLimitsExceeded time.Duration\nserver.go:472:// msg to the client if there are more than Server.Concurrency concurrent\nserver.go:483:// msg to the client if there are more than Server.Concurrency concurrent\nserver.go:741:// Server limits such as Concurrency, ReadTimeout, WriteTimeout, etc.\nserver.go:1909:// DefaultConcurrency is the maximum number of concurrent connections\nserver.go:1910:// the Server may serve by default (i.e. if Server.Concurrency isn't set).\nserver.go:1911:const DefaultConcurrency = 256 * 1024\nserver.go:1920:\tmaxWorkersCount := s.getConcurrency()\nserver.go:1964:\t\t\t\t\"The connection cannot be served because Server.Concurrency limit exceeded\")\nserver.go:1969:\t\t\t\t\t\"Try increasing Server.Concurrency\", maxWorkersCount)\n```"
},
{
"finding_title": "No concurrency limit on DNS resolution — unlimited concurrent resolver queries",
"verdict": "INVALID",
"reasoning": "The report claims that an attacker can trigger unbounded concurrent DNS lookups by issuing many concurrent `Dial` calls with distinct addresses. However, the threat model for a library defines the attacker as the entity controlling the data passed to the public API (adversarial payloads), while the host application (the caller of the public API) is the trusted entity responsible for managing its own resource usage, such as the number of goroutines spawned. The `TCPDialer` is a client-side component used by the host application to dial upstream servers. The number of concurrent `Dial` calls is determined by the host application's architecture and request handling logic, not by the content of the `addr` parameter or any external network payload. An external network attacker (e.g., an HTTP client) cannot directly invoke the `TCPDialer.Dial` method; they can only influence the host application to do so. Therefore, the 'unlimited concurrency' is a property of the host application's usage of the library, not a vulnerability in the library's handling of untrusted input. The library provides a `Concurrency` limit for the TCP connection phase, but the lack of a limit for the DNS phase is a design choice regarding internal resource management, not a security flaw exploitable by an external attacker via the defined entry points. This falls under 'misuse' or 'lack of feature' by the trusted caller, which is out of scope for a security vulnerability in the library under the given threat model.\n\nCRUX: The number of concurrent Dial calls is controlled by the trusted host application, not by untrusted input to the library, making it a host application resource management issue rather than a library security vulnerability.\nGREP: func (d \\*TCPDialer) Dial",
"elapsed": 99.0,
"tokens": 8825,
"tokens_in": 8450,
"tokens_out": 375,
"cost_usd": 0.004674,
"file": "tcpdialer.go",
"round": 3,
"grep_used": true,
"grep_results": "GREP `func (d *TCPDialer) Dial (simplified to: TCPDialer)`:\n```\ntcpdialer.go:120:var defaultDialer = &TCPDialer{Concurrency: 1000}\ntcpdialer.go:127:// TCPDialer contains options to control a group of Dial calls.\ntcpdialer.go:128:type TCPDialer struct {\ntcpdialer.go:130:\t// var dialer = &fasthttp.TCPDialer{\ntcpdialer.go:191:func (d *TCPDialer) Dial(addr string) (net.Conn, error) {\ntcpdialer.go:216:func (d *TCPDialer) DialTimeout(addr string, timeout time.Duration) (net.Conn, error) {\ntcpdialer.go:244:func (d *TCPDialer) DialDualStack(addr string) (net.Conn, error) {\ntcpdialer.go:270:func (d *TCPDialer) DialDualStackTimeout(addr string, timeout time.Duration) (net.Conn, error) {\ntcpdialer.go:276:func (d *TCPDialer) FlushDNSCache() {\ntcpdialer.go:290:func (d *TCPDialer) dial(addr string, dualStack bool, timeout time.Duration) (net.Conn, error) {\ntcpdialer.go:331:func (d *TCPDialer) tryDial(\ntcpdialer.go:427:func (d *TCPDialer) cleanExpiredDNSEntries() {\ntcpdialer.go:439:func (d *TCPDialer) tcpAddrsClean() {\ntcpdialer.go:446:func (d *TCPDialer) getTCPAddrs(addr string, dualStack bool, deadline time.Time) ([]net.TCPAddr, uint32, error) {\nclient_test.go:3705:func TestTCPDialerFlushDNSCache(t *testing.T) {\nclient_test.go:3711:\tdialer := &TCPDialer{\nfasthttpproxy/dialer.go:24:// Dialer embeds both fasthttp.TCPDialer and httpproxy.Config, allowing it\nfasthttpproxy/dialer.go:28:\tfasthttp.TCPDialer\nfasthttpproxy/dialer.go:158:\t\treturn d.TCPDialer.Dial(addr)\nfasthttpproxy/dialer.go:164:\t\treturn d.TCPDialer.DialDualStack(addr)\nexamples/client/client.go:38:\t\tDial: (&fasthttp.TCPDialer{\n```"
},
{
"finding_title": "No concurrency limit on DNS resolution — unlimited concurrent resolver queries",
"verdict": "INVALID",
"reasoning": "The report claims that the `Concurrency` limit is not enforced during the DNS resolution phase (`getTCPAddrs`), allowing for unbounded concurrent DNS lookups if an attacker issues many distinct addresses. I verified the code in `tcpdialer.go`: the `dial` function calls `getTCPAddrs` *before* entering `tryDial`, and `getTCPAddrs` does not acquire a slot from `concurrencyCh`. The `concurrencyCh` is only acquired inside `tryDial`. Thus, the bug pattern (DNS lookups not limited by the `Concurrency` setting) exists in the code.\n\nHowever, the verdict depends on the threat model and reachability. The threat model for a library defines the attacker as the code calling the public API, specifically regarding the *data* (payloads) passed to it. In `fasthttp`, `Dial` is a client-side method used to establish connections to upstream servers. The `addr` parameter is typically determined by the host application's configuration or routing logic, not directly by an external network payload. More critically, the *concurrency* (the number of simultaneous `Dial` calls) is controlled by the host application's logic (e.g., how many requests it processes, how many goroutines it spawns). An external network attacker cannot directly control the number of concurrent `Dial` calls made by the host application simply by sending data to the host, unless the host application has a specific flaw where it maps one request to multiple unique dials without limiting. The library itself does not accept a 'payload' that dictates 'please run N concurrent dials'. The `Concurrency` field is a hint/config for resource management of the *connection* phase, but the lack of a limit in the *DNS* phase is a design characteristic of the library's resource management. \n\nAccording to the rules: 'misuse that violates a documented precondition of the public API' is not a vulnerability. The 'precondition' here is effectively that the caller manages its own load. If the caller spawns 10,000 goroutines calling `Dial` with unique addresses, they are misusing the library's resource management expectations or simply not utilizing a feature that doesn't exist (a DNS concurrency limit). The 'attacker' (untrusted input) cannot be the *source* of the concurrency; the source is the trusted host code. Therefore, this is a host application resource management issue, not a security vulnerability in the library itself reachable via untrusted input. The library correctly handles the `addr` input (parsing, validation), but it cannot protect the host from the host itself calling it too many times.\n\nCRUX: The number of concurrent Dial calls is controlled by the trusted host application's architecture, not by untrusted input to the library, making this a host-side resource management issue rather than a library vulnerability.\nGREP: func (d \\*TCPDialer) dial",
"elapsed": 31.1,
"tokens": 9495,
"tokens_in": 8868,
"tokens_out": 627,
"cost_usd": 0.00560556,
"file": "tcpdialer.go",
"round": 4,
"grep_used": true,
"grep_results": "GREP `func (d *TCPDialer) dial (simplified to: TCPDialer)`:\n```\nfasthttpproxy/dialer.go:24:// Dialer embeds both fasthttp.TCPDialer and httpproxy.Config, allowing it\nfasthttpproxy/dialer.go:28:\tfasthttp.TCPDialer\nfasthttpproxy/dialer.go:158:\t\treturn d.TCPDialer.Dial(addr)\nfasthttpproxy/dialer.go:164:\t\treturn d.TCPDialer.DialDualStack(addr)\nexamples/client/client.go:38:\t\tDial: (&fasthttp.TCPDialer{\nclient_test.go:3705:func TestTCPDialerFlushDNSCache(t *testing.T) {\nclient_test.go:3711:\tdialer := &TCPDialer{\ntcpdialer.go:120:var defaultDialer = &TCPDialer{Concurrency: 1000}\ntcpdialer.go:127:// TCPDialer contains options to control a group of Dial calls.\ntcpdialer.go:128:type TCPDialer struct {\ntcpdialer.go:130:\t// var dialer = &fasthttp.TCPDialer{\ntcpdialer.go:191:func (d *TCPDialer) Dial(addr string) (net.Conn, error) {\ntcpdialer.go:216:func (d *TCPDialer) DialTimeout(addr string, timeout time.Duration) (net.Conn, error) {\ntcpdialer.go:244:func (d *TCPDialer) DialDualStack(addr string) (net.Conn, error) {\ntcpdialer.go:270:func (d *TCPDialer) DialDualStackTimeout(addr string, timeout time.Duration) (net.Conn, error) {\ntcpdialer.go:276:func (d *TCPDialer) FlushDNSCache() {\ntcpdialer.go:290:func (d *TCPDialer) dial(addr string, dualStack bool, timeout time.Duration) (net.Conn, error) {\ntcpdialer.go:331:func (d *TCPDialer) tryDial(\ntcpdialer.go:427:func (d *TCPDialer) cleanExpiredDNSEntries() {\ntcpdialer.go:439:func (d *TCPDialer) tcpAddrsClean() {\ntcpdialer.go:446:func (d *TCPDialer) getTCPAddrs(addr string, dualStack bool, deadline time.Time) ([]net.TCPAddr, uint32, error) {\n```"
},
{
"finding_title": "No concurrency limit on DNS resolution — unlimited concurrent resolver queries",
"verdict": "INVALID",
"reasoning": "The reported vulnerability claims that an attacker can trigger unbounded concurrent DNS lookups because the `Concurrency` limit is only applied to the TCP connection phase (`tryDial`) and not the DNS resolution phase (`getTCPAddrs`). While it is true that the code does not enforce the `Concurrency` limit during the DNS lookup step, the threat model for this library defines the attacker as the code calling the public API and the *data* it passes (adversarial payloads). The number of concurrent `Dial` calls is a property of the host application's architecture and concurrency management, not a field in a network payload or a parameter passed to the `Dial` function that an external network attacker can control. An external attacker (e.g., an HTTP client) cannot directly invoke `TCPDialer.Dial` or dictate how many goroutines the host application spawns to make these calls. The `addr` parameter is data, but it does not control the *frequency* or *concurrency* of the calls to the library. Therefore, the 'unbounded concurrency' is a resource management issue for the host application (the trusted caller), not a security vulnerability in the library exploitable via untrusted input. The library cannot prevent the host from calling it too many times; that is a responsibility of the host. This falls under 'misuse' or 'lack of feature' by the trusted caller, which is out of scope for a security vulnerability under the binding threat model.\n\nCRUX: The number of concurrent Dial calls is controlled by the trusted host application, not by untrusted input to the library, making it a host-side resource management issue rather than a library security vulnerability.\nGREP: func (d \\*TCPDialer) dial",
"elapsed": 23.4,
"tokens": 9948,
"tokens_in": 9577,
"tokens_out": 371,
"cost_usd": 0.00513534,
"file": "tcpdialer.go",
"round": 5,
"grep_used": true,
"grep_results": "GREP `func (d *TCPDialer) dial (simplified to: TCPDialer)`:\n```\ntcpdialer.go:120:var defaultDialer = &TCPDialer{Concurrency: 1000}\ntcpdialer.go:127:// TCPDialer contains options to control a group of Dial calls.\ntcpdialer.go:128:type TCPDialer struct {\ntcpdialer.go:130:\t// var dialer = &fasthttp.TCPDialer{\ntcpdialer.go:191:func (d *TCPDialer) Dial(addr string) (net.Conn, error) {\ntcpdialer.go:216:func (d *TCPDialer) DialTimeout(addr string, timeout time.Duration) (net.Conn, error) {\ntcpdialer.go:244:func (d *TCPDialer) DialDualStack(addr string) (net.Conn, error) {\ntcpdialer.go:270:func (d *TCPDialer) DialDualStackTimeout(addr string, timeout time.Duration) (net.Conn, error) {\ntcpdialer.go:276:func (d *TCPDialer) FlushDNSCache() {\ntcpdialer.go:290:func (d *TCPDialer) dial(addr string, dualStack bool, timeout time.Duration) (net.Conn, error) {\ntcpdialer.go:331:func (d *TCPDialer) tryDial(\ntcpdialer.go:427:func (d *TCPDialer) cleanExpiredDNSEntries() {\ntcpdialer.go:439:func (d *TCPDialer) tcpAddrsClean() {\ntcpdialer.go:446:func (d *TCPDialer) getTCPAddrs(addr string, dualStack bool, deadline time.Time) ([]net.TCPAddr, uint32, error) {\nclient_test.go:3705:func TestTCPDialerFlushDNSCache(t *testing.T) {\nclient_test.go:3711:\tdialer := &TCPDialer{\nexamples/client/client.go:38:\t\tDial: (&fasthttp.TCPDialer{\nfasthttpproxy/dialer.go:24:// Dialer embeds both fasthttp.TCPDialer and httpproxy.Config, allowing it\nfasthttpproxy/dialer.go:28:\tfasthttp.TCPDialer\nfasthttpproxy/dialer.go:158:\t\treturn d.TCPDialer.Dial(addr)\nfasthttpproxy/dialer.go:164:\t\treturn d.TCPDialer.DialDualStack(addr)\n```"
}
]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."
}
]
```