Fix unbounded memory growth DoS vectors in ollama and opencodecommon#100
Fix unbounded memory growth DoS vectors in ollama and opencodecommon#100matdev83 wants to merge 1 commit into
Conversation
Added io.LimitReader to io.ReadAll calls in: - `internal/plugins/backends/ollama/plugin.go` for `fetchVersion` (1MB limit) - `internal/plugins/backends/opencodecommon/catalog/loader.go` for `getJSON` (10MB limit) Added regression tests `TestFetchVersion_OOM_Regression` and `TestGetJSON_OOM_Regression` to prove the unbounded reads are bounded and avoid OOM issues for large payloads. Co-authored-by: matdev83 <211248003+matdev83@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
📝 WalkthroughSummary by CodeRabbit
WalkthroughResponse body reads in the ollama plugin's fetchVersion and opencodecommon catalog's getJSON functions are now bounded using io.LimitReader (1 MiB and 10 MB respectively) instead of unbounded io.ReadAll calls. Corresponding regression tests verify errors occur when servers return oversized payloads. ChangesBound unbounded HTTP body reads
Estimated code review effort: 2 (Simple) | ~10 minutes Related issues: None specified in the provided context. Suggested labels: bug, security, tests Suggested reviewers: matdev83 Poem
🚥 Pre-merge checks | ✅ 8✅ Passed checks (8 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/plugins/backends/ollama/plugin_test.go`:
- Around line 142-159: The regression test in TestFetchVersion_OOM_Regression
does not prove the read is bounded because the 50MB zero-filled body is invalid
JSON either way. Update the test to use a payload that would be valid JSON if
fully read, or verify the server only sees about 1MiB read from fetchVersion, so
the test fails if the io.LimitReader behavior is removed. Use fetchVersion, the
httptest server handler, and the test’s assertion to make the behavior
observable rather than relying on err != nil alone.
In `@internal/plugins/backends/ollama/plugin.go`:
- Line 141: The version-check read in the Ollama backend does not detect when
`io.ReadAll(io.LimitReader(resp.Body, 1024*1024))` hits the 1 MiB cap, so
truncation is indistinguishable from a valid full response. Update the
`/api/version` handling in `plugin.go` to use a truncation-aware read path in
the same block where `resp.Body` is consumed, and return a clear “response too
large” error when the limit is reached instead of letting `json.Unmarshal` fail
later. Keep the fix localized around the body read and parse logic so the `body,
err` flow remains easy to find.
In `@internal/plugins/backends/opencodecommon/catalog/loader_test.go`:
- Around line 119-136: The OOM regression test in getJSON is too weak because
the all-zero payload is invalid JSON even when fully read, so it does not prove
reads are bounded. Update TestGetJSON_OOM_Regression to use a payload served by
httptest.NewServer that is valid JSON when read completely but becomes invalid
if truncated at the 10MB boundary, so getJSON only passes with the
io.LimitReader(resp.Body, 10*1024*1024) safeguard. Keep the assertion in
TestGetJSON_OOM_Regression focused on distinguishing bounded from unbounded
reads, rather than merely checking for any error.
In `@internal/plugins/backends/opencodecommon/catalog/loader.go`:
- Line 135: The success-path read in loader.go is silently truncating the
catalog payload at 10 MB, which can make oversized but valid responses look like
invalid JSON. Update the response handling around the io.ReadAll/io.LimitReader
logic in the catalog loader so it detects when the body exceeds the cap and
returns a distinct, truncation-aware error from the catalog fetch/parse flow
instead of proceeding to json.Valid on partial data. Keep the fix localized to
the loader’s response-reading path and make the error clearly identify the size
limit condition.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b908683c-d3a7-42d3-8285-1b2b511872d8
📒 Files selected for processing (4)
internal/plugins/backends/ollama/plugin.gointernal/plugins/backends/ollama/plugin_test.gointernal/plugins/backends/opencodecommon/catalog/loader.gointernal/plugins/backends/opencodecommon/catalog/loader_test.go
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
- GitHub Check: qa
🧰 Additional context used
📓 Path-based instructions (5)
internal/plugins/backends/**/*.go
📄 CodeRabbit inference engine (AGENTS.md)
In
internal/plugins/backends/, implement provider/local/compatible backend adapters, with the exact bundle defined ininternal/pluginreg/standard_table.go.
Files:
internal/plugins/backends/ollama/plugin_test.gointernal/plugins/backends/opencodecommon/catalog/loader.gointernal/plugins/backends/ollama/plugin.gointernal/plugins/backends/opencodecommon/catalog/loader_test.go
**/*.go
📄 CodeRabbit inference engine (Custom checks)
**/*.go: For server, CLI, worker, or network Go code, ensurecontext.Contextis propagated correctly, cancellation is respected, and new goroutines cannot leak indefinitely.
Do not make accidental public API breaks in Go code: underpkg/**or anywhere exported Go identifiers are changed, warn if the PR changes exported types, function signatures, error behavior, JSON fields, CLI flags, config keys, or documented behavior without clearly explaining the compatibility impact.
Files:
internal/plugins/backends/ollama/plugin_test.gointernal/plugins/backends/opencodecommon/catalog/loader.gointernal/plugins/backends/ollama/plugin.gointernal/plugins/backends/opencodecommon/catalog/loader_test.go
⚙️ CodeRabbit configuration file
**/*.go: Review as production Go code. Prioritize correctness, race conditions, goroutine leaks, context cancellation, timeout handling, error wrapping, nil-pointer risks, resource cleanup, defer placement, API compatibility, interface design, dependency boundaries, and testability. Avoid generic style comments when gofmt/golangci-lint already covers the issue.
Files:
internal/plugins/backends/ollama/plugin_test.gointernal/plugins/backends/opencodecommon/catalog/loader.gointernal/plugins/backends/ollama/plugin.gointernal/plugins/backends/opencodecommon/catalog/loader_test.go
**/*
📄 CodeRabbit inference engine (Custom checks)
Do not introduce hardcoded credentials, API keys, tokens, private keys, passwords, production secrets, or sensitive internal URLs.
Files:
internal/plugins/backends/ollama/plugin_test.gointernal/plugins/backends/opencodecommon/catalog/loader.gointernal/plugins/backends/ollama/plugin.gointernal/plugins/backends/opencodecommon/catalog/loader_test.go
internal/**
⚙️ CodeRabbit configuration file
internal/**: Focus on package boundaries, hidden coupling, unexported API design, concurrency safety, deterministic behavior, and whether logic belongs in this internal package.
Files:
internal/plugins/backends/ollama/plugin_test.gointernal/plugins/backends/opencodecommon/catalog/loader.gointernal/plugins/backends/ollama/plugin.gointernal/plugins/backends/opencodecommon/catalog/loader_test.go
**/*_test.go
⚙️ CodeRabbit configuration file
**/*_test.go: Review tests for meaningful assertions, table-driven coverage, race-prone tests, t.Parallel misuse, nondeterminism, leaked goroutines, real network or filesystem dependencies, fragile sleeps, and missing edge cases. Prefer testing observable behavior over implementation details.
Files:
internal/plugins/backends/ollama/plugin_test.gointernal/plugins/backends/opencodecommon/catalog/loader_test.go
| func TestFetchVersion_OOM_Regression(t *testing.T) { | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| w.WriteHeader(200) | ||
| chunk := make([]byte, 1024*1024) | ||
| for i := 0; i < 50; i++ { | ||
| w.Write(chunk) | ||
| } | ||
| })) | ||
| defer srv.Close() | ||
|
|
||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| _, err := fetchVersion(ctx, srv.Client(), srv.URL) | ||
| if err == nil { | ||
| t.Fatalf("expected error due to large payload or invalid json, got none") | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Test doesn't actually validate the OOM fix — it would pass identically without io.LimitReader.
The handler writes 50MB of zero-valued bytes, which is never valid JSON regardless of how much is read. fetchVersion would return an error from json.Unmarshal on an all-zero payload whether the read was bounded to 1MiB or fully unbounded — the test can't distinguish "bounded read, then invalid JSON" from "full unbounded read, then invalid JSON." This means reverting the io.LimitReader fix would not make this regression test fail, defeating its purpose as a regression guard.
To actually assert bounded memory, either:
- Serve a payload that would successfully parse as valid JSON if fully read (e.g.,
{"version":"1.0.0", "padding":"...50MB of chars..."}), so the test fails specifically when reading is truncated versus when it's unbounded, or - Instrument/verify that only ~1MiB was actually read from the server (e.g., track bytes written vs. connection closed early), rather than only asserting
err != nil.
As per path instructions, tests should be "testing observable behavior over implementation details" and provide "meaningful assertions" — the current assertion (err == nil) is too weak to catch a regression where the limit is removed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/plugins/backends/ollama/plugin_test.go` around lines 142 - 159, The
regression test in TestFetchVersion_OOM_Regression does not prove the read is
bounded because the 50MB zero-filled body is invalid JSON either way. Update the
test to use a payload that would be valid JSON if fully read, or verify the
server only sees about 1MiB read from fetchVersion, so the test fails if the
io.LimitReader behavior is removed. Use fetchVersion, the httptest server
handler, and the test’s assertion to make the behavior observable rather than
relying on err != nil alone.
Source: Path instructions
| return "", fmt.Errorf("version HTTP status %d", resp.StatusCode) | ||
| } | ||
| body, err := io.ReadAll(resp.Body) | ||
| body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
No truncation detection when the 1 MiB limit is hit.
io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) returns no error when the body is truncated at exactly 1 MiB — it looks identical to a legitimately-sized response that ends at that byte count. If a real /api/version payload were ever larger (e.g., a proxy injecting extra headers/whitespace), the truncated JSON would fail with a generic parse error rather than a clear "response too large" error, making the failure mode confusing to diagnose. Low risk here since version payloads are tiny, but worth a quick fix for clarity.
♻️ Suggested truncation-aware read
- body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
+ const maxVersionBody = 1024 * 1024
+ body, err := io.ReadAll(io.LimitReader(resp.Body, maxVersionBody+1))
if err != nil {
return "", err
}
+ if len(body) > maxVersionBody {
+ return "", fmt.Errorf("version response too large (exceeds %d bytes)", maxVersionBody)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) | |
| const maxVersionBody = 1024 * 1024 | |
| body, err := io.ReadAll(io.LimitReader(resp.Body, maxVersionBody+1)) | |
| if err != nil { | |
| return "", err | |
| } | |
| if len(body) > maxVersionBody { | |
| return "", fmt.Errorf("version response too large (exceeds %d bytes)", maxVersionBody) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/plugins/backends/ollama/plugin.go` at line 141, The version-check
read in the Ollama backend does not detect when
`io.ReadAll(io.LimitReader(resp.Body, 1024*1024))` hits the 1 MiB cap, so
truncation is indistinguishable from a valid full response. Update the
`/api/version` handling in `plugin.go` to use a truncation-aware read path in
the same block where `resp.Body` is consumed, and return a clear “response too
large” error when the limit is reached instead of letting `json.Unmarshal` fail
later. Keep the fix localized around the body read and parse logic so the `body,
err` flow remains easy to find.
| func TestGetJSON_OOM_Regression(t *testing.T) { | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| w.WriteHeader(200) | ||
| chunk := make([]byte, 1024*1024) | ||
| for i := 0; i < 50; i++ { | ||
| w.Write(chunk) | ||
| } | ||
| })) | ||
| defer srv.Close() | ||
|
|
||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| _, err := getJSON(ctx, srv.Client(), srv.URL, nil) | ||
| if err == nil { | ||
| t.Fatalf("expected error due to large payload or invalid json, got none") | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Same weak-assertion issue as the ollama regression test — doesn't verify bounded reads.
The 50MB payload here is also all-zero bytes, which fails json.Valid regardless of whether it was read in full or truncated at 10MB. Reverting the io.LimitReader(resp.Body, 10*1024*1024) fix would not cause this test to fail, since json.Valid would still reject the fully-read, unbounded zero-byte payload. This test therefore does not actually guard against the OOM regression it's named for.
Consider serving a payload that is valid JSON when fully read (so the pre-fix code would succeed) but becomes invalid once truncated at the boundary — this way the test fails without the fix and passes with it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/plugins/backends/opencodecommon/catalog/loader_test.go` around lines
119 - 136, The OOM regression test in getJSON is too weak because the all-zero
payload is invalid JSON even when fully read, so it does not prove reads are
bounded. Update TestGetJSON_OOM_Regression to use a payload served by
httptest.NewServer that is valid JSON when read completely but becomes invalid
if truncated at the 10MB boundary, so getJSON only passes with the
io.LimitReader(resp.Body, 10*1024*1024) safeguard. Keep the assertion in
TestGetJSON_OOM_Regression focused on distinguishing bounded from unbounded
reads, rather than merely checking for any error.
Source: Path instructions
| return nil, fmt.Errorf("opencodecommon: model discovery HTTP status %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) | ||
| } | ||
| body, err := io.ReadAll(resp.Body) | ||
| body, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
Silent truncation at 10 MB on the success path with no distinguishing error.
Unlike the non-2xx error path above (line 132, which only embeds a snippet in an error string), this line bounds the actual catalog payload that gets parsed and returned to callers. If a real catalog response ever exceeds 10MB, the body is silently truncated and json.Valid(body) will fail with "invalid JSON" — giving no indication that the real cause is a size cap rather than a malformed upstream response. This makes future debugging of legitimate large-catalog growth harder to distinguish from actual upstream corruption.
♻️ Suggested truncation-aware read
- body, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024))
+ const maxCatalogBody = 10 * 1024 * 1024
+ body, err := io.ReadAll(io.LimitReader(resp.Body, maxCatalogBody+1))
if err != nil {
return nil, fmt.Errorf("opencodecommon: model discovery read: %w", err)
}
+ if len(body) > maxCatalogBody {
+ return nil, fmt.Errorf("opencodecommon: model discovery response too large (exceeds %d bytes)", maxCatalogBody)
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| body, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) | |
| const maxCatalogBody = 10 * 1024 * 1024 | |
| body, err := io.ReadAll(io.LimitReader(resp.Body, maxCatalogBody+1)) | |
| if err != nil { | |
| return nil, fmt.Errorf("opencodecommon: model discovery read: %w", err) | |
| } | |
| if len(body) > maxCatalogBody { | |
| return nil, fmt.Errorf("opencodecommon: model discovery response too large (exceeds %d bytes)", maxCatalogBody) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@internal/plugins/backends/opencodecommon/catalog/loader.go` at line 135, The
success-path read in loader.go is silently truncating the catalog payload at 10
MB, which can make oversized but valid responses look like invalid JSON. Update
the response handling around the io.ReadAll/io.LimitReader logic in the catalog
loader so it detects when the body exceeds the cap and returns a distinct,
truncation-aware error from the catalog fetch/parse flow instead of proceeding
to json.Valid on partial data. Keep the fix localized to the loader’s
response-reading path and make the error clearly identify the size limit
condition.
|
Closing this in favor of a clean implementation. The hardening goal is valid, but the current patch silently truncates oversized JSON responses, the regression tests do not prove bounded reads, and CI is failing on the new test code. A replacement should use truncation-aware reads with explicit response-too-large errors and tests that fail if the bound is removed. The replacement should keep limits scoped to discovery responses only, not agent sessions, streaming responses, transcripts, or tool-call traffic. |
Understood. Acknowledging that this work is now obsolete and stopping work on this task. A cleaner implementation with truncation-aware reads, explicit errors, and tests that truly verify bounds should be the focus moving forward. |
Fixes a potential DoS / DDoS vector where the application reads an HTTP response entirely into memory without bounds.
The fixes wrap
resp.Bodywithio.LimitReaderbefore passing it toio.ReadAllto constrain the maximum allocation size. Regression tests have been added to verify that large payloads error correctly without unbounded memory allocations.PR created automatically by Jules for task 11865792398649973572 started by @matdev83