{"record":{"id":"dbc3f57147cd5e5f","repo":"EpicGames/lore","slug":"file-shrank-while-reading","errorCode":null,"errorMessage":"file shrank while reading","messagePattern":"file shrank while reading","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"lore-io/src/psync.rs","lineNumber":155,"sourceCode":"    }\n\n    pub(crate) async fn read_file_bytes(&self, path: PathBuf) -> std::io::Result<Bytes> {\n        SyscallPool::global()\n            .submit(move || {\n                let file = crate::file::OpenOptions::new()\n                    .read(true)\n                    .to_std_blocking()\n                    .open(path)?;\n                let len = file.metadata()?.len() as usize;\n                crate::driver::check_whole_file_len(len)?;\n                // SAFETY: every byte up to `len` is filled before returning, and a file that\n                // shrank returns an error rather than the buffer.\n                let mut buffer = unsafe { crate::buffer::uninit_buffer(len) };\n                let mut done = 0;\n                while done < len {\n                    let read = read_at_impl(&file, &mut buffer[done..len], done as u64)?;\n                    if read == 0 {\n                        return Err(std::io::Error::new(\n                            std::io::ErrorKind::UnexpectedEof,\n                            \"file shrank while reading\",\n                        ));\n                    }\n                    done += read;\n                }\n                Ok(buffer.freeze())\n            })\n            .await\n    }\n\n    pub(crate) async fn write_file_bytes(\n        &self,\n        path: PathBuf,\n        data: Bytes,\n        durable: bool,\n    ) -> std::io::Result<std::fs::Metadata> {\n        SyscallPool::global()","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/EpicGames/lore/blob/074eb0b0d1194c997d7cf28b55519e3e197b3e23/lore-io/src/psync.rs#L137-L173","documentation":"read_file_bytes observed a zero-byte read while filling a buffer of the size taken from the file's own metadata — meaning the file shrank (was truncated or replaced) between the metadata read and the data read. ErrorKind::UnexpectedEof is raised so the caller never receives a short, partially-stale buffer.","triggerScenarios":"Calling read_file_bytes on a file that another process truncates or atomically replaces (rename-over) between the stat and the read loop; reading a log/tmp file that gets rotated during the read.","commonSituations":"Log rotation truncating the file being read; a build/cache process rewriting the file in place; reading /proc-style or temp files that shrink; TOCTOU race in a monitoring tool.","solutions":["Re-open the file and retry the read_file_bytes call; the new file is likely stable.","Read the file once into an owned snapshot (e.g. copy to a temp path or open + fstat the same fd) to avoid the race.","Coordinate with the writer: use file locking or read after the writer signals completion.","Tolerate short reads by looping with plain positioned reads and accepting fewer bytes."],"exampleFix":"// before\nlet bytes = psync::read_file_bytes(&options, path)?; // fails if file shrinks mid-read\n// after\nlet bytes = loop {\n    match psync::read_file_bytes(&options, path) {\n        Ok(b) => break b,\n        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => continue, // retried on rotation\n        Err(e) => return Err(e),\n    }\n};","handlingStrategy":"retry","validationCode":"// Rust\nlet before = fs::metadata(path)?.len();\n// after read_file_bytes succeeds, confirm the file did not change:\nif fs::metadata(path)?.len() != before { /* retry */ }","typeGuard":null,"tryCatchPattern":"// Rust\nlet bytes = loop {\n    match psync::read_file_bytes(&options, path) {\n        Ok(b) => break Ok(b),\n        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof && attempts < 3 => { attempts += 1; }\n        Err(e) => break Err(e),\n    }\n}?;","preventionTips":["Have writers replace files atomically (write temp + rename)","Use advisory locks (flock) between reader and writer","Reopen and retry on UnexpectedEof — truncation races are transient","Snapshot files before reading if they change frequently"],"tags":["io","filesystem","race-condition","read"],"backgroundTag":"file-read-failed","analyzedSha":"074eb0b0d1194c997d7cf28b55519e3e197b3e23","analyzedAt":"2026-09-13T09:00:57.509Z","contentChangedAt":"2026-09-13T09:00:57.509Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}