BudgetScan

Unbounded multipart form memory allocation via default FormValue/MultipartForm (DoS)

medium swesweep-fasthttp · server.go · RequestCtx.FormValue / RequestCtx.MultipartForm · run #4

Description

The default `FormValue` implementation and `MultipartForm()` call `MultipartFormWithLimit(0)`, imposing no explicit cap on multipart parsing. While the initial body read is bounded by `MaxRequestBodySize` (4 MiB default), the multipart parser allocates additional memory for file headers, temporary files, and parsed values. If the application sets `MaxRequestBodySize` to 0 or a very large value, an attacker can send a large multipart body and force the server to allocate proportional (or greater) memory, leading to OOM. Even with the default 4 MiB limit, the parser's overhead amplifies per-request memory usage under high concurrency.

Triage

Verdict
INVALID confidence 0.0
Model
qwen/qwen3.8-27b
Rounds
5
Cost
$0.0703 · 238.2 s
The report claims that the default `FormValue` and `MultipartForm` implementations lead to unbounded memory allocation because they call `MultipartFormWithLimit(0)`, which the reporter argues imposes no cap. However, tracing the data flow in `server.go` shows that the request body is strictly bounded *before* the handler (and thus before multipart parsing) is invoked. In the `serveConn` function, the request body is read using `ctx.Request.readLimitBody` (or `readBodyStream`) with the `maxRequestBodySize` parameter. The `maxRequestBodySize` is determined as follows: 1. If `s.MaxRequestBodySize` is set to a positive value, that value is used. 2. If `s.MaxRequestBodySize` is 0 (or not set), it defaults to `DefaultMaxRequestBodySize`. 3. `DefaultMaxRequestBodySize` is defined as `4 * 1024 * 1024` (4 MiB). If the incoming body exceeds this limit, `readLimitBody` returns an error (`ErrBodyTooLarge`), and the server rejects the request without invoking the handler. Therefore, the body data available to `MultipartForm` is strictly limited to `MaxRequestBodySize`. Since `MultipartForm` parses an in-memory buffer that is already capped at 4 MiB by default (or the configured limit), the memory allocation for parsing is proportional to this bounded input. The `0` limit in `MultipartFormWithLimit(0)` means the parser does not impose an *additional* stricter limit than the input stream's size, but since the input stream is a finite in-memory buffer of known size (bounded by `MaxRequestBodySize`), the allocation is not unbounded. An attacker cannot send a body larger than `MaxRequestBodySize` without the request being rejected. Setting `MaxRequestBodySize` to a very large value is a configuration choice by the application developer, not a library vulnerability, as the library provides the control mechanism (`MaxRequestBodySize`) and a safe default. Thus, the bug pattern of *unbounded* allocation reachable by an external attacker under default or reasonable configurations does not exist.

CRUX: The request body is strictly bounded by MaxRequestBodySize (default 4 MB) before multipart parsing occurs, preventing unbounded memory allocation.
GREP: DefaultMaxRequestBodySize
All rounds
[
  {
    "finding_title": "Unbounded multipart form memory allocation via default FormValue/MultipartForm (DoS)",
    "verdict": "INVALID",
    "reasoning": "The vulnerability report claims that the default `FormValue` and `MultipartForm` functions call `MultipartFormWithLimit(0)`, which imposes no limit on multipart parsing, leading to potential memory exhaustion (DoS) if an attacker sends a large multipart body. The report suggests that this is a security issue because `MaxRequestBodySize` (4 MiB default) only limits the initial body read, but the multipart parser might allocate additional memory.\n\nHowever, tracing the code flow:\n1.  `Request.readLimitBody` (called in `serveConn`) reads the body into the request struct. It is bounded by `maxRequestBodySize`. If the body exceeds this limit, it returns `ErrBodyTooLarge` and the request is rejected before the handler is called. This is enforced at the network boundary in `serveConn`.\n2.  `Request.MultipartForm()` parses the body *already present* in the request struct (i.e., `req.body`). It does not read from the network stream again. The `mime/multipart` parser operates on the in-memory body.\n3.  Therefore, the memory usage for multipart parsing is proportional to the size of the body, which is already strictly bounded by `MaxRequestBodySize` (default 4 MiB).\n4.  The parameter `maxBodySize` in `MultipartFormWithLimit` is passed to the `mime/multipart` package's `ReadForm`. In the standard Go implementation, this limit applies to the *total* size of the form values and files during parsing. If the body is already limited to 4 MiB, the maximum memory allocated for parsing will be proportional to that 4 MiB, not unlimited.\n5.  The report's premise that `MultipartFormWithLimit(0)` means \"no limit\" is misleading in this context because the input data (the body) is already constrained. The `0` limit in `ReadForm` just means \"don't impose an *additional* limit stricter than what the input stream provides\". Since the input stream is a `bytes.Buffer` or similar of fixed size 4 MiB, the parser cannot allocate more than proportional to that size.\n6.  Even if an application sets `MaxRequestBodySize` to 0 (which in fasthttp defaults to `DefaultMaxRequestBodySize` if `<= 0`), the default is 4 MiB. If an app explicitly sets a very large value, that is a configuration choice, not a library vulnerability. The library provides the `MaxRequestBodySize` setting precisely to control this. The default behavior is safe.\n7.  The \"amplification\" claim is not supported by the threat model for the *default* configuration. With 4 MiB limit, memory usage is bounded. An attacker cannot trigger OOM with a \"large\" body because large bodies are rejected or truncated/bounded. If they send a small body with many parts, the overhead is small. The `mime/multipart` package is designed to handle such cases without unbounded memory growth relative to the input size.\n\nThus, the bug pattern (unbounded allocation) does not exist in the default configuration because the input is bounded. The library correctly enforces the body size limit before parsing. This is not a vulnerability.\n\nCRUX: Request body size is strictly bounded by MaxRequestBodySize (default 4 MiB) before multipart parsing occurs, preventing unbounded memory allocation.\nGREP: DefaultMaxRequestBodySize",
    "elapsed": 49.2,
    "tokens": 28080,
    "tokens_in": 27341,
    "tokens_out": 739,
    "cost_usd": 0.013700219999999999,
    "file": "server.go",
    "round": 1,
    "grep_used": true,
    "grep_results": "GREP `DefaultMaxRequestBodySize`:\n```\nserver.go:332:\t// Request body size is limited by DefaultMaxRequestBodySize by default.\nserver.go:2230:// DefaultMaxRequestBodySize is the maximum request body size the server\nserver.go:2234:const DefaultMaxRequestBodySize = 4 * 1024 * 1024\nserver.go:2294:\t\tmaxRequestBodySize = DefaultMaxRequestBodySize\nserver.go:2423:\t\t\t\t\t\tmaxRequestBodySize = DefaultMaxRequestBodySize\n```"
  },
  {
    "finding_title": "Unbounded multipart form memory allocation via default FormValue/MultipartForm (DoS)",
    "verdict": "INVALID",
    "reasoning": "The report claims that `MultipartForm()` with a 0 limit leads to unbounded memory allocation. However, in the `fasthttp` architecture, the request body is read from the network into an in-memory buffer (or stream) *before* the handler is invoked. In `serveConn`, the function `readLimitBody` or `readBodyStream` is called with `maxRequestBodySize` as an argument. If `MaxRequestBodySize` is not set, it defaults to `DefaultMaxRequestBodySize` (4 MiB). If the incoming body exceeds this limit, the server returns an error (`ErrBodyTooLarge`) and does not proceed to invoke the user's handler. Therefore, by the time `FormValue()` or `MultipartForm()` is called by the user (or internally), the body data available to the parser is strictly bounded by `MaxRequestBodySize`. The `mime/multipart` parser operates on this bounded in-memory data. While `MultipartFormWithLimit(0)` tells the Go standard library's `ReadForm` not to impose an *additional* strict limit on file sizes (defaulting to 32 MB for files in stdlib, but here the source is an in-memory buffer of max 4 MB), the total memory consumption cannot exceed the size of the source buffer plus a reasonable constant overhead for parsing headers and structures. Since the input is bounded, the memory allocation is bounded. The report's concern about setting `MaxRequestBodySize` to a very large value is a configuration choice by the application developer, not a library vulnerability, and setting it to 0 results in the default 4 MB limit. Thus, there is no unbounded allocation reachable by an attacker under default or reasonable configurations.\n\nCRUX: The request body is strictly bounded by MaxRequestBodySize (default 4 MB) before multipart parsing occurs, preventing unbounded memory allocation.\nGREP: DefaultMaxRequestBodySize",
    "elapsed": 40.3,
    "tokens": 28740,
    "tokens_in": 28332,
    "tokens_out": 408,
    "cost_usd": 0.013123439999999998,
    "file": "server.go",
    "round": 2,
    "grep_used": true,
    "grep_results": "GREP `DefaultMaxRequestBodySize`:\n```\nserver.go:332:\t// Request body size is limited by DefaultMaxRequestBodySize by default.\nserver.go:2230:// DefaultMaxRequestBodySize is the maximum request body size the server\nserver.go:2234:const DefaultMaxRequestBodySize = 4 * 1024 * 1024\nserver.go:2294:\t\tmaxRequestBodySize = DefaultMaxRequestBodySize\nserver.go:2423:\t\t\t\t\t\tmaxRequestBodySize = DefaultMaxRequestBodySize\n```"
  },
  {
    "finding_title": "Unbounded multipart form memory allocation via default FormValue/MultipartForm (DoS)",
    "verdict": "INVALID",
    "reasoning": "The report claims that the default `FormValue` implementation leads to unbounded memory allocation because it calls `MultipartForm()` with a limit of 0. Reviewer 1 and Reviewer 2 argue that the request body is already bounded by `MaxRequestBodySize` (default 4 MiB) before the handler is called, thus making the allocation bounded. However, they both assume that `MaxRequestBodySize` always acts as a hard cap on memory. We must verify how `fasthttp` handles request bodies that exceed `MaxRequestBodySize`, specifically when `StreamRequestBody` is enabled or when the body is read via `readLimitBody`. \n\nIn `server.go`, `serveConn` calls `ctx.Request.readLimitBody` or `ctx.Request.readBodyStream` with `maxRequestBodySize`. If `StreamRequestBody` is false (the default), `readLimitBody` is used. If the body exceeds `maxRequestBodySize`, fasthttp typically rejects the request with `ErrBodyTooLarge` OR it may truncate/store it depending on implementation details. More critically, we need to check if there is a path where the multipart parser reads from a stream that is NOT strictly bounded by a small default, or if the `MaxRequestBodySize` default can be effectively bypassed or set to a huge value by default in specific configurations.\n\nHowever, a more subtle issue is the `MultipartFormWithLimit` behavior. In Go's `mime/multipart`, `ReadForm(maxMemory)` limits the amount of data stored in memory, spilling to disk if it exceeds `maxMemory`. If `maxMemory` is 0, it uses the default (usually 32MB for files, but for the whole form, the behavior might differ or it might just mean 'no explicit limit' relative to the input). The key is whether the *input* to the parser is bounded. \n\nLet's look at `readLimitBody`. If `maxRequestBodySize` is 4MiB, the body stored in `req.body` is at most 4MiB. The `MultipartForm()` method parses `req.body`. Therefore, the input to the parser is strictly <= 4MiB. The parser's overhead is proportional to this. Thus, under default settings, memory is bounded.\n\nBUT, the report states: \"If the application sets `MaxRequestBodySize` to 0 or a very large value...\". In `serveConn`, if `s.MaxRequestBodySize` is 0, it defaults to `DefaultMaxRequestBodySize` (4MiB). So setting it to 0 does NOT result in unlimited body size; it results in the default 4MiB. To get a large body, an application developer must explicitly set `s.MaxRequestBodySize` to a large value. This is a configuration choice, not a library bug. The library provides the control. If a developer sets `MaxRequestBodySize` to 10GB, they are opting into allowing 10GB bodies. Is that a vulnerability in the *library*? Usually, libraries are not considered vulnerable for allowing large allocations if the user explicitly configures them to do so, provided the library doesn't have a *default* that is dangerous or an *accidental* bypass. \n\nThe report's claim that \"default FormValue... imposing no explicit cap... attacker can send a large multipart body\" is flawed because the *default* `MaxRequestBodySize` IS a cap (4MiB). The attacker cannot send a \"large\" body (e.g., > 4MiB) under default settings; the connection will be closed or the request rejected. \n\nTherefore, the bug pattern (unbounded allocation via *default* configuration) does not exist. The allocation is bounded by the default `MaxRequestBodySize`. While `MultipartForm()` uses a limit of 0 (meaning \"let the input size dictate the bound\" or \"use stdlib defaults\"), the input itself is pre-bounded by the server's body reading logic. Since the input is capped at 4MiB by default, the memory usage is capped. The threat of OOM via multipart parsing under *default* settings is not valid. The library provides the necessary defense (`MaxRequestBodySize`) by default.\n\nCRUX: The request body is strictly bounded by MaxRequestBodySize (default 4 MB) before multipart parsing occurs, preventing unbounded memory allocation under default configuration.\nGREP: DefaultMaxRequestBodySize",
    "elapsed": 29.3,
    "tokens": 29767,
    "tokens_in": 28824,
    "tokens_out": 943,
    "cost_usd": 0.01493508,
    "file": "server.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `DefaultMaxRequestBodySize`:\n```\nserver.go:332:\t// Request body size is limited by DefaultMaxRequestBodySize by default.\nserver.go:2230:// DefaultMaxRequestBodySize is the maximum request body size the server\nserver.go:2234:const DefaultMaxRequestBodySize = 4 * 1024 * 1024\nserver.go:2294:\t\tmaxRequestBodySize = DefaultMaxRequestBodySize\nserver.go:2423:\t\t\t\t\t\tmaxRequestBodySize = DefaultMaxRequestBodySize\n```"
  },
  {
    "finding_title": "Unbounded multipart form memory allocation via default FormValue/MultipartForm (DoS)",
    "verdict": "INVALID",
    "reasoning": "The vulnerability report claims that the default `FormValue` and `MultipartForm` implementations lead to unbounded memory allocation (DoS) because they call `MultipartFormWithLimit(0)`, which supposedly imposes no cap on multipart parsing. The report suggests that even with a default `MaxRequestBodySize` of 4 MiB, the multipart parser could allocate proportional or greater memory, and if `MaxRequestBodySize` is set to 0 or a large value, an attacker could trigger OOM. \n\nHowever, tracing the data flow in `server.go` reveals that the request body is strictly bounded *before* the handler (and thus before multipart parsing) is invoked. In the `serveConn` function, the request body is read using `ctx.Request.readLimitBody` (or `readBodyStream`) with the `maxRequestBodySize` parameter. The `maxRequestBodySize` is determined as follows:\n1. If `s.MaxRequestBodySize` is set to a positive value, that value is used.\n2. If `s.MaxRequestBodySize` is 0 (or not set), it defaults to `DefaultMaxRequestBodySize`.\n3. `DefaultMaxRequestBodySize` is defined as `4 * 1024 * 1024` (4 MiB).\n\nIf the incoming body exceeds this limit, `readLimitBody` returns an error (`ErrBodyTooLarge`), and the server rejects the request without invoking the handler. Therefore, the body data available to `MultipartForm` is strictly limited to `MaxRequestBodySize`. Since `MultipartForm` parses an in-memory buffer that is already capped at 4 MiB by default (or the configured limit), the memory allocation for parsing is proportional to this bounded input. \n\nThe `0` limit in `MultipartFormWithLimit(0)` means the parser does not impose an *additional* stricter limit than the input stream's size, but since the input stream is a finite in-memory buffer of known size (bounded by `MaxRequestBodySize`), the allocation is not unbounded. An attacker cannot send a body larger than `MaxRequestBodySize` without the request being rejected. Setting `MaxRequestBodySize` to a very large value is a configuration choice by the application developer, not a library vulnerability, as the library provides the control mechanism (`MaxRequestBodySize`) and a safe default. Thus, the bug pattern of *unbounded* allocation reachable by an external attacker under default or reasonable configurations does not exist.\n\nCRUX: The request body is strictly bounded by MaxRequestBodySize (default 4 MB) before multipart parsing occurs, preventing unbounded memory allocation.\nGREP: DefaultMaxRequestBodySize",
    "elapsed": 52.7,
    "tokens": 30413,
    "tokens_in": 29839,
    "tokens_out": 574,
    "cost_usd": 0.014254379999999999,
    "file": "server.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `DefaultMaxRequestBodySize`:\n```\nserver.go:332:\t// Request body size is limited by DefaultMaxRequestBodySize by default.\nserver.go:2230:// DefaultMaxRequestBodySize is the maximum request body size the server\nserver.go:2234:const DefaultMaxRequestBodySize = 4 * 1024 * 1024\nserver.go:2294:\t\tmaxRequestBodySize = DefaultMaxRequestBodySize\nserver.go:2423:\t\t\t\t\t\tmaxRequestBodySize = DefaultMaxRequestBodySize\n```"
  },
  {
    "finding_title": "Unbounded multipart form memory allocation via default FormValue/MultipartForm (DoS)",
    "verdict": "INVALID",
    "reasoning": "The report claims that the default `FormValue` and `MultipartForm` implementations lead to unbounded memory allocation because they call `MultipartFormWithLimit(0)`, which the reporter argues imposes no cap. However, tracing the data flow in `server.go` shows that the request body is strictly bounded *before* the handler (and thus before multipart parsing) is invoked. In the `serveConn` function, the request body is read using `ctx.Request.readLimitBody` (or `readBodyStream`) with the `maxRequestBodySize` parameter. The `maxRequestBodySize` is determined as follows: 1. If `s.MaxRequestBodySize` is set to a positive value, that value is used. 2. If `s.MaxRequestBodySize` is 0 (or not set), it defaults to `DefaultMaxRequestBodySize`. 3. `DefaultMaxRequestBodySize` is defined as `4 * 1024 * 1024` (4 MiB). If the incoming body exceeds this limit, `readLimitBody` returns an error (`ErrBodyTooLarge`), and the server rejects the request without invoking the handler. Therefore, the body data available to `MultipartForm` is strictly limited to `MaxRequestBodySize`. Since `MultipartForm` parses an in-memory buffer that is already capped at 4 MiB by default (or the configured limit), the memory allocation for parsing is proportional to this bounded input. The `0` limit in `MultipartFormWithLimit(0)` means the parser does not impose an *additional* stricter limit than the input stream's size, but since the input stream is a finite in-memory buffer of known size (bounded by `MaxRequestBodySize`), the allocation is not unbounded. An attacker cannot send a body larger than `MaxRequestBodySize` without the request being rejected. Setting `MaxRequestBodySize` to a very large value is a configuration choice by the application developer, not a library vulnerability, as the library provides the control mechanism (`MaxRequestBodySize`) and a safe default. Thus, the bug pattern of *unbounded* allocation reachable by an external attacker under default or reasonable configurations does not exist.\n\nCRUX: The request body is strictly bounded by MaxRequestBodySize (default 4 MB) before multipart parsing occurs, preventing unbounded memory allocation.\nGREP: DefaultMaxRequestBodySize",
    "elapsed": 66.7,
    "tokens": 30997,
    "tokens_in": 30491,
    "tokens_out": 506,
    "cost_usd": 0.01432422,
    "file": "server.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `DefaultMaxRequestBodySize`:\n```\nserver.go:332:\t// Request body size is limited by DefaultMaxRequestBodySize by default.\nserver.go:2230:// DefaultMaxRequestBodySize is the maximum request body size the server\nserver.go:2234:const DefaultMaxRequestBodySize = 4 * 1024 * 1024\nserver.go:2294:\t\tmaxRequestBodySize = DefaultMaxRequestBodySize\nserver.go:2423:\t\t\t\t\t\tmaxRequestBodySize = DefaultMaxRequestBodySize\n```"
  }
]
Raw scan report for this file

Looking at this file, I need to trace how untrusted data (HTTP requests from `net.Conn`) flows through the server's request-processing loop and identify where malformed input could trigger panics, resource exhaustion, or memory corruption.

**Key untrusted data flows:**
- Raw TCP bytes → `bufio.Reader` → `ctx.Request.Header.Read(br)` → `parseURI()` → `readLimitBody`/`readBodyStream` → `s.Handler(ctx)`
- Multipart form data → `ctx.MultipartForm()` / `ctx.FormValue()` / `ctx.FormFile()`
- Response writing → `writeResponse(ctx, bw)` → `br`/`bw` pools

**Function-by-function analysis:**

1. **`ServeConn` (top-level)**: `handler` parameter is assigned to `s.Handler` without nil check. If nil, `s.Handler(ctx)` in `serveConn` panics. Programmer error but reachable via public API.

2. **`serveConn`**: The main loop. `maxRequestBodySize` defaults to 4MB, bounding body reads. `br.Peek(1)` on keep-alive handles EOF. `s.Handler(ctx)` is called without panic recovery — a panic here kills the goroutine and leaks `br`, `bw`, `ctx` (no deferred cleanup for these). The `concurrency`/`open` counters are properly balanced via `defer s.serveConnCleanup()`.

3. **`FormFile`**: `mf.File[key]` returns `[]*FileHeader`. The nil check catches missing keys, but if the slice is non-nil yet empty (a parser invariant violation), `fhh[0]` panics with index-out-of-range.

4. **`FormValue` / `defaultFormValue`**: Falls through to `ctx.MultipartForm()` → `MultipartFormWithLimit(0)` — no size cap on multipart parsing. Body is bounded by `maxRequestBodySize`, but if the application sets `MaxRequestBodySize` to 0 or very large, an attacker can drive unbounded memory allocation through multipart form parsing.

5. **`TimeoutWithCodeHandler`**: Spawns a goroutine that does `<-concurrencyCh` after `h(ctx)` returns. If `h(ctx)` panics, the goroutine dies and the concurrency slot is never released — a permanent leak per panic.

6. **`acquireByteReader`**: `var b [1]byte; c.Read(b[:])` — bounded, safe. Panic on `n != 1` is a developer sanity check, not reachable with conforming `net.Conn`.

7. **`SaveMultipartFile`**: `fh.Open()` panics on nil `fh`. `path` is used directly in `os.Create`/`os.Rename` — documented as requiring server-trusted paths.

8. **`EarlyHints`**: Reads Link headers from `ctx.Response.Header` (handler-set, not attacker-set). Writes them to the connection. No untrusted data path.

9. **`writeErrorResponse`**: Ignores return values of `writeResponse` and `bw.Flush()` — errors are silently dropped, potentially leaving the connection in an inconsistent state.

10. **Pool type assertions** (`v.(*Server)`, `v.(*RequestCtx)`, `v.(*bufio.Reader)`, `v.(*atomic.Int64)`): Unchecked, but pools are internal and only populated by the server itself. Safe.

```json
[
  {
    "severity": "medium",
    "title": "Index out of range panic in FormFile on empty file header slice",
    "function": "RequestCtx.FormFile",
    "description": "After checking `fhh == nil`, the code returns `fhh[0]` without verifying `len(fhh) > 0`. If the multipart parser produces a non-nil empty `[]*FileHeader` for a given key (e.g., due to a crafted or edge-case multipart body), this triggers an index-out-of-range panic, crashing the connection goroutine. An attacker can attempt to trigger this by crafting a multipart form where a file field key maps to an empty header list."
  },
  {
    "severity": "medium",
    "title": "Unbounded multipart form memory allocation via default FormValue/MultipartForm (DoS)",
    "function": "RequestCtx.FormValue / RequestCtx.MultipartForm",
    "description": "The default `FormValue` implementation and `MultipartForm()` call `MultipartFormWithLimit(0)`, imposing no explicit cap on multipart parsing. While the initial body read is bounded by `MaxRequestBodySize` (4 MiB default), the multipart parser allocates additional memory for file headers, temporary files, and parsed values. If the application sets `MaxRequestBodySize` to 0 or a very large value, an attacker can send a large multipart body and force the server to allocate proportional (or greater) memory, leading to OOM. Even with the default 4 MiB limit, the parser's overhead amplifies per-request memory usage under high concurrency."
  },
  {
    "severity": "low",
    "title": "Concurrency slot leak when handler panics in TimeoutWithCodeHandler",
    "function": "TimeoutWithCodeHandler",
    "description": "The spawned goroutine executes `h(ctx); ch <- struct{}{}; <-concurrencyCh`. If `h(ctx)` panics (e.g., on malformed input reaching the handler), the goroutine terminates before `<-concurrencyCh` executes, permanently leaking one slot in the bounded `concurrencyCh`. Since fasthttp performs no panic recovery, each panicked request permanently reduces available concurrency. Repeated malformed requests can exhaust all concurrency slots, causing all subsequent requests to receive 429 Too Many Responses — a targeted DoS."
  },
  {
    "severity": "low",
    "title": "Nil handler in ServeConn causes panic on first request",
    "function": "ServeConn (package-level)",
    "description": "The exported `ServeConn(c, handler)` assigns `s.Handler = handler` without a nil check. If `handler` is nil, the subsequent `s.Handler(ctx)` call inside `serveConn` invokes a nil function, panicking and crashing the connection goroutine. While this is a programmer error, it is reachable through the public API and results in an unhandled panic with no recovery."
  },
  {
    "severity": "low",
    "title": "Resource leak (br/bw/ctx) on handler panic in serveConn",
    "function": "Server.serveConn",
    "description": "The `serveConn` method acquires `br`, `bw`, and `ctx` from sync.Pools but releases them only at the end of the function body (after the for-loop). If `s.Handler(ctx)` panics, the panic bypasses all cleanup code, leaking the pooled reader, writer, and context. The `defer s.serveConnCleanup()` handles counter bookkeeping but not pool object release. Under repeated panics (e.g., crafted malformed requests), pool objects accumulate, increasing memory pressure until OOM."
  }
]
```