{"record":{"id":"5486c1195450cc41","repo":"Hmbown/CodeWhale","slug":"sub-agent-state-path-must-stay-within-state-root","errorCode":null,"errorMessage":"sub-agent state path must stay within state root: {}","messagePattern":"sub-agent state path must stay within state root: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/subagent/mod.rs","lineNumber":7691,"sourceCode":"    let absolute = if path.is_absolute() {\n        path.to_path_buf()\n    } else {\n        state_root.join(path)\n    };\n    let file_name = absolute\n        .file_name()\n        .ok_or_else(|| anyhow!(\"sub-agent state path must include a file name\"))?;\n    let parent = absolute\n        .parent()\n        .ok_or_else(|| anyhow!(\"sub-agent state path must include a parent directory\"))?;\n    let parent = match parent.canonicalize() {\n        Ok(parent) => parent,\n        Err(err) if err.kind() == std::io::ErrorKind::NotFound => normalize_path_components(parent),\n        Err(err) => return Err(err.into()),\n    };\n    let state_path = parent.join(file_name);\n    if !state_path.starts_with(&state_root) {\n        return Err(anyhow!(\n            \"sub-agent state path must stay within state root: {}\",\n            state_path.display()\n        ));\n    }\n    reject_root_relative_symlinks(&state_root, &state_path)?;\n    Ok(state_path)\n}\n\nfn normalize_subagent_workspace(workspace: &Path) -> PathBuf {\n    if let Ok(canonical) = workspace.canonicalize() {\n        return canonical;\n    }\n    let absolute = if workspace.is_absolute() {\n        workspace.to_path_buf()\n    } else {\n        std::env::current_dir()\n            .unwrap_or_else(|_| PathBuf::from(\".\"))\n            .join(workspace)","sourceCodeStart":7673,"sourceCodeEnd":7709,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/tools/subagent/mod.rs#L7673-L7709","documentation":"`checked_subagent_state_path` canonicalizes (or component-normalizes) the parent directory, rejoins the file name, and requires the result to start with the normalized state root. This error means the fully resolved path escapes the state root — typically \"..\" traversal or an absolute parent outside the root. It is a fail-closed path-containment guard against model-supplied or tampered paths.","triggerScenarios":"A relative path with ../ segments that resolves above the state root; an absolute path pointing elsewhere on disk; a parent that canonicalizes (via existing prefix) outside the root.","commonSituations":"Prompt-injected or hallucinated paths from a model; state dirs relocated while old absolute paths persisted; tools forwarding user input unfiltered.","solutions":["Strip leading ../ and reject any relative path that cannot be contained under the state root before calling.","Always pass paths relative to the manager's state root; never absolute external paths.","Treat the error as hostile-input signal: audit where the path string came from rather than just fixing the path."],"exampleFix":"// before\nchecked_subagent_state_path(root, Path::new(\"../../etc/passwd\"))?; // Err 1212\n\n// after: sanitize to a contained relative path\nlet rel = rel.replace('..', \"_\");\nchecked_subagent_state_path(root, &root.join(rel))?;","handlingStrategy":"validation","validationCode":"fn contained_rel_path(state_root: &Path, rel: &str) -> Option<PathBuf> {\n    let p = state_root.join(rel);\n    let norm = p.components().collect::<PathBuf>(); // squashes ..\n    norm.starts_with(state_root).then_some(norm)\n}","typeGuard":"fn is_contained(state_root: &Path, candidate: &Path) -> bool {\n    candidate.starts_with(state_root)\n        && !candidate.components().any(|c| matches!(c, std::path::Component::ParentDir))\n}","tryCatchPattern":"match checked_subagent_state_path(&state_root, path) {\n    Err(e) if e.to_string().contains(\"within state root\") => { /* treat as hostile input: reject and audit source */ }\n    r => r?,\n}","preventionTips":["Only pass relative paths; resolve them under the manager's own state root.","Strip or reject '..' segments at the input boundary.","Treat escape attempts from model output as a security signal."],"tags":["subagent","path-traversal","security","rust"],"backgroundTag":"path-traversal-blocked","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}