{"record":{"id":"5a82ee2297408c84","repo":"Hmbown/CodeWhale","slug":"sub-agent-state-path-must-not-traverse-symlinks","errorCode":null,"errorMessage":"sub-agent state path must not traverse symlinks: {}","messagePattern":"sub-agent state path must not traverse symlinks: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/subagent/mod.rs","lineNumber":7747,"sourceCode":"        normalized\n    }\n}\n\nfn reject_root_relative_symlinks(root: &Path, path: &Path) -> Result<()> {\n    let relative = path.strip_prefix(root).map_err(|_| {\n        anyhow!(\n            \"sub-agent state path must stay within state root: {}\",\n            path.display()\n        )\n    })?;\n    let mut current = root.to_path_buf();\n    for component in relative.components() {\n        current.push(component.as_os_str());\n        let Ok(metadata) = fs::symlink_metadata(&current) else {\n            continue;\n        };\n        if metadata.file_type().is_symlink() {\n            return Err(anyhow!(\n                \"sub-agent state path must not traverse symlinks: {}\",\n                current.display()\n            ));\n        }\n    }\n    Ok(())\n}\n\nfn read_subagent_state_file(state_root: &Path, path: &Path) -> Result<String> {\n    let state_root = normalize_subagent_workspace(state_root);\n    reject_root_relative_symlinks(&state_root, path)?;\n    let metadata = fs::symlink_metadata(path)?;\n    let file_type = metadata.file_type();\n    if file_type.is_symlink() || !file_type.is_file() {\n        return Err(anyhow!(\n            \"sub-agent state path must be a regular file: {}\",\n            path.display()\n        ));","sourceCodeStart":7729,"sourceCodeEnd":7765,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/tools/subagent/mod.rs#L7729-L7765","documentation":"Walking the path component by component from the state root, `reject_root_relative_symlinks` found an existing component that is a symlink. The guard refuses to traverse any symlink inside a state path so a tampered link cannot redirect reads or writes outside the root, even when the final target looks contained.","triggerScenarios":"Any directory component of the state path replaced by a symlink: an attacker- or child-created link inside the state dir, a symlinked projects folder, or sync tools (Dropbox-style) materializing links.","commonSituations":"State directory inside a symlinked home or workspace; symlink-based version switching of state dirs; adversarial model output planting links; restoring state from archives that preserve links.","solutions":["Find the offending component (the error names the exact path) and replace the symlink with a real directory.","Move the state root to a path with no symlinked components (e.g. a direct path under ~/.local/share or an explicit non-linked dir).","When unpacking state archives, use flags that don't preserve symlinks for internal components."],"exampleFix":"# before: state root contains a symlink\nln -s /tmp/escape ~/.codewhale/agents\n# -> Err 1214 on next artifact read\n\n# after\nrm ~/.codewhale/agents && mkdir ~/.codewhale/agents","handlingStrategy":"validation","validationCode":"fn path_traverses_symlink(root: &Path, rel: &Path) -> bool {\n    let mut cur = root.to_path_buf();\n    for c in rel.components() {\n        cur.push(c.as_os_str());\n        if let Ok(m) = std::fs::symlink_metadata(&cur) {\n            if m.file_type().is_symlink() { return true; }\n        }\n    }\n    false\n}","typeGuard":"fn is_symlink_free(root: &Path, rel: &Path) -> bool {\n    !path_traverses_symlink(root, rel)\n}","tryCatchPattern":"match read_subagent_state_file(&state_root, &path) {\n    Err(e) if e.to_string().contains(\"must not traverse symlinks\") => { /* error names the component: replace it with a real dir */ }\n    r => r?,\n}","preventionTips":["Keep the entire state tree symlink-free; use real directories.","Avoid placing state under symlinked home/workspace dirs.","Unpack state archives without preserving internal symlinks."],"tags":["subagent","symlink","path-traversal","security","rust"],"backgroundTag":"symlink-path-traversal","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}