{"record":{"id":"81c0d9a368d680b5","repo":"Hmbown/CodeWhale","slug":"fleet-artifact-size-changed-while-being-read","errorCode":null,"errorMessage":"Fleet artifact size changed while being read","messagePattern":"Fleet artifact size changed while being read","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/fleet/artifacts.rs","lineNumber":84,"sourceCode":"    let mut hasher = Sha256::new();\n    let mut preview = Vec::new();\n    let mut buffer = [0_u8; 8192];\n    let mut total = 0_u64;\n    // The digest and returned preview consume exactly the same bytes from the\n    // same opened file. A changed/replaced pathname is never reopened for data.\n    let mut reader = (&file).take(size + 1);\n    loop {\n        let count = reader.read(&mut buffer)?;\n        if count == 0 {\n            break;\n        }\n        total += count as u64;\n        ensure!(total <= size, \"Fleet artifact grew while being read\");\n        hasher.update(&buffer[..count]);\n        let remaining = preview_limit.saturating_sub(preview.len() as u64) as usize;\n        preview.extend_from_slice(&buffer[..count.min(remaining)]);\n    }\n    ensure!(\n        total == size && file.metadata()?.len() == size,\n        \"Fleet artifact size changed while being read\"\n    );\n    ensure!(\n        format!(\"sha256:{}\", crate::hashing::hex_bytes(hasher.finalize())) == checksum,\n        \"Fleet artifact checksum does not match the recorded receipt\"\n    );\n    Ok((preview, size))\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use codewhale_protocol::fleet::FleetArtifactKind;\n\n    fn reference(path: &str, bytes: &[u8]) -> FleetArtifactRef {\n        FleetArtifactRef {\n            kind: FleetArtifactKind::Receipt,","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/fleet/artifacts.rs#L66-L102","documentation":"After streaming the whole file, read_verified re-checks that both the bytes read and a fresh metadata().len() equal the size captured at open. If either disagrees, the file's size changed during the read (truncated, rewritten, or replaced) and the computed digest cannot represent a stable artifact, so verification fails.","triggerScenarios":"The artifact file is modified (shrunk via truncation/rewrite, or replaced via rename) between the initial metadata read and the post-read metadata check inside read_verified.","commonSituations":"A concurrent process rewriting the artifact in place; an external cleanup job truncating 'stale' files mid-verification; two readers/writers racing without the immutability contract being enforced.","solutions":["Eliminate concurrent mutation: artifacts must be immutable after publish; stop any rewriter/truncator.","Retry verification once the workspace is quiescent and confirm the receipt's size matches.","Re-publish the artifact (new receipt) if the new content is legitimate, then verify against the new FleetArtifactRef.","Restore the original bytes if the file was corrupted or truncated by an external job."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"match read_verified(ws, &artifact, preview_limit) {\n    Err(e) if e.to_string().contains(\"size changed while being read\") => {\n        eprintln!(\"artifact mutated during read; quiesce writers and retry\");\n    }\n    other => other?,\n}","preventionTips":["Disable cleanup/truncation jobs against the Fleet workspace during verification.","Serialize publication and verification per artifact path.","Re-publish and re-capture the receipt if content legitimately changed."],"tags":["fleet","artifacts","concurrency","integrity"],"backgroundTag":"checksum-mismatch","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}