jdx/mise · error · eyre::Report

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, store_file_inner checks the staged file's byte length equals digest.size. This fires when hash verification passed (or was skipped on the internal store_verified_file path) but the declared size disagrees with the bytes on disk — in practice a hand-built CacheDigest with an incorrect size field, or a source that mutated under the pre-verified fast path.

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 9dcfcaa0dc)

Solutions

  1. Always derive size from the same content as the hash — use CacheDigest::blake3()/blake3_file(), which set both consistently
  2. Never populate the size field from a separate source (headers, earlier metadata) than the bytes hashed
  3. Validate deserialized digests against the actual content before attempting to store them

Example fix

// before: size taken from a header, hash from the body
let digest = CacheDigest {
    algorithm: "blake3".into(),
    hash,
    size: content_length_from_header,
};

// after: hash and size from the same content
let digest = CacheDigest::blake3_file(&path)?;
Defensive patterns

Strategy: validation

Validate before calling

fn consistent_digest(path: &Path) -> eyre::Result<CacheDigest> {
    // hash and size both derived from the same read
    Ok(CacheDigest::blake3_file(path)?)
}

// reject hand-built digests whose size disagrees with a fresh read
fn digest_is_consistent(digest: &CacheDigest, path: &Path) -> eyre::Result<bool> {
    Ok(digest.matches_file(path)?)
}

Prevention

When it happens

Trigger: Constructing CacheDigest with a size taken from a different source than the hashed content (an HTTP Content-Length, a stat at a different time); the crate-internal store_verified_file racing a concurrent writer; unit conversions (KB vs bytes) when filling the field.

Common situations: Assembling digests from metadata headers rather than the payload; deserializing digests from a schema where size meant something else; partial downloads whose reported length differs from the bytes received.

Related errors


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