{"record":{"id":"6d0246d2a66bea5a","repo":"jdx/mise","slug":"remote-cache-symlink-target-must-be-relative","errorCode":null,"errorMessage":"remote cache symlink target must be relative","messagePattern":"remote cache symlink target must be relative","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/task/task_cache_store.rs","lineNumber":543,"sourceCode":"}\n\nfn validate_cache_name(name: &str) -> Result<()> {\n    let path = Path::new(name);\n    if name.is_empty()\n        || name == \".\"\n        || name == \"..\"\n        || name.contains(['/', '\\\\', '\\0'])\n        || path.components().count() != 1\n        || !matches!(path.components().next(), Some(Component::Normal(_)))\n    {\n        bail!(\"invalid remote cache path component\");\n    }\n    Ok(())\n}\n\nfn validate_cache_symlink_target(path: &Path, target: &Path) -> Result<()> {\n    if target.is_absolute() {\n        bail!(\"remote cache symlink target must be relative\");\n    }\n    let resolved = path.parent().unwrap_or(Path::new(\"\")).join(target);\n    let mut depth = 0_i64;\n    for component in resolved.components() {\n        match component {\n            Component::Normal(_) => depth += 1,\n            Component::ParentDir => depth -= 1,\n            Component::CurDir => {}\n            _ => bail!(\"remote cache symlink target is unsafe\"),\n        }\n        if depth < 0 {\n            bail!(\"remote cache symlink target escapes its output root\");\n        }\n    }\n    Ok(())\n}\n\nfn archive_to_cas(path: &Path, staging_dir: &Path) -> Result<(CacheDigest, Vec<BlobUpload>)> {","sourceCodeStart":525,"sourceCodeEnd":561,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/task/task_cache_store.rs#L525-L561","documentation":"validate_cache_symlink_target() is applied to every symlink mise stores in or restores from the task cache - both tar archive entries on upload and remote directory proto entries on restore. The first rule is that the target must be a relative path; an absolute target like \"/usr/bin/tool\" bails with 'remote cache symlink target must be relative'.","triggerScenarios":"A task output contains a symlink created with an absolute target (ln -s /absolute/path link) and the task gets cached: upload goes through archive_to_cas -> validate_cache_symlink_target; restore hits the same check on a directory proto's symlink target.","commonSituations":"Build tasks creating convenience symlinks to system tools or /tmp paths; node_modules/.bin-style links made with absolute paths; CI caches created on one machine and restored on another where the absolute path does not exist.","solutions":["Create the symlink with a target relative to the link's location","Exclude outputs containing absolute symlinks from the task's cache output roots","Delete the cached entry so the next run rebuilds it without the offending link","In scripts, prefer relative ln -s targets or copy the file instead"],"exampleFix":"# before\nln -s /opt/toolchain/bin/gcc ./gcc\n# after (relative to the link's location)\nln -s ../toolchain/bin/gcc ./gcc","handlingStrategy":"validation","validationCode":"fn output_tree_has_only_relative_symlinks(root: &Path) -> std::io::Result<bool> {\n    for entry in walkdir::WalkDir::new(root).follow_links(false) {\n        let entry = entry?;\n        if entry.file_type().is_symlink()\n            && std::fs::read_link(entry.path())?.is_absolute()\n        {\n            return Ok(false);\n        }\n    }\n    Ok(true)\n}\n\nassert!(output_tree_has_only_relative_symlinks(&out_dir)?);","typeGuard":"fn is_relative_symlink_target(link: &std::path::Path) -> bool {\n    std::fs::read_link(link)\n        .map(|t| !t.is_absolute())\n        .unwrap_or(true)\n}","tryCatchPattern":"match cache_commit(&task).await {\n    Ok(()) => (),\n    Err(err) if err.to_string().contains(\"symlink target must be relative\") => {\n        eprintln!(\"output contains an absolute symlink; excluding from cache\");\n        narrow_cache_roots(&task) // then rebuild\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Always create cached-output symlinks with targets relative to the link location","Lint task outputs for absolute symlinks before enabling remote cache in CI","Prefer copying binaries over symlinking them into cached output roots","Remember caches restore on other machines: absolute targets are wrong even when they pass"],"tags":["mise","task-cache","symlink","path-traversal","remote-cache"],"backgroundTag":"symlink-path-traversal","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}