jdx/mise · error
remote cache symlink target escapes its output root
Error message
remote cache symlink target escapes its output root
What it means
After the depth walk, if the resolved symlink path goes above the output root (depth < 0, i.e. more '..' than normal components from the link's parent), the store rejects it. Such a symlink would escape the materialized artifact directory, so the entry is treated as unsafe.
Solutions
- Adjust the task to keep symlinks inside the output root (use relative targets that stay within the tree) and republish.
- Copy the linked resource into the output tree instead of symlinking outside it.
- Purge the offending remote cache entry and force a local rebuild.
- Audit the remote cache source/credentials if the entry was not produced by your own tasks.
Example fix
// before (inside out/) ln -s ../shared/config.json out/config.json // escapes output root // after cp ../shared/config.json out/config.json
Defensive patterns
Strategy: validation
Validate before calling
fn link_stays_in_root(parent: &std::path::Path, target: &std::path::Path) -> bool {
let mut depth: i64 = 0;
for c in parent.join(target).components() {
match c {
std::path::Component::Normal(_) => depth += 1,
std::path::Component::ParentDir => depth -= 1,
_ => {}
}
if depth < 0 { return false; }
}
true
} Type guard
fn resolves_inside_root(parent: &std::path::Path, target: &std::path::Path) -> bool {
parent.join(target).components()
.scan(0i64, |d, c| {
match c {
std::path::Component::Normal(_) => *d += 1,
std::path::Component::ParentDir => *d -= 1,
_ => {}
}
Some(*d)
}).all(|d| d >= 0)
} Try / catch
match validate_cache_symlink_target(&link, &target) {
Err(e) if e.to_string().contains("escapes its output root") => {
// purge entry and rebuild locally
}
other => other?,
} Prevention
- Keep all symlink targets within the task's output directory.
- Copy out-of-tree dependencies into the output instead of linking upward.
- Only trust cache entries from authenticated, controlled servers.
- Scan packaged trees for upward links before publishing.
When it happens
Trigger: archive_to_cas() or materialize_remote_tree() validates a symlink like target "../../../../etc/passwd" (or any target whose parent-relative resolution crosses the root) (task_cache_store.rs:555).
Common situations: Tasks that create upward-relative symlinks outside their output directory; malicious or compromised remote cache servers; archives of trees where links point to sibling build directories.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- brew-cask: refusing generic artifact source outside the…
- brew-cask: refusing generic artifact source outside the…
- brew-cask: staged symlink path escaped extraction root
- brew-cask: staged symlink path escaped extraction root
- expected symlinked appdir tail to be rejected
AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17).
Data as JSON: /api/errors/566a47803ff462ef.
Report an issue: GitHub.
Appendix: source
Thrown at src/task/task_cache_store.rs:555
}
Ok(())
}
fn validate_cache_symlink_target(path: &Path, target: &Path) -> Result<()> {
if target.is_absolute() {
bail!("remote cache symlink target must be relative");
}
let resolved = path.parent().unwrap_or(Path::new("")).join(target);
let mut depth = 0_i64;
for component in resolved.components() {
match component {
Component::Normal(_) => depth += 1,
Component::ParentDir => depth -= 1,
Component::CurDir => {}
_ => bail!("remote cache symlink target is unsafe"),
}
if depth < 0 {
bail!("remote cache symlink target escapes its output root");
}
}
Ok(())
}
fn archive_to_cas(path: &Path, staging_dir: &Path) -> Result<(CacheDigest, Vec<BlobUpload>)> {
file::create_dir_all(staging_dir)?;
let decoder = zstd::Decoder::new(File::open(path)?)?;
let mut archive = Archive::new(decoder);
let mut nodes = BTreeMap::<PathBuf, ArchiveNode>::new();
nodes.insert(PathBuf::new(), ArchiveNode::Directory { mode: 0o755 });
for entry in archive.entries()? {
let mut entry = entry?;
let entry_path = entry.path()?.into_owned();
validate_cache_path(&entry_path)?;
let mode = entry.header().mode() & 0o7777;
let entry_type = entry.entry_type();View on GitHub (pinned to 533346cc37)