Pending dial holds listener mutex and can deadlock Close
high swesweep-fasthttp · fasthttputil/inmemory_listener.go · DialWithLocalAddr · run #4
Description
DialWithLocalAddr acquires ln.lock, sends the server-side connection to ln.conns, then blocks on <-accepted while still holding ln.lock. If the acceptor is absent, delayed, or blocked, the Dial call never returns. Close also requires ln.lock, so closing the listener deadlocks behind the pending dial. This can leak the dial goroutine, the PipeConns pair, and the accepted channel, and can freeze all further Dial calls. A caller can trigger this by invoking Dial without an active Accept loop or with an acceptor that never consumes the queued connection.
Triage
- Verdict
- INVALID confidence 0.0
- Model
- qwen/qwen3.8-27b
- Rounds
- 5
- Cost
- $0.0137 · 95.9 s
The reported deadlock is technically real in the code: `DialWithLocalAddr` acquires `ln.lock`, sends a connection to `ln.conns`, and then blocks on the unbuffered `accepted` channel while still holding the lock. `Close` requires `ln.lock` to close `ln.conns`. If no `Accept` goroutine is running to consume from `ln.conns` and signal `accepted`, `Dial` blocks indefinitely, preventing `Close` from executing and causing a deadlock. However, under the binding threat model for a reusable library, the attacker is defined as code calling the public API with untrusted data (parsers, decoders, etc.). `InmemoryListener` is an in-memory transport utility that does not parse external protocol bytes. Triggering this deadlock requires the host application to misuse the API by calling `Dial` without a concurrent `Accept` loop, which violates the documented semantics ('Just like a real Dial it only returns once the server has accepted the connection'). This is a concurrency correctness issue or API misuse in the host application's logic, not a security vulnerability triggered by untrusted external input. An external attacker cannot send a 'packet' to an in-memory listener to induce this state. Therefore, it is invalid as a security vulnerability under the specified threat model.
CRUX: The deadlock is caused by API misuse (missing Accept loop) within the same process, not by external untrusted input data.
GREP: DialWithLocalAddrAll rounds
[
{
"finding_title": "Pending dial holds listener mutex and can deadlock Close",
"verdict": "INVALID",
"reasoning": "The reported deadlock is technically real in the code structure: `DialWithLocalAddr` holds `ln.lock` while blocking on the unbuffered `accepted` channel, and `Close` requires `ln.lock` to close the `ln.conns` channel. If no `Accept` goroutine is running, `Dial` blocks forever holding the lock, preventing `Close` from executing. However, under the threat model for a reusable library (swesweep-fasthttp), `InmemoryListener` is a utility intended for in-process communication and testing. The 'attacker' is code calling the public API. To trigger this deadlock, a caller must invoke `Dial` without simultaneously running `Accept`. This constitutes a misuse of the API's documented semantics ('Just like a real Dial it only returns once the server has accepted the connection'). It is not triggered by untrusted input data (such as network packets or file contents) flowing through the library; it is a concurrency state error in the host application's logic. Furthermore, the library is not an external service boundary where an attacker can send packets to hang a goroutine; it is a helper for the application itself. Therefore, this is a code quality/API usage issue, not a security vulnerability reachable by an external attacker via untrusted input.\n\nCRUX: The deadlock is triggered by missing Accept loop (logic error/misuse), not by untrusted input data.\nGREP: InmemoryListener",
"elapsed": 15.1,
"tokens": 3050,
"tokens_in": 2738,
"tokens_out": 312,
"cost_usd": 0.00208596,
"file": "fasthttputil/inmemory_listener.go",
"round": 1,
"grep_used": true,
"grep_results": "GREP `InmemoryListener`:\n```\nfasthttpadaptor/adaptor_test.go:237:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:306:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:389:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:477:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:533:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:624:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:646:func sendRequest(ln *fasthttputil.InmemoryListener, req *http.Request, responseTimeout time.Duration) ([]byte, error) {\nstreaming_test.go:30:\tln := fasthttputil.NewInmemoryListener()\nstreaming_test.go:105:func getChunkedTestEnv(t testing.TB) (*fasthttputil.InmemoryListener, []byte) {\nstreaming_test.go:126:\tln := fasthttputil.NewInmemoryListener()\nstreaming_test.go:164:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:30:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:90:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:130:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:159:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:200:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:255:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:287:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:325:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:362:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:404:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:435:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:461:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:487:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:549:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:766:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:812:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:880:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:912:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:944:\tln := fasthttputil.NewInmemoryListener()\n```"
},
{
"finding_title": "Pending dial holds listener mutex and can deadlock Close",
"verdict": "INVALID",
"reasoning": "The reported deadlock is technically present in the code: `DialWithLocalAddr` acquires `ln.lock` and then blocks on the unbuffered `accepted` channel, preventing `Close` from acquiring the same lock and closing the connection if no `Accept` goroutine is running. However, under the binding threat model for a reusable library, a vulnerability must be triggerable by an external attacker via untrusted input (e.g., crafted network packets, files, or API arguments) leading to meaningful harm (crash, code execution, auth bypass). `InmemoryListener` is an in-memory utility for in-process communication or testing. It does not parse external protocol data. Triggering the deadlock requires the host application to misuse the API by calling `Dial` without a concurrent `Accept` loop, which is a logic error or concurrency bug in the application code, not a result of processing malicious untrusted data. The 'attacker' (caller) must already be executing arbitrary code within the same process to manipulate the listener state in this specific way, and even then, the outcome is a hang/deadlock of internal components rather than memory corruption or privilege escalation. This falls under 'misuse that violates a documented precondition' or code quality/concurrency correctness, which is explicitly out of scope for a security vulnerability under this threat model.\n\nCRUX: The deadlock is triggered by API misuse (missing Accept loop) within the same process, not by external untrusted input data.\nGREP: DialWithLocalAddr",
"elapsed": 17.2,
"tokens": 4223,
"tokens_in": 3891,
"tokens_out": 332,
"cost_usd": 0.00263022,
"file": "fasthttputil/inmemory_listener.go",
"round": 2,
"grep_used": true,
"grep_results": "GREP `DialWithLocalAddr`:\n```\nfasthttputil/inmemory_listener.go:101:\treturn ln.DialWithLocalAddr(nil)\nfasthttputil/inmemory_listener.go:104:// DialWithLocalAddr creates new client<->server connection.\nfasthttputil/inmemory_listener.go:110:func (ln *InmemoryListener) DialWithLocalAddr(local net.Addr) (net.Conn, error) {\nfasthttputil/inmemory_listener_test.go:266:\tc, err := ln.DialWithLocalAddr(clientAddr)\n```"
},
{
"finding_title": "Pending dial holds listener mutex and can deadlock Close",
"verdict": "INVALID",
"reasoning": "The reported vulnerability is a deadlock in `InmemoryListener.DialWithLocalAddr`. The function acquires `ln.lock` and then blocks on an unbuffered channel (`<-accepted`) waiting for `Accept` to consume the connection. `Close` requires `ln.lock` to close the `ln.conns` channel. If no `Accept` goroutine is running, `Dial` blocks indefinitely, preventing `Close` from executing, leading to a deadlock. \n\nHowever, under the specified threat model for a reusable library (swesweep-fasthttp):\n1. **Attacker & Input**: The attacker is code calling the public API, and the untrusted input is the data passed through (parsers, decoders, etc.). `InmemoryListener` does not parse external protocol bytes (like HTTP packets); it provides an in-memory transport mechanism.\n2. **Trigger Condition**: The deadlock is triggered by a lack of a concurrent `Accept` call. This is a usage pattern error (misuse) where the caller fails to satisfy the documented precondition that 'Dial only returns once the server has accepted the connection'.\n3. **External Reachability**: An external attacker cannot send a 'packet' to this in-memory listener to trigger this state. The state is determined entirely by the internal control flow of the host application. For an attacker to trigger this, they would need to already be executing code within the process that instantiates and mismanages the listener, which implies they already have code execution in the host process.\n4. **Severity**: The impact is a hang/deadlock of the specific listener instance. It does not lead to memory corruption, privilege escalation, or cross-boundary data leakage. It is a concurrency correctness issue in the library's implementation, but not a security vulnerability reachable via untrusted external input.\n\nThus, this is a code quality/API usage issue, not a security vulnerability under the given threat model.\n\nCRUX: The deadlock is caused by internal concurrency misuse (missing Accept loop) and is not triggerable by external untrusted input data.\nGREP: InmemoryListener",
"elapsed": 21.9,
"tokens": 4173,
"tokens_in": 3724,
"tokens_out": 449,
"cost_usd": 0.00291108,
"file": "fasthttputil/inmemory_listener.go",
"round": 3,
"grep_used": true,
"grep_results": "GREP `InmemoryListener`:\n```\nfasthttpadaptor/adaptor_test.go:237:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:306:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:389:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:477:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:533:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:624:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:646:func sendRequest(ln *fasthttputil.InmemoryListener, req *http.Request, responseTimeout time.Duration) ([]byte, error) {\nstreaming_test.go:30:\tln := fasthttputil.NewInmemoryListener()\nstreaming_test.go:105:func getChunkedTestEnv(t testing.TB) (*fasthttputil.InmemoryListener, []byte) {\nstreaming_test.go:126:\tln := fasthttputil.NewInmemoryListener()\nstreaming_test.go:164:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:30:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:90:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:130:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:159:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:200:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:255:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:287:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:325:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:362:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:404:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:435:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:461:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:487:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:549:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:766:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:812:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:880:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:912:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:944:\tln := fasthttputil.NewInmemoryListener()\n```"
},
{
"finding_title": "Pending dial holds listener mutex and can deadlock Close",
"verdict": "INVALID",
"reasoning": "The reported deadlock is technically real in the code: `DialWithLocalAddr` holds `ln.lock` while blocking on the unbuffered `accepted` channel, and `Close` requires `ln.lock` to close the `ln.conns` channel. If no `Accept` goroutine is running, `Dial` blocks indefinitely, preventing `Close` from executing. However, under the binding threat model for a reusable library, a vulnerability must be triggerable by an external attacker via untrusted input (e.g., crafted network packets, files, or API arguments) leading to meaningful harm like memory corruption or privilege escalation. `InmemoryListener` is an in-memory utility for in-process communication or testing; it does not parse external protocol data. The deadlock is triggered by a misuse of the API (calling `Dial` without a concurrent `Accept` loop), which is a logic error in the host application's control flow, not a result of processing malicious untrusted data. An external attacker cannot send a 'packet' to this in-memory listener to trigger this state; the state depends entirely on the host application's internal goroutine management. Therefore, this is a code quality/concurrency correctness issue, not a security vulnerability reachable via untrusted external input.\n\nCRUX: The deadlock is caused by internal concurrency misuse (missing Accept loop) and is not triggerable by external untrusted input data.\nGREP: InmemoryListener",
"elapsed": 23.8,
"tokens": 5153,
"tokens_in": 4846,
"tokens_out": 307,
"cost_usd": 0.0029563199999999997,
"file": "fasthttputil/inmemory_listener.go",
"round": 4,
"grep_used": true,
"grep_results": "GREP `InmemoryListener`:\n```\nfasthttpadaptor/adaptor_test.go:237:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:306:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:389:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:477:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:533:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:624:\tln := fasthttputil.NewInmemoryListener()\nfasthttpadaptor/adaptor_test.go:646:func sendRequest(ln *fasthttputil.InmemoryListener, req *http.Request, responseTimeout time.Duration) ([]byte, error) {\nstreaming_test.go:30:\tln := fasthttputil.NewInmemoryListener()\nstreaming_test.go:105:func getChunkedTestEnv(t testing.TB) (*fasthttputil.InmemoryListener, []byte) {\nstreaming_test.go:126:\tln := fasthttputil.NewInmemoryListener()\nstreaming_test.go:164:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:30:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:90:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:130:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:159:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:200:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:255:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:287:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:325:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:362:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:404:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:435:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:461:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:487:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:549:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:766:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:812:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:880:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:912:\tln := fasthttputil.NewInmemoryListener()\nclient_test.go:944:\tln := fasthttputil.NewInmemoryListener()\n```"
},
{
"finding_title": "Pending dial holds listener mutex and can deadlock Close",
"verdict": "INVALID",
"reasoning": "The reported deadlock is technically real in the code: `DialWithLocalAddr` acquires `ln.lock`, sends a connection to `ln.conns`, and then blocks on the unbuffered `accepted` channel while still holding the lock. `Close` requires `ln.lock` to close `ln.conns`. If no `Accept` goroutine is running to consume from `ln.conns` and signal `accepted`, `Dial` blocks indefinitely, preventing `Close` from executing and causing a deadlock. However, under the binding threat model for a reusable library, the attacker is defined as code calling the public API with untrusted data (parsers, decoders, etc.). `InmemoryListener` is an in-memory transport utility that does not parse external protocol bytes. Triggering this deadlock requires the host application to misuse the API by calling `Dial` without a concurrent `Accept` loop, which violates the documented semantics ('Just like a real Dial it only returns once the server has accepted the connection'). This is a concurrency correctness issue or API misuse in the host application's logic, not a security vulnerability triggered by untrusted external input. An external attacker cannot send a 'packet' to an in-memory listener to induce this state. Therefore, it is invalid as a security vulnerability under the specified threat model.\n\nCRUX: The deadlock is caused by API misuse (missing Accept loop) within the same process, not by external untrusted input data.\nGREP: DialWithLocalAddr",
"elapsed": 17.9,
"tokens": 5563,
"tokens_in": 5243,
"tokens_out": 320,
"cost_usd": 0.00316206,
"file": "fasthttputil/inmemory_listener.go",
"round": 5,
"grep_used": true,
"grep_results": "GREP `DialWithLocalAddr`:\n```\nfasthttputil/inmemory_listener.go:101:\treturn ln.DialWithLocalAddr(nil)\nfasthttputil/inmemory_listener.go:104:// DialWithLocalAddr creates new client<->server connection.\nfasthttputil/inmemory_listener.go:110:func (ln *InmemoryListener) DialWithLocalAddr(local net.Addr) (net.Conn, error) {\nfasthttputil/inmemory_listener_test.go:266:\tc, err := ln.DialWithLocalAddr(clientAddr)\n```"
}
]Raw scan report for this file
```json
[
{
"severity": "high",
"title": "Pending dial holds listener mutex and can deadlock Close",
"function": "DialWithLocalAddr",
"description": "DialWithLocalAddr acquires ln.lock, sends the server-side connection to ln.conns, then blocks on <-accepted while still holding ln.lock. If the acceptor is absent, delayed, or blocked, the Dial call never returns. Close also requires ln.lock, so closing the listener deadlocks behind the pending dial. This can leak the dial goroutine, the PipeConns pair, and the accepted channel, and can freeze all further Dial calls. A caller can trigger this by invoking Dial without an active Accept loop or with an acceptor that never consumes the queued connection."
},
{
"severity": "low",
"title": "Dial creates a client connection with a nil local address",
"function": "Dial",
"description": "Dial hard-codes local to nil and passes it to DialWithLocalAddr, which stores nil as the client-side local address. Generic net.Conn consumers often call conn.LocalAddr().Network() or conn.LocalAddr().String() without a nil check, so using the returned connection in such code can panic. This is reachable through the exported Dial API and any code path that uses the returned connection's local address."
},
{
"severity": "low",
"title": "Caller-supplied net.Addr implementations are trusted and can cause side-effect DoS",
"function": "SetLocalAddr",
"description": "SetLocalAddr stores an arbitrary caller-supplied net.Addr, and Addr/DialWithLocalAddr expose it to other code. If a hostile or buggy net.Addr implementation performs expensive, blocking, or unsafe work in Network()/String(), invoking those methods on the listener or connection addresses can cause denial of service or unexpected behavior. The library performs no validation, isolation, or thread-safety checks on the address implementation."
}
]
```