{"record":{"id":"53f01f0c08817a97","repo":"astrid-runtime/astrid","slug":"workspace-path-must-not-contain-redirects-or-unexp","errorCode":null,"errorMessage":"workspace path must not contain redirects or unexpected file types: {}","messagePattern":"workspace path must not contain redirects or unexpected file types: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-core/src/workspace_security.rs","lineNumber":199,"sourceCode":"\n        let mut current = self.state_dir.clone();\n        for (index, component) in components.iter().enumerate() {\n            let Component::Normal(component) = component else {\n                unreachable!(\"components validated above\")\n            };\n            current.push(component);\n            let metadata = match std::fs::symlink_metadata(&current) {\n                Ok(metadata) => metadata,\n                Err(error) if error.kind() == io::ErrorKind::NotFound => continue,\n                Err(error) => return Err(error),\n            };\n            let final_component = index == components.len().saturating_sub(1);\n            let expected_file = final_component && kind == DescendantKind::File;\n            if metadata.file_type().is_symlink()\n                || (expected_file && !metadata.is_file())\n                || (!expected_file && !metadata.is_dir())\n            {\n                return Err(io::Error::new(\n                    io::ErrorKind::InvalidInput,\n                    format!(\n                        \"workspace path must not contain redirects or unexpected file types: {}\",\n                        current.display()\n                    ),\n                ));\n            }\n            if std::fs::canonicalize(&current)? != current {\n                return Err(io::Error::new(\n                    io::ErrorKind::InvalidInput,\n                    format!(\n                        \"workspace path redirects from its selected target: {}\",\n                        current.display()\n                    ),\n                ));\n            }\n        }\n        Ok(self.state_dir.join(relative))","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-core/src/workspace_security.rs#L181-L217","documentation":"While walking each component of the descendant path, resolve_descendant stats each intermediate/current path and rejects it with InvalidInput if it is a symlink, if the final component is not a regular file when a file was requested, or if any non-final component is not a directory. This enforces that the resolved route contains no redirects and matches the requested kind.","triggerScenarios":"resolve_file called on a path whose final component is a directory (or vice versa resolve_directory on a file); any component of the path being a symlink; intermediate components that are regular files (e.g. treating \"a.txt/b\" style paths).","commonSituations":"Caller assuming a file exists but a directory with the same name is present (or the file was replaced by a symlink); symlinked dotfile setups (e.g. dotfiles managed with symlinks into the workspace); race where another process swaps a directory for a symlink mid-walk.","solutions":["Ensure the final component's type matches the call: use resolve_file for files and resolve_directory for directories.","Replace symlinks inside the workspace with real files/directories.","Check what exists at the reported path (ls -la) and reconcile it with what the code expects.","If symlinked config is intentional, copy the file into the workspace instead of linking it."],"exampleFix":"// before\nlet f = ws.resolve_file(Path::new(\"config/settings.toml\"))?; // settings.toml is actually a directory\n// after\nlet meta = std::fs::metadata(root.join(\"config/settings.toml\"))?;\nlet entry = if meta.is_file() {\n    ws.resolve_file(Path::new(\"config/settings.toml\"))?\n} else {\n    ws.resolve_directory(Path::new(\"config/settings.toml\"))?\n};","handlingStrategy":"validation","validationCode":"fn entry_matches(p: &Path, want_file: bool) -> bool {\n    match std::fs::metadata(p) {\n        Ok(m) => !m.file_type().is_symlink() && if want_file { m.is_file() } else { m.is_dir() },\n        Err(_) => false,\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Match the API to intent: resolve_file only for real files, resolve_directory only for dirs","Avoid symlink-farm layouts (e.g. symlinked dotfiles) inside workspaces","Re-check path types when another process may mutate the tree concurrently"],"tags":["security","symlink","filesystem","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}