BudgetScan

Pooled wrapper can be closed after reuse, closing a different connection

medium swesweep-fasthttp · peripconn.go · perIPConn.Close / perIPTLSConn.Close · run #4

Description

Close nils c.Conn, unregisters the IP, and puts the wrapper back into sync.Pool. acquirePerIPConn may later retrieve the same wrapper and assign a new Conn/ip without an ownership token or synchronization with a stale Close. A delayed second Close from an old reference will close the newly assigned Conn and Unregister the new IP, causing connection resets, accounting errors, and a data race on c.Conn/c.ip. A remote endpoint can influence this by causing an early close/abort and rapid connection reuse.

Triage

Verdict
INVALID confidence 0.0
Model
qwen/qwen3.8-27b
Rounds
6
Cost
$0.0240 · 742.8 s
[ARBITER] The reported vulnerability relies on a misuse of the public API where a caller retains a stale reference to a returned net.Conn wrapper and invokes Close on it after it has been recycled by the library. Under the provided threat model for a reusable library, misuse that violates the standard contract of the net.Conn interface (which states that Close releases resources and the connection becomes unusable) is not considered a security vulnerability. The library correctly synchronizes access to internal state using locks; the risk of closing the 'wrong' connection only arises if the host application fails to respect the lifecycle of the connection object it received, which is outside the scope of the library's security invariants.
All rounds
[
  {
    "finding_title": "Pooled wrapper can be closed after reuse, closing a different connection",
    "verdict": "UNCERTAIN",
    "reasoning": "The reported vulnerability claims that a delayed second `Close` call on a `perIPConn` wrapper can close a *different* connection that was assigned to the same wrapper after it was returned to the `sync.Pool`. \n\nLet's analyze the `Close` method in `peripconn.go`:\n```go\nfunc (c *perIPConn) Close() error {\n    c.lock.Lock()\n    cc := c.Conn\n    c.Conn = nil\n    c.lock.Unlock()\n\n    if cc == nil {\n        return nil\n    }\n\n    err := cc.Close()\n    c.perIPConnCounter.Unregister(c.ip)\n    c.perIPConnCounter.perIPConnPool.Put(c)\n    return err\n}\n```\n\n1. **Atomic Nil-ing**: The `Close` method acquires `c.lock`, reads `c.Conn` into local variable `cc`, and sets `c.Conn` to `nil`. This is done atomically.\n2. **Reentrancy/Double Close Protection**: If `Close` is called again (or concurrently) after the first call has started, it will acquire the lock. If the first call has already set `c.Conn` to `nil`, the second call will see `cc == nil` (because `c.Conn` is nil) and return early without closing any connection or putting the wrapper back into the pool.\n3. **Pool Reuse Safety**: The wrapper is only put back into the pool (`perIPConnPool.Put(c)`) *after* it has been closed and nil-ed. When `acquirePerIPConn` retrieves a wrapper from the pool, it assigns a *new* `conn` to `c.Conn`. Since the previous `Close` has already completed and nil-ed `c.Conn`, there is no stale `Close` holding a reference to the *old* connection. The old connection is closed. The new connection is assigned. A future `Close` on the wrapper will close the *new* connection.\n4. **Race Condition**: The report suggests a \"stale Close\" from an \"old reference\". However, the `Close` method does not operate on a snapshot of the connection that persists across calls; it operates on the current state of `c.Conn` protected by a mutex. There is no asynchronous callback or deferred function that would hold a reference to the old connection and call `Close` later. The `Close` is a synchronous method on the wrapper. If the caller holds a reference to the wrapper, calling `Close` again is a no-op (returns nil) because `c.Conn` is nil. If a *new* connection is assigned, the wrapper is effectively ",
    "elapsed": 35.9,
    "tokens": 3367,
    "tokens_in": 2793,
    "tokens_out": 574,
    "cost_usd": 0.0028950599999999996,
    "file": "peripconn.go",
    "round": 1
  },
  {
    "finding_title": "Pooled wrapper can be closed after reuse, closing a different connection",
    "verdict": "UNCERTAIN",
    "reasoning": "The report claims a race condition where a delayed `Close` on a pooled `perIPConn` wrapper might close a newly assigned connection after the wrapper has been reused. To evaluate this, I must verify if such a 'delayed' or 'stale' Close is possible. \n\n1. **Synchronization in `acquirePerIPConn`**: When a wrapper is retrieved from `sync.Pool` in `acquirePerIPConn`, the code assigns a new connection and IP: `c.Conn = conn` and `c.ip = ip`. Crucially, this assignment happens **without** acquiring `c.lock`. \n2. **Synchronization in `Close`**: The `Close` method acquires `c.lock`, reads `c.Conn`, and sets it to `nil`. \n3. **The Race**: If a `Close` is in progress (specifically between `c.lock.Unlock()` and the return, or if a second `Close` is called concurrently), consider the state. However, the standard usage implies that once `Close` completes, the object is in the pool. \n\nLet's look at the specific ",
    "elapsed": 82.3,
    "tokens": 3737,
    "tokens_in": 3490,
    "tokens_out": 247,
    "cost_usd": 0.0022068,
    "file": "peripconn.go",
    "round": 2
  },
  {
    "finding_title": "Pooled wrapper can be closed after reuse, closing a different connection",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability describes a race condition where a stale `Close` call on a `perIPConn` wrapper could close a different connection after the wrapper has been reused from `sync.Pool`. To determine if this is valid, I must analyze the synchronization and lifecycle of the wrapper.\n\n1.  **Synchronization Analysis**: The `Close` method uses `c.lock` to atomically read `c.Conn` into a local variable `cc` and set `c.Conn` to `nil`. This ensures that any subsequent concurrent `Close` call will see `c.Conn` as `nil` and return early (no-op). This effectively serializes the close operation and prevents double-closing the *same* connection object stored in the struct.\n\n2.  **Pool Reuse and 'Stale' Close**: The report suggests a 'delayed second Close'. In Go, a `sync.Pool` object is only considered 'reused' when `Get` is called. `Put` is called at the end of `Close`.\n    *   Scenario A: A thread holds a reference to the `perIPConn` wrapper `c`. It calls `Close()`. `c.Conn` becomes nil. `c` is put into the pool. The thread might call `Close()` again on `c` (double close). As established, this is a no-op because `c.Conn` is nil. It does *not* affect a newly assigned connection because the new assignment happens in `acquirePerIPConn`.\n    *   Scenario B: The report implies that a 'stale' close happens *after* reuse. If the wrapper `c` is reused, it is retrieved from the pool, and `acquirePerIPConn` assigns a new `conn` to `c.Conn`. If a previous holder of `c` (from before the first close) somehow triggers a close *now*, they would call `Close` on `c`. This `Close` would acquire `c.lock`, read `c.Conn` (which is now the *new* connection), and close it. This is the critical race.\n    \n    Is it possible for a 'previous holder' to still exist and call `Close` after the wrapper has been reused?\n    In standard library usage, the `net.Conn` returned by `acquirePerIPConn` is passed to the caller. The caller is responsible for calling `Close` on it. If the caller does not call `Close`, the connection is leaked (not put in the pool). If the caller *does* call `Close`, the wrapper is put in the pool. Once put in the pool, the caller should not retain references or interact with it. However, if the caller *does* retain a reference and calls `Close` again, it's a no-op.\n    \n    The danger arises if the `perIPConn` wrapper is shared or if the 'close' logic is triggered by an internal mechanism (like a timeout) rather than just the user. Let's look at how `acquirePerIPConn` is used. It returns a `net.Conn`. The underlying `net.Conn` is the real network connection. The `perIPConn` is a wrapper. \n    If the user closes the connection, `Close` is called. The wrapper is pooled.\n    If the user calls `Close` again, it's a no-op.\n    \n    The report claims: \"A delayed second Close from an old reference will close the newly assigned Conn\". This implies that the 'old reference' is valid and holds the pointer `c`. If the user holds a pointer to `c` and calls `Close` twice, the second call is a no-op. It does *not* see the new connection because the new connection is assigned to `c.Conn` *after* the first `Close` has finished and put `c` in the pool. Wait. If `c` is in the pool, and someone gets it and assigns a new Conn, does the 'old reference' still point to `c`? Yes. If the old reference calls `Close` *after* the new assignment, it *will* close the new connection.\n    \n    So the question is: **Can a stale reference to a `perIPConn` wrapper trigger a `Close` after the wrapper has been reused?**\n    \n    In `fasthttp`, connections are typically managed by the client. If the client dials, gets a `perIPConn`, passes it to the application. The application uses it and closes it. The application should not keep a reference to the internal `perIPConn` wrapper after closing it. If the application holds a reference to the `net.Conn` interface, and that interface happens to be a `perIPConn`, and the application calls `Close` later... but why would it? It already closed it. \n    \n    However, consider `sync.Pool` semantics. Objects in the pool can be evicted or reused at any time. If a goroutine `G1` calls `Close`, and then `G2` retrieves `c` from the pool and assigns a new `Conn`. If `G1` still holds a reference to `c` and calls `Close` again (or if there is an internal timer/goroutine associated with the connection that calls `Close`), then `G1`'s `Close` will close `G2`'s connection.\n    \n    Does `fasthttp` or the standard library create background goroutines that hold references to the `perIPConn` wrapper and call `Close`? The `perIPConn` wrapper is created in `acquirePerIPConn`. It wraps the `net.Conn`. The `net.Conn` itself might have internal state. But the `perIPConn` struct is just a wrapper. \n    \n    Crucially, `acquirePerIPConn` is called when a connection is dialed. The returned `net.Conn` is the `perIPConn`. If the user does `conn := client.Do(...)`, they get a response. The connection is managed internally. The user doesn't usually hold onto the `perIPConn`.\n    \n    However, if this is a general library, and the user uses `fasthttp.Client` or similar, the internal pool management handles the lifecycle. The user does not interact with `perIPConn` directly; they interact with `fasthttp.Request`/`Response`. The underlying `net.Conn` is an internal detail. The user calls `client.Close()` or the client manages the pool. \n    \n    If the user does *not* have access to the `perIPConn` instance (because it's wrapped in a `fasthttp.Conn` or similar), they cannot hold a stale reference to call `Close` on it directly. They can only close the higher-level object. If the higher-level object is closed, it closes the `perIPConn`. If they try to close it again, the higher-level object likely prevents it or it's a no-op at that level. \n    \n    Let's check if `perIPConn` is exported or accessible. It is `type perIPConn struct` (unexported). It is returned as `net.Conn`. So the caller *does* hold a reference to it via the interface. If the caller retains the `net.Conn` interface and calls `Close` on it after it has been pooled and reused, the race exists.\n    \n    Is it valid for the caller to retain the `net.Conn` and call `Close` later? Usually, you use a connection, read/write, then close it. If you don't close it, it stays in the pool (if managed by a pool) or is leaked. If it's managed by a `sync.Pool` directly in the code shown, it's only put back when `Close` is called. So if the caller doesn't call `Close`, it's not in the pool. If the caller *does* call `Close`, it's in the pool. If the caller calls `Close` *again*, it's a no-op. \n    \n    The race requires: \n    1. Caller holds ref to `c`.\n    2. Caller calls `Close` (1st time). `c` goes to pool.\n    3. Someone else retrieves `c` from pool, assigns new `Conn`.\n    4. Caller calls `Close` (2nd time) on `c`.\n    \n    Step 4 closes the new `Conn`. This is a bug. \n    \n    However, is this a *security* vulnerability reachable by an external attacker? The attacker controls the network data, not the application's memory management of connection handles. The application (host process) is responsible for not holding stale references. This is a programming error/misuse of the API by the host application, or a flaw in the library's robustness against such misuse. \n    \n    The threat model says: \"Misuse that violates a documented precondition of the public API\" is not a vulnerability. Is holding a reference to a closed `net.Conn` and calling `Close` again a misuse? Usually, closing a closed connection is safe. But here, closing a 'closed' (pooled and reused) connection closes a *different* connection. This is not a standard invariant. \n    \n    However, can an *external* attacker trigger this? The attacker cannot force the application to hold a reference to a pooled wrapper and close it later. This depends entirely on the application code. If the application code is written correctly (close and discard), this doesn't happen. If the application code has a bug (closes, keeps ref, closes again), that is a bug in the application, not the library. \n    \n    Furthermore, `sync.Pool` objects are not guaranteed to be the same object. But here the wrapper *is* the same object. \n    \n    Given the threat model focuses on external attackers triggering issues via untrusted input, and this requires specific flawed memory management by the *host application* (holding stale refs to internal objects), this is likely a code quality/robustness issue rather than a directly exploitable security vulnerability by a remote attacker. The attacker cannot control the host's GC/reference counting. \n    \n    Therefore, INVALID.\n\nCRUX: Whether a remote attacker can force the host application to hold a stale reference to a pooled `perIPConn` wrapper and invoke `Close` on it after it has been reused.\nGREP: acquirePerIPConn",
    "elapsed": 74.7,
    "tokens": 5987,
    "tokens_in": 3727,
    "tokens_out": 2260,
    "cost_usd": 0.00834534,
    "file": "peripconn.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `acquirePerIPConn`:\n```\nperipconn.go:57:func acquirePerIPConn(conn net.Conn, ip uint32, counter *perIPConnCounter) net.Conn {\nserver.go:2117:\treturn acquirePerIPConn(c, ip, &s.perIPConnCounter)\n```"
  },
  {
    "finding_title": "Pooled wrapper can be closed after reuse, closing a different connection",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability describes a race condition where a delayed `Close` on a `perIPConn` wrapper (which has been returned to `sync.Pool` and reused for a new connection) could close the newly assigned connection. \n\n1. **Bug Mechanism**: The `Close` method sets `c.Conn` to `nil` and returns the wrapper to the pool. If a caller retains a reference to the `perIPConn` struct (or the `net.Conn` interface wrapping it) after the first `Close`, and `acquirePerIPConn` reuses that exact wrapper instance from the pool, a subsequent `Close` call on the stale reference would acquire the lock, read the *new* `c.Conn`, and close it. This is a valid code pattern flaw (lack of an ownership token or generation counter). \n\n2. **Attacker Reachability**: The threat model defines the attacker as providing untrusted data to the public API. The entry point is `acquirePerIPConn`, which is called by the server (grep `server.go:2117`). The `net.Conn` passed in is from a dial operation. While a remote attacker can influence network behavior (e.g., causing resets, early closes), they cannot control the host application's memory management or reference counting. Specifically, they cannot force the host application to:\n   a) Retain a reference to a closed internal wrapper object.\n   b) Trigger a second `Close` on that specific stale reference *after* the wrapper has been reused.\n\n3. **Trust Boundary**: The `perIPConn` struct is unexported. It is wrapped in a `net.Conn` interface returned to the host application. Standard usage of `net.Conn` in Go implies that once `Close` is called, the connection is no longer valid and should not be used. Holding a reference and calling `Close` again is typically a no-op or a benign error in standard implementations. The flaw here is that the library recycles the underlying object in a way that makes a second `Close` dangerous. However, triggering this requires the host application to violate standard usage patterns (holding stale references to closed connections). \n\n4. **Verdict**: Since the remote attacker cannot manipulate the host process's reference retention or trigger the specific sequence of pool reuse + stale close invocation, this is not reachable via untrusted input. It is a robustness flaw that requires specific application-side misuse (holding stale refs to internal pooled objects), which is outside the threat model's definition of an external attacker triggering a vulnerability via crafted payload. The attacker does not control the host's GC or memory handling of interface values.\n\nTherefore, the issue is INVALID under the provided threat model.\n\nCRUX: Whether a remote attacker can force the host application to retain a stale reference to a pooled internal wrapper and invoke Close on it after reuse.\nGREP: acquirePerIPConn",
    "elapsed": 33.0,
    "tokens": 6602,
    "tokens_in": 5982,
    "tokens_out": 620,
    "cost_usd": 0.00437244,
    "file": "peripconn.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `acquirePerIPConn`:\n```\nperipconn.go:57:func acquirePerIPConn(conn net.Conn, ip uint32, counter *perIPConnCounter) net.Conn {\nserver.go:2117:\treturn acquirePerIPConn(c, ip, &s.perIPConnCounter)\n```"
  },
  {
    "finding_title": "Pooled wrapper can be closed after reuse, closing a different connection",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that a stale `Close` call on a pooled `perIPConn` wrapper can close a newly assigned connection. To be valid, an attacker must be able to trigger this race or misuse through untrusted input. \n\n1. **Code Analysis**: The `perIPConn` struct is unexported. It is returned via the `net.Conn` interface. The `Close` method atomically sets `c.Conn` to `nil` and puts the wrapper back into `sync.Pool`. If `Close` is called a second time *before* the wrapper is reused, it is a no-op. If the wrapper is reused (new `Conn` assigned) and then `Close` is called on the old reference, it *will* close the new connection. This confirms the bug pattern exists in code.\n\n2. **Reachability**: The attacker is a remote network endpoint. The host application calls `net.Dial` (or similar) which internally uses `acquirePerIPConn` (via `server.go` or client logic, though the grep shows `server.go:2117` which is likely the server side accepting connections or dialing out). In the context of a library like fasthttp, connections are typically managed by the client/server instance. The application code (host) interacts with `fasthttp.Client` or `fasthttp.Server`. The `perIPConn` wrapper is an internal detail. The application receives high-level abstractions (e.g., `*fasthttp.Request`). The underlying `net.Conn` is not typically exposed to the application in a way that allows it to hold a reference to the specific `perIPConn` wrapper struct.\n\nEven if the application did have access to the `net.Conn` interface, triggering the specific race requires:\n   a) The application to call `Close` on the connection.\n   b) The wrapper to be immediately reused by another connection (from the pool).\n   c) The application to call `Close` *again* on the *same* interface reference *after* the reuse has happened.\n\nStandard Go `net.Conn` usage implies that once `Close` is called, the connection is done. Calling `Close` again is generally considered a no-op or benign. The scenario where a remote attacker *forces* the host application to hold a stale reference to an internal pooled object and close it again is not feasible. The attacker controls network data, not the host's memory references or the sequence of method calls on internal objects. This is a robustness issue or a potential bug if the library itself mishandles references, but not a security vulnerability triggered by external input. The threat model excludes issues that require violation of documented preconditions or misuse by the host application (holding stale refs).\n\nCRUX: Whether a remote attacker can force the host application to hold a stale reference to an internal pooled wrapper and invoke Close on it after reuse.\nGREP: acquirePerIPConn",
    "elapsed": 344.4,
    "tokens": 7280,
    "tokens_in": 6653,
    "tokens_out": 627,
    "cost_usd": 0.00467526,
    "file": "peripconn.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `acquirePerIPConn`:\n```\nperipconn.go:57:func acquirePerIPConn(conn net.Conn, ip uint32, counter *perIPConnCounter) net.Conn {\nserver.go:2117:\treturn acquirePerIPConn(c, ip, &s.perIPConnCounter)\n```"
  },
  {
    "verdict": "INVALID",
    "reasoning": "[ARBITER] The reported vulnerability relies on a misuse of the public API where a caller retains a stale reference to a returned net.Conn wrapper and invokes Close on it after it has been recycled by the library. Under the provided threat model for a reusable library, misuse that violates the standard contract of the net.Conn interface (which states that Close releases resources and the connection becomes unusable) is not considered a security vulnerability. The library correctly synchronizes access to internal state using locks; the risk of closing the 'wrong' connection only arises if the host application fails to respect the lifecycle of the connection object it received, which is outside the scope of the library's security invariants.",
    "round": 6,
    "file": "peripconn.go",
    "finding_title": "Pooled wrapper can be closed after reuse, closing a different connection",
    "elapsed": 172.5,
    "tokens_in": 2502,
    "tokens_out": 150,
    "cost_usd": 0.0015008399999999998
  }
]
Raw scan report for this file

Tracing the untrusted input: `RemoteAddr` → `getUint32IP` → `ip` → `Register` → `acquirePerIPConn` → `Close` → `Unregister`.

Key exploitable issues:

1. **Unbounded map growth**: `Unregister` sets counts to zero but never deletes the map entry. Every IPv4 address ever observed remains in `cc.m`. A remote attacker with many source IPs, or a proxy-protocol path exposing many client IPs, can cause unbounded memory growth by opening/closing connections.

2. **IP normalization collapse**: Pure IPv6 and non-`*net.TCPAddr` addresses map to `0`. If `MaxConnsPerIP` is enabled, all such connections share the same counter key, allowing one attacker to deny service to other IPv6/non-TCP clients.

3. **Pooled wrapper lifecycle race**: `Close` puts the wrapper back into `sync.Pool` after niling `Conn`. `acquirePerIPConn` can reuse the same wrapper without an ownership token or synchronizing with a stale `Close`. A delayed `Close` can close the newly assigned underlying connection and unregister the new IP, causing connection resets, accounting errors, and a data race on `Conn`/`ip`.

Other helpers are mostly safe: `ip2uint32` bounds-checks `len(ip)`, `uint322ip` uses a fixed 4-byte buffer safely, and `Register`/`Unregister` are mutex-protected.

```json
[
  {
    "severity": "high",
    "title": "Unbounded per-IP counter map growth from retained zero-count entries",
    "function": "perIPConnCounter.Unregister",
    "description": "Unregister decreases the counter and clamps to zero but never deletes the map entry. Every remote IPv4 address that is ever seen remains in cc.m forever. A remote attacker with many source IPs, or a proxy-protocol path that exposes many client IPs, can open/close connections to cause unbounded memory growth and denial of service, even if connections are rejected after Register."
  },
  {
    "severity": "medium",
    "title": "IPv6 and non-TCP remote addresses collapse to the same counter key 0",
    "function": "getConnIP4",
    "description": "getConnIP4 returns nil for addresses whose IP is not IPv4, and ip2uint32 maps nil or len != 4 to 0. Consequently all pure IPv6 connections and non-*net.TCPAddr connections share key 0. If MaxConnsPerIP is configured, an attacker using IPv6, or non-TCP connections if supported, can exhaust the shared key and deny service to other IPv6/non-TCP clients."
  },
  {
    "severity": "medium",
    "title": "Pooled wrapper can be closed after reuse, closing a different connection",
    "function": "perIPConn.Close / perIPTLSConn.Close",
    "description": "Close nils c.Conn, unregisters the IP, and puts the wrapper back into sync.Pool. acquirePerIPConn may later retrieve the same wrapper and assign a new Conn/ip without an ownership token or synchronization with a stale Close. A delayed second Close from an old reference will close the newly assigned Conn and Unregister the new IP, causing connection resets, accounting errors, and a data race on c.Conn/c.ip. A remote endpoint can influence this by causing an early close/abort and rapid connection reuse."
  }
]
```