jdx/mise · error
remote cache symlink target is unsafe
Error message
remote cache symlink target is unsafe
What it means
After the relative check, validate_cache_symlink_target resolves the target against the link's parent directory and walks its components. Only Normal, ParentDir, and CurDir components are allowed; anything else - in practice a Windows drive Prefix such as "C:libs" or a stray RootDir - bails with 'remote cache symlink target is unsafe'.
Source
Thrown at src/task/task_cache_store.rs:552
|| !matches!(path.components().next(), Some(Component::Normal(_)))
{
bail!("invalid remote cache path component");
}
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();View on GitHub (pinned to 9dcfcaa0dc)
Solutions
- Rewrite the symlink target as a plain relative path without a drive prefix
- Avoid caching outputs that contain drive-relative links
- Purge the cached entry and let the task rebuild the outputs cleanly
Example fix
# before: target carries a drive prefix (C:libs) link -> C:libs\tool # after: plain relative target link -> libs/tool
Defensive patterns
Strategy: validation
Validate before calling
use std::path::{Component, Path};
fn symlink_target_is_safe(target: &Path) -> bool {
!target.is_absolute()
&& target.components().all(|c| matches!(
c,
Component::Normal(_) | Component::ParentDir | Component::CurDir
))
}
assert!(symlink_target_is_safe(&std::fs::read_link(link)?)); Type guard
fn symlink_target_is_safe(target: &std::path::Path) -> bool {
!target.is_absolute()
&& target.components().all(|c| matches!(
c,
std::path::Component::Normal(_)
| std::path::Component::ParentDir
| std::path::Component::CurDir
))
} Try / catch
match restore_or_commit(&task).await {
Ok(v) => v,
Err(err) if err.to_string().contains("symlink target is unsafe") => {
purge_cache_entry(&task).await?; // rebuild without the drive-prefixed link
run_uncached(&task).await?
}
Err(err) => return Err(err),
} Prevention
- Never put drive-prefixed relative paths (C:foo) in symlink targets
- Test cache restore on the same OS family that produced the cache
- Purge shared caches when changing how task outputs build symlinks
When it happens
Trigger: A symlink target like "C:libs\x" (a drive-relative Windows path: not absolute, so it passes the first check, but it parses with a Prefix component) stored in a cached archive or in a remote directory proto's symlinks list.
Common situations: Windows build outputs with drive-relative symlink targets; caches produced by tooling that stores Windows targets verbatim; cross-OS sharing of one remote cache between Windows and POSIX machines.
Related errors
- remote cache symlink target must be relative
- remote cache symlink target escapes its output root
- OCI system package layers with symlinks require a unix host
- 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/84cd949d47f5354e.
Report an issue: GitHub.