{"record":{"id":"4a26216331e626c1","repo":"Hmbown/CodeWhale","slug":"lsp-file-is-outside-the-workspace","errorCode":null,"errorMessage":"LSP file is outside the workspace","messagePattern":"LSP file is outside the workspace","errorType":"validation","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/lsp/mod.rs","lineNumber":228,"sourceCode":"    pub fn config(&self) -> &LspConfig {\n        &self.config\n    }\n\n    async fn read_workspace_text(&self, file: &Path) -> std::io::Result<String> {\n        let root = self.workspace.clone();\n        let requested = file.to_path_buf();\n        tokio::task::spawn_blocking(move || {\n            use std::io::Read;\n            // The caller may already have resolved the configured workspace\n            // alias (macOS /var -> /private/var, or a workspace-root symlink).\n            // Resolve only that authorized root, never the requested file:\n            // internal links must still be rejected by the confined opener.\n            let canonical_root = root.canonicalize()?;\n            let relative = requested\n                .strip_prefix(&root)\n                .or_else(|_| requested.strip_prefix(&canonical_root))\n                .map_err(|_| {\n                    std::io::Error::new(\n                        std::io::ErrorKind::PermissionDenied,\n                        \"LSP file is outside the workspace\",\n                    )\n                })?;\n            // Match the existing workspace file serving ceiling. Decode only\n            // bounded UTF-8 bytes from the same no-follow workspace opener.\n            const MAX_DOCUMENT_BYTES: u64 = 16 * 1024 * 1024;\n            let file = crate::fleet::files::WorkspaceFile::open(&canonical_root, relative, false)?;\n            let mut bytes = Vec::new();\n            file.open_file()?\n                .take(MAX_DOCUMENT_BYTES + 1)\n                .read_to_end(&mut bytes)?;\n            if bytes.len() as u64 > MAX_DOCUMENT_BYTES {\n                return Err(std::io::Error::new(\n                    std::io::ErrorKind::InvalidData,\n                    \"LSP file exceeds the document limit\",\n                ));\n            }","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/lsp/mod.rs#L210-L246","documentation":"The LSP workspace file reader confines every requested path to the workspace root. It tries to strip the workspace root (or its canonicalized form) from the requested absolute path; if the path is not under either, it raises PermissionDenied with this message. This is a path-traversal guard preventing LSP document requests from reading files outside the workspace.","triggerScenarios":"Calling read_workspace_text (directly or via diagnostics_for, diagnostics_for_custom, intelligence_at_revision, semantic_locations, diagnostics_for_paths) with an absolute path that is not under the workspace root or its canonical root.","commonSituations":"Passing a path containing ../ segments that escape the workspace; requesting a file from a sibling project; the LSP client sending a document URI that was resolved through a symlink outside the root.","solutions":["Ensure the requested path resolves under the workspace root; use path joining relative to root instead of ../ escapes.","Canonicalize your path first and verify it starts with the workspace root before calling.","If the file genuinely lives elsewhere, add that directory to the workspace or open a separate workspace rooted there."],"exampleFix":"// before\nread_workspace_text(\"/other/repo/src/main.rs\")\n// after\nlet p = root.join(\"src/main.rs\").canonicalize()?; // inside workspace\nread_workspace_text(&p)","handlingStrategy":"validation","validationCode":"let root = root.canonicalize()?;\nlet p = requested.canonicalize()?;\nif !p.starts_with(&root) { eprintln!(\"refusing: outside workspace\"); }","typeGuard":"fn in_workspace(root: &Path, p: &Path) -> bool {\n    p.canonicalize().map(|c| c.starts_with(root)).unwrap_or(false)\n}","tryCatchPattern":"match read_workspace_text(p) {\n    Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => eprintln!(\"outside workspace: {p:?}\"),\n    Err(e) => return Err(e),\n    Ok(text) => use_text(text),\n}","preventionTips":["Build LSP paths by joining onto the workspace root, never with ../ segments.","Canonicalize before passing absolute paths.","Be aware symlinks pointing out of the workspace will be rejected."],"tags":["lsp","path-traversal","security","workspace"],"backgroundTag":"path-traversal-blocked","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}