jdx/mise · error

staged blob does not match the declared CAS digest

Error message

staged blob does not match the declared CAS digest

What it means

`store_file` reflinks or copies the source into a staging temp file and then re-hashes the staged copy against the declared digest before publishing. This error means the staged bytes do not match: in practice the source file changed between when the digest was computed and when the copy was made.

Solutions

  1. Recompute the digest immediately before staging: `let digest = CacheDigest::blake3_file(&path)?; cas.store_file(&digest, &path)?` and ensure no writer is active.
  2. If the source may still be mutating, copy it to a stable location first, then hash and store the copy.
  3. Retry once the writer has finished; a racing writer that has since exited makes the next attempt succeed.

Example fix

// before: digest computed earlier, file changed since
cas.store_file(&stale_digest, &path)?; // bails

// after: hash the current file right before storing
let digest = CacheDigest::blake3_file(&path)?;
cas.store_file(&digest, &path)?;
Defensive patterns

Strategy: validation

Validate before calling

// hash the file right before staging so mutation windows are minimal
let digest = CacheDigest::blake3_file(&source)?;
if !digest.matches_file(&source)? {
    // source is still changing; wait for the writer to finish
    return wait_and_retry();
}
cas.store_file(&digest, &source)?;

Prevention

When it happens

Trigger: The digest was computed from a file that is concurrently rewritten (build output still being written by the build, another process touching the file), or it was computed from a different path than the one passed to `store_file`.

Common situations: Caching build artifacts while the build still holds the file open; hashing then copying with an intervening mutation; network filesystems with weak read consistency.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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

Appendix: 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 6f52dcdf99)