diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs index c1f5c37..f180760 100644 --- a/src-tauri/src/commands/ai/tool_registry.rs +++ b/src-tauri/src/commands/ai/tool_registry.rs @@ -361,12 +361,18 @@ pub fn build_ai_tool_registry(db: &Arc) -> AiToolRegistry { args["path"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 path 参数"))?, )?; let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?; - let metadata = tokio::fs::metadata(path).await + // 单次 File::open 取 metadata+read 消除 TOCTOU(FR-S2:原 metadata+read 两步 syscall 间 symlink 替换破 1MB 限) + use tokio::fs::File; + use tokio::io::AsyncReadExt; + let mut file = File::open(path).await .map_err(|e| anyhow::anyhow!("无法访问文件 {}: {}", path, e))?; + let metadata = file.metadata().await + .map_err(|e| anyhow::anyhow!("读取元数据失败 {}: {}", path, e))?; if metadata.len() > 1_048_576 { anyhow::bail!("文件超过 1MB 限制 ({} 字节)", metadata.len()); } - let content = tokio::fs::read_to_string(path).await + let mut content = String::new(); + file.read_to_string(&mut content).await .map_err(|e| anyhow::anyhow!("读取文件失败: {}", e))?; let result = if let Some(offset) = args["offset"].as_u64() { let lines: Vec<&str> = content.lines().collect(); @@ -406,6 +412,10 @@ pub fn build_ai_tool_registry(db: &Arc) -> AiToolRegistry { )?; let path = resolved.to_str().ok_or_else(|| anyhow::anyhow!("路径含非法字符"))?; let content = args["content"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 content 参数"))?; + // 写入大小上限(FR-S2:原无限制,LLM 经审批可写超大文件撑爆磁盘/对话历史) + if content.len() > 1_048_576 { + anyhow::bail!("写入内容超过 1MB 限制 ({} 字节)", content.len()); + } if let Some(parent) = std::path::Path::new(path).parent() { tokio::fs::create_dir_all(parent).await .map_err(|e| anyhow::anyhow!("创建目录失败: {}", e))?;