jdx/mise · error
remote cache symlink target escapes its output root
Error message
remote cache symlink target escapes its output root
What it means
validate_cache_symlink_target walks the resolved target components keeping a running depth: +1 for Normal, -1 for ParentDir. If depth ever goes negative the target climbs above the output root and mise bails with 'remote cache symlink target escapes its output root' - the classic nested-dot-dot traversal.
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 9dcfcaa0dc)
Solutions
- Point the symlink at a path inside the output tree, relative to the link's location
- If the link must reach outside the output, exclude that output from the cache roots
- Purge the cached entry and let the task rebuild its outputs
Example fix
# before: link at out/a/b/link climbs above the output root ln -s ../../../vendor/lib out/a/b/link # after: target stays under the output root ln -s ../../vendor/lib out/a/b/link
Defensive patterns
Strategy: validation
Validate before calling
use std::path::{Component, Path};
fn symlink_target_escapes(link: &Path, target: &Path) -> bool {
if target.is_absolute() {
return true;
}
let resolved = link.parent().unwrap_or(Path::new("")).join(target);
let mut depth = 0i64;
for c in resolved.components() {
match c {
Component::Normal(_) => depth += 1,
Component::ParentDir => {
depth -= 1;
if depth < 0 {
return true;
}
}
Component::CurDir => {}
_ => return true,
}
}
false
}
assert!(!symlink_target_escapes(&link, &std::fs::read_link(&link)?)); Type guard
fn symlink_target_escapes(link: &std::path::Path, target: &std::path::Path) -> bool {
if target.is_absolute() {
return true;
}
let resolved = link.parent().unwrap_or(std::path::Path::new("")).join(target);
let mut depth = 0i64;
for c in resolved.components() {
match c {
std::path::Component::Normal(_) => depth += 1,
std::path::Component::ParentDir => {
depth -= 1;
if depth < 0 {
return true;
}
}
std::path::Component::CurDir => {}
_ => return true,
}
}
false
} Try / catch
match cache_commit(&task).await {
Ok(()) => (),
Err(err) if err.to_string().contains("symlink target escapes its output root") => {
purge_cache_entry(&task).await?;
exclude_output_root_and_rebuild(&task).await? // the link must leave the tree
}
Err(err) => return Err(err),
} Prevention
- Count dot-dot segments in symlink targets against the link's depth before caching outputs
- Keep cached output trees self-contained: no links reaching outside the root
- Purge the cache entry after fixing symlink structure so stale blobs are not restored
When it happens
Trigger: A symlink deeper in the cached tree whose target has more ".." components than the link has ancestors, e.g. a link at "a/b/link" with target "../../../outside", encountered during archive_to_cas upload or materialize_remote_tree restore.
Common situations: Symlinks pointing at the repository root or a sibling directory from a nested output; links generated from absolute paths by tools that relativize incorrectly; crafted malicious cache blobs.
Related errors
- remote cache symlink target must be relative
- remote cache path escapes its output root
- remote cache symlink target is unsafe
- path must not escape the task directory
- output path {} traverses symlink ancestor {}
AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17).
Data as JSON: /api/errors/566a47803ff462ef.
Report an issue: GitHub.