{"record":{"id":"bfb0a57cb3b7a122","repo":"Hmbown/CodeWhale","slug":"fleet-artifact-grew-while-being-read","errorCode":null,"errorMessage":"Fleet artifact grew while being read","messagePattern":"Fleet artifact grew while being read","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/fleet/artifacts.rs","lineNumber":79,"sourceCode":"    );\n    let checksum = artifact\n        .checksum\n        .as_deref()\n        .context(\"Fleet artifact has no recorded checksum\")?;\n    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::*;","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/fleet/artifacts.rs#L61-L97","documentation":"During streaming verification, read_verified accumulates the byte count and asserts it never exceeds the size captured from file metadata at open time. If the file grew while being read (bytes appended between the metadata read and the streaming loop), verification aborts rather than hashing a moving target.","triggerScenarios":"Another process/thread appends to the artifact file while read_verified is streaming it — total bytes read exceed the size recorded at open.","commonSituations":"A concurrent writer still flushing output while an evidence reader starts; a background job appending logs to the artifact path; racing publications that ignore the immutable-publication contract.","solutions":["Ensure publication is complete before verification — coordinate with the writer or wait for the run to settle.","Stop any process appending to files under the Fleet workspace; artifacts are immutable once published.","Re-run verification after the writer finishes; re-capture the receipt if the final content is legitimate.","Use unique per-run artifact paths so concurrent runs cannot share a file."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for attempt in 0..3 {\n    match read_verified(ws, &artifact, preview_limit) {\n        Ok(out) => return Ok(out),\n        Err(e) if e.to_string().contains(\"grew while being read\") && attempt < 2 => continue,\n        Err(e) => return Err(e),\n    }\n}","preventionTips":["Verify artifacts only after the producing run has completed.","Enforce immutable publication — never append to published artifacts.","Give each run unique artifact paths to avoid cross-run races."],"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"}