fix(lmstudio): bound model load success response body to prevent OOM#96042
Conversation
The /api/v1/models/load success path read the response with an unbounded await response.json(), so a misbehaving or compromised LM Studio server could stream an arbitrarily large JSON body that is fully buffered into memory before any size check. Read it through the shared byte-capped readProviderJsonResponse helper instead (16 MiB provider-JSON cap, cancels the stream on overflow, wraps malformed JSON), matching the discovery path and the already-bounded error body. Migrate the model fetch/load test mocks to real Response objects (the bounded readers need a real body stream) and add a regression test that streams an oversized success body and asserts a bounded error plus stream cancellation. Label: security
|
@clawsweeper review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Codex review: needs maintainer review before merge. Reviewed June 23, 2026, 5:09 PM ET / 21:09 UTC. Summary PR surface: Source +2, Tests +27. Total +29 across 2 files. Reproducibility: yes. Source inspection shows current main still buffers the LM Studio model-load success body with Review metrics: none identified. Stored data model Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Next step before merge
Security Review detailsBest possible solution: Land the narrow helper reuse after normal maintainer checks so LM Studio load success bodies use the same 16 MiB provider JSON cap as other provider JSON reads. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection shows current main still buffers the LM Studio model-load success body with Is this the best way to solve the issue? Yes. Reusing the existing AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 654544b6b7c4. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Source +2, Tests +27. Total +29 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
LGTM/merge-ready:
|
…penclaw#96042) The /api/v1/models/load success path read the response with an unbounded await response.json(), so a misbehaving or compromised LM Studio server could stream an arbitrarily large JSON body that is fully buffered into memory before any size check. Read it through the shared byte-capped readProviderJsonResponse helper instead (16 MiB provider-JSON cap, cancels the stream on overflow, wraps malformed JSON), matching the discovery path and the already-bounded error body. Migrate the model fetch/load test mocks to real Response objects (the bounded readers need a real body stream) and add a regression test that streams an oversized success body and asserts a bounded error plus stream cancellation. Label: security
…sonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http.
…sonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http.
…sonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http.
…sonResponse (#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's #96042, #96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…penclaw#96042) The /api/v1/models/load success path read the response with an unbounded await response.json(), so a misbehaving or compromised LM Studio server could stream an arbitrarily large JSON body that is fully buffered into memory before any size check. Read it through the shared byte-capped readProviderJsonResponse helper instead (16 MiB provider-JSON cap, cancels the stream on overflow, wraps malformed JSON), matching the discovery path and the already-bounded error body. Migrate the model fetch/load test mocks to real Response objects (the bounded readers need a real body stream) and add a regression test that streams an oversized success body and asserts a bounded error plus stream cancellation. Label: security
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]>
The fal music generation provider in extensions/fal/music-generation-provider.ts parsed its HTTP success response with an unbounded await response.json(). A hostile or buggy fal.ai endpoint — the base URL is user-configurable via models.providers.fal.baseUrl (resolved by resolveFalHttpRequestConfig in http-config.ts, defaulting to https://fal.run) — could stream an arbitrarily large JSON body into memory before parsing, forcing the music generation handler into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap (PROVIDER_JSON_RESPONSE_MAX_BYTES), cancels the stream on overflow, and wraps malformed JSON with the caller label. The change is minimal — a single .json() call replaced by readProviderJsonResponse<unknown> — and preserves the existing downstream logic unchanged. Update the test suite: replace fake { json: async () => ... } response stubs with proper Response objects carrying JSON body streams via a new jsonBodyResponse() helper, so the real readProviderJsonResponse implementation actually exercises the byte-bounded reader under test. Preserve the real readProviderJsonResponse in the provider-http mock (via importOriginal) so the bounded reader streams and cancels oversized bodies. Add a focused regression test: when the music generation stream exceeds the JSON byte cap (32 MiB body, double the 16 MiB cap), generateMusic rejects with a "fal-music-generation: JSON response exceeds" error and the reader cancels the body mid-flight (ReadableStream.cancel fires, bytesPulled < 32 MiB). Existing parse/HTTP-error cases keep passing. Symmetric counterpart to the openclaw#96027/openclaw#96038/openclaw#96042/openclaw#96606/openclaw#96607 response-limit campaign.
The fal music generation provider in extensions/fal/music-generation-provider.ts parsed its HTTP success response with an unbounded await response.json(). A hostile or buggy fal.ai endpoint — the base URL is user-configurable via models.providers.fal.baseUrl (resolved by resolveFalHttpRequestConfig in http-config.ts, defaulting to https://fal.run) — could stream an arbitrarily large JSON body into memory before parsing, forcing the music generation handler into OOM. Route the read through the shared readProviderJsonResponse (from openclaw/plugin-sdk/provider-http), which enforces the 16 MiB byte cap (PROVIDER_JSON_RESPONSE_MAX_BYTES), cancels the stream on overflow, and wraps malformed JSON with the caller label. The change is minimal — a single .json() call replaced by readProviderJsonResponse<unknown> — and preserves the existing downstream logic unchanged. Update the test suite: replace fake { json: async () => ... } response stubs with proper Response objects carrying JSON body streams via a new jsonBodyResponse() helper, so the real readProviderJsonResponse implementation actually exercises the byte-bounded reader under test. Preserve the real readProviderJsonResponse in the provider-http mock (via importOriginal) so the bounded reader streams and cancels oversized bodies. Add a focused regression test: when the music generation stream exceeds the JSON byte cap (32 MiB body, double the 16 MiB cap), generateMusic rejects with a "fal-music-generation: JSON response exceeds" error and the reader cancels the body mid-flight (ReadableStream.cancel fires, bytesPulled < 32 MiB). Existing parse/HTTP-error cases keep passing. Symmetric counterpart to the openclaw#96027/openclaw#96038/openclaw#96042/openclaw#96606/openclaw#96607 response-limit campaign.
…sonResponse (openclaw#96772) * fix(googlechat): replace unbounded response.json() with readProviderJsonResponse Replace the local readGoogleChatJsonResponse and readGoogleChatCertsResponse wrappers with the existing SDK helper readProviderJsonResponse (from openclaw/plugin-sdk/provider-http) so the Google Chat API JSON responses are bounded at 16 MiB, matching the non-streaming cap already used by 15+ other extensions. What changed: - extensions/googlechat/src/api.ts: readGoogleChatJsonResponse now delegates to readProviderJsonResponse. Removed the local try/catch wrapper. - extensions/googlechat/src/auth.ts: readGoogleChatCertsResponse now delegates to readProviderJsonResponse. Error message preserved. Removed the local try/catch wrapper. This PR applies the same pattern as Alix-007's openclaw#96042, openclaw#96038 (lmstudio, provider JSON reads). No SDK promotion needed — readProviderJsonResponse is already available in openclaw/plugin-sdk/provider-http. * fix(googlechat): add inline bounded-read regression tests Co-Authored-By: Claude <[email protected]> * fix(googlechat): remove unused variable flagged by oxlint Co-Authored-By: Claude <[email protected]> * fix(googlechat): bound api error body reads --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Vincent Koc <[email protected]> (cherry picked from commit 4f3d81b)
What Problem This Solves
ensureLmstudioModelLoadedinextensions/lmstudio/src/models.fetch.tsPOSTs to LM Studio's/api/v1/models/loadand reads the success response with an unboundedawait response.json(). LM Studio is a self-hosted/local server endpoint that the agent reaches over HTTP; a misbehaving, misconfigured, or compromised server (or anything sitting on the configuredbaseUrl, including an SSRF-redirected target) can return a success body with noContent-Lengthand stream an arbitrarily large JSON payload.Because
response.json()buffers the entire body before parsing, that unbounded body is fully read into memory on the provider/discovery path before any size check runs. That is a memory-pressure / hang vector on a code path that runs during model discovery and before first inference — exactly the kind of unbounded external-response read the response-limit campaign has been closing across the codebase.The sibling error body on the same endpoint was already bounded (
readResponseTextLimited, 8 KiB) in a prior change. This PR closes the remaining unbounded read: the success JSON body.Changes
extensions/lmstudio/src/models.fetch.ts: replace the unboundedawait response.json()on the/api/v1/models/loadsuccess path with the shared, already-boundedreadProviderJsonResponse<LmstudioLoadResponse>(...)helper (re-exported fromopenclaw/plugin-sdk/provider-http, same helper the/api/v1/modelsdiscovery path already uses). It reads the body throughreadResponseWithLimitunder the 16 MiB provider-JSON cap, cancels the stream on overflow, and wraps malformed JSON with the caller's label. No new abstraction is introduced and the redundant localtry/catchis removed.extensions/lmstudio/src/models.test.ts: migrate the model fetch/load mocks from bare{ ok, json }placeholders to realResponseobjects (the bounded readers require a real body stream), and add a regression test that streams an oversized (>16 MiB) success body and asserts the load path throws a bounded error and cancels the stream instead of buffering it all. Updates the malformed-load assertion to the helper's owned message.Real behavior proof
/api/v1/models/loadsuccess JSON body; after the fix the read is capped at 16 MiB and the stream is cancelled on overflow.node:httpserver (real sockets, chunked transfer-encoding, noContent-Length) driving the real exportedensureLmstudioModelLoaded/readProviderJsonResponsevia the globalfetch(realResponsebody streams). Run withnode --import tsx proof.mts.{ "status": "loaded" }response, end-to-endensureLmstudioModelLoaded; (2) oversized 20 MiB chunked success body, end-to-end; (3) a body that hangs forever after a prefix, drivingreadProviderJsonResponsewith an explicit small cap; (4) negative control reading the same 20 MiB body with the pre-fix unboundedawait response.text()."proof-model"(still parses normally); (2) throwsLM Studio model load: JSON response exceeds 16777216 bytesand the server observed only ~17.5 MiB sent before the socket closed (vs 20 MiB total) — the stream was cancelled mid-flight; (3) throws... exceeds 2048 bytesand the server socket was closed (cancelled) instead of hanging; (4) the unbounded read consumed the full 20 MiB, confirming the cap is load-bearing.Evidence
Unit tests:
node scripts/run-vitest.mjs extensions/lmstudio/src/models.test.ts --run→ 24 passed (24), including the newbounds oversized model load success bodies.oxlintandtsgo(extensions source + test) clean on the changed files.Label: security
This is the symmetric companion to the response-limit campaign (e.g. #95103 / #95108 / #95218): the LM Studio discovery
/api/v1/modelspath and the/api/v1/models/loaderror body were already bounded; this PR finishes the set by bounding the/api/v1/models/loadsuccess body with the same shared helper and cap.AI-assisted: implemented and proof-tested with AI assistance; reviewed by a human before submission.