jdx/mise · error
remote cache directory graph contains a cycle
Error message
remote cache directory graph contains a cycle
What it means
materialize_remote_tree walks the remote cache's directory protos, carrying the set of ancestor digests for the current branch. If a fetched directory's digest is already among its own ancestors, the graph cycles and mise bails with 'remote cache directory graph contains a cycle'. This prevents infinite recursion and resource exhaustion from a corrupt or hostile CAS; the same digest appearing in two sibling branches is fine.
Source
Thrown at src/task/task_cache_store.rs:715
digest: CacheDigest,
executable: bool,
mode: u32,
},
Symlink {
mode: u32,
target: PathBuf,
},
}
async fn materialize_remote_tree(
store: &HttpTaskCacheStore,
root: &CacheDigest,
) -> Result<tempfile::NamedTempFile> {
let mut pending = vec![(PathBuf::new(), root.clone(), BTreeSet::new())];
let mut nodes = BTreeMap::<PathBuf, RestoredNode>::new();
while let Some((path, digest, mut ancestors)) = pending.pop() {
if !ancestors.insert(digest.clone()) {
bail!("remote cache directory graph contains a cycle");
}
let bytes = store.client.get_blob(&digest, DIRECTORY_MEDIA_TYPE).await?;
let directory: RemoteDirectory = serde_json::from_slice(&bytes)?;
if canonical_json(&serde_json::to_value(&directory)?)? != bytes {
bail!("remote cache directory is not canonical JSON");
}
if directory.version != 1 {
bail!("unsupported remote cache directory version");
}
let mut names = BTreeSet::new();
for directory in directory.directories {
validate_cache_name(&directory.name)?;
if !names.insert(directory.name.clone()) {
bail!("remote cache directory contains duplicate names");
}
let child = path.join(&directory.name);
validate_cache_path(&child)?;
nodes.insert(View on GitHub (pinned to 9dcfcaa0dc)
Solutions
- Purge the affected action result and its blobs, then re-run to repopulate
- Verify the cache server serves the exact blob content for each requested digest
- Keep all writers on one pinned mise version
- Inspect the failing digest (MISE_DEBUG=1) to confirm what the server actually returned
Defensive patterns
Strategy: fallback
Try / catch
match materialize_remote_tree(&store, &root).await {
Ok(tree) => tree,
Err(err) if err.to_string().contains("contains a cycle") => {
warn!("corrupt remote directory graph for {key}; running without cache");
purge_action_result(&key).await.ok();
run_task_uncached(&task).await?
}
Err(err) => return Err(err),
} Prevention
- Serve directory protos from content-addressed storage so digests cannot loop
- Treat cycle errors as cache corruption: purge the entry and rebuild locally
- Pin one mise version across all writers to a shared CAS
- Alert on repeated restore-integrity failures rather than retrying the same blob
When it happens
Trigger: A directory blob references, directly or transitively, one of its own ancestors' digests - corrupt storage serving the wrong blob for a digest, a broken cache server, or deliberately crafted data.
Common situations: Bit rot or key mapping errors on a misconfigured object store; a CAS proxy mapping many keys to one blob; tampering or pentest payloads against shared caches.
Related errors
- remote action result is missing its output root
- remote cache directory is not canonical JSON
- remote cache directory contains duplicate names
- unsupported remote cache client metadata version
- remote cache metadata kind is not a task
AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17).
Data as JSON: /api/errors/f4d459e788c85c60.
Report an issue: GitHub.