jdx/mise · error

staged blob size does not match the declared CAS digest

Error message

staged blob size does not match the declared CAS digest

What it means

After staging a file into the CAS, the staged file's byte length must equal `digest.size`. This check primarily guards the pre-verified fast path (`store_verified_file`, used when this crate already verified the content earlier): if the source's length changed between that verification and the staging copy, publishing is refused rather than storing a truncated or extended blob under a wrong size. When full verification runs (`store_file`), the digest check already includes size and would fail first.

Solutions

  1. Only pass fully-written, quiesced files to the CAS; finalize outputs before caching them.
  2. Recompute the digest from the current file with `CacheDigest::blake3_file` and use the verifying `store_file` instead of a pre-verified path.
  3. Retry after the writer has finished.

Example fix

// before: digest verified earlier, file grew since - size check bails
cas.store_verified_file(&digest, &path);

// after: hash the current file and use the verifying store
let digest = CacheDigest::blake3_file(&path)?;
cas.store_file(&digest, &path)?;
Defensive patterns

Strategy: validation

Validate before calling

let len = std::fs::metadata(&source)?.len();
if len != digest.size {
    // the file changed since the digest was verified; recompute it
    let digest = CacheDigest::blake3_file(&source)?;
    return cas.store_file(&digest, &source);
}
cas.store_verified_file(&digest, &source)?;

Prevention

When it happens

Trigger: The source file is appended to or truncated between the crate's earlier verification pass and the staging copy on the verified fast path - e.g. a log or output file still being written when it reaches the CAS.

Common situations: Mutable build outputs passed to the verified store path; writers racing the cache layer; files on synchronizing network mounts changing length between passes.

Related errors


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

Appendix: source

Thrown at crates/mise-cache-core/src/local.rs:107

        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> {
        let destination = self.path_for(digest)?;
        if let Some(existing) = self.find(digest)? {
            return Ok(existing);

View on GitHub (pinned to 6f52dcdf99)