BudgetScan

EOF detection in prefetched Content-Length read uses per-call n instead of totalBytesRead

high swesweep-fasthttp · streaming.go · requestStream.Read · run #4

Description

After reading from rs.prefetchedBytes, the code returns io.EOF when n == rs.header.ContentLength(). n is only the number of bytes returned by the current Read call. The correct completion test is rs.totalBytesRead == rs.header.ContentLength(). With multi-call reads or an over-sized prefetched buffer, this can signal EOF prematurely or fail to signal EOF correctly, allowing truncated bodies or incorrect connection reuse.

Triage

Verdict
INVALID confidence 0.0
Model
qwen/qwen3.8-27b
Rounds
5
Cost
$0.0284 · 559.8 s
The reported vulnerability claims that the check `if n == rs.header.ContentLength()` in the `prefetchedBytes` reading path causes incorrect EOF signaling. To determine if this is a real security issue, we must analyze if this condition can ever be true in a way that violates the HTTP protocol or leads to desync.

1. **Context of the check**: The check occurs after reading `n` bytes from `rs.prefetchedBytes`. Before this read, `p` is truncated to `left = prefetchedSize - rs.totalBytesRead`. Thus, `n <= prefetchedSize - rs.totalBytesRead`.
2. **Condition for Premature EOF**: For `n == rs.header.ContentLength()` to be true, we must have `prefetchedSize - rs.totalBytesRead >= ContentLength`. Since `rs.totalBytesRead` is cumulative and starts at 0, if `totalBytesRead` is 0, this requires `prefetchedSize >= ContentLength`. In a well-formed HTTP request, the server parses headers and buffers the body start into `prefetchedBytes`. It is highly unlikely, but possible in edge cases (e.g., pipelined requests or specific buffering implementations), that `prefetchedSize` exceeds `ContentLength`. However, even if `prefetchedSize == ContentLength`, and we read it all in one go (`n == ContentLength`), then `rs.totalBytesRead` becomes `ContentLength`. Returning `io.EOF` here is **correct** because the entire declared body has been consumed. It is not premature.
3. **Condition for Missed EOF**: If `n < ContentLength`, the code returns `n, nil` (assuming `bytes.Reader.Read` didn't return EOF, which it won't if we truncated `p` to exactly the remaining bytes available). On the *next* call, the first check `if rs.totalBytesRead == rs.header.ContentLength()` (line 57) will catch the completion if `totalBytesRead` reached `ContentLength` in the previous step. If `totalBytesRead` is still less, it will proceed to read more. Thus, EOF is never permanently missed.
4. **Security Impact**: The HTTP connection reuse and body parsing rely on the `io.Reader` interface returning the correct number of bytes and eventually `io.EOF`. Since the logic ensures that `io.EOF` is returned exactly when the `ContentLength` bytes have been read (either in the current call if it was the last chunk, or in the subsequent call), the state machine remains consistent. There is no desync, truncation, or infinite loop. The use of `n` instead of `totalBytesRead` is a stylistic choice that relies on the invariant that `n` can only equal `ContentLength` if it was the first and only read (or a specific edge case where it aligns), but even if it were slightly incorrect, it doesn't lead to memory corruption or bypass. More importantly, as analyzed, it is functionally correct for the standard case.

All prior reviewers correctly identified that the bug pattern does not lead to a security vulnerability. The code adheres to `io.Reader` semantics where EOF can be deferred to the next call. The specific line in question is a safe optimization. Therefore, the finding is a false positive.

CRUX: The check `if n == rs.header.ContentLength()` only results in returning `io.EOF` when the total bytes read (`rs.totalBytesRead`) equals `ContentLength` (in the feasible case where `totalBytesRead` was 0 before the read), which is the correct condition for EOF. Any other case is handled by the cumulative check at the start of the next `Read` call.
GREP: if rs.totalBytesRead == rs.header.ContentLength()
All rounds
[
  {
    "finding_title": "EOF detection in prefetched Content-Length read uses per-call n instead of totalBytesRead",
    "verdict": "INVALID",
    "reasoning": "The reported bug concerns the EOF logic in `requestStream.Read` for fixed `Content-Length` bodies. Specifically, after reading from `rs.prefetchedBytes`, the code checks `if n == rs.header.ContentLength()` to return `io.EOF`. The report argues this is incorrect because `n` is only the bytes read in the current call, while the correct check should be against `rs.totalBytesRead`. \n\nAnalysis:\n1. **Code Flow**: \n   - `rs.totalBytesRead` is initialized to 0 in `acquireRequestStream` (via zero-value struct from pool) and incremented after every read.\n   - The first check in `Read` is `if rs.totalBytesRead == rs.header.ContentLength() { return 0, io.EOF }`. This handles the case where we have already read everything in a previous call.\n   - If we enter the `prefetchedBytes` block, it implies `rs.totalBytesRead < rs.header.ContentLength()` and `prefetchedSize > rs.totalBytesRead`.\n   - Inside the `prefetchedBytes` block: `left := prefetchedSize - rs.totalBytesRead`. If `len(p) > left`, `p` is truncated to `left`. Then `n, err := rs.prefetchedBytes.Read(p)`.\n   - `rs.totalBytesRead += n`.\n   - The check `if n == rs.header.ContentLength()` is performed.\n\n2. **Scenario for Premature EOF**: \n   - Let `ContentLength = 100`.\n   - Let `prefetchedSize = 100` (all body prefetched).\n   - **Call 1**: `rs.totalBytesRead = 0`. `left = 100 - 0 = 100`. Caller passes `len(p) = 50`. `p` becomes `p[:50]`. `n = 50`. `rs.totalBytesRead = 50`. Check `n (50) == ContentLength (100)`? No. Return `50, nil`.\n   - **Call 2**: `rs.totalBytesRead = 50`. First check `50 == 100`? No. `prefetchedSize = 100 > 50`. `left = 100 - 50 = 50`. Caller passes `len(p) = 50`. `n = 50`. `rs.totalBytesRead = 100`. Check `n (50) == ContentLength (100)`? No. Return `50, nil`.\n   - **Call 3**: `rs.totalBytesRead = 100`. First check `100 == 100`? Yes. Return `0, io.EOF`.\n   - In this scenario, EOF is correctly signaled on Call 3. The buggy check `n == ContentLength` was false, but the *correct* check `totalBytesRead == ContentLength` (implicitly handled by the next call's first check, or if it was in the `rs.reader` path) would have been true. However, notice that in Call 2, the code returns `50, nil`. It does *not* return `io.EOF` even though `totalBytesRead` has now reached `ContentLength`. The caller must call `Read` again to get `io.EOF`. This is standard `io.Reader` behavior (EOF is not always returned on the last chunk of data, but on the subsequent call). \n\n3. **Scenario for Buggy Check Triggering Prematurely**: \n   - For `n == rs.header.ContentLength()` to be true, `n` (bytes read in this call) must equal the *total* content length. This can only happen if `rs.totalBytesRead` was 0 before this call (since `totalBytesRead` is non-negative and only increases). If `rs.totalBytesRead` was > 0, then `n` (which is bounded by `left = prefetchedSize - totalBytesRead`) must be less than `prefetchedSize`. Since `prefetchedSize <= ContentLength` (usually, as it's just the buffered part), `n` cannot exceed `prefetchedSize`. If `totalBytesRead > 0`, `n < prefetchedSize <= ContentLength`, so `n == ContentLength` is impossible. \n   - Therefore, the check `n == rs.header.ContentLength()` is **only** true if `rs.totalBytesRead` was 0 and we read the entire body in one go from prefetched bytes. In that case, `n == ContentLength` AND `totalBytesRead == ContentLength`. The behavior is correct (return data + EOF). \n\n4. **Scenario for Failing to Signal EOF**: \n   - If `n < ContentLength`, the code returns `n, nil`. The caller calls again. The first check `if rs.totalBytesRead == rs.header.ContentLength()` catches the completion. Thus, EOF is never missed; it is just deferred to the next call. This is valid `io.Reader` semantics. \n\n5. **Conclusion**: The logic is technically suboptimal (mixing `n` and `totalBytesRead` checks) but **not incorrect** in a way that causes premature EOF or missed EOF that leads to security issues. \n   - If `n == ContentLength` is true, it implies `totalBytesRead == ContentLength` (since start was 0). Returning EOF is correct. \n   - If `n < ContentLength`, it implies `totalBytesRead < ContentLength` (or ==, if previous reads happened). If `totalBytesRead == ContentLength`, the *next* call will return EOF. \n   - There is no case where `n == ContentLength` is true but `totalBytesRead < ContentLength` (impossible as `totalBytesRead` only grows). \n   - There is no case where `totalBytesRead == ContentLength` but the code fails to eventually return EOF (the next call's first check handles it). \n   - The bug pattern described (using `n` instead of `totalBytesRead` leading to premature EOF) does not exist because `n == ContentLength` is a subset of `totalBytesRead == ContentLength` in this specific code path context (where `prefetchedBytes` is the source). Even if it were a bug, it would result in returning `io.EOF` alongside data, which is valid. It cannot result in *truncated* bodies or *incorrect* connection reuse in a security sense (e.g., desync). The `io.EOF` signal is robustly provided on the subsequent call if not on the current one. \n\n   Wait, let's look closer at `releaseRequestStream`. It resets `totalBytesRead` to 0. The pool is used. If a stream is reused, it starts fresh. The logic holds. \n\n   Is there a case where `n == ContentLength` is true, but we *shouldn't* return EOF? No, if we read `ContentLength` bytes, we are done. \n   Is there a case where we *should* return EOF (i.e., `totalBytesRead == ContentLength`) but `n != ContentLength` and `err != io.EOF`? Yes, the case where we read part of it. But we return `nil`. The caller reads again. The next call returns EOF. This is standard. \n\n   The report claims this allows \"truncated bodies or incorrect connection reuse\". Since the EOF is correctly signaled on the next call, the `io.Copy` or similar consumers will terminate correctly. There is no data corruption or desync. \n\n   Therefore, the bug pattern is effectively a false positive regarding security impact. The code is correct in its observable behavior (EOF is eventually returned). The specific check `n == ContentLength` is redundant or slightly imprecise style, but it does not create a security vulnerability because it never triggers when it shouldn't (prematurely) and the fallback (next call check) handles the rest. \n\n   Actually, let's re-read the code carefully. \n   `if n == rs.header.ContentLength() { return n, io.EOF }`. \n   If `ContentLength` is 10. \n   Call 1: `prefetched` has 10 bytes. `total=0`. `left=10`. `p` size 10. `n=10`. `total=10`. `10==10` -> return `10, EOF`. Correct. \n   Call 1: `prefetched` has 10 bytes. `total=0`. `left=10`. `p` size 5. `p=p[:5]`. `n=5`. `total=5`. `5==10`? No. return `5, nil`. Correct. \n   Call 2: `total=5`. `5==10`? No. `prefetched` has 10. `5<10`. `left=5`. `p` size 5. `n=5`. `total=10`. `5==10`? No. return `5, nil`. \n   Call 3: `total=10`. `10==10`? Yes. return `0, EOF`. Correct. \n\n   The behavior is correct. The scanner is flagging a style issue or a misunderstanding of `io.Reader` semantics (where EOF can be deferred). It is not a security vulnerability.\n\nCRUX: Whether the check `n == rs.header.ContentLength()` causes incorrect EOF signaling that leads to security issues like desync or truncation.\nGREP: func (rs \\*requestStream) Read",
    "elapsed": 116.6,
    "tokens": 4714,
    "tokens_in": 2595,
    "tokens_out": 2119,
    "cost_usd": 0.007446899999999999,
    "file": "streaming.go",
    "round": 1,
    "grep_used": true,
    "grep_results": "GREP `func (rs *requestStream) Read (simplified to: requestStream)`:\n```\nstreaming.go:17:type requestStream struct {\nstreaming.go:25:func (rs *requestStream) Read(p []byte) (int, error) {\nstreaming.go:89:func acquireRequestStream(b *bytebufferpool.ByteBuffer, r *bufio.Reader, h headerInterface) *requestStream {\nstreaming.go:90:\trs := requestStreamPool.Get().(*requestStream)\nstreaming.go:97:func releaseRequestStream(rs *requestStream) {\nstreaming.go:103:\trequestStreamPool.Put(rs)\nstreaming.go:106:var requestStreamPool = sync.Pool{\nstreaming.go:108:\t\treturn &requestStream{}\nclient.go:3172:\t\t\tif r, ok := rbs.(*requestStream); ok {\nserver.go:2656:\t\t\tif rs, ok := ctx.Request.bodyStream.(*requestStream); ok {\nhttp.go:1020:\t// This code assumes that if a requestStream was swapped the headers are also swapped or copied.\nhttp.go:1021:\tif rs, ok := a.bodyStream.(*requestStream); ok {\nhttp.go:1024:\tif rs, ok := b.bodyStream.(*requestStream); ok {\nhttp.go:2252:\tif rs, ok := req.bodyStream.(*requestStream); ok {\nhttp.go:2270:\tif bsr, ok := resp.bodyStream.(*requestStream); ok {\nhttp.go:2618:\t\t// handled in requestStream.Read()\n```"
  },
  {
    "finding_title": "EOF detection in prefetched Content-Length read uses per-call n instead of totalBytesRead",
    "verdict": "INVALID",
    "reasoning": "The report claims that the EOF detection logic in `requestStream.Read` for fixed `Content-Length` bodies is flawed because it checks `n == rs.header.ContentLength()` instead of `rs.totalBytesRead == rs.header.ContentLength()` after reading from `rs.prefetchedBytes`. I analyzed the code flow and the specific conditions under which this check is reached. \n\n1. **Reachability of the buggy check**: The check is inside the block `if prefetchedSize > rs.totalBytesRead`. This implies that we are reading from the prefetched buffer. The number of bytes read, `n`, is bounded by `left = prefetchedSize - rs.totalBytesRead`. Thus, `n <= prefetchedSize - rs.totalBytesRead`. \n\n2. **Condition for `n == ContentLength`**: For the check `n == rs.header.ContentLength()` to be true, we must have `prefetchedSize - rs.totalBytesRead >= ContentLength`. \n\n3. **Invariant of `prefetchedSize`**: `prefetchedSize` is the size of the bytes already read into the buffer. In a well-formed HTTP request with `Content-Length: L`, the server/client does not typically prefetch more than `L` bytes. Even if it did (e.g., if the buffer contained extra data from a previous keep-alive request, which is handled by connection state management, not this struct), the `requestStream` is created per request. The `prefetchedBytes` comes from `b.B` which contains the parsed body start. It is highly unlikely `prefetchedSize > ContentLength`. Assuming `prefetchedSize <= ContentLength`:\n   - If `rs.totalBytesRead == 0`, then `n <= prefetchedSize`. For `n == ContentLength`, we need `prefetchedSize == ContentLength` and `n == ContentLength` (read all). This is correct: if we read the whole body in one go, we should return EOF.\n   - If `rs.totalBytesRead > 0`, then `n <= prefetchedSize - rs.totalBytesRead < ContentLength` (assuming `prefetchedSize <= ContentLength`). Thus, `n == ContentLength` is impossible. \n\n4. **Consequence of the check**: \n   - If the condition is true (only possible when `totalBytesRead` was 0 and we read the full body), returning `io.EOF` is correct. \n   - If the condition is false, we return `n, err` (where `err` is likely nil from `bytes.Reader.Read` unless the buffer is exhausted, but here we just read some bytes). The next call will hit the check `if rs.totalBytesRead == rs.header.ContentLength()` at the top of `Read`, which will return `0, io.EOF`. \n\n5. **Security Impact**: The report suggests \"truncated bodies or incorrect connection reuse\". Since the correct EOF is always signaled on the *next* call if not on the current one, the body is never truncated from the consumer's perspective (it will keep reading until EOF). The connection reuse is managed by the HTTP client/server state machine, which relies on the correct EOF from the body reader. Since EOF is eventually returned correctly, the state machine behaves correctly. \n\n6. **Conclusion**: The code is functionally correct for its intended purpose, adhering to `io.Reader` semantics where EOF can be returned with data or on the subsequent call. The specific check `n == ContentLength` is a valid optimization/shortcut for the common case where the body fits in the prefetched buffer and is read in one call. It does not introduce a security vulnerability because it does not lead to premature EOF (impossible given the bounds) nor missed EOF (handled by next call).\n\nCRUX: Whether the check `n == rs.header.ContentLength()` can cause premature EOF or missed EOF that leads to security issues like desync or truncation.\nGREP: func (rs \\*requestStream) Read",
    "elapsed": 146.7,
    "tokens": 6024,
    "tokens_in": 5171,
    "tokens_out": 853,
    "cost_usd": 0.00473082,
    "file": "streaming.go",
    "round": 2,
    "grep_used": true,
    "grep_results": "GREP `func (rs *requestStream) Read (simplified to: requestStream)`:\n```\nstreaming.go:17:type requestStream struct {\nstreaming.go:25:func (rs *requestStream) Read(p []byte) (int, error) {\nstreaming.go:89:func acquireRequestStream(b *bytebufferpool.ByteBuffer, r *bufio.Reader, h headerInterface) *requestStream {\nstreaming.go:90:\trs := requestStreamPool.Get().(*requestStream)\nstreaming.go:97:func releaseRequestStream(rs *requestStream) {\nstreaming.go:103:\trequestStreamPool.Put(rs)\nstreaming.go:106:var requestStreamPool = sync.Pool{\nstreaming.go:108:\t\treturn &requestStream{}\nserver.go:2656:\t\t\tif rs, ok := ctx.Request.bodyStream.(*requestStream); ok {\nhttp.go:1020:\t// This code assumes that if a requestStream was swapped the headers are also swapped or copied.\nhttp.go:1021:\tif rs, ok := a.bodyStream.(*requestStream); ok {\nhttp.go:1024:\tif rs, ok := b.bodyStream.(*requestStream); ok {\nhttp.go:2252:\tif rs, ok := req.bodyStream.(*requestStream); ok {\nhttp.go:2270:\tif bsr, ok := resp.bodyStream.(*requestStream); ok {\nhttp.go:2618:\t\t// handled in requestStream.Read()\nclient.go:3172:\t\t\tif r, ok := rbs.(*requestStream); ok {\n```"
  },
  {
    "finding_title": "EOF detection in prefetched Content-Length read uses per-call n instead of totalBytesRead",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that using `n` (bytes read in current call) instead of `rs.totalBytesRead` (cumulative bytes read) for the EOF check in the `prefetchedBytes` branch leads to premature or missed EOF signals, causing security issues like connection desync. \n\nAnalysis of the code logic:\n1. **Bounds Analysis**: Inside the `prefetchedBytes` block, `n` is bounded by `left = prefetchedSize - rs.totalBytesRead`. Specifically, `n <= prefetchedSize - rs.totalBytesRead`.\n2. **Condition for Buggy Check**: The check is `if n == rs.header.ContentLength()`. For this to be true, we must have `prefetchedSize - rs.totalBytesRead >= ContentLength`.\n3. **Invariant**: `prefetchedSize` is the number of bytes available in the prefetched buffer. In a standard HTTP request with `Content-Length: L`, the server reads the body into the buffer. It is structurally unlikely for `prefetchedSize` to exceed `ContentLength` significantly, but even if it did, the check `n == ContentLength` implies that we have just read `ContentLength` bytes in this specific call.\n4. **Correctness of Premature EOF**: If `n == ContentLength`, then `rs.totalBytesRead` (after incrementing) is at least `ContentLength` (since it was non-negative before). If `rs.totalBytesRead` becomes exactly `ContentLength` (or more), returning `io.EOF` is correct because we have consumed the declared body length. There is no case where `n == ContentLength` is true but we have NOT read enough bytes (since `n` is the number of bytes just read, and if it equals the total length, we are done). \n5. **Correctness of Missed EOF**: If `n < ContentLength`, the code returns `n, nil`. The next call to `Read` will enter the function, hit the first check `if rs.totalBytesRead == rs.header.ContentLength()`, and return `0, io.EOF` if appropriate. If `rs.totalBytesRead < ContentLength`, it will proceed to read more. Thus, EOF is never permanently missed; it is just deferred to the next call if the previous call didn't hit the `n == ContentLength` shortcut. This is valid `io.Reader` semantics (EOF may be returned on the call after the last data chunk).\n6. **Security Impact**: Since the EOF is correctly signaled either immediately (if `n == ContentLength`) or on the next call (via the `totalBytesRead` check at the top), the HTTP connection state machine receives the correct signal to close the body stream. There is no desync or truncation. The `n == ContentLength` check is a safe optimization for the case where the entire body fits in the prefetched buffer and is read in one go. It does not introduce a security vulnerability.\n\nBoth prior reviewers correctly identified that the logic is functionally correct. The scanner finding is a false positive based on a misunderstanding of `io.Reader` semantics or a static analysis pattern match that ignores the cumulative state check at the top of the function.\n\nCRUX: Whether the `n == ContentLength` check can trigger prematurely (when `totalBytesRead < ContentLength`) or fail to trigger when it should, causing desync.\nGREP: func (rs \\*requestStream) Read",
    "elapsed": 93.2,
    "tokens": 6825,
    "tokens_in": 6087,
    "tokens_out": 738,
    "cost_usd": 0.00477054,
    "file": "streaming.go",
    "round": 3,
    "grep_used": true,
    "grep_results": "GREP `func (rs *requestStream) Read (simplified to: requestStream)`:\n```\nstreaming.go:17:type requestStream struct {\nstreaming.go:25:func (rs *requestStream) Read(p []byte) (int, error) {\nstreaming.go:89:func acquireRequestStream(b *bytebufferpool.ByteBuffer, r *bufio.Reader, h headerInterface) *requestStream {\nstreaming.go:90:\trs := requestStreamPool.Get().(*requestStream)\nstreaming.go:97:func releaseRequestStream(rs *requestStream) {\nstreaming.go:103:\trequestStreamPool.Put(rs)\nstreaming.go:106:var requestStreamPool = sync.Pool{\nstreaming.go:108:\t\treturn &requestStream{}\nclient.go:3172:\t\t\tif r, ok := rbs.(*requestStream); ok {\nhttp.go:1020:\t// This code assumes that if a requestStream was swapped the headers are also swapped or copied.\nhttp.go:1021:\tif rs, ok := a.bodyStream.(*requestStream); ok {\nhttp.go:1024:\tif rs, ok := b.bodyStream.(*requestStream); ok {\nhttp.go:2252:\tif rs, ok := req.bodyStream.(*requestStream); ok {\nhttp.go:2270:\tif bsr, ok := resp.bodyStream.(*requestStream); ok {\nhttp.go:2618:\t\t// handled in requestStream.Read()\nserver.go:2656:\t\t\tif rs, ok := ctx.Request.bodyStream.(*requestStream); ok {\n```"
  },
  {
    "finding_title": "EOF detection in prefetched Content-Length read uses per-call n instead of totalBytesRead",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that using `n` (bytes read in the current call) instead of `rs.totalBytesRead` (cumulative bytes) for the EOF check in the `prefetchedBytes` branch leads to incorrect EOF signaling (premature or missed), causing security issues like connection desync or truncated bodies. \n\nAnalysis of the code logic:\n1. **Context**: The check `if n == rs.header.ContentLength()` occurs after reading from `rs.prefetchedBytes`. Before this, `p` is truncated to `left = prefetchedSize - rs.totalBytesRead`. Thus, `n <= left`.\n2. **Premature EOF**: For `n == ContentLength` to be true, we must have `left >= ContentLength`. This implies `prefetchedSize - rs.totalBytesRead >= ContentLength`. Since `rs.totalBytesRead` starts at 0 and increases, if it were > 0, `left` would be smaller. In the case where it triggers, it means we just read `ContentLength` bytes in this specific call. Since `totalBytesRead` was previously `totalBytesRead` and we added `ContentLength`, `totalBytesRead` becomes `previous + ContentLength`. However, note that if `previous > 0`, then `totalBytesRead` would exceed `ContentLength` or `prefetchedSize` would have to be very large. More importantly, if `n == ContentLength`, we have definitely read at least `ContentLength` bytes in total (if `previous==0`, exactly `ContentLength`; if `previous>0`, more, but `prefetchedSize` is usually bounded by what was buffered). If the code returns `io.EOF` here, it signals that the stream is finished. Is this premature? Only if `totalBytesRead < ContentLength`. But if `n == ContentLength`, then `totalBytesRead` (new value) = `old_total + ContentLength`. If `old_total` was 0, `new_total == ContentLength`. Correct. If `old_total > 0`, `new_total > ContentLength`. This implies we read more bytes than the declared length from the prefetched buffer. This scenario would only happen if `prefetchedSize > ContentLength + old_total`. Even if this happens, returning EOF is correct because we have consumed the declared length (and possibly more, which would indicate a desync upstream, but the stream reader itself correctly indicates the end of the declared body). Crucially, it does *not* indicate EOF *before* the declared length is met. It indicates EOF *when* the declared length is met (in the `old_total==0` case, which is the only feasible case where `n` can equal `ContentLength` given typical buffer sizes).\n3. **Missed EOF**: If `n < ContentLength`, the code returns `n, nil` (or `err`). The next call to `Read` will enter the function. The very first check is `if rs.totalBytesRead == rs.header.ContentLength() { return 0, io.EOF }`. If the previous call brought `totalBytesRead` up to `ContentLength`, this check will catch it and return EOF. If `totalBytesRead < ContentLength`, it will proceed to read more. Thus, EOF is never permanently missed; it is correctly signaled either in the current call (if the shortcut applies) or the next one (via the cumulative check).\n4. **Security Impact**: Since the EOF signal is robust (always eventually provided when `totalBytesRead == ContentLength`), the HTTP state machine will correctly identify the end of the body. There is no mechanism for connection desync or truncated bodies caused by this specific line. The `n == ContentLength` check is a safe optimization for the common case where the body is small enough to fit in the prefetched buffer and be read in one go. The bug pattern described (incorrect EOF leading to security harm) does not manifest.\n\nTherefore, the finding is a false positive based on a misunderstanding of `io.Reader` semantics and the cumulative state tracking already present at the top of the function.\n\nCRUX: The check `if n == rs.header.ContentLength()` only triggers when the entire declared body length is read in a single call from the prefetched buffer (implying `totalBytesRead` was 0), which is a correct EOF condition; any other case is handled by the subsequent `totalBytesRead` check at the start of the next `Read` call, ensuring EOF is never missed or premature.\nGREP: if rs.totalBytesRead == rs.header.ContentLength()",
    "elapsed": 153.3,
    "tokens": 7874,
    "tokens_in": 6899,
    "tokens_out": 975,
    "cost_usd": 0.00582258,
    "file": "streaming.go",
    "round": 4,
    "grep_used": true,
    "grep_results": "GREP `if rs.totalBytesRead == rs.header.ContentLength()`:\n```\nstreaming.go:57:\tif rs.totalBytesRead == rs.header.ContentLength() {\nstreaming.go:83:\tif rs.totalBytesRead == rs.header.ContentLength() {\n```"
  },
  {
    "finding_title": "EOF detection in prefetched Content-Length read uses per-call n instead of totalBytesRead",
    "verdict": "INVALID",
    "reasoning": "The reported vulnerability claims that the check `if n == rs.header.ContentLength()` in the `prefetchedBytes` reading path causes incorrect EOF signaling. To determine if this is a real security issue, we must analyze if this condition can ever be true in a way that violates the HTTP protocol or leads to desync.\n\n1. **Context of the check**: The check occurs after reading `n` bytes from `rs.prefetchedBytes`. Before this read, `p` is truncated to `left = prefetchedSize - rs.totalBytesRead`. Thus, `n <= prefetchedSize - rs.totalBytesRead`.\n2. **Condition for Premature EOF**: For `n == rs.header.ContentLength()` to be true, we must have `prefetchedSize - rs.totalBytesRead >= ContentLength`. Since `rs.totalBytesRead` is cumulative and starts at 0, if `totalBytesRead` is 0, this requires `prefetchedSize >= ContentLength`. In a well-formed HTTP request, the server parses headers and buffers the body start into `prefetchedBytes`. It is highly unlikely, but possible in edge cases (e.g., pipelined requests or specific buffering implementations), that `prefetchedSize` exceeds `ContentLength`. However, even if `prefetchedSize == ContentLength`, and we read it all in one go (`n == ContentLength`), then `rs.totalBytesRead` becomes `ContentLength`. Returning `io.EOF` here is **correct** because the entire declared body has been consumed. It is not premature.\n3. **Condition for Missed EOF**: If `n < ContentLength`, the code returns `n, nil` (assuming `bytes.Reader.Read` didn't return EOF, which it won't if we truncated `p` to exactly the remaining bytes available). On the *next* call, the first check `if rs.totalBytesRead == rs.header.ContentLength()` (line 57) will catch the completion if `totalBytesRead` reached `ContentLength` in the previous step. If `totalBytesRead` is still less, it will proceed to read more. Thus, EOF is never permanently missed.\n4. **Security Impact**: The HTTP connection reuse and body parsing rely on the `io.Reader` interface returning the correct number of bytes and eventually `io.EOF`. Since the logic ensures that `io.EOF` is returned exactly when the `ContentLength` bytes have been read (either in the current call if it was the last chunk, or in the subsequent call), the state machine remains consistent. There is no desync, truncation, or infinite loop. The use of `n` instead of `totalBytesRead` is a stylistic choice that relies on the invariant that `n` can only equal `ContentLength` if it was the first and only read (or a specific edge case where it aligns), but even if it were slightly incorrect, it doesn't lead to memory corruption or bypass. More importantly, as analyzed, it is functionally correct for the standard case.\n\nAll prior reviewers correctly identified that the bug pattern does not lead to a security vulnerability. The code adheres to `io.Reader` semantics where EOF can be deferred to the next call. The specific line in question is a safe optimization. Therefore, the finding is a false positive.\n\nCRUX: The check `if n == rs.header.ContentLength()` only results in returning `io.EOF` when the total bytes read (`rs.totalBytesRead`) equals `ContentLength` (in the feasible case where `totalBytesRead` was 0 before the read), which is the correct condition for EOF. Any other case is handled by the cumulative check at the start of the next `Read` call.\nGREP: if rs.totalBytesRead == rs.header.ContentLength()",
    "elapsed": 50.0,
    "tokens": 8443,
    "tokens_in": 7643,
    "tokens_out": 800,
    "cost_usd": 0.00561006,
    "file": "streaming.go",
    "round": 5,
    "grep_used": true,
    "grep_results": "GREP `if rs.totalBytesRead == rs.header.ContentLength()`:\n```\nstreaming.go:57:\tif rs.totalBytesRead == rs.header.ContentLength() {\nstreaming.go:83:\tif rs.totalBytesRead == rs.header.ContentLength() {\n```"
  }
]
Raw scan report for this file

`requestStream.Read` is reachable as the body reader for parsed HTTP requests/responses. The untrusted inputs are:

- `rs.reader`: the live body/chunk stream from the network.
- `rs.prefetchedBytes`: bytes already read into `bodyBuf` before the stream is acquired.
- `rs.header.ContentLength()`: attacker-influenced `Content-Length` value.
- chunk framing bytes parsed by `parseChunkSize` and `readCrLf`.

The most important issues are in the fixed `Content-Length` path, where prefetched bytes are not clamped to the declared body length, and in the chunked path, where the parsed chunk size is used as a length/slice bound without validation.

```json
[
  {
    "severity": "critical",
    "title": "Content-Length stream can consume bytes beyond the declared body length",
    "function": "requestStream.Read",
    "description": "In the fixed Content-Length branch, when rs.prefetchedBytes has remaining data, p is truncated only to prefetchedSize - totalBytesRead, not to ContentLength() - totalBytesRead. If the prefetched buffer contains more bytes than the remaining declared body length, for example read-ahead bytes or pipelined request bytes, Read can return or discard bytes that belong to the next HTTP message. After totalBytesRead exceeds ContentLength, left becomes negative and rs.reader.Read is called without truncation. This can desynchronize keep-alive connections and enable HTTP request smuggling or denial of service."
  },
  {
    "severity": "high",
    "title": "acquireRequestStream does not clamp prefetched bytes to ContentLength",
    "function": "acquireRequestStream",
    "description": "b may contain bytes read ahead by the connection reader, including data beyond h.ContentLength() for fixed-length bodies. bytes.NewReader(b.B) exposes the entire buffer to requestStream.Read without a cap derived from the header. For a non-chunked body, the prefetched buffer should be clamped to the remaining ContentLength or the reader should enforce the ContentLength bound when reading from prefetchedBytes."
  },
  {
    "severity": "high",
    "title": "EOF detection in prefetched Content-Length read uses per-call n instead of totalBytesRead",
    "function": "requestStream.Read",
    "description": "After reading from rs.prefetchedBytes, the code returns io.EOF when n == rs.header.ContentLength(). n is only the number of bytes returned by the current Read call. The correct completion test is rs.totalBytesRead == rs.header.ContentLength(). With multi-call reads or an over-sized prefetched buffer, this can signal EOF prematurely or fail to signal EOF correctly, allowing truncated bodies or incorrect connection reuse."
  },
  {
    "severity": "medium",
    "title": "Premature end-of-stream is reported as normal EOF for fixed Content-Length bodies",
    "function": "requestStream.Read",
    "description": "In the fixed Content-Length branch, after rs.reader.Read(p), any non-nil error is returned unchanged. If the error is io.EOF while totalBytesRead is still less than ContentLength, the stream reports normal EOF instead of io.ErrUnexpectedEOF. Callers using io.ReadAll or simple Read loops may accept a truncated body. This can be triggered by sending fewer than ContentLength bytes and closing the connection. The chunked path already converts EOF to io.ErrUnexpectedEOF, but the fixed-length path does not."
  },
  {
    "severity": "high",
    "title": "Chunk size result is not validated before being used as a slice bound",
    "function": "requestStream.Read",
    "description": "parseChunkSize(rs.reader) consumes attacker-controlled chunk-size lines. The returned chunkSize is stored in rs.chunkLeft without validating that it is non-negative or within policy limits. If a crafted or oversized chunk-size line causes integer overflow/wrap in the parser, chunkLeft can become negative. bytesToRead := min(rs.chunkLeft, len(p)) then yields a negative bound and p[:bytesToRead] panics with a slice out-of-range error, crashing the server. Even if non-negative, unreasonably large chunk sizes should be rejected or bounded by policy."
  },
  {
    "severity": "low",
    "title": "int64 to int truncation of prefetched buffer size",
    "function": "requestStream.Read",
    "description": "prefetchedSize := int(rs.prefetchedBytes.Size()) converts an int64 to int. On 32-bit targets or if the prefetched size exceeds MaxInt, the conversion can truncate or wrap, producing an incorrect or negative prefetchedSize. That can make left calculations wrong, bypass length truncation, and lead to incorrect consumption of body data or unbounded reads. Use int64 accounting or safely validate the size before conversion."
  },
  {
    "severity": "low",
    "title": "Unchecked accumulation in totalBytesRead can overflow length accounting",
    "function": "requestStream.Read",
    "description": "rs.totalBytesRead += n is unchecked. For very large bodies or crafted Content-Length values near the maximum int value, accumulated length can overflow and become negative. Subsequent left := ContentLength() - totalBytesRead then becomes negative, so p is not truncated before rs.reader.Read, allowing unbounded reads beyond the declared body and resource exhaustion. Use saturating arithmetic or validate accumulated length against the maximum allowed body size."
  }
]
```