{"record":{"id":"f73b815786bf4ea3","repo":"CherryHQ/cherry-studio","slug":"file-not-found-displaypath","errorCode":null,"errorMessage":"File not found: ${displayPath}","messagePattern":"File not found: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/security/localFileResolver.ts","lineNumber":36,"sourceCode":"}\n\n/**\n * Read an already-canonical path into a `FileAttachment`. Performs NO path\n * authorization — the caller owns containment (see `resolveWorkspaceFile`).\n */\nexport async function readCanonicalLocalFile(\n  requestedPath: string,\n  canonicalPath: string,\n  displayPath: string\n): Promise<FileAttachment> {\n  const target = AbsoluteFilePathSchema.parse(canonicalPath)\n\n  let stats: Awaited<ReturnType<typeof lstat>>\n  try {\n    stats = await lstat(target)\n  } catch (error) {\n    if (isErrnoException(error) && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {\n      throw new Error(`File not found: ${displayPath}`)\n    }\n    throw error\n  }\n  if (!stats.isFile) {\n    throw new Error(`Not a regular file: ${displayPath}`)\n  }\n\n  // The snapshot pins the inode and fixes the read length at open time, so the size\n  // check and the read see the same file even if the path is replaced meanwhile.\n  const snapshot = await openReadableFileSnapshot(target)\n  try {\n    if (snapshot.size > MAX_FILE_SIZE_BYTES) {\n      throw new Error(`File exceeds the ${MAX_FILE_SIZE_BYTES} byte limit (${snapshot.size} bytes): ${displayPath}`)\n    }\n\n    const data = await readStreamToBuffer(snapshot.createReadStream())\n    // Re-check against the actual read size: the file can grow between stat and read.\n    if (data.length > MAX_FILE_SIZE_BYTES) {","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/security/localFileResolver.ts#L18-L54","documentation":"Thrown by readCanonicalLocalFile() when lstat(target) fails with ENOENT or ENOTDIR on a path that was already confirmed to exist by a prior realpath call. This indicates a TOCTOU (time-of-check-to-time-of-use) race: the file existed when realpath resolved it but was deleted or replaced before lstat ran. The error is intentionally generic ('File not found') rather than exposing the race, so callers treat it as a missing-file condition.","triggerScenarios":"Called after realpath succeeds in either resolveWorkspaceFile() or resolveLocalFile(). The file is deleted between the realpath call and the lstat call inside readCanonicalLocalFile. Common in concurrent environments where multiple agents or processes modify the same workspace simultaneously.","commonSituations":"A concurrent agent step or external process deleted the file between the path resolution and the stat; a file-watcher or build tool cleaned the file; the workspace is on a network filesystem with eventual-consistency delays.","solutions":["Retry the resolution from the top-level caller (resolveWorkspaceFile or resolveLocalFile) — if the file was transiently gone, it may reappear.","If the deletion is expected (e.g., a cleanup step), coordinate timing so reads happen before deletes.","On network filesystems, add a short retry with exponential backoff for ENOENT on recently-listed files."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"import { exists } from '@main/utils/file'\n\n// Pre-check existence before calling resolveLocalFile or resolveWorkspaceFile\nif (!(await exists(requestedPath))) {\n  return null // file is gone, caller can handle gracefully\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await readCanonicalLocalFile(requestedPath, canonicalPath, displayPath)\n} catch (error) {\n  if (error instanceof Error && error.message.startsWith('File not found:')) {\n    // TOCTOU race — file existed at realpath time but gone by lstat time\n    logger.warn('File disappeared during resolution (TOCTOU)', { displayPath })\n    return null\n  }\n  throw error\n}","preventionTips":["Minimize the time between path resolution and file read in concurrent environments.","If files are routinely deleted during processing, implement a file-existence pre-check.","On network filesystems, treat transient ENOENT as retryable with a short backoff."],"tags":["filesystem","race-condition","toctou","file-not-found","enoent","channels"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}