jdx/mise · error · eyre::Report
staged blob does not match the declared CAS digest
Error message
staged blob does not match the declared CAS digest
What it means
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.
Source
Thrown at crates/mise-cache-core/src/local.rs:104
verify: bool,
) -> Result<PathBuf> {
let destination = self.path_for(digest)?;
if let Some(existing) = self.find(digest)? {
return Ok(existing);
}
let parent = destination.parent().expect("CAS path has a parent");
fs::create_dir_all(parent)?;
let staging = tempfile::tempdir_in(parent)?;
let temporary = staging.path().join("blob");
reflink_copy::reflink_or_copy(source, &temporary)?;
let temporary = tempfile::TempPath::try_from_path(temporary)?;
make_owner_writable(&temporary)?;
fs::OpenOptions::new()
.write(true)
.open(&temporary)?
.sync_all()?;
if verify && !digest.matches_file(&temporary)? {
bail!("staged blob does not match the declared CAS digest");
}
if fs::metadata(&temporary)?.len() != digest.size {
bail!("staged blob size does not match the declared CAS digest");
}
match temporary.persist_noclobber(&destination) {
Ok(()) => Ok(destination),
Err(error) if error.error.kind() == std::io::ErrorKind::AlreadyExists => self
.find(digest)?
.ok_or_else(|| eyre::eyre!("concurrent CAS write did not publish a valid blob")),
Err(error) => Err(error.error.into()),
}
}
fn store_with(
&self,
digest: &CacheDigest,
write: impl FnOnce(&mut tempfile::NamedTempFile) -> Result<()>,
) -> Result<PathBuf> {View on GitHub (pinned to 9dcfcaa0dc)
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)
Example fix
// before: file may still be changing between hashing and storing let digest = CacheDigest::blake3_file(&path)?; cas.store_file(&digest, &path)?; // after: hash and store an immutable snapshot let bytes = std::fs::read(&path)?; let digest = CacheDigest::blake3(&bytes); cas.store_bytes(&digest, &bytes)?;
Defensive patterns
Strategy: validation
Validate before calling
fn store_file_when_quiet(cas: &LocalCas, path: &Path) -> eyre::Result<PathBuf> {
// snapshot first so writer races cannot split hash and content
let bytes = std::fs::read(path)?;
let digest = CacheDigest::blake3(&bytes);
cas.store_bytes(&digest, &bytes)
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- local CAS blob failed digest verification: {}
- bytes do not match the declared CAS digest
- staged blob size does not match the declared CAS digest
- unsupported remote cache digest algorithm
- invalid remote cache digest
AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17).
Data as JSON: /api/errors/9e86f8d79c092432.
Report an issue: GitHub.