Skip to content

Commit 50766d1

Browse files
committed
fix(copilot): use sharp's own pixel default rather than a rounder, lower one
The vulnerability was disabling sharp's pixel guard with limitInputPixels: false, so restoring the library's default is the fix; 100MP was a tidier number we picked, and tidy landed mid-market — a Fuji GFX 100 frame is 11648x8736, or 101.7MP, and was refused by a ceiling that claimed to clear every camera. 268402689 still removes the unbounded declaration the bug allowed and caps a rung near 400ms (measured; the curve is sublinear), while refusing nothing a camera produces. Tests now pin both ends: a 900MP bomb is refused, and 48MP / 61MP / 102MP frames are not.
1 parent 0e2b55a commit 50766d1

2 files changed

Lines changed: 27 additions & 9 deletions

File tree

apps/sim/lib/copilot/vfs/file-reader.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,21 @@ describe('readFileRecord', () => {
9595
SHARP_TEST_TIMEOUT_MS
9696
)
9797

98+
it.each([
99+
['48MP iPhone', 8064, 6048],
100+
['61MP full-frame', 9504, 6336],
101+
['102MP medium format', 11648, 8736],
102+
])('does not refuse a %s frame on pixel count', async (_camera, width, height) => {
103+
// Guards the ceiling from being tightened below real hardware. These reach the
104+
// resize ladder and fail there on the stub's truncated pixel data — what matters
105+
// is that they are not turned away by the pixel budget first.
106+
fetchWorkspaceFileBuffer.mockResolvedValue(await makeBombPng(width, height))
107+
108+
const result = await readFileRecord(imageRecord('photo.png', 4_000_000))
109+
110+
expect(result?.content).not.toContain('It is too large to decode safely.')
111+
})
112+
98113
it('reports the too-large placeholder when an understated record.size hides an oversized object', async () => {
99114
fetchWorkspaceFileBuffer.mockRejectedValue(
100115
new PayloadSizeLimitError({

apps/sim/lib/copilot/vfs/file-reader.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,20 @@ export const MAX_IMAGE_SOURCE_BYTES = MAX_WORKSPACE_FORMDATA_FILE_SIZE
6565
* Pixel ceiling on the decoded image, and the actual decompression-bomb defence: a
6666
* few hundred KB of PNG can declare an arbitrarily large raster.
6767
*
68-
* The cost it bounds is CPU, not memory. libvips decodes this pipeline sequentially,
69-
* so peak RSS stays flat (tens of MB) no matter what the header declares — measured
70-
* on this exact pipeline, 100MP..1024MP all sat under ~120MB. What scales is time,
71-
* roughly linearly: ~240ms at 100MP, ~1.35s at 1024MP, once per resize rung. So the
72-
* budget caps what one read can burn, and the `break` below caps how many rungs a
73-
* failing image gets.
68+
* This is sharp's own default rather than a number of our own — the vulnerability
69+
* was disabling it with `limitInputPixels: false`, so restoring it is the fix, and
70+
* any tighter value would be us inventing a ceiling the library did not ask for. A
71+
* round 100MP looked tidy but lands mid-market: a Fuji GFX 100 frame is 11648x8736,
72+
* or 101.7MP, and would have been refused.
7473
*
75-
* 100MP clears every single-shot camera (a 48MP iPhone still is 8064x6048) but will
76-
* refuse a stitched gigapixel panorama, which is the known cost of the ceiling.
74+
* The cost it bounds is CPU, not memory. libvips decodes this pipeline sequentially,
75+
* so peak RSS stays flat (tens of MB) whatever the header declares — measured here,
76+
* 100MP..1024MP all sat under ~120MB. Time scales sublinearly: ~240ms at 100MP,
77+
* ~400ms at 256MP, ~1.35s at 1024MP, once per resize rung. So the budget caps the
78+
* worst case at roughly 400ms a rung, the `break` below caps a failing image at four
79+
* rungs, and an unbounded declaration — which is what `false` allowed — is gone.
7780
*/
78-
const MAX_IMAGE_INPUT_PIXELS = 100_000_000
81+
const MAX_IMAGE_INPUT_PIXELS = 268_402_689
7982
const MAX_IMAGE_DIMENSION = 1568
8083
const IMAGE_RESIZE_DIMENSIONS = [1568, 1280, 1024, 768]
8184
const IMAGE_QUALITY_STEPS = [85, 70, 55, 40]

0 commit comments

Comments
 (0)