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
- Always derive size from the same content as the hash — use CacheDigest::blake3()/blake3_file(), which set both consistently
- Never populate the size field from a separate source (headers, earlier metadata) than the bytes hashed
- 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
- Never fill CacheDigest.size from a different source than the hashed bytes
- Prefer constructors (CacheDigest::blake3/blake3_file) over struct literals
- Cross-check deserialized digests with matches_file before storing
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
- local CAS blob failed digest verification: {}
- bytes do not match the declared CAS digest
- staged blob 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/591e5f7573c9c6a4.
Report an issue: GitHub.