{"record":{"id":"9069c0b7d8509261","repo":"Hmbown/CodeWhale","slug":"built-in-plugin-path-may-not-be-a-symbolic-link-or-reparse-9069c0","errorCode":null,"errorMessage":"built-in plugin path may not be a symbolic link or reparse point: {path}","messagePattern":"built-in plugin path may not be a symbolic link or reparse point: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/plugins/builtin.rs","lineNumber":413,"sourceCode":"            MOVEFILE_WRITE_THROUGH,\n        )\n    }\n    .map_err(|_| io::Error::last_os_error())\n}\n\n#[cfg(not(any(target_os = \"macos\", target_os = \"linux\", windows)))]\nfn publish_snapshot(_source: &Path, _destination: &Path) -> io::Result<()> {\n    Err(io::Error::new(\n        io::ErrorKind::Unsupported,\n        \"atomic built-in snapshot publication is unsupported on this platform\",\n    ))\n}\n\n/// Refuse to write through a symbolic link or reparse point, the same rule\n/// [`super::discovery`] applies when it scans a plugin root.\nfn reject_symlink(path: &Path) -> io::Result<()> {\n    match fs::symlink_metadata(path) {\n        Ok(metadata) if metadata_is_link_or_reparse(&metadata) => Err(io::Error::new(\n            io::ErrorKind::InvalidInput,\n            format!(\n                \"built-in plugin path may not be a symbolic link or reparse point: {}\",\n                path.display()\n            ),\n        )),\n        Ok(_) => Ok(()),\n        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),\n        Err(error) => Err(error),\n    }\n}\n\n#[cfg(test)]\n#[path = \"builtin_tests.rs\"]\nmod snapshot_tests;\n\n#[cfg(test)]\nmod tests {","sourceCodeStart":395,"sourceCodeEnd":431,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/plugins/builtin.rs#L395-L431","documentation":"reject_symlink refuses to write through any path component target that is a symbolic link or reparse point, using symlink_metadata plus metadata_is_link_or_reparse. This mirrors discovery's rule when scanning plugin roots and prevents an attacker from redirecting built-in plugin writes (e.g. via a planted symlink) to arbitrary locations.","triggerScenarios":"materialize_at_home or write_bundle encounters a symlink/reparse point at a path it is about to create or write inside the built-in plugin snapshot tree.","commonSituations":"Someone (or a dotfile manager) replaced a plugin directory or file with a symlink; sync tools substituted links; a malicious repo planted links in a plugin path.","solutions":["Remove the symlink and let the plugin system recreate the real file/directory.","Exclude the plugin directory from symlink-creating tools (stow, chezmoi, Dropbox link substitution).","If you intentionally linked plugin storage, move the real storage location instead of linking inside the snapshot tree."],"exampleFix":"// before\nln -s /mnt/bigdisk/plugins ~/.local/share/codewhale/plugins/builtin\n// after\nrm ~/.local/share/codewhale/plugins/builtin\nmkdir ~/.local/share/codewhale/plugins/builtin   # real directory","handlingStrategy":"validation","validationCode":"let md = std::fs::symlink_metadata(path)?;\nif md.file_type().is_symlink() { eprintln!(\"{path:?} is a link; remove it before materializing plugins\"); }","typeGuard":"fn not_a_link(p: &Path) -> bool {\n    std::fs::symlink_metadata(p).map(|m| !m.file_type().is_symlink()).unwrap_or(true) // missing is fine, will be created\n}","tryCatchPattern":"match materialize_at_home() {\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidInput && e.to_string().contains(\"symbolic link\") => {\n        eprintln!(\"remove the symlink inside the plugin tree and retry\");\n    }\n    other => other?,\n}","preventionTips":["Never symlink anything inside the built-in plugin snapshot tree.","Configure dotfile managers and sync tools to leave the plugin directory alone.","Treat link rejection as a security feature — don't work around it by relaxing the check."],"tags":["plugins","symlink","security","filesystem"],"backgroundTag":"permission-denied","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T10:30:35.592Z"}