{"record":{"id":"9e86f8d79c092432","repo":"jdx/mise","slug":"staged-blob-does-not-match-the-declared-cas-digest","errorCode":null,"errorMessage":"staged blob does not match the declared CAS digest","messagePattern":"staged blob does not match the declared CAS digest","errorType":"exception","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"crates/mise-cache-core/src/local.rs","lineNumber":104,"sourceCode":"        verify: bool,\n    ) -> Result<PathBuf> {\n        let destination = self.path_for(digest)?;\n        if let Some(existing) = self.find(digest)? {\n            return Ok(existing);\n        }\n        let parent = destination.parent().expect(\"CAS path has a parent\");\n        fs::create_dir_all(parent)?;\n        let staging = tempfile::tempdir_in(parent)?;\n        let temporary = staging.path().join(\"blob\");\n        reflink_copy::reflink_or_copy(source, &temporary)?;\n        let temporary = tempfile::TempPath::try_from_path(temporary)?;\n        make_owner_writable(&temporary)?;\n        fs::OpenOptions::new()\n            .write(true)\n            .open(&temporary)?\n            .sync_all()?;\n        if verify && !digest.matches_file(&temporary)? {\n            bail!(\"staged blob does not match the declared CAS digest\");\n        }\n        if fs::metadata(&temporary)?.len() != digest.size {\n            bail!(\"staged blob size does not match the declared CAS digest\");\n        }\n        match temporary.persist_noclobber(&destination) {\n            Ok(()) => Ok(destination),\n            Err(error) if error.error.kind() == std::io::ErrorKind::AlreadyExists => self\n                .find(digest)?\n                .ok_or_else(|| eyre::eyre!(\"concurrent CAS write did not publish a valid blob\")),\n            Err(error) => Err(error.error.into()),\n        }\n    }\n\n    fn store_with(\n        &self,\n        digest: &CacheDigest,\n        write: impl FnOnce(&mut tempfile::NamedTempFile) -> Result<()>,\n    ) -> Result<PathBuf> {","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/crates/mise-cache-core/src/local.rs#L86-L122","documentation":"LocalCas::store_file stages a copy (reflink or full copy) of the source file, then re-verifies the staged copy against the digest before atomically publishing it. A mismatch almost always means the source file changed while it was being read — a time-of-check/time-of-use race — or the digest was computed from different content entirely.","triggerScenarios":"The source file is still being written while store_file runs (build output streaming, logs appending); the digest was computed earlier from an older version of the file; reflink sharing blocks that the writer then mutates in place.","commonSituations":"Caching build artifacts before the build step has finished closing its outputs; directory watchers racing the writer; hashing at pipeline start but storing at the end.","solutions":["Finish writing and close the source file before calling store_file","Recompute the digest from the same file immediately before storing: CacheDigest::blake3_file(path)","If the writer cannot be stopped, copy to a stable location first and hash/store from the copy (or read into memory and use store_bytes)"],"exampleFix":"// before: file may still be changing between hashing and storing\nlet digest = CacheDigest::blake3_file(&path)?;\ncas.store_file(&digest, &path)?;\n\n// after: hash and store an immutable snapshot\nlet bytes = std::fs::read(&path)?;\nlet digest = CacheDigest::blake3(&bytes);\ncas.store_bytes(&digest, &bytes)?;","handlingStrategy":"validation","validationCode":"fn store_file_when_quiet(cas: &LocalCas, path: &Path) -> eyre::Result<PathBuf> {\n    // snapshot first so writer races cannot split hash and content\n    let bytes = std::fs::read(path)?;\n    let digest = CacheDigest::blake3(&bytes);\n    cas.store_bytes(&digest, &bytes)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only store files whose writer has finished and closed them","Hash with CacheDigest::blake3_file immediately before store_file, not earlier in the pipeline","For hot files, snapshot to memory or a stable copy first"],"tags":["cas","local-cache","race-condition","digest-verification"],"backgroundTag":"checksum-verification-failed","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}