{"record":{"id":"46d21fc569358ded","repo":"xai-org/grok-build","slug":"journal-changed-during-open","errorCode":null,"errorMessage":"journal changed during open","messagePattern":"journal changed during open","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"warning","filePath":"crates/codegen/xai-workflow/src/journal.rs","lineNumber":279,"sourceCode":"        ));\n    }\n    if metadata.len() > MAX_JOURNAL_BYTES {\n        return Err(std::io::Error::new(\n            std::io::ErrorKind::InvalidData,\n            format!(\"journal exceeds {MAX_JOURNAL_BYTES} bytes\"),\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() > MAX_JOURNAL_BYTES {\n        return Err(std::io::Error::new(\n            std::io::ErrorKind::InvalidData,\n            \"journal changed during open\",\n        ));\n    }\n    let mut content = Vec::with_capacity(opened.len() as usize);\n    file.take(MAX_JOURNAL_BYTES.saturating_add(1))\n        .read_to_end(&mut content)?;\n    if content.len() as u64 > MAX_JOURNAL_BYTES {\n        return Err(std::io::Error::new(\n            std::io::ErrorKind::InvalidData,\n            format!(\"journal exceeds {MAX_JOURNAL_BYTES} bytes\"),\n        ));\n    }\n    Ok(content)\n}\n\nfn validate_sequence(entries: &[JournalEntry], entry: &JournalEntry) -> Result<(), JournalError> {\n    let expected = entries.len() as u64;","sourceCodeStart":261,"sourceCodeEnd":297,"githubUrl":"https://github.com/xai-org/grok-build/blob/bc7f02eddd3d84085849dc19ed216f11c23b0571/crates/codegen/xai-workflow/src/journal.rs#L261-L297","documentation":"read_journal_bounded opens the journal file with O_NOFOLLOW, then re-statfs the opened fd and rejects it if it is not a regular file or its size already exceeds MAX_JOURNAL_BYTES. This detects a TOCTOU race: the file was replaced, truncated, or swapped between path resolution and metadata read, so the content about to be loaded cannot be trusted. The library throws std::io::Error with ErrorKind::InvalidData.","triggerScenarios":"Calling load() (which calls read_journal_bounded) while another process/thread concurrently writes, truncates, replaces, or rotates the journal file; the path being swapped (e.g. mv/rename over it) during the open window; a non-regular file (device, fifo) substituted at the path.","commonSituations":"Two agent processes sharing the same journal directory; a log-rotation or cleanup job racing with startup; editors or sync tools (Dropbox/rsync) replacing the file; running against a stale symlinked path.","solutions":["Retry load() after a short backoff - the race is transient and a fresh open usually sees a stable file","Ensure only one process owns the journal (use a lockfile or flock before load)","Exclude the journal path from rotators/sync tools, or point it at a private directory","If it reproduces deterministically, inspect the path for symlinks or non-regular files (ls -la, stat)"],"exampleFix":"// before\nlet journal = Journal::load(path)?;\n// after\nlet journal = match Journal::load(path) {\n    Ok(j) => j,\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidData\n        && e.to_string().contains(\"changed during open\") =>\n    {\n        std::thread::sleep(std::time::Duration::from_millis(50));\n        Journal::load(path)?\n    }\n    Err(e) => return Err(e),\n};","handlingStrategy":"retry","validationCode":"let md = std::fs::metadata(path)?;\nif !md.is_file() { return Err(anyhow!(\"journal path is not a regular file\")); }\n// note: size check alone cannot close the race; retry handles it","typeGuard":"fn is_transient_journal_race(e: &std::io::Error) -> bool {\n    e.kind() == std::io::ErrorKind::InvalidData\n        && e.to_string().contains(\"changed during open\")\n}","tryCatchPattern":"match Journal::load(path) {\n    Ok(j) => j,\n    Err(e) if is_transient_journal_race(&e) => retry_with_backoff(3, || Journal::load(path))?,\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Acquire an exclusive lock (flock/lockfile) on the journal before load","Run only one process per journal directory","Exclude the journal from log rotation and file-sync tools","Retry with backoff on InvalidData mentioning 'changed during open'"],"tags":["io","race-condition","file-system","rust"],"backgroundTag":"file-changed-during-open","analyzedSha":"bc7f02eddd3d84085849dc19ed216f11c23b0571","analyzedAt":"2026-08-31T04:59:42.031Z","schemaVersion":2},"datasetVersion":"2026-08-31T09:17:48.483Z"}