{"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":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/subagent/mod.rs","lineNumber":8899,"sourceCode":"                    {\n                        return Err(anyhow!(\n                            \"sub-agent state path must stay within state root: {}\",\n                            path.display()\n                        ));\n                    }\n                }\n            };\n            (resolved_root, relative)\n        }\n    };\n    let mut current = walk_root;\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\n/// Maximum bytes read from a sub-agent state or transcript artifact file.\n/// Generous for long session histories; anything larger is corrupt.\nconst MAX_SUBAGENT_STATE_BYTES: u64 = 16 * 1024 * 1024;\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() {","sourceCodeStart":8881,"sourceCodeEnd":8917,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/tools/subagent/mod.rs#L8881-L8917","documentation":"Even after top-level containment passes, the library walks each component of the path relative to the state root with `fs::symlink_metadata`, rejecting the operation if any intermediate component is itself a symlink. This closes the TOCTOU-style hole where an in-root symlink redirects deeper segments outside the state root.","triggerScenarios":"Any sub-agent state read/write where a directory component between the state root and the final file is a symlink — e.g. `state/link/session.json` where `link` is a symlink.","commonSituations":"Users symlink state subdirectories into dotfile-managed or shared locations; container images replace state dirs with symlinks to volumes; backup/restore tooling replaced directories with links.","solutions":["Replace the intermediate symlink with a real directory (copy contents back into the state root).","If using dotfile managers (stow, chezmoi, GNU stow layouts), configure them not to symlink inside the state root.","Bind-mount or copy the external directory into the state root instead of linking.","Identify the offending component from the error's displayed path and remove that link."],"exampleFix":"// before: symlinked subdirectory inside state root\nln -s /var/lib/shared state/sessions\n// after\nmkdir state/sessions && cp /var/lib/shared/* state/sessions/","handlingStrategy":"validation","validationCode":"use std::fs;\nfn no_symlink_components(root: &std::path::Path, p: &std::path::Path) -> bool {\n    let mut cur = root.to_path_buf();\n    for comp in p.strip_prefix(root).unwrap_or(p).components() {\n        cur.push(comp.as_os_str());\n        if fs::symlink_metadata(&cur).map(|m| m.file_type().is_symlink()).unwrap_or(false) {\n            return false;\n        }\n    }\n    true\n}\n// guard: no_symlink_components(&state_root, &path)","typeGuard":null,"tryCatchPattern":"match load_state(root, path) {\n    Ok(v) => v,\n    Err(e) if e.to_string().contains(\"must not traverse symlinks\") => {\n        eprintln!(\"symlinked component in state path: {e}\");\n        // replace the symlink with a real directory, then retry once\n        Default::default()\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Configure dotfile managers (stow/chezmoi) to copy, not symlink, under the state root.","Bind-mount external directories instead of linking them into the state root.","Scan the state root periodically for symlinked directories."],"tags":["subagent","symlink","security","path-traversal"],"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"}