{"record":{"id":"f4d459e788c85c60","repo":"jdx/mise","slug":"remote-cache-directory-graph-contains-a-cycle","errorCode":null,"errorMessage":"remote cache directory graph contains a cycle","messagePattern":"remote cache directory graph contains a cycle","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/task/task_cache_store.rs","lineNumber":715,"sourceCode":"        digest: CacheDigest,\n        executable: bool,\n        mode: u32,\n    },\n    Symlink {\n        mode: u32,\n        target: PathBuf,\n    },\n}\n\nasync fn materialize_remote_tree(\n    store: &HttpTaskCacheStore,\n    root: &CacheDigest,\n) -> Result<tempfile::NamedTempFile> {\n    let mut pending = vec![(PathBuf::new(), root.clone(), BTreeSet::new())];\n    let mut nodes = BTreeMap::<PathBuf, RestoredNode>::new();\n    while let Some((path, digest, mut ancestors)) = pending.pop() {\n        if !ancestors.insert(digest.clone()) {\n            bail!(\"remote cache directory graph contains a cycle\");\n        }\n        let bytes = store.client.get_blob(&digest, DIRECTORY_MEDIA_TYPE).await?;\n        let directory: RemoteDirectory = serde_json::from_slice(&bytes)?;\n        if canonical_json(&serde_json::to_value(&directory)?)? != bytes {\n            bail!(\"remote cache directory is not canonical JSON\");\n        }\n        if directory.version != 1 {\n            bail!(\"unsupported remote cache directory version\");\n        }\n        let mut names = BTreeSet::new();\n        for directory in directory.directories {\n            validate_cache_name(&directory.name)?;\n            if !names.insert(directory.name.clone()) {\n                bail!(\"remote cache directory contains duplicate names\");\n            }\n            let child = path.join(&directory.name);\n            validate_cache_path(&child)?;\n            nodes.insert(","sourceCodeStart":697,"sourceCodeEnd":733,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/task/task_cache_store.rs#L697-L733","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"match materialize_remote_tree(&store, &root).await {\n    Ok(tree) => tree,\n    Err(err) if err.to_string().contains(\"contains a cycle\") => {\n        warn!(\"corrupt remote directory graph for {key}; running without cache\");\n        purge_action_result(&key).await.ok();\n        run_task_uncached(&task).await?\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["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"],"tags":["mise","remote-cache","task-cache","cycle","integrity"],"backgroundTag":"circular-reference-detected","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}