{"record":{"id":"744fd1520f4da4e7","repo":"jdx/mise","slug":"blob-digest-mismatch-got-actual-expected-dige","errorCode":null,"errorMessage":"blob digest mismatch: got {actual}, expected {digest}","messagePattern":"blob digest mismatch: got (.+?), expected (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/oci/layout.rs","lineNumber":71,"sourceCode":"\n    /// Copy a blob into the layout by its known digest.\n    ///\n    /// Two guards:\n    ///  1. The digest must be `sha256:` followed by 64 lowercase hex chars.\n    ///     This prevents path traversal (e.g. a malicious registry returning\n    ///     `sha256:../../etc/passwd` would otherwise let us write attacker-\n    ///     controlled bytes to an arbitrary filesystem path).\n    ///  2. We verify `sha256(bytes) == digest` before writing so corrupted\n    ///     or tampered content surfaces with a clear \"got X, wanted Y\"\n    ///     message instead of later as a confusing mismatch from skopeo /\n    ///     podman.\n    pub(crate) fn write_blob_with_digest(&self, digest: &str, bytes: &[u8]) -> Result<()> {\n        validate_sha256_digest(digest)?;\n        let mut h = Sha256::new();\n        h.update(bytes);\n        let actual = format!(\"sha256:{}\", crate::oci::layer::hex_encode(&h.finalize()));\n        if actual != digest {\n            eyre::bail!(\"blob digest mismatch: got {actual}, expected {digest}\");\n        }\n        let path = self.blob_path(digest);\n        if !path.exists() {\n            file::write(&path, bytes)?;\n        }\n        Ok(())\n    }\n\n    pub(crate) fn blob_path(&self, digest: &str) -> PathBuf {\n        let hex = digest.trim_start_matches(\"sha256:\");\n        self.root.join(\"blobs/sha256\").join(hex)\n    }\n\n    pub(crate) fn read_blob(&self, digest: &str) -> Result<Vec<u8>> {\n        // Validate before turning the digest into a path component — a crafted\n        // layout (`mise oci push/run --image-dir <untrusted>`) could otherwise\n        // use `sha256:../../etc/passwd` to read outside the blobs directory.\n        validate_sha256_digest(digest)?;","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/oci/layout.rs#L53-L89","documentation":"`write_blob_with_digest` in src/oci/layout.rs verifies that the blob bytes it is about to write hash (SHA-256) to the digest the caller claims. If the computed digest differs from `digest`, it refuses to write, protecting the OCI layout from corrupt or substituted content pulled from a registry.","triggerScenarios":"Calling `write_blob_with_digest(digest, bytes)` where `bytes` do not hash to `digest` — e.g. a registry returned a different blob than advertised, the download was truncated/corrupted, or the caller mixed up blobs (wrote layer bytes under the manifest's digest).","commonSituations":"`pull_base_image` downloading layers over a flaky/middleboxed network; a compromised or misbehaving registry returning wrong content; CDN caching bugs; caller-side bookkeeping bugs passing the wrong digest for the bytes.","solutions":["Re-pull the blob from the registry (delete any partially cached content first) and retry","Verify the digest you pass matches the blob you downloaded (recompute sha256 over the exact bytes received)","Check the registry/CDN for corruption; try a different mirror or proxy","Fix caller bookkeeping so each digest is paired with its own bytes"],"exampleFix":"// before\nlayout.write_blob_with_digest(layer_digest, &manifest_bytes)?; // wrong bytes for this digest\n// after\nlet blob_bytes = registry.fetch_blob(layer_digest).await?;\nlayout.write_blob_with_digest(&layer_digest, &blob_bytes)?;","handlingStrategy":"validation","validationCode":"fn sha256_hex(bytes: &[u8]) -> String {\n    let mut h = Sha256::new(); h.update(bytes);\n    format!(\"sha256:{}\", hex::encode(h.finalize()))\n}\nassert_eq!(sha256_hex(&bytes), digest, \"refusing to write mismatched blob\");","typeGuard":null,"tryCatchPattern":"match layout.write_blob_with_digest(&digest, &bytes) {\n    Ok(()) => /* ... */,\n    Err(e) if e.to_string().contains(\"digest mismatch\") => {\n        // discard cached bytes and re-pull from registry\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Recompute the digest over received bytes before writing","Never reuse a digest variable across different blobs","Re-download blobs on mismatch instead of retrying writes"],"tags":["oci","sha256","integrity"],"backgroundTag":"checksum-mismatch","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}