Skip to content

fix(compose): free WASM images on throw + cap pixel/raw sizes (review M2/M3)#116

Merged
vinsonconsulting merged 1 commit into
mainfrom
fix/review-batch-b-compose
Jun 29, 2026
Merged

fix(compose): free WASM images on throw + cap pixel/raw sizes (review M2/M3)#116
vinsonconsulting merged 1 commit into
mainfrom
fix/review-batch-b-compose

Conversation

@vinsonconsulting

Copy link
Copy Markdown
Owner

Batch B of the max-strength review fixes — the two confirmed-Medium resource-safety findings in the compose path (both 3/3 on the adversarial pass).

M2 — photon-ops free WASM images on throw

photon-ops.ts freed each PhotonImage only on the success path. A caller-triggerable throw (out-of-bounds crop, undecodable bytes) orphaned the decoded WASM buffer; the server catch keeps the worker alive, so the leak is silent and accumulates across requests in a warm isolate toward the 128 MB cap. Every op now wraps allocation in try/finally. Shared core → protects both the MCP and CMA consumers.

M3 — compose pixel cap + raw-length validation

width/height had no pixel-count ceiling, so 100000×100000 forced a multi-GB RGBA allocation → isolate OOM. And encode built a Uint8ClampedArray from caller raw.data without checking length === width*height*4. Added a 10 MP cap (MAX_COMPOSE_PIXELS, the documented in-isolate ceiling; oversize routes to cf-images) on resize/crop/renderText/encode, plus an exact raw-length check on encode. Mirrored into @limner/cma-tools for parity (D-RA-12) — the twin had the identical gap.

Tests

  • Mocked-photon free-on-throw for all 7 ops (watermark frees both images).
  • Oversize-resize and encode-length-mismatch rejected cleanly in both the MCP and CMA dispatchers.

Suite 677 → 692. Typecheck + lint clean.

🤖 Generated with Claude Code

… M2/M3)

Two confirmed-Medium resource-safety findings in the compose path.

M2 — packages/limner-core/src/compose/photon-ops.ts: every wrapper freed its
PhotonImage(s) only on the success path. A caller-triggerable throw
(out-of-bounds crop, undecodable bytes) orphaned the decoded WASM buffer
(multi-MB) — the server catch keeps the worker alive, so the leak is silent and
accumulates across requests in a warm isolate toward the 128 MB cap. Wrapped
each op in try/finally so img/out (and watermark's two images) are freed on any
path. Protects both the MCP and CMA consumers (shared core).

M3 — compose handler (mcp tools/compose.ts + the cma-tools twin): width/height
were validated only as positive ints with no pixel-count ceiling, so a request
like 100000x100000 forced a multi-GB RGBA allocation inside the isolate (OOM).
And encode built a Uint8ClampedArray from caller raw.data without checking its
length matched width*height*4, so the codec could read past the buffer (corrupt
output) or pair a tiny buffer with huge dimensions. Added a 10 MP cap
(MAX_COMPOSE_PIXELS, the documented in-isolate ceiling; oversize -> cf-images)
on resize/crop/renderText/encode, and an exact raw-length check on encode.
Mirrored into @limner/cma-tools for parity (D-RA-12).

Tests: mocked-photon free-on-throw for all 7 ops (incl. watermark frees both);
oversize-resize and encode-length-mismatch rejected cleanly in both the MCP and
CMA dispatchers. Suite 677 -> 692.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jim Vinson <jim@vinson.org>
@vinsonconsulting vinsonconsulting merged commit 5e3fd4f into main Jun 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant