BudgetScan

Empty client list causes slice index out-of-range panic

high swesweep-fasthttp · lbclient.go · LBClient.get · run #4

Description

RemoveClients can truncate cc.cs to length zero if rc returns true for every client. A later Do, DoTimeout, or DoDeadline calls get, which executes minC := cs[0] without checking len(cs) > 0, causing a runtime panic and host process crash.

Triage

Verdict
INVALID confidence 0.17
Model
qwen/qwen3.8-27b
Rounds
6
Cost
$0.0187 · 829.4 s
[ARBITER] The threat model explicitly states that 'misuse that violates a documented precondition of the public API' is not a vulnerability. The source code documentation for the `LBClient.Clients` field explicitly mandates: 'Clients must contain non-zero clients list.' Although `RemoveClients` allows the internal slice to become empty, doing so violates the documented precondition that the client list must remain non-empty. Therefore, the resulting panic is a result of API misuse rather than a security vulnerability exploitable via the trust boundary defined in the threat model.
All rounds
[
  {
    "finding_title": "Empty client list causes slice index out-of-range panic",
    "verdict": "VALID",
    "reasoning": "The vulnerability report claims that `RemoveClients` can empty the `cc.cs` slice, leading to an index out-of-range panic in `get()` when accessing `cs[0]`. \n\n1. **Bug Pattern Verification**: The code for `RemoveClients` iterates through `cc.cs` and compacts it. If the callback `rc` returns true for every client, `n` remains 0, and `cc.cs` becomes an empty slice (`cc.cs[:0]`). The `get` function executes `minC := cs[0]` without checking `len(cs) > 0`. In Go, accessing index 0 of an empty slice causes a runtime panic (`runtime error: index out of range`). Thus, the bug pattern exists in the code.\n\n2. **Attacker Reachability**: The threat model defines the attacker as \"Code that calls the public API... and above all the data it passes through\". The entry points are public methods like `Do`, `DoTimeout`, `DoDeadline`, `AddClient`, and `RemoveClients`. \n   - To trigger the panic, an attacker must first call `RemoveClients` with a function `rc` that returns `true` for all clients, thereby emptying the list. Then, they must call `Do`, `DoTimeout`, or `DoDeadline`.\n   - `RemoveClients` takes a callback `rc func(BalancingClient) bool`. Since the caller controls this callback, they can easily make it return `true` always.\n   - `Do` takes `req *Request` and `resp *Response`. While the report mentions these are attacker-influenced, the critical input here is the sequence of API calls controlled by the host application (which might be driven by untrusted configuration, API commands, or logic errors in the host).\n   - However, looking closer at the threat model: \"Attacker: Code that calls the public API... parsers, decoders, deserializers... receive adversarial payloads\". In a typical library usage scenario, the host application manages the lifecycle of the `LBClient`. If the host application is vulnerable to a logic flaw or accepts untrusted instructions (e.g., via a control plane) to remove all backends and then serve traffic, it can crash. More directly, if the library is used in a context where the set of backends is dynamic and managed by an input that can be influenced (e.g., a service mesh sidecar updating endpoints), an empty update followed by a request could crash the process.\n   - Crucially, does the *public API* allow an attacker to inject the state change? The attacker controls the arguments to `RemoveClients`. If the attacker can invoke `RemoveClients` (which they can, as it's a public method) and then `Do`, they trigger the panic. The threat model states: \"A crash... reachable through the public API with a crafted payload is VALID\". While the 'payload' here is more of an API sequence, the input to `RemoveClients` (the callback behavior) and the subsequent `Do` are part of the public surface. \n   - Wait, is the attacker *external* to the library? The threat model says \"Code that calls the public API\". This usually implies the host application is the entity being analyzed, but the vulnerability is in the library. If the library crashes the host process, it's a DoS. If an attacker can manipulate the host application to call `RemoveClients(true, all)` and then `Do`, it crashes. Is that a security vulnerability in the library? Yes, because the library fails to handle a valid state transition (empty list) safely. The documentation says \"Clients must contain non-zero clients list\" but `RemoveClients` allows violating this invariant, and `Do` doesn't re-check. The library should either prevent `RemoveClients` from emptying the list or handle the empty case in `Do` gracefully (e.g., return an error) rather than panicking. A panic in a library is a serious code quality issue that leads to a process crash (DoS). Under the model, \"correct behaviour on malformed input\" and \"Memory safety... (crash)\" are assets. An empty client list is a 'malformed' state that the library allows to be created via its own public API and then fails to handle safely in another public API call.\n\nTherefore, the bug is real, reachable via the public API, and causes a crash.\n\nCRUX: The public API `RemoveClients` can set the internal client list to empty, and the public API `Do` (via `get`) performs an unchecked index access `cs[0]` which panics if the list is empty.\nGREP: minC := cs[0]",
    "elapsed": 208.5,
    "tokens": 4308,
    "tokens_in": 3297,
    "tokens_out": 1011,
    "cost_usd": 0.00441774,
    "file": "lbclient.go",
    "round": 1,
    "grep_used": true,
    "grep_results": "GREP `minC := cs[0]`:\n```\nlbclient.go:137:\tminC := cs[0]\n```"
  },
  {
    "finding_title": "Empty client list causes slice index out-of-range panic",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that `RemoveClients` can empty the `cc.cs` slice, causing a panic in `get()` when it executes `cs[0]`. While the code does indeed allow `RemoveClients` to set `n=0` and slice `cc.cs` to empty, and `get()` does perform `cs[0]` without a length check, we must evaluate this against the threat model and library contracts. \n\n1. **Precondition Violation**: The documentation for the `Clients` field explicitly states: \"Clients must contain non-zero clients list.\" This establishes a contract that the list must never be empty. The `init` function enforces this at startup by panicking if `len(cc.Clients) == 0`.\n\n2. **Threat Model Scope**: The threat model defines the attacker as providing adversarial *data* (payloads) to parsers/decoders. It explicitly states: \"Misuse that violates a documented precondition of the public API\" is **NOT** a vulnerability. Calling `RemoveClients` with a callback that removes all clients violates the documented precondition that the list must remain non-zero. This is a programming error by the library consumer (the host application), not an untrusted input issue handled by the library. \n\n3. **Reachability**: To trigger this, the attacker would need to control the host application's logic such that it calls `RemoveClients` in a way that empties the list, while simultaneously allowing traffic to hit `Do`. In the context of a reusable library, the library is not responsible for validating logical preconditions established in its API documentation against malicious logic in the host. The library assumes the host maintains the invariant. \n\n4. **Comparison to 'Malformed Input'**: Unlike a network packet that crashes a parser, this requires a specific sequence of API calls that contradicts the library's own usage instructions. This falls under 'Code Quality' or 'Robustness' rather than a security vulnerability exploitable by an external attacker providing data.\n\nCRUX: The threat model explicitly excludes 'misuse that violates a documented precondition of the public API' from being a vulnerability, and the library documentation mandates that the Clients list must not be empty.\nGREP: Clients must contain non-zero clients list",
    "elapsed": 161.8,
    "tokens": 4950,
    "tokens_in": 4463,
    "tokens_out": 487,
    "cost_usd": 0.00333546,
    "file": "lbclient.go",
    "round": 2,
    "grep_used": true,
    "grep_results": "GREP `Clients must contain non-zero clients list`:\n```\nlbclient.go:41:\t// Clients must contain non-zero clients list.\n```"
  },
  {
    "finding_title": "Empty client list causes slice index out-of-range panic",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability is an index out-of-range panic in `LBClient.get()` if the internal client list `cc.cs` becomes empty. This can happen if `RemoveClients` is called with a callback that returns true for all clients. While the code technically allows this state transition, the threat model explicitly states: \"Not a vulnerability under this model: ... misuse that violates a documented precondition of the public API.\" The source code documentation for `LBClient.Clients` clearly states: \"Clients must contain non-zero clients list.\" Furthermore, `init()` enforces this invariant at startup by panicking if the list is empty. The panic in `get()` is a result of violating this documented invariant. Since the attacker (or more accurately, the library consumer/host application in this context) would be violating a documented contract of the public API, this is considered invalid under the binding threat model rules. It is a robustness issue, not a security vulnerability exploitable via untrusted data flow.\n\nCRUX: The threat model explicitly excludes 'misuse that violates a documented precondition of the public API' as a vulnerability, and the library documentation mandates that the Clients list must not be empty.\nGREP: Clients must contain non-zero clients list",
    "elapsed": 133.6,
    "tokens": 5225,
    "tokens_in": 4960,
    "tokens_out": 265,
    "cost_usd": 0.0028782,
    "file": "lbclient.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `Clients must contain non-zero clients list`:\n```\nlbclient.go:41:\t// Clients must contain non-zero clients list.\n```"
  },
  {
    "finding_title": "Empty client list causes slice index out-of-range panic",
    "verdict": "INVALID",
    "reasoning": "The bug pattern is real: `RemoveClients` can reduce `cc.cs` to an empty slice if the callback returns true for all clients, and `get()` subsequently accesses `cs[0]` without checking `len(cs) > 0`, causing a panic. However, under the provided threat model, this is not a security vulnerability. The threat model explicitly excludes \"misuse that violates a documented precondition of the public API\" from being a vulnerability. The code documentation for the `Clients` field states: \"Clients must contain non-zero clients list.\" The `init()` function also enforces this invariant at startup by panicking if the list is empty. Therefore, calling `RemoveClients` in a way that empties the list constitutes a violation of the documented precondition by the host application (the library consumer). This is a programming error/robustness issue, not an exploitable vulnerability via untrusted input, because the 'attacker' (the code calling the public API) is violating the contract defined by the library. There is no path where untrusted *data* (network packets, files, etc.) directly causes this state without the host application explicitly making the logic error to remove all clients. Thus, it falls under INVALID per the threat model rules.\n\nCRUX: The threat model explicitly excludes violations of documented preconditions, and the code documentation mandates that the Clients list must never be empty.\nGREP: Clients must contain non-zero clients list",
    "elapsed": 123.3,
    "tokens": 5555,
    "tokens_in": 5243,
    "tokens_out": 312,
    "cost_usd": 0.00313806,
    "file": "lbclient.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `Clients must contain non-zero clients list`:\n```\nlbclient.go:41:\t// Clients must contain non-zero clients list.\n```"
  },
  {
    "finding_title": "Empty client list causes slice index out-of-range panic",
    "verdict": "INVALID",
    "reasoning": "The reported bug is a real code defect: `RemoveClients` allows emptying the internal client list (`cc.cs`), and subsequent calls to `Do`, `DoTimeout`, or `DoDeadline` will panic in `get()` when accessing `cs[0]`. However, the verdict depends on the threat model. The binding threat model rules for this library explicitly state: 'Not a vulnerability under this model: ... misuse that violates a documented precondition of the public API.' The source code documentation for the `LBClient.Clients` field explicitly states: 'Clients must contain non-zero clients list.' Furthermore, the `init` function enforces this invariant at startup by panicking if the list is empty. Therefore, calling `RemoveClients` in a way that results in an empty list constitutes a violation of a documented precondition by the library consumer (the host application). Under the defined threat model, this is considered invalid because it requires the 'attacker' (code calling the public API) to violate the library's own contract, rather than being triggered by untrusted data passed *through* the API (like malformed HTTP payloads). There is no external untrusted input that directly causes the list to become empty without the host application explicitly performing the invalid API sequence.\n\nCRUX: The threat model explicitly excludes 'misuse that violates a documented precondition of the public API', and the library documentation mandates that the Clients list must never be empty.\nGREP: Clients must contain non-zero clients list",
    "elapsed": 101.5,
    "tokens": 5889,
    "tokens_in": 5573,
    "tokens_out": 316,
    "cost_usd": 0.00328866,
    "file": "lbclient.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `Clients must contain non-zero clients list`:\n```\nlbclient.go:41:\t// Clients must contain non-zero clients list.\n```"
  },
  {
    "verdict": "INVALID",
    "reasoning": "[ARBITER] The threat model explicitly states that 'misuse that violates a documented precondition of the public API' is not a vulnerability. The source code documentation for the `LBClient.Clients` field explicitly mandates: 'Clients must contain non-zero clients list.' Although `RemoveClients` allows the internal slice to become empty, doing so violates the documented precondition that the client list must remain non-empty. Therefore, the resulting panic is a result of API misuse rather than a security vulnerability exploitable via the trust boundary defined in the threat model.",
    "round": 6,
    "file": "lbclient.go",
    "finding_title": "Empty client list causes slice index out-of-range panic",
    "elapsed": 100.7,
    "tokens_in": 3019,
    "tokens_out": 123,
    "cost_usd": 0.00163698
  }
]
Raw scan report for this file

## Security analysis

This file implements a load-balancing client wrapper. The main trust boundary is the public `LBClient` API: `Do`, `DoTimeout`, `DoDeadline`, `AddClient`, and `RemoveClients`. The wrapper forwards caller-supplied `Request`/`Response` values to user-provided `BalancingClient` implementations and user-provided callbacks. The most relevant impact here is availability: panics, nil dereferences, slice out-of-range errors, and deadlocks in the host process.

### Data flow

- `LBClient.Do`
  - reads `cc.Timeout`
  - calls `DoTimeout`

- `LBClient.DoTimeout`
  - computes a deadline
  - calls `get()`
  - calls the selected `lbClient.DoDeadline`

- `LBClient.DoDeadline`
  - calls `get()`
  - calls the selected `lbClient.DoDeadline`

- `LBClient.get`
  - lazily initializes via `init()`
  - reads `cc.cs` under `RLock`
  - selects one `*lbClient`
  - releases the lock
  - returns the selected client

- `lbClient.DoDeadline`
  - calls `c.c.DoDeadline`
  - evaluates health
  - updates `penalty` or `total`

### Notable issues

1. **`RemoveClients` can leave zero clients**
   - `RemoveClients` removes clients based on `rc`.
   - If `rc` returns `true` for every client, `cc.cs` is truncated to length zero.
   - `get()` then executes `minC := cs[0]` without checking `len(cs) > 0`.
   - This causes a slice index out-of-range panic on the next request.

2. **Nil `BalancingClient` is not rejected**
   - `init()` and `AddClient()` store `BalancingClient` values directly.
   - If a nil `BalancingClient` is present, `lbClient.DoDeadline` calls `c.c.DoDeadline` on a nil interface.
   - This causes a nil pointer dereference panic.

3. **`RemoveClients` invokes a callback while holding the write lock**
   - `rc` is called while `cc.mu` is locked.
   - If `rc` reentrantly calls `AddClient`, `RemoveClients`, or `Do`/`DoTimeout`/`DoDeadline`, it can block on the same mutex and deadlock.
   - If `rc` panics, `cc.mu.Unlock` is not deferred, so the mutex remains locked forever.

4. **Lazy init can panic if `Clients` is empty but `AddClient` was used**
   - `init()` checks only `len(cc.Clients)`, not `len(cc.cs)`.
   - A zero-value `LBClient` can have clients added via `AddClient` before the first request.
   - The first `get()` still calls `init()`, which panics because `cc.Clients` is empty.

5. **Nil `req`/`resp` are forwarded unchecked**
   - The public entry points accept `req` and `resp` without nil checks.
   - They are passed to `c.c.DoDeadline` and, during health evaluation, to `c.healthCheck`.
   - If the downstream client or callback assumes non-nil pointers, this can panic.

6. **Lower-confidence concurrency issues**
   - `RemoveClients` does not check whether `rc` is nil.
   - Exported fields such as `HealthCheck` and `Timeout` can be mutated concurrently with method calls, even though methods are documented as safe for concurrent use.

### Non-issues / lower-confidence observations

- `penalty` and `total` use atomic operations consistently.
- `decPenalty` subtracts one from `penalty`; under normal operation this should not underflow because decrements are only created by successful `incPenalty` calls.
- `time.AfterFunc` timers are bounded per client by `maxPenalty`, so they are not an obvious unbounded goroutine/timer leak under normal use.
- No fixed-size buffer copies exist in this file.

```json
[
  {
    "severity": "high",
    "title": "Empty client list causes slice index out-of-range panic",
    "function": "LBClient.get",
    "description": "RemoveClients can truncate cc.cs to length zero if rc returns true for every client. A later Do, DoTimeout, or DoDeadline calls get, which executes minC := cs[0] without checking len(cs) > 0, causing a runtime panic and host process crash."
  },
  {
    "severity": "high",
    "title": "Nil BalancingClient causes nil pointer dereference",
    "function": "lbClient.DoDeadline",
    "description": "LBClient.init and LBClient.AddClient store nil BalancingClient values without validation. When get selects such an lbClient, DoDeadline calls c.c.DoDeadline on a nil interface, causing a panic. If client lists or AddClient input can be influenced by untrusted data, this is a DoS."
  },
  {
    "severity": "high",
    "title": "RemoveClients callback invoked while holding write lock can deadlock or lock permanently",
    "function": "LBClient.RemoveClients",
    "description": "rc is called while cc.mu is write-locked and without a deferred Unlock. If rc reentrantly calls AddClient, RemoveClients, or Do/DoTimeout/DoDeadline, it blocks on the same mutex and deadlocks. If rc panics, the mutex is not released, permanently locking the LBClient."
  },
  {
    "severity": "medium",
    "title": "Lazy init panics when Clients is empty even if AddClient populated cs",
    "function": "LBClient.init",
    "description": "get calls init via sync.Once. init only checks len(cc.Clients), not len(cc.cs). A zero-value LBClient can have one or more clients added with AddClient before the first Do; the later init still panics because cc.Clients is empty, crashing the host process."
  },
  {
    "severity": "medium",
    "title": "Nil request or response forwarded unchecked to client and health callback",
    "function": "lbClient.DoDeadline",
    "description": "DoDeadline, DoTimeout, and Do accept req and resp without nil checks and pass them to c.c.DoDeadline and, during health evaluation, to c.healthCheck. If the BalancingClient implementation or HealthCheck assumes non-nil pointers, an attacker-supplied nil request or response can trigger a panic."
  },
  {
    "severity": "low",
    "title": "RemoveClients accepts nil callback and panics without releasing lock",
    "function": "LBClient.RemoveClients",
    "description": "The rc parameter is invoked without a nil check. Passing nil causes a nil function call panic; because Unlock is not deferred, the panic leaves cc.mu locked. This is a lower-severity caller-error path, but it can be triggered if rc comes from untrusted configuration or code."
  },
  {
    "severity": "low",
    "title": "Exported field mutation can race with AddClient and Do",
    "function": "LBClient.AddClient",
    "description": "AddClient reads cc.HealthCheck while holding the mutex, but HealthCheck is an exported field that callers may assign to concurrently. Do also reads Timeout without synchronization. The type documents concurrent method safety, but unsynchronized exported field updates can cause data races and inconsistent health-checking behavior."
  }
]
```