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

  1. Purge the affected action result and its blobs, then re-run to repopulate
  2. Verify the cache server serves the exact blob content for each requested digest
  3. Keep all writers on one pinned mise version
  4. 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

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


AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17). Data as JSON: /api/errors/f4d459e788c85c60. Report an issue: GitHub.