{"record":{"id":"a78e72f224c897e7","repo":"stablyai/orca","slug":"spritesheet-exceeded-the-size-limit","errorCode":null,"errorMessage":"Spritesheet exceeded the size limit.","messagePattern":"Spritesheet exceeded the size limit\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ipc/pet.ts","lineNumber":333,"sourceCode":"    } catch {\n      throw new Error('Spritesheet file not found.')\n    }\n    if (!sheetStat.isFile()) {\n      throw new Error('Spritesheet path is not a file.')\n    }\n    if (sheetStat.size > MAX_BYTES) {\n      throw new Error(\n        `Spritesheet is too large (${(sheetStat.size / (1024 * 1024)).toFixed(1)} MB).`\n      )\n    }\n\n    let sprite: NonNullable<CustomPet['sprite']> | undefined\n    if (manifest.frame) {\n      // Why: only decode when a frame layout needs validating — nativeImage can fail on some WebP variants in headless contexts.\n      const sheetBuf = await readFile(sheetSrc)\n      // Why: defend against TOCTOU — file may have grown between stat and read.\n      if (sheetBuf.byteLength > MAX_BYTES) {\n        throw new Error('Spritesheet exceeded the size limit.')\n      }\n      const dims = await readSheetDimensions(sheetBuf)\n      if (!dims) {\n        throw new Error('Could not decode the spritesheet image.')\n      }\n      const { width: fw, height: fh } = manifest.frame\n      if (dims.width % fw !== 0 || dims.height % fh !== 0) {\n        throw new Error(\n          `Spritesheet ${dims.width}×${dims.height} is not a clean multiple of frame ${fw}×${fh}.`\n        )\n      }\n      const columns = dims.width / fw\n      const rows = dims.height / fh\n      if (manifest.animations) {\n        for (const [name, anim] of Object.entries(manifest.animations)) {\n          if (anim.row >= rows) {\n            throw new Error(`Animation \"${name}\" references row ${anim.row} but sheet has ${rows}.`)\n          }","sourceCodeStart":315,"sourceCodeEnd":351,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/ipc/pet.ts#L315-L351","documentation":"Thrown at pet.ts:332-333 inside the manifest.frame decode block, after re-reading the spritesheet into a buffer: if sheetBuf.byteLength > MAX_BYTES (64 MB), the file grew between the earlier stat (line 314) and the readFile (line 330). This is an explicit TOCTOU defense — the comment at line 331 states it. It only runs when manifest.frame is set, because that is the only path that re-reads the file.","triggerScenarios":"Between stat() at line 314 and readFile() at line 330, another process writes enough data to the spritesheet to push it over 64 MB. Concurrent bundle editing, a log file appended to the sprite path, or a deliberate TOCTOU attack on a local import.","commonSituations":"Rare in practice; most likely when the 'spritesheet' path is actually a live log or a file being actively written by another tool, or under a deliberate local attack during import.","solutions":["Ensure no other process is writing to the spritesheet file during import.","Re-export the bundle to a stable, read-only copy and retry.","If the file legitimately exceeds 64 MB, shrink it (see error 1248)."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { await importPetBundle(p) }\ncatch (e) { if (e instanceof Error && e.message === 'Spritesheet exceeded the size limit.') { /* file changed during import; retry with a stable copy */ } else throw e }","preventionTips":["Do not point the import at a file another process is writing to.","Copy the bundle to a stable, read-only location before importing."],"tags":["pet-bundle","toctou","size-limit","security"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}