{"record":{"id":"591e5f7573c9c6a4","repo":"jdx/mise","slug":"staged-blob-size-does-not-match-the-declared-cas-d","errorCode":null,"errorMessage":"staged blob size does not match the declared CAS digest","messagePattern":"staged blob size does not match the declared CAS digest","errorType":"exception","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"crates/mise-cache-core/src/local.rs","lineNumber":107,"sourceCode":"        if let Some(existing) = self.find(digest)? {\n            return Ok(existing);\n        }\n        let parent = destination.parent().expect(\"CAS path has a parent\");\n        fs::create_dir_all(parent)?;\n        let staging = tempfile::tempdir_in(parent)?;\n        let temporary = staging.path().join(\"blob\");\n        reflink_copy::reflink_or_copy(source, &temporary)?;\n        let temporary = tempfile::TempPath::try_from_path(temporary)?;\n        make_owner_writable(&temporary)?;\n        fs::OpenOptions::new()\n            .write(true)\n            .open(&temporary)?\n            .sync_all()?;\n        if verify && !digest.matches_file(&temporary)? {\n            bail!(\"staged blob does not match the declared CAS digest\");\n        }\n        if fs::metadata(&temporary)?.len() != digest.size {\n            bail!(\"staged blob size does not match the declared CAS digest\");\n        }\n        match temporary.persist_noclobber(&destination) {\n            Ok(()) => Ok(destination),\n            Err(error) if error.error.kind() == std::io::ErrorKind::AlreadyExists => self\n                .find(digest)?\n                .ok_or_else(|| eyre::eyre!(\"concurrent CAS write did not publish a valid blob\")),\n            Err(error) => Err(error.error.into()),\n        }\n    }\n\n    fn store_with(\n        &self,\n        digest: &CacheDigest,\n        write: impl FnOnce(&mut tempfile::NamedTempFile) -> Result<()>,\n    ) -> Result<PathBuf> {\n        let destination = self.path_for(digest)?;\n        if let Some(existing) = self.find(digest)? {\n            return Ok(existing);","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/crates/mise-cache-core/src/local.rs#L89-L125","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: size taken from a header, hash from the body\nlet digest = CacheDigest {\n    algorithm: \"blake3\".into(),\n    hash,\n    size: content_length_from_header,\n};\n\n// after: hash and size from the same content\nlet digest = CacheDigest::blake3_file(&path)?;","handlingStrategy":"validation","validationCode":"fn consistent_digest(path: &Path) -> eyre::Result<CacheDigest> {\n    // hash and size both derived from the same read\n    Ok(CacheDigest::blake3_file(path)?)\n}\n\n// reject hand-built digests whose size disagrees with a fresh read\nfn digest_is_consistent(digest: &CacheDigest, path: &Path) -> eyre::Result<bool> {\n    Ok(digest.matches_file(path)?)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["cas","digest-verification","size-mismatch","local-cache"],"backgroundTag":"content-length-mismatch","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}