{"record":{"id":"426ec2fceea48bd7","repo":"CherryHQ/cherry-studio","slug":"agent-memory-file-must-be-a-real-file-filepath","errorCode":null,"errorMessage":"Agent memory file must be a real file: ${filePath}","messagePattern":"Agent memory file must be a real file: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/mcp/servers/agentMemory.ts","lineNumber":199,"sourceCode":"    }\n    return assertedPath\n  }\n\n  private async assertMemoryDirectory(): Promise<string> {\n    const agentDataPath = await this.getAgentDataPath()\n    const memoryDir = path.join(agentDataPath, 'memory')\n    const memoryStat = await lstat(memoryDir)\n    if (!memoryStat.isDirectory() || memoryStat.isSymbolicLink()) {\n      throw new Error(`Agent memory directory must be a real directory: ${memoryDir}`)\n    }\n    return memoryDir\n  }\n\n  private async assertRegularFileOrMissing(filePath: string): Promise<void> {\n    try {\n      const fileStat = await lstat(filePath)\n      if (!fileStat.isFile() || fileStat.isSymbolicLink()) {\n        throw new Error(`Agent memory file must be a real file: ${filePath}`)\n      }\n    } catch (error) {\n      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error\n    }\n  }\n\n  private async memoryUpdate(args: Record<string, string | undefined>) {\n    const content = args.content\n    if (!content) throw new McpError(ErrorCode.InvalidParams, \"'content' is required for update action\")\n\n    const memoryDir = await this.assertMemoryDirectory()\n    const factPath = await resolveFileCI(memoryDir, 'FACT.md')\n    await this.assertRegularFileOrMissing(factPath)\n\n    // Atomic write via temp file + rename\n    const tmpPath = path.join(memoryDir, `.FACT.md.${randomUUID()}.tmp`)\n    const handle = await open(tmpPath, 'wx', 0o600)\n    try {","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/mcp/servers/agentMemory.ts#L181-L217","documentation":"assertRegularFileOrMissing() is called before writing FACT.md or JOURNAL.jsonl. If the target exists and lstat shows it is not a regular file or is a symlink, it throws. This is a TOCTOU defense: it re-validates the file type immediately before the write, complementing the earlier resolveFileCI check.","triggerScenarios":"Between resolveFileCI and the write, the target file was replaced with a symlink, directory, or special file. The pre-write check at line 197-200 catches the swap.","commonSituations":"A race condition where another process swaps FACT.md for a symlink mid-operation; an adversarial local process attempting a TOCTOU attack; a sync tool creating symlinks during operation.","solutions":["Investigate what process replaced the file between resolution and write.","Remove the offending non-regular entry so the write can recreate a normal file.","Run the memory tool again after cleanup."],"exampleFix":"# before: FACT.md swapped to symlink mid-flight\nls -la memory/FACT.md  # symlink\n\n# after\nrm memory/FACT.md\n# re-run the update action; temp file + rename will recreate a regular file","handlingStrategy":"validation","validationCode":"import { lstat } from 'node:fs/promises'\n\nasync function isRegularOrMissing(p: string): Promise<boolean> {\n  try {\n    const s = await lstat(p)\n    return s.isFile() && !s.isSymbolicLink()\n  } catch (err) {\n    return (err as NodeJS.ErrnoException).code === 'ENOENT'\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat the pre-write assert as a TOCTOU defense; keep the window between resolve and write small.","Do not run external processes that swap memory files for symlinks during operation.","If you observe this in production, suspect an adversarial local process or a buggy sync tool."],"tags":["security","filesystem","toctou","symlink-guard","agent-memory"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}