{"record":{"id":"f0857f7d090f788d","repo":"jdx/mise","slug":"a-parent-directory-is-now-a-symlink-left-untouche","errorCode":null,"errorMessage":"a parent directory is now a symlink; left untouched","messagePattern":"a parent directory is now a symlink; left untouched","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/history/recovery.rs","lineNumber":115,"sourceCode":"                }\n            ))\n    {\n        bail!(\"directory contents cannot be verified safely; left untouched\");\n    }\n    validate_snapshot(state_dir, prior)?;\n    if PathState::observe(path) != *after {\n        bail!(\"changed while preparing recovery; left untouched\");\n    }\n    restore(state_dir, path, prior)\n}\n\nfn validate_destination(path: &Path) -> Result<()> {\n    if !path.is_absolute() || path.components().any(|c| matches!(c, Component::ParentDir)) {\n        bail!(\"invalid recovery destination\");\n    }\n    for parent in path.ancestors().skip(1) {\n        if std::fs::symlink_metadata(parent).is_ok_and(|meta| meta.is_symlink()) {\n            bail!(\"a parent directory is now a symlink; left untouched\");\n        }\n    }\n    Ok(())\n}\n\nfn validate_blob_id(hash: &str) -> Result<()> {\n    if hash.len() != 64 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {\n        bail!(\"invalid recovery content identifier\");\n    }\n    Ok(())\n}\n\npub(super) fn read_blob(state_dir: &Path, blob: &Blob) -> Result<Vec<u8>> {\n    use base64::Engine;\n    validate_blob_id(&blob.sha256)?;\n    let bytes = match &blob.inline {\n        Some(inline) => base64::engine::general_purpose::STANDARD.decode(inline)?,\n        None => {","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/history/recovery.rs#L97-L133","documentation":"During recovery validation the library walks every ancestor of the destination path and checks for symlinks via symlink_metadata. If any parent directory has become a symlink since the snapshot was recorded, the recovery is aborted and the parent left untouched, because writing through the symlink could redirect the restore to an attacker- or accident-controlled location. The message signals the snapshot destination's directory tree changed shape and recovery is intentionally skipped.","triggerScenarios":"Any recover_path/restore attempt where a directory in the destination's ancestor chain (e.g. `/a` in `/a/b/c/file`) is now a symlink, detected with std::fs::symlink_metadata during validate_destination.","commonSituations":"A user replaced a real directory with a symlink (e.g. moving data to another volume and symlinking); package managers or dotfile managers rewrote directories; tmpfs/var directories got re-pointed between snapshot and recovery.","solutions":["Inspect the destination's ancestors and replace the symlink with a real directory (or re-point the snapshot) before retrying recovery","Recreate the original directory layout that existed when the snapshot was taken","If the symlink is intentional, restore to a new non-symlink absolute path and copy results manually, or re-record the snapshot at the new location"],"exampleFix":"// before\n# /srv/data -> /mnt/big/data (symlink created after snapshot)\nrecover_path(Path::new(\"/srv/data/state\"), prior)?; // errors\n// after\nrm /srv/data && mkdir /srv/data   # make parents real directories again\nrecover_path(Path::new(\"/srv/data/state\"), prior)?;","handlingStrategy":"validation","validationCode":"use std::fs::symlink_metadata;\nfn parents_are_real(p: &std::path::Path) -> bool {\n    p.ancestors().skip(1).all(|a| {\n        symlink_metadata(a).map_or(true, |m| !m.is_symlink())\n    })\n}","typeGuard":null,"tryCatchPattern":"match recover_path(dest, prior) {\n    Err(e) if e.to_string().contains(\"now a symlink\") => {\n        eprintln!(\"resolve the symlinked parent before recovering: {e}\");\n    }\n    other => other?,\n}","preventionTips":["Avoid replacing snapshot-covered directories with symlinks (move data with bind mounts or config instead)","Re-checkpoint after any directory-tree restructuring so metadata reflects reality","Audit ancestor directories with symlink_metadata before running recovery"],"tags":["symlink","security","filesystem"],"backgroundTag":"symlink-parent-directory","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}