fix(memory): retune image and orca.yaml ceilings that rejected valid input (#10815)

This commit is contained in:
Neil 2026-07-28 01:51:22 -07:00 committed by GitHub
parent 380034edf9
commit de162c632b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 111 additions and 31 deletions

View File

@ -39,12 +39,17 @@ describe('buildImageDataUri', () => {
expect(buildImageDataUri('application/octet-stream', 'AAAA')).toBeNull()
})
it('rejects malformed and oversized known rasters before native decode', () => {
expect(buildImageDataUri('image/png', 'bmV3')).toBeNull()
it('rejects oversized known rasters before native decode', () => {
expect(buildImageDataUri('image/png', pngBase64(32_769, 1))).toBeNull()
expect(buildImageDataUri('image/png', pngBase64(8192, 8192))).toBeNull()
})
it('still renders bytes whose header we cannot measure', () => {
// Failing to read a header means we could not measure the image, not that it is oversized.
// The decoder handles encodings this parser does not, so suppressing here blanks valid images.
expect(buildImageDataUri('image/png', 'bmV3')).toBe('data:image/png;base64,bmV3')
})
it('preserves SVG behavior because vectors do not have encoded raster dimensions', () => {
expect(buildImageDataUri('image/svg+xml', 'PHN2Zy8+')).toBe(
'data:image/svg+xml;base64,PHN2Zy8+'

View File

@ -1,4 +1,4 @@
import { readRasterImagePreviewDimensionsFromBase64 } from './raster-image-base64-preview'
import { exceedsRasterImagePreviewLimits } from './raster-image-base64-preview'
import { isKnownRasterImageMimeType } from './raster-image-preview-limits'
// Builds an inline `data:` URI for base64 image bytes, shared by the desktop
@ -19,7 +19,9 @@ export function buildImageDataUri(
if (!cleaned) {
return null
}
if (readRasterImagePreviewDimensionsFromBase64(cleaned, mimeType) === null) {
// Only suppress when the header says the image is too large to render safely. An unreadable
// header is not evidence of an oversized image, and the decoder handles formats we cannot parse.
if (exceedsRasterImagePreviewLimits(cleaned, mimeType)) {
return null
}
return `data:${mimeType};base64,${cleaned}`

View File

@ -12,16 +12,36 @@ scripts:
).toMatchObject({ scripts: { setup: 'pnpm install' } })
})
it('rejects alias expansion beyond the explicit conversion cap', () => {
const aliases = Array.from({ length: 21 }, () => '*items').join(', ')
it('keeps reusing one anchor across a realistic tab list', () => {
// Flat reuse costs anchor size x uses, which the file-size limit already bounds. Rejecting it
// broke ordinary configs that merge shared defaults into every tab.
const tabs = Array.from(
{ length: 40 },
(_, index) => ` - <<: *shared\n title: tab${index}`
).join('\n')
expect(
parseOrcaYaml(`
items: &items [one, two]
expanded: [${aliases}]
scripts:
setup: pnpm install
shared: &shared
command: pnpm dev
defaultTabs:
${tabs}
`)
).toBeNull()
).toMatchObject({
defaultTabs: expect.arrayContaining([{ title: 'tab39', command: 'pnpm dev' }])
})
})
it('rejects nested aliases that expand exponentially', () => {
// The parser rejects on uses x subtree-alias-count, so depth is what it catches: this is a few
// hundred bytes of source that would otherwise materialize millions of nodes.
let source = 'a0: &a0 [x, x, x, x, x, x, x, x, x]\n'
for (let level = 1; level <= 8; level += 1) {
source += `a${level}: &a${level} [${Array(9)
.fill(`*a${level - 1}`)
.join(', ')}]\n`
}
expect(parseOrcaYaml(`${source}scripts:\n setup: *a8\n`)).toBeNull()
})
})

View File

@ -5,7 +5,10 @@ export const MAX_ORCA_YAML_CODE_UNITS = 256 * 1024
export const MAX_ORCA_YAML_FIELD_BYTES = 64 * 1024
export const MAX_ORCA_YAML_FIELD_CODE_UNITS = 64 * 1024
export const MAX_ORCA_YAML_COLLECTION_ENTRIES = 256
export const MAX_ORCA_YAML_ALIAS_COUNT = 20
// The yaml parser rejects on `useCount * subtreeAliasCount`, so exponential expansion is caught by
// the multiplication regardless of this value; lowering it only rejects flat, linear reuse. Keep the
// library default so an orca.yaml that merges one anchor into many tabs still parses.
export const MAX_ORCA_YAML_ALIAS_COUNT = 100
export function isOrcaYamlTextWithinLimit(content: string): boolean {
return (

View File

@ -1,7 +1,7 @@
import type { RasterImageDimensions } from './raster-image-dimensions'
import { readRasterImageDimensions } from './raster-image-dimensions'
import {
assertRasterImagePreviewWithinLimits,
isKnownRasterImageMimeType,
isRasterImagePreviewDimensions,
RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
} from './raster-image-preview-limits'
@ -121,21 +121,24 @@ function decodeBase64Prefix(content: string, maxBytes: number): Uint8Array | nul
return output.subarray(0, outputLength)
}
/** Returns undefined for non-raster MIME types and null for rejected raster bytes. */
export function readRasterImagePreviewDimensionsFromBase64(
/**
* Whether the encoded dimensions are known to exceed the preview limits.
*
* Distinct from a failed read: an unrecognized or truncated header means we could not measure the
* image, not that it is too large. Treating those the same blanks out valid images that no decoder
* has trouble with, so only a confident over-limit answer should suppress a preview.
*/
export function exceedsRasterImagePreviewLimits(
content: string,
mimeType: string | undefined
): RasterImageDimensions | null | undefined {
): boolean {
if (!isKnownRasterImageMimeType(mimeType)) {
return undefined
return false
}
const prefix = decodeBase64Prefix(content, RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES)
if (!prefix) {
return null
}
try {
return assertRasterImagePreviewWithinLimits(prefix, mimeType) ?? null
} catch {
return null
return false
}
const dimensions = readRasterImageDimensions(prefix)
return dimensions !== null && !isRasterImagePreviewDimensions(dimensions)
}

View File

@ -20,6 +20,39 @@ function bmpHeader(width: number, height: number): Buffer {
return bmp
}
/** SOI, then `metadataBytes` of APP2 padding (plus `extraSegments` empty ones), then SOF0. */
function jpegWithMetadata(
metadataBytes: number,
width: number,
height: number,
extraSegments = 0
): Buffer {
const parts = [Buffer.from([0xff, 0xd8])]
for (let written = 0; written < metadataBytes; ) {
// A JPEG segment length field is 16 bits, so real files chain many segments to carry a profile.
const size = Math.min(65_533, metadataBytes - written)
const header = Buffer.alloc(4)
header.writeUInt16BE(0xffe2)
header.writeUInt16BE(size + 2, 2)
parts.push(header, Buffer.alloc(size))
written += size
}
for (let index = 0; index < extraSegments; index += 1) {
const empty = Buffer.alloc(4)
empty.writeUInt16BE(0xffe2)
empty.writeUInt16BE(2, 2)
parts.push(empty)
}
const sof = Buffer.alloc(11)
sof.writeUInt16BE(0xffc0)
sof.writeUInt16BE(8, 2)
sof[4] = 8
sof.writeUInt16BE(height, 5)
sof.writeUInt16BE(width, 7)
parts.push(sof)
return Buffer.concat(parts)
}
function icoWithPayload(payload: Buffer, width = 1, height = 1): Buffer {
const header = Buffer.alloc(22)
header.writeUInt16LE(1, 2)
@ -59,4 +92,17 @@ describe('readRasterImageDimensions', () => {
expect(readRasterImageDimensions(truncated)).toBeNull()
expect(readRasterImageDimensions(bmpHeader(0, 16))).toBeNull()
})
it('reads a JPEG whose frame header sits behind large chained metadata', () => {
// Cameras and editors emit ICC/MPF profiles split across many 64 KiB segments, pushing SOF0 far
// into the file. Both shapes below decode everywhere, so neither may read as an unknown size.
expect(readRasterImageDimensions(jpegWithMetadata(1_400 * 1024, 4_000, 3_000))).toEqual({
width: 4_000,
height: 3_000
})
expect(readRasterImageDimensions(jpegWithMetadata(0, 640, 480, 6_000))).toEqual({
width: 640,
height: 480
})
})
})

View File

@ -1,7 +1,7 @@
export type RasterImageDimensions = { width: number; height: number }
const JPEG_DIMENSION_SCAN_MAX_BYTES = 1024 * 1024
const JPEG_DIMENSION_SCAN_MAX_MARKERS = 4_096
// No scan cap: this is a forward seek over a caller-bounded buffer, so it costs O(1) memory and at
// most one pass. Capping it only made valid images with large ICC/MPF metadata unreadable.
const ICO_MAX_IMAGES = 1_024
const JPEG_START_OF_FRAME_MARKERS = new Set([
0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf
@ -100,15 +100,13 @@ function readJpegDimensions(bytes: Uint8Array): RasterImageDimensions | null {
return null
}
let offset = 2
let markersRead = 0
const scanEnd = Math.min(bytes.byteLength, JPEG_DIMENSION_SCAN_MAX_BYTES)
while (offset < scanEnd && markersRead < JPEG_DIMENSION_SCAN_MAX_MARKERS) {
const scanEnd = bytes.byteLength
while (offset < scanEnd) {
while (offset < scanEnd && bytes[offset] === 0xff) {
offset += 1
}
const marker = bytes[offset]
offset += 1
markersRead += 1
if (marker === undefined || marker === 0x00 || marker === 0xd9 || marker === 0xda) {
return null
}

View File

@ -2,7 +2,10 @@ import { readRasterImageDimensions, type RasterImageDimensions } from './raster-
export const MAX_RASTER_IMAGE_PREVIEW_DIMENSION_PX = 32_768
export const MAX_RASTER_IMAGE_PREVIEW_PIXELS = 32 * 1024 * 1024
export const RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES = 1024 * 1024
// Bounds the transient decode buffer, not the image. 1 MiB cut off valid photos whose SOF sits past
// a large ICC/MPF block; 8 MiB clears every real-world metadata layout while staying a fraction of
// the base64 string the caller already holds.
export const RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES = 8 * 1024 * 1024
export const INVALID_RASTER_IMAGE_PREVIEW_ERROR =
'Image preview has invalid or unsupported raster dimensions'
export const RASTER_IMAGE_PREVIEW_TOO_LARGE_ERROR =