BudgetScan

Nil request or response forwarded unchecked to client and health callback

medium swesweep-fasthttp · lbclient.go · lbClient.DoDeadline · run #4

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.

Triage

Verdict
INVALID confidence 0.17
Model
qwen/qwen3.8-27b
Rounds
6
Cost
$0.0219 · 853.9 s
[ARBITER] The finding is invalid under the specified threat model for a reusable library. The 'nil pointer' input is an internal state error (misuse of the API) rather than untrusted data controlled by an external attacker. An attacker sending malicious HTTP payloads to a host application using this library can only control the *contents* of the `Request` and `Response` structs (e.g., headers, body), not the pointer values themselves. The host application (trusted code) is responsible for allocating these objects. Passing `nil` pointers is a documented precondition violation (or at least a trusted-code bug) that leads to a panic in the host process, but it is not reachable via untrusted network input. Therefore, it does not constitute a security vulnerability in the library itself.
All rounds
[
  {
    "finding_title": "Nil request or response forwarded unchecked to client and health callback",
    "verdict": "VALID",
    "reasoning": "The reported vulnerability claims that passing `nil` request or response pointers to `LBClient.Do`, `DoTimeout`, or `DoDeadline` leads to a panic because these values are forwarded unchecked to `c.c.DoDeadline` and `c.healthCheck`. \n\n1. **Bug Pattern**: The code indeed does not perform nil checks on `req` or `resp` in `LBClient.DoDeadline` (and its callers `DoTimeout`, `Do`). It passes them directly to `cc.get().DoDeadline(req, resp, deadline)`. The internal `lbClient.DoDeadline` then calls `c.c.DoDeadline(req, resp, deadline)` and `c.isHealthy(req, resp, err)`, which calls `c.healthCheck(req, resp, err)`. If `c.c` or `c.healthCheck` expects non-nil pointers (e.g., accesses fields like `req.URI` or `resp.Header`), a nil dereference panic will occur.\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... parsers, decoders, deserializers and protocol handlers receive adversarial payloads that the host application obtained from untrusted sources.\" The entry point is the public `LBClient.Do` method. In a typical fasthttp application using load balancing, an HTTP server handler receives an HTTP request, populates `req` and `resp` structures, and passes them to the `LBClient` to fetch resources. If the application logic allows for these structures to be nil (e.g., a bug in the handler, or a specific code path where the response is not initialized before forwarding), or if the attacker can influence the application to call the LBClient with nil pointers, a panic (DoS) can be triggered. Even though standard fasthttp usage involves non-nil `req`/`resp`, the library's public API accepts these pointers and does not validate them. A malicious or buggy caller passing nil pointers will cause a crash. The threat model explicitly includes \"correct behaviour on malformed input\" and \"Memory safety... (out-of-bounds access... )\" and \"Crash... reachable through the public API with a crafted payload is VALID\". While nil pointers are not strictly \"crafted payloads\" in the byte-string sense, they are a form of malformed input to the API. More importantly, the lack of validation on public API inputs is a robustness/security flaw in a library, as it violates the expectation that the library should handle invalid arguments gracefully or at least document them strictly, but here it leads to an uncontrolled panic (DoS). \n\n3. **Defense**: There is no nil check. `init()` only checks if the `Clients` list is empty. It does not check the validity of the `req`/`resp` arguments in `Do`.\n\n4. **Verdict**: The bug is real (nil pointers are forwarded). It is reachable via the public API. A panic causes a crash (DoS). Therefore, it is a valid security vulnerability under the threat model of a library that must ensure memory safety and correct behavior on malformed/invalid inputs provided via the public API.\n\nCRUX: The public functions LBClient.Do, DoTimeout, and DoDeadline do not validate that the req and resp arguments are non-nil before passing them to the underlying client or health check callback, which can cause a nil pointer dereference panic (Denial of Service) if nil values are passed.\nGREP: func (cc *LBClient) DoDeadline",
    "elapsed": 277.3,
    "tokens": 4053,
    "tokens_in": 3302,
    "tokens_out": 751,
    "cost_usd": 0.0036398399999999997,
    "file": "lbclient.go",
    "round": 1,
    "grep_used": true,
    "grep_results": "GREP `func (cc *LBClient) DoDeadline (simplified to: DoDeadline)`:\n```\nclient_test.go:147:\tif err := c.DoDeadline(req, nil, time.Now().Add(-time.Second)); err != ErrTimeout {\nclient_test.go:153:func TestDoDeadlineRetry(t *testing.T) {\nclient_test.go:187:\tif err := c.DoDeadline(req, nil, time.Now().Add(time.Millisecond*200)); err != ErrTimeout {\nclient_test.go:478:\tif err := c.DoDeadline(req, nil, time.Now().Add(-time.Second)); err != ErrTimeout {\nclient_test.go:507:\tif err := c.DoDeadline(req, nil, time.Now().Add(time.Second)); err != nil {\nclient_test.go:1506:\t\t\t\tif err := c.DoDeadline(req, resp, time.Now().Add(timeout)); err != nil {\nclient_test.go:3190:\t\t\tif err := c.DoDeadline(req, resp, time.Now().Add(timeout)); err != nil {\nhttp.go:60:\t// Request timeout. Usually set by DoDeadline or DoTimeout\nlbclient.go:12:\tDoDeadline(req *Request, resp *Response, deadline time.Time) error\nlbclient.go:63:// DoDeadline calls DoDeadline on the least loaded client.\nlbclient.go:64:func (cc *LBClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nlbclient.go:65:\treturn cc.get().DoDeadline(req, resp, deadline)\nlbclient.go:68:// DoTimeout calculates deadline and calls DoDeadline on the least loaded client.\nlbclient.go:71:\treturn cc.get().DoDeadline(req, resp, deadline)\nlbclient.go:162:func (c *lbClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nlbclient.go:163:\terr := c.c.DoDeadline(req, resp, deadline)\nclient.go:67:// DoDeadline performs the given request and waits for response until\nclient.go:90:func DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nclient.go:91:\treturn defaultClient.DoDeadline(req, resp, deadline)\nclient.go:421:// DoDeadline performs the given request and waits for response until\nclient.go:445:func (c *Client) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nclient.go:697:// This return value is meaningful only when you use `Request.SetTimeout`, `DoTimeout`, or `DoDeadline`.\nclient.go:1374:// DoDeadline performs the given request and waits for response until\nclient.go:1393:func (c *HostClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nclient.go:2528:\treturn c.DoDeadline(req, resp, time.Now().Add(timeout))\nclient.go:2531:// DoDeadline performs the given request and waits for response until\nclient.go:2546:func (c *PipelineClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nclient.go:2547:\treturn c.getConnClient().DoDeadline(req, resp, deadline)\nclient.go:2550:func (c *pipelineConnClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\n```"
  },
  {
    "finding_title": "Nil request or response forwarded unchecked to client and health callback",
    "verdict": "INVALID",
    "reasoning": "The report claims that passing `nil` request or response pointers to `LBClient.Do`, `DoTimeout`, or `DoDeadline` causes a panic (DoS) because they are forwarded unchecked.\n\n1. **Bug Pattern Analysis**: The code in `lbclient.go` indeed passes `req` and `resp` without nil checks to `cc.get().DoDeadline`, which then calls `c.c.DoDeadline` (the underlying `BalancingClient`) and `c.isHealthy` (which calls the `healthCheck` callback).\n\n2. **Attacker Reachability & Input Control**: The threat model for a library identifies the attacker as code calling the public API with adversarial payloads. However, in the context of `fasthttp`, `Request` and `Response` structs are typically allocated by the application framework (e.g., the HTTP server handler) *before* being passed to client methods to fetch downstream resources. The `req` contains the data fetched *from* the network (which might be malicious content), but the struct pointer itself is managed by the trusted application logic. The `resp` is usually an empty struct allocated by the app to receive data. An external network attacker cannot directly force the application to pass a `nil` pointer; they can only control the *contents* of the `Request`/`Response` structs (headers, body, etc.). Passing a `nil` pointer is a bug in the host application's logic (a developer error), not a result of parsing a malicious network packet. Therefore, the input that triggers the crash (the `nil` pointer) is not controlled by the external attacker.\n\n3. **Defense/Usage Context**: The standard usage pattern for `fasthttp.Client` (and by extension `LBClient`) requires valid `Request` and `Response` objects. While `nil` checks are good defensive programming practice to prevent panics from internal bugs, the absence of such checks does not constitute a security vulnerability against an external network attacker because the attacker cannot manipulate the pointer value itself through the network channel. The library's responsibility is to handle malformed *data* within the request, not to protect against the host application passing invalid memory handles due to internal state errors. If the host app has a bug where it passes `nil`, that is a local crash caused by internal logic failure, not a remote denial of service triggered by untrusted input.\n\n4. **Conclusion**: The crash is reachable only if the calling application passes a `nil` pointer, which is a violation of the API contract by trusted code, not a consequence of processing untrusted network data. Therefore, it is not a security vulnerability under the provided threat model.\n\nCRUX: The 'nil pointer' input is generated by the host application's internal logic, not by an external attacker manipulating network data, so it is not attacker-reachable via untrusted input.\nGREP: nil, resp, req.*= nil",
    "elapsed": 93.7,
    "tokens": 5564,
    "tokens_in": 4947,
    "tokens_out": 617,
    "cost_usd": 0.00392874,
    "file": "lbclient.go",
    "round": 2,
    "grep_used": true,
    "grep_results": "GREP `nil, resp, req.*= nil`:\n```\n(no matches in repo)\n```"
  },
  {
    "finding_title": "Nil request or response forwarded unchecked to client and health callback",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that passing `nil` pointers to `LBClient.Do`, `DoTimeout`, or `DoDeadline` leads to a panic. I must evaluate this based on the threat model: 'Attacker: Code that calls the public API... and above all the data it passes through... parsers, decoders, deserializers and protocol handlers receive adversarial payloads that the host application obtained from untrusted sources.'\n\n1. **Bug Existence**: The code in `lbclient.go` indeed does not check if `req` or `resp` are nil before passing them to the underlying `BalancingClient` or the `HealthCheck` callback. If a nil pointer is passed, and the receiver dereferences it (which `fasthttp.Client` likely does), a panic will occur.\n\n2. **Attacker Reachability**: The key question is whether an external attacker can control the `req` or `resp` pointers to be nil.\n   - In a typical fasthttp application, the host application acts as an intermediary. It receives an HTTP request from an untrusted source, parses it into a `*Request` object (usually allocated on the stack or heap by the framework), and may then forward this request to a backend service using `LBClient`.\n   - The `Request` and `Response` objects are managed by the host application's logic. The external attacker controls the *content* of the HTTP packet (headers, body), which influences the *fields* within the `Request` struct, but the attacker does **not** control the pointer values passed to `LBClient.Do`.\n   - To trigger the vulnerability, the host application must explicitly pass a `nil` pointer. This is a programming error in the host application (trusted code), not a result of processing malformed data from the network. An attacker cannot send an HTTP packet that causes the fasthttp server to generate a `nil` request pointer.\n   - The threat model states: 'Misuse that violates a documented precondition of the public API' is 'Not a vulnerability under this model'. While not explicitly documented as 'non-nil', the standard contract of such APIs is that they operate on valid structures. More importantly, the 'attacker' is defined as influencing data through parsers/protocol handlers. Passing nil is a control-flow decision made by the application developer, not a data-driven consequence of untrusted input.\n\n3. **Comparison with Reviewers**: Reviewer 1 argued it is a valid DoS because the library should be robust. However, Reviewer 2 correctly identified that the `nil` pointer is an internal state error, not untrusted input. The threat model emphasizes 'untrusted input' (data). Since the attacker cannot force the `req` pointer to be nil via network traffic, this is not a security vulnerability in the context of a library processing untrusted data. It is a code quality issue or a developer bug.\n\n4. **Conclusion**: The bug pattern exists, but it is not reachable by an external attacker through untrusted input. The attacker cannot influence the nil-ness of the pointers. Therefore, it is INVALID.\n\nCRUX: The `nil` pointer is an internal state error of the host application, not a value controlled by an external attacker through untrusted network input.\nGREP: func (cc \\*LBClient) DoDeadline",
    "elapsed": 228.3,
    "tokens": 5623,
    "tokens_in": 4913,
    "tokens_out": 710,
    "cost_usd": 0.0041934599999999995,
    "file": "lbclient.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `func (cc *LBClient) DoDeadline (simplified to: DoDeadline)`:\n```\nclient_test.go:147:\tif err := c.DoDeadline(req, nil, time.Now().Add(-time.Second)); err != ErrTimeout {\nclient_test.go:153:func TestDoDeadlineRetry(t *testing.T) {\nclient_test.go:187:\tif err := c.DoDeadline(req, nil, time.Now().Add(time.Millisecond*200)); err != ErrTimeout {\nclient_test.go:478:\tif err := c.DoDeadline(req, nil, time.Now().Add(-time.Second)); err != ErrTimeout {\nclient_test.go:507:\tif err := c.DoDeadline(req, nil, time.Now().Add(time.Second)); err != nil {\nclient_test.go:1506:\t\t\t\tif err := c.DoDeadline(req, resp, time.Now().Add(timeout)); err != nil {\nclient_test.go:3190:\t\t\tif err := c.DoDeadline(req, resp, time.Now().Add(timeout)); err != nil {\nlbclient.go:12:\tDoDeadline(req *Request, resp *Response, deadline time.Time) error\nlbclient.go:63:// DoDeadline calls DoDeadline on the least loaded client.\nlbclient.go:64:func (cc *LBClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nlbclient.go:65:\treturn cc.get().DoDeadline(req, resp, deadline)\nlbclient.go:68:// DoTimeout calculates deadline and calls DoDeadline on the least loaded client.\nlbclient.go:71:\treturn cc.get().DoDeadline(req, resp, deadline)\nlbclient.go:162:func (c *lbClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nlbclient.go:163:\terr := c.c.DoDeadline(req, resp, deadline)\nclient.go:67:// DoDeadline performs the given request and waits for response until\nclient.go:90:func DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nclient.go:91:\treturn defaultClient.DoDeadline(req, resp, deadline)\nclient.go:421:// DoDeadline performs the given request and waits for response until\nclient.go:445:func (c *Client) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nclient.go:697:// This return value is meaningful only when you use `Request.SetTimeout`, `DoTimeout`, or `DoDeadline`.\nclient.go:1374:// DoDeadline performs the given request and waits for response until\nclient.go:1393:func (c *HostClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nclient.go:2528:\treturn c.DoDeadline(req, resp, time.Now().Add(timeout))\nclient.go:2531:// DoDeadline performs the given request and waits for response until\nclient.go:2546:func (c *PipelineClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nclient.go:2547:\treturn c.getConnClient().DoDeadline(req, resp, deadline)\nclient.go:2550:func (c *pipelineConnClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nhttp.go:60:\t// Request timeout. Usually set by DoDeadline or DoTimeout\n```"
  },
  {
    "finding_title": "Nil request or response forwarded unchecked to client and health callback",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that passing `nil` pointers to `LBClient.Do`, `DoTimeout`, or `DoDeadline` causes a panic. While the code indeed forwards `req` and `resp` without nil checks, the critical question is reachability under the defined threat model. The threat model specifies the attacker as 'Code that calls the public API... parsers, decoders, deserializers and protocol handlers receive adversarial payloads that the host application obtained from untrusted sources.' The input that triggers the bug is the *pointer value* itself being nil. In a standard HTTP client workflow (which is the primary use of fasthttp), the `Request` and `Response` structs are allocated by the host application (trusted code). An external network attacker can control the *contents* of the HTTP payload (headers, body), which populate the fields inside the `Request` struct, but they cannot influence the memory address of the struct pointer passed to the client method. To trigger this panic, the host application developer must explicitly pass a `nil` pointer, which is a logic error in the trusted calling code, not a consequence of processing untrusted network data. The threat model explicitly excludes 'misuse that violates a documented precondition of the public API' and 'Missing parameter validation... when the public boundary already enforces the invariant' (though here the invariant is implicit, the key is that the attacker doesn't control the pointer). Since an external attacker cannot craft a network packet that results in a `nil` pointer being passed to `LBClient.Do`, this is a robustness/code quality issue (missing defensive check against trusted internal misuse) rather than a security vulnerability reachable via untrusted input. Therefore, it is INVALID.\n\nCRUX: An external attacker cannot control the `req` or `resp` pointer values (making them nil) through untrusted network input; they can only control the data inside the structs.\nGREP: func (cc \\*LBClient) Do",
    "elapsed": 31.1,
    "tokens": 6792,
    "tokens_in": 6377,
    "tokens_out": 415,
    "cost_usd": 0.003923339999999999,
    "file": "lbclient.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `func (cc *LBClient) Do (simplified to: LBClient)`:\n```\nlbclient.go:10:// to LBClient.Clients.\nlbclient.go:16:// LBClient balances requests among available LBClient.Clients.\nlbclient.go:24:// It is forbidden copying LBClient instances. Create new instances instead.\nlbclient.go:26:// It is safe calling LBClient methods from concurrently running goroutines.\nlbclient.go:27:type LBClient struct {\nlbclient.go:47:\t// Timeout is the request timeout used when calling LBClient.Do.\nlbclient.go:49:\t// DefaultLBClientTimeout is used by default.\nlbclient.go:57:// DefaultLBClientTimeout is the default request timeout used by LBClient\nlbclient.go:58:// when calling LBClient.Do.\nlbclient.go:60:// The timeout may be overridden via LBClient.Timeout.\nlbclient.go:61:const DefaultLBClientTimeout = time.Second\nlbclient.go:64:func (cc *LBClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {\nlbclient.go:69:func (cc *LBClient) DoTimeout(req *Request, resp *Response, timeout time.Duration) error {\nlbclient.go:74:// Do calculates timeout using LBClient.Timeout and calls DoTimeout\nlbclient.go:76:func (cc *LBClient) Do(req *Request, resp *Response) error {\nlbclient.go:79:\t\ttimeout = DefaultLBClientTimeout\nlbclient.go:84:func (cc *LBClient) init() {\nlbclient.go:89:\t\tpanic(\"BUG: LBClient.Clients cannot be empty\")\nlbclient.go:101:func (cc *LBClient) AddClient(c BalancingClient) int {\nlbclient.go:114:func (cc *LBClient) RemoveClients(rc func(BalancingClient) bool) int {\nlbclient.go:131:func (cc *LBClient) get() *lbClient {\nclient.go:726:// load among them, it would be better using LBClient instead, since HostClient\nlbclient_example_test.go:10:func ExampleLBClient() {\nlbclient_example_test.go:19:\tvar lbc fasthttp.LBClient\n```"
  },
  {
    "finding_title": "Nil request or response forwarded unchecked to client and health callback",
    "verdict": "INVALID",
    "reasoning": "The report claims that passing nil pointers to `LBClient.Do` causes a panic. While the code indeed lacks nil checks, the threat model for this library defines the attacker as an entity controlling data passed through parsers and protocol handlers (untrusted payloads). The `req` and `resp` arguments to `LBClient.Do` are pointers to struct instances allocated and managed by the trusted host application (the developer's code). An external attacker sending malicious HTTP data can only control the *contents* of the `Request` struct (headers, body), not the pointer value itself. To trigger the nil pointer dereference, the host application must explicitly pass a `nil` pointer, which is a programming error (logic bug) in the trusted calling code, not a consequence of processing untrusted network input. The threat model explicitly excludes issues where the input triggering the bug is not controlled by the attacker via untrusted data channels. Therefore, this is a robustness/code quality issue regarding internal API misuse, not a security vulnerability reachable by an external attacker.\n\nCRUX: An external attacker cannot control the `req` or `resp` pointer values to be nil; they can only control the data within the structs, so the nil pointer is a trusted code error, not untrusted input.\nGREP: LBClient.Do",
    "elapsed": 144.2,
    "tokens": 6918,
    "tokens_in": 6639,
    "tokens_out": 279,
    "cost_usd": 0.00362538,
    "file": "lbclient.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `LBClient.Do`:\n```\nlbclient.go:47:\t// Timeout is the request timeout used when calling LBClient.Do.\nlbclient.go:58:// when calling LBClient.Do.\n```"
  },
  {
    "verdict": "INVALID",
    "reasoning": "[ARBITER] The finding is invalid under the specified threat model for a reusable library. The 'nil pointer' input is an internal state error (misuse of the API) rather than untrusted data controlled by an external attacker. An attacker sending malicious HTTP payloads to a host application using this library can only control the *contents* of the `Request` and `Response` structs (e.g., headers, body), not the pointer values themselves. The host application (trusted code) is responsible for allocating these objects. Passing `nil` pointers is a documented precondition violation (or at least a trusted-code bug) that leads to a panic in the host process, but it is not reachable via untrusted network input. Therefore, it does not constitute a security vulnerability in the library itself.",
    "round": 6,
    "file": "lbclient.go",
    "finding_title": "Nil request or response forwarded unchecked to client and health callback",
    "elapsed": 79.3,
    "tokens_in": 4995,
    "tokens_out": 173,
    "cost_usd": 0.0026169
  }
]
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."
  }
]
```