{"record":{"id":"0fb01f9caeac3e25","repo":"xai-org/grok-build","slug":"workflow-artifact-changed-during-open","errorCode":null,"errorMessage":"workflow artifact changed during open: {}","messagePattern":"workflow artifact changed during open: (.+?)","errorType":"validation","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"crates/codegen/xai-grok-shell/src/session/workflow/store.rs","lineNumber":337,"sourceCode":"        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\n                \"workflow artifact exceeds {limit} bytes: {}\",\n                path.display()\n            ),\n        ));\n    }\n    let mut options = std::fs::OpenOptions::new();\n    options.read(true);\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::OpenOptionsExt;\n        options.custom_flags(libc::O_NOFOLLOW);\n    }\n    let file = options.open(path)?;\n    let opened = file.metadata()?;\n    if !opened.is_file() || opened.len() > limit {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\"workflow artifact changed during open: {}\", path.display()),\n        ));\n    }\n    let mut bytes = Vec::with_capacity(opened.len() as usize);\n    file.take(limit.saturating_add(1)).read_to_end(&mut bytes)?;\n    if bytes.len() as u64 > limit {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\n                \"workflow artifact exceeds {limit} bytes: {}\",\n                path.display()\n            ),\n        ));\n    }\n    Ok(bytes)\n}\n","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/xai-org/grok-build/blob/bc7f02eddd3d84085849dc19ed216f11c23b0571/crates/codegen/xai-grok-shell/src/session/workflow/store.rs#L319-L355","documentation":"After opening with O_NOFOLLOW, read_bounded_nofollow re-checks the opened file's metadata. If the opened target is no longer a regular file or already exceeds `limit`, the file changed between the pre-open check and open — it returns InvalidData with this message rather than reading a swapped-in object.","triggerScenarios":"Concurrent modification of the artifact during load_workflow_runs_sync: another process replaces the file (e.g. with a symlink or fifo) between symlink_metadata and open, or a writer truncates/extends it past the limit in that window.","commonSituations":"Two shell instances persisting the same run concurrently; an attacker racing the reader (TOCTOU exploit); a background compaction job rewriting artifacts while runs are being loaded.","solutions":["Ensure only one process writes run state (file lock or single-owner actor)","Retry the load; transient races usually clear on a second attempt","Restore a stable artifact via atomic_write (rename is atomic so readers never see partial files)","Investigate the concurrent writer or tampering if it recurs"],"exampleFix":"// before\n// direct writes to runs.json while another reader opens it -> race\n// after\nlet bytes = (|| store.read_bounded_nofollow(&path, LIMIT))( ).or_else(|e| if e.kind()==BrokenPipe||e.kind()==InvalidData { retry_once() } else { Err(e) })?; // and all writers use atomic_write_replace","handlingStrategy":"retry","validationCode":"fn stable_snapshot<F>(read: F) -> std::io::Result<Vec<u8>> where F: Fn() -> std::io::Result<Vec<u8>> {\n    const TRIES: usize = 3;\n    (0..TRIES).find_map(|_| read().ok()).ok_or_else(|| {\n        std::io::Error::new(std::io::ErrorKind::InvalidData, \"artifact unstable across reads\")\n    })\n}","typeGuard":null,"tryCatchPattern":"let bytes = loop {\n    match store.read_bounded_nofollow(&path, LIMIT) {\n        Ok(b) => break b,\n        Err(e) if e.kind() == std::io::ErrorKind::InvalidData\n            && e.to_string().contains(\"changed during open\") && retries < 3 => {\n            retries += 1;\n            tokio::time::sleep(Duration::from_millis(50)).await;\n        }\n        Err(e) => return Err(e.into()),\n    }\n};","preventionTips":["Route all artifact writes through the persistence actor (single writer)","Use atomic_write_replace (temp+rename) so readers never see swaps","Take a filesystem lock when multiple processes may touch the runs dir","Investigate repeated 'changed during open' as a possible tampering/race signal"],"tags":["io","race-condition","toctou","invalid-data"],"backgroundTag":"artifact-changed-during-open","analyzedSha":"bc7f02eddd3d84085849dc19ed216f11c23b0571","analyzedAt":"2026-08-31T04:59:42.031Z","schemaVersion":2},"datasetVersion":"2026-08-31T09:17:48.483Z"}