BudgetScan

Write errors silently swallowed in stackless compression path

medium swesweep-fasthttp · brotli.go · WriteBrotliLevel / nonblockingWriteBrotli · run #2

Description

In the stackless branch of WriteBrotliLevel, stacklessWriteBrotli(ctx) is called and its result discarded; the function unconditionally returns (len(p), nil). Internally, nonblockingWriteBrotli also ignores zw.Write errors. If the underlying io.Writer fails, the caller is told success while output is truncated, enabling silent data corruption and masking writer-targeted attacks.

Triage

Verdict
INVALID confidence 0.0
Model
qwen/qwen3.8-27b
Rounds
5
Cost
$0.0173 · 422.4 s
The report claims that `WriteBrotliLevel` silently swallows write errors for specific in-memory writers (`*byteSliceWriter`, `*bytes.Buffer`, `*bytebufferpool.ByteBuffer`). I verified the code: in the `stackless` branch, `stacklessWriteBrotli` is called, and the function unconditionally returns `len(p), nil` without checking for errors. Internally, `nonblockingWriteBrotli` calls `zw.Write` and ignores its error. 

However, under the provided threat model, this is not a security vulnerability. The types restricted to this path are standard in-memory buffers. In Go, these implementations do not return I/O errors (like network failures or disk full) during a `Write` operation; they only fail if memory allocation fails (OOM). An external attacker cannot control the destination `io.Writer` to be a failing type while remaining in this specific code path, nor can they force these in-memory buffers to return an I/O error while the process remains stable. If an OOM occurs, the process is crashing or subject to runtime panic, which is a system-level failure, not a distinct logic flaw exploitable for code execution or auth bypass. Cases where the writer *can* fail (e.g., network sockets) fall into the `default` branch, which correctly propagates errors. Thus, the issue is a code quality or API design flaw, not a security vulnerability reachable by an attacker to cause meaningful harm.

CRUX: The writers triggering the error-swallowing path are in-memory types that cannot return I/O errors under attacker-controlled conditions without causing system-level OOM.
GREP: func WriteBrotliLevel
All rounds
[
  {
    "finding_title": "Write errors silently swallowed in stackless compression path",
    "verdict": "INVALID",
    "reasoning": "The report claims that `WriteBrotliLevel` discards errors from `stacklessWriteBrotli` when writing to `byteSliceWriter`, `bytes.Buffer`, or `bytebufferpool.ByteBuffer`, returning `len(p), nil` unconditionally. While this code pattern exists, it is not a security vulnerability under the provided threat model. \n\n1. **Nature of the Bug**: The functions involved in the 'stackless' path (`byteSliceWriter`, `bytes.Buffer`, `bytebufferpool.ByteBuffer`) are in-memory writers. In Go, writes to these types do not fail with `io.ErrClosedPipe` or network errors; they either succeed or trigger a panic (which is not a silent data corruption issue) or allocate memory. The `io.Writer` interface contract for these specific in-memory implementations implies that `Write` returns `(n, nil)` on success. The 'error' would only occur in scenarios of memory exhaustion (OOM), which is a system-level failure, not a logic flaw exploitable by an attacker to cause silent corruption in the host process in the context of normal operation. \n\n2. **Attacker Reachability & Harm**: An attacker cannot control the `io.Writer` implementation to be a failing in-memory buffer. If the host application passes a custom `io.Writer` that fails, it falls into the `default` case, which *does* propagate errors. The 'stackless' case is restricted to known-safe in-memory types. Therefore, the scenario where an attacker causes 'silent data corruption' by making the writer fail is not reachable because the writers that hit the error-swallowing code path are controlled by the host application and do not fail under attacker input.\n\n3. **Threat Model Compliance**: The threat model excludes 'misuse that violates a documented precondition' and focuses on memory safety, OOB access, or unbounded resources. Swallowing an error from an in-memory write that effectively cannot fail (short of OOM) is a code quality or API design choice, not a security vulnerability leading to crash, code execution, or auth bypass. It does not expose the host to memory corruption or allow an attacker to bypass bounds checks.\n\nCRUX: The 'stackless' path is restricted to in-memory writers (bytes.Buffer, byteSliceWriter, ByteBuffer) which do not produce I/O errors under normal operation, making the 'silent failure' scenario non-exploitable for security harm.\nGREP: func WriteBrotliLevel",
    "elapsed": 12.6,
    "tokens": 4163,
    "tokens_in": 3628,
    "tokens_out": 535,
    "cost_usd": 0.0031287600000000004,
    "file": "brotli.go",
    "round": 1,
    "grep_used": true,
    "grep_results": "GREP `func WriteBrotliLevel`:\n```\nbrotli.go:114:func WriteBrotliLevel(w io.Writer, p []byte, level int) (int, error) {\n```"
  },
  {
    "finding_title": "Write errors silently swallowed in stackless compression path",
    "verdict": "INVALID",
    "reasoning": "The report claims that `WriteBrotliLevel` silently swallows write errors when the destination is an in-memory buffer (`byteSliceWriter`, `bytes.Buffer`, or `bytebufferpool.ByteBuffer`). The code confirms this: in the `stackless` branch, `stacklessWriteBrotli(ctx)` is called without checking its result, and the function unconditionally returns `(len(p), nil)`. Internally, `nonblockingWriteBrotli` also ignores the error from `zw.Write(ctx.p)`. \n\nHowever, under the provided threat model, this is not a security vulnerability. \n1. **Nature of the Destination**: The type switch restricts the error-swallowing path to specific in-memory types. `bytes.Buffer` and `bytebufferpool.ByteBuffer` are standard Go types that do not return errors from `Write` unless the process is out of memory (OOM). `byteSliceWriter` is an internal fasthttp type (typically wrapping a `[]byte`) which also cannot fail to write in normal operation. \n2. **Attack Feasibility**: An attacker cannot force these specific in-memory writers to fail (e.g., simulate a network drop or disk full) while the process remains stable. If the writer fails due to OOM, the host process is likely crashing or severely degraded anyway, and the 'silent corruption' is a symptom of the OOM, not a distinct vulnerability introduced by this code. \n3. **Alternative Paths**: If a caller passes a custom `io.Writer` that *can* fail (e.g., a network socket), it falls into the `default` case, where `acquireStacklessBrotliWriter` is used and errors are properly propagated. \n\nThus, the 'silent data corruption' is not reachable by an external attacker in a way that causes meaningful security harm (code execution, auth bypass, etc.) distinct from standard resource exhaustion. It is a code quality issue or API design flaw, not a security vulnerability.\n\nCRUX: The in-memory writers used in the error-swallowing path (bytes.Buffer, byteSliceWriter, ByteBuffer) cannot produce I/O errors under attacker-controlled conditions without causing system-level OOM, rendering the 'silent corruption' non-exploitable for security harm.\nGREP: func WriteBrotliLevel",
    "elapsed": 102.7,
    "tokens": 4827,
    "tokens_in": 4333,
    "tokens_out": 494,
    "cost_usd": 0.00330186,
    "file": "brotli.go",
    "round": 2
  },
  {
    "finding_title": "Write errors silently swallowed in stackless compression path",
    "verdict": "INVALID",
    "reasoning": "The report identifies a code path in `WriteBrotliLevel` where errors are ignored. Specifically, when the destination writer `w` is a `*byteSliceWriter`, `*bytes.Buffer`, or `*bytebufferpool.ByteBuffer`, the code calls `stacklessWriteBrotli` and returns `len(p), nil` regardless of whether the write succeeded. Internally, `nonblockingWriteBrotli` calls `zw.Write` but ignores the error. \n\nHowever, analyzing the security impact under the provided threat model:\n1. **Writer Types**: The types that trigger the error-swallowing path are all in-memory buffer implementations. In Go, `bytes.Buffer.Write` and `bytebufferpool.ByteBuffer.Write` do not return errors under normal operation; they allocate memory internally. `byteSliceWriter` is an internal fasthttp type wrapping a byte slice, which also does not fail writes (it appends to a slice). \n2. **Failure Conditions**: These writers only fail if the process runs out of memory (OOM). If an OOM occurs, the host process is in a critical state (likely crashing or subject to the runtime's panic handling). The 'silent' nature of the error does not create a new security vulnerability distinct from OOM itself, nor does it allow an attacker to bypass bounds checks or cause memory corruption that could be exploited. \n3. **Attacker Control**: An attacker can control the input data `p` (causing OOM via compression bomb scenarios, which is a resource exhaustion issue), but they cannot control the `io.Writer` implementation to be a failing type while remaining in the 'stackless' branch. If a caller passes a custom `io.Writer` that can fail (like a network socket), it falls into the `default` case, where errors are properly propagated. \n4. **Security Relevance**: The issue is a lack of error handling for an in-memory write that effectively cannot fail without catastrophic system failure. This is a code quality or API design issue, not a security vulnerability allowing an external attacker to cause meaningful harm like code execution, auth bypass, or distinct memory corruption. The 'silent data corruption' is not exploitable because the precondition for the error (writer failure) is not attacker-reachable in a way that leaves the process stable enough to be exploited. \n\nTherefore, the bug pattern exists, but it is not a security vulnerability under the given threat model.\n\nCRUX: The error-swallowing path is restricted to in-memory writers (bytes.Buffer, byteSliceWriter, ByteBuffer) which cannot return I/O errors without causing system-level OOM, making the 'silent failure' non-exploitable for security harm.\nGREP: func WriteBrotliLevel",
    "elapsed": 19.5,
    "tokens": 5341,
    "tokens_in": 4758,
    "tokens_out": 583,
    "cost_usd": 0.00374736,
    "file": "brotli.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `func WriteBrotliLevel`:\n```\nbrotli.go:114:func WriteBrotliLevel(w io.Writer, p []byte, level int) (int, error) {\n```"
  },
  {
    "finding_title": "Write errors silently swallowed in stackless compression path",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that write errors are silently swallowed in the 'stackless' compression path of `WriteBrotliLevel`, leading to silent data corruption. I verified the code: `WriteBrotliLevel` uses a type switch to route in-memory writers (`*byteSliceWriter`, `*bytes.Buffer`, `*bytebufferpool.ByteBuffer`) to a stackless path where `stacklessWriteBrotli` is called and its result is ignored, returning `len(p), nil` unconditionally. \n\nHowever, this is not a security vulnerability under the provided threat model. The specific writers that trigger this code path are standard in-memory buffers. In Go, `bytes.Buffer.Write` and similar in-memory implementations do not return errors for I/O failures (like network drops or disk full); they only fail if memory allocation fails (OOM). An attacker cannot craft input that causes these specific in-memory buffers to return an I/O error while the process remains stable and exploitable. If OOM occurs, the process is crashing or severely degraded, and the 'silent error' is a symptom of resource exhaustion, not a distinct exploitable flaw allowing code execution, auth bypass, or memory corruption distinct from the OOM itself. \n\nFurthermore, if a caller uses a writer that *can* fail (e.g., a network socket or file), it falls into the `default` case of the type switch, which correctly propagates errors. Thus, the error-swallowing behavior is restricted to contexts where errors are effectively impossible without system failure. This is a code quality or API design issue (lack of robust error handling), not a security vulnerability exploitable by an external attacker to cause meaningful harm as defined by the threat model.\n\nCRUX: The writers triggering the error-swallowing path (bytes.Buffer, byteSliceWriter, ByteBuffer) are in-memory types that cannot return I/O errors under attacker-controlled conditions without causing system-level OOM, rendering the 'silent corruption' non-exploitable for distinct security harm.\nGREP: func WriteBrotliLevel",
    "elapsed": 103.8,
    "tokens": 5817,
    "tokens_in": 5380,
    "tokens_out": 437,
    "cost_usd": 0.0035706,
    "file": "brotli.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `func WriteBrotliLevel`:\n```\nbrotli.go:114:func WriteBrotliLevel(w io.Writer, p []byte, level int) (int, error) {\n```"
  },
  {
    "finding_title": "Write errors silently swallowed in stackless compression path",
    "verdict": "INVALID",
    "reasoning": "The report claims that `WriteBrotliLevel` silently swallows write errors for specific in-memory writers (`*byteSliceWriter`, `*bytes.Buffer`, `*bytebufferpool.ByteBuffer`). I verified the code: in the `stackless` branch, `stacklessWriteBrotli` is called, and the function unconditionally returns `len(p), nil` without checking for errors. Internally, `nonblockingWriteBrotli` calls `zw.Write` and ignores its error. \n\nHowever, under the provided threat model, this is not a security vulnerability. The types restricted to this path are standard in-memory buffers. In Go, these implementations do not return I/O errors (like network failures or disk full) during a `Write` operation; they only fail if memory allocation fails (OOM). An external attacker cannot control the destination `io.Writer` to be a failing type while remaining in this specific code path, nor can they force these in-memory buffers to return an I/O error while the process remains stable. If an OOM occurs, the process is crashing or subject to runtime panic, which is a system-level failure, not a distinct logic flaw exploitable for code execution or auth bypass. Cases where the writer *can* fail (e.g., network sockets) fall into the `default` branch, which correctly propagates errors. Thus, the issue is a code quality or API design flaw, not a security vulnerability reachable by an attacker to cause meaningful harm.\n\nCRUX: The writers triggering the error-swallowing path are in-memory types that cannot return I/O errors under attacker-controlled conditions without causing system-level OOM.\nGREP: func WriteBrotliLevel",
    "elapsed": 183.8,
    "tokens": 6209,
    "tokens_in": 5846,
    "tokens_out": 363,
    "cost_usd": 0.0035443199999999992,
    "file": "brotli.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `func WriteBrotliLevel`:\n```\nbrotli.go:114:func WriteBrotliLevel(w io.Writer, p []byte, level int) (int, error) {\n```"
  }
]
Raw scan report for this file

## Security analysis of `brotli.go`

This file exposes public brotli compression/decompression helpers for `fasthttp`. The trust boundary is the public API:

- `AppendBrotliBytesLevel(dst, src, level)`
- `WriteBrotliLevel(w, p, level)`
- `WriteBrotli(w, p)`
- `AppendBrotliBytes(dst, src)`
- `WriteUnbrotli(w, p)`
- `AppendUnbrotliBytes(dst, src)`

The main risks are:

1. **Unbounded decompression / decompression bomb**
   - `WriteUnbrotli` calls the internal `writeUnbrotli(w, p, 0)`.
   - `writeUnbrotli` passes `maxBodySize = 0` to `copyZeroAllocWithLimit(w, zr, maxBodySize)`.
   - In `fasthttp`, `0` normally means “no limit”.
   - Therefore, a small malicious brotli payload can expand to a very large amount of output.
   - `AppendUnbrotliBytes` makes this worse because it wraps `dst` in `byteSliceWriter`, so the decompressed output is appended to `dst` with no external cap.
   - The later check:
     ```go
     nn := int(n)
     if int64(nn) != n {
         return 0, fmt.Errorf("too much data unbrotlied: %d", n)
     }
     ```
     only helps on 32-bit platforms and only after the copy has already happened. On 64-bit platforms it does not impose a limit.
   - Impact: remote denial of service through memory exhaustion when an untrusted brotli payload is decompressed.

2. **Unvalidated compression level passed to the brotli writer**
   - Public functions accept `level int` directly.
   - `normalizeBrotliCompressLevel` clamps the value only for use as a pool index:
     ```go
     if level < 0 || level > 11 {
         level = CompressBrotliDefaultCompression
     }
     ```
   - However, `acquireRealBrotliWriter` passes the **original** `level` to `brotli.NewWriterLevel(w, level)`.
   - If the dependency assumes brotli levels are `[0..11]`, out-of-range values such as `-1` or `12` can cause a panic, for example by indexing a level table outside bounds.
   - Even if the dependency clamps the level internally, the pool key is normalized while the actual writer state is not. Writers created with out-of-range levels can be stored under the default pool bucket and later reused for a legitimate level such as `4`.
   - This can lead to:
     - process crash / panic,
     - incorrect compression behavior,
     - cross-contamination of pooled writers.

3. **Ignored stackless deferred-execution return value can cause a data race**
   - For non-blocking writer types:
     ```go
     case *byteSliceWriter,
         *bytes.Buffer,
         *bytebufferpool.ByteBuffer:
     ```
     `WriteBrotliLevel` uses `stacklessWriteBrotli(ctx)`.
   - `stacklessWriteBrotli` does this:
     ```go
     stacklessWriteBrotliFunc(ctx)
     ```
     and ignores the returned `bool`.
   - In `fasthttp/stackless`, that boolean generally indicates whether the function ran synchronously in the current goroutine or whether execution was deferred to another goroutine.
   - If the call is deferred, `WriteBrotliLevel` returns before the compressed data has actually been written.
   - For `AppendBrotliBytesLevel`, that means `dst` can be returned to the caller before the brotli writer has appended the compressed bytes.
   - This creates a data race on:
     - `dst` / `byteSliceWriter.b`,
     - possibly `src` / `p`,
     - internal writer state.
   - It can also cause:
     - incomplete compressed output,
     - corrupted compressed output,
     - lost write errors,
     - incorrect returned byte count.
   - This is especially important because `AppendBrotliBytesLevel` is a public API that returns the final buffer.

4. **Incorrect returned byte count for non-blocking writers**
   - For the non-blocking writer cases, `WriteBrotliLevel` returns:
     ```go
     return len(p), nil
     ```
   - The documentation says the function returns “the number of compressed bytes written to `w`”.
   - `len(p)` is the **uncompressed input length**, not the compressed output length.
   - If a caller uses this return value to set `Content-Length`, manage response buffers, or perform protocol framing, it may produce incorrect lengths or protocol desynchronization.
   - This is lower severity than the previous issues, but it is still a public API correctness problem with security relevance.

## Areas reviewed and deprioritized

- **Pool type assertions** such as `v.(*brotli.Reader)`, `v.(*brotli.Writer)`, and `v.(stackless.Writer)` are internal. The pools are only populated by internal release functions, so they are not directly attacker-controllable.
- **Nil `p` or nil `dst`** are mostly safe:
  - `p == nil` compresses/decompresses empty input.
  - `dst == nil` works with `byteSliceWriter` because `append` can start from `nil`.
- **Nil `w`** can panic, but that is primarily a caller-error issue rather than a typical untrusted-data issue.
- **Ignored compression errors** in `nonblockingWriteBrotli` are real, but for the specific non-blocking writer types used here the practical impact is mostly correctness/data-race related rather than a direct memory-safety bug.
- There are **no fixed-size stack buffers** in this file, so there is no classic C-style buffer overflow.

```json
[
  {
    "severity": "high",
    "title": "Unbounded brotli decompression can exhaust memory",
    "function": "WriteUnbrotli",
    "description": "WriteUnbrotli calls writeUnbrotli with maxBodySize=0, and writeUnbrotli passes that value to copyZeroAllocWithLimit. In fasthttp, 0 normally disables the limit, so an attacker-controlled brotli payload can decompress to arbitrarily large output. AppendUnbrotliBytes makes this exploitable as a decompression bomb because it appends the decompressed data to dst without any cap. The int overflow check in writeUnbrotli runs after the copy has already written data and is only effective on 32-bit platforms, so it does not prevent denial of service via memory exhaustion."
  },
  {
    "severity": "medium",
    "title": "Out-of-range compression level is passed to brotli.NewWriterLevel and pooled under a normalized key",
    "function": "acquireRealBrotliWriter",
    "description": "WriteBrotliLevel and AppendBrotliBytesLevel accept an arbitrary int level. normalizeBrotliCompressLevel only bounds the value for pool indexing, but acquireRealBrotliWriter passes the original level to brotli.NewWriterLevel. If the brotli dependency expects levels in [0..11], values such as -1 or 12 can cause a panic in the host process. Even if the dependency clamps the level, writers created with out-of-range levels are stored under the normalized default pool bucket, and Reset does not change their configured level. Later legitimate requests for the default level can reuse a writer configured for a different level, causing incorrect compression behavior or further undefined behavior."
  },
  {
    "severity": "high",
    "title": "Ignored stackless deferred-execution return value can cause a data race in brotli compression",
    "function": "stacklessWriteBrotli",
    "description": "stacklessWriteBrotli ignores the bool returned by stacklessWriteBrotliFunc. In the fasthttp stackless runtime, this return value indicates whether nonblockingWriteBrotli executed synchronously in the current goroutine or was deferred to another goroutine. If execution is deferred, WriteBrotliLevel returns len(p), nil and AppendBrotliBytesLevel returns dst before the compressed bytes have been written. This creates a data race on dst/src and internal writer state, can return incomplete or corrupted compressed output, and hides write errors. The race is reachable when compression is invoked with a stack near the limit or from a deep call stack."
  },
  {
    "severity": "low",
    "title": "WriteBrotliLevel returns uncompressed input length instead of compressed byte count for non-blocking writers",
    "function": "WriteBrotliLevel",
    "description": "For *byteSliceWriter, *bytes.Buffer, and *bytebufferpool.ByteBuffer, WriteBrotliLevel returns len(p), nil. That value is the uncompressed input length, not the number of compressed bytes written to the writer, contrary to the documented contract. Callers that use the returned value to set Content-Length, manage response buffers, or perform protocol framing may produce incorrect lengths or misuse memory, which can lead to protocol desynchronization or resource handling bugs."
  }
]
```