CherryHQ/cherry-studio · error · Error

File exceeds the ${MAX_FILE_SIZE_BYTES} byte limit (${data.l

Error message

File exceeds the ${MAX_FILE_SIZE_BYTES} byte limit (${data.length} bytes): ${displayPath}

What it means

Thrown by readCanonicalLocalFile() as the second of two size checks. After reading the entire file stream into a buffer, the actual buffer length is re-checked against MAX_FILE_SIZE_BYTES. This catches a TOCTOU race where the file grew between the snapshot size check (line 48) and the completion of the stream read. The comment in the source explains: 'the file can grow between stat and read'.

Source

Thrown at src/main/ai/channels/security/localFileResolver.ts:55

    }
    throw error
  }
  if (!stats.isFile) {
    throw new Error(`Not a regular file: ${displayPath}`)
  }

  // The snapshot pins the inode and fixes the read length at open time, so the size
  // check and the read see the same file even if the path is replaced meanwhile.
  const snapshot = await openReadableFileSnapshot(target)
  try {
    if (snapshot.size > MAX_FILE_SIZE_BYTES) {
      throw new Error(`File exceeds the ${MAX_FILE_SIZE_BYTES} byte limit (${snapshot.size} bytes): ${displayPath}`)
    }

    const data = await readStreamToBuffer(snapshot.createReadStream())
    // Re-check against the actual read size: the file can grow between stat and read.
    if (data.length > MAX_FILE_SIZE_BYTES) {
      throw new Error(`File exceeds the ${MAX_FILE_SIZE_BYTES} byte limit (${data.length} bytes): ${displayPath}`)
    }
    const filename = path.basename(requestedPath)
    return {
      filename,
      data: data.toString('base64'),
      media_type: mimeForFilename(filename),
      size: data.length
    }
  } finally {
    // Swallow close errors so they can't mask an in-flight resolution error.
    await snapshot.close().catch(() => {})
  }
}

/**
 * Resolve and read a local file. Relative paths are resolved from `basePath`.
 * NOTE: no containment check — an absolute `userPath` escapes `basePath`.
 */

View on GitHub (pinned to 726446b54c)

Solutions

  1. Pause or quiesce the process writing to the file before reading it.
  2. Copy the file to a stable snapshot before attaching (e.g., cp then read the copy).
  3. Read only the tail or a fixed range of the file instead of the entire growing file.
Defensive patterns

Strategy: try-catch

Validate before calling

// Cannot fully prevent a TOCTOU growth race, but can reduce the window:
// Copy the file to a stable location before reading
import { copy } from '@main/utils/file'
const snapshot = await join(os.tmpdir(), `snap-${uuid()}`)
await copy(canonicalPath, snapshot)
// Read the snapshot — it will not grow
return await readCanonicalLocalFile(snapshot, snapshot, displayPath)

Try / catch

try {
  return await readCanonicalLocalFile(requestedPath, canonicalPath, displayPath)
} catch (error) {
  if (error instanceof Error && error.message.includes('byte limit')) {
    // File grew past the limit during read — TOCTOU
    logger.warn('File grew past size limit during read', { displayPath })
    return null
  }
  throw error
}

Prevention

When it happens

Trigger: Called after readStreamToBuffer completes in readCanonicalLocalFile(). The file was under 100 MB at open time (snapshot.size passed the first check) but grew past the limit by the time the stream finished reading. This happens with actively-written log files, growing database files, or files being appended to by another process.

Common situations: A log file being actively written to grew past 100 MB during the read; a database or data file was being appended to by another process; the file is on a filesystem with delayed allocation that flushed additional data during the read window.

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/98fecac02afe9ed7. Report an issue: GitHub.