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

  1. Finish writing and close the source file before calling store_file
  2. Recompute the digest from the same file immediately before storing: CacheDigest::blake3_file(path)
  3. 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

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


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