{"record":{"id":"df2a54bbbfb4b90a","repo":"astrid-runtime/astrid","slug":"mounted-file-changed-during-replacement","errorCode":null,"errorMessage":"mounted file changed during replacement","messagePattern":"mounted file changed during replacement","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/storage_mount/filesystem/range.rs","lineNumber":80,"sourceCode":"        } else {\n            let boundary = if self.position < self.offset {\n                self.offset\n            } else {\n                self.new_length\n            };\n            let count = count\n                .min(usize::try_from(boundary.strict_sub(self.position)).unwrap_or(usize::MAX));\n            if self.position < self.old_length {\n                let count = count.min(\n                    usize::try_from(self.old_length.strict_sub(self.position))\n                        .unwrap_or(usize::MAX),\n                );\n                let bytes = self\n                    .filesystem\n                    .read(self.path, self.position, count as u64)\n                    .map_err(|error| std::io::Error::other(error.to_string()))?;\n                if bytes.len() != count {\n                    return Err(std::io::Error::new(\n                        std::io::ErrorKind::UnexpectedEof,\n                        \"mounted file changed during replacement\",\n                    ));\n                }\n                output[..count].copy_from_slice(&bytes);\n                count\n            } else {\n                output[..count].fill(0);\n                count\n            }\n        };\n        self.position = self.position.strict_add(count as u64);\n        Ok(count)\n    }\n}\n\n#[cfg(test)]\nmod tests {","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/storage_mount/filesystem/range.rs#L62-L98","documentation":"The mounted-file reader reads a requested count of bytes from the backing filesystem and verifies it got exactly that many. A short read means the mounted file shrank or was replaced concurrently while it was being read for a replacement operation, so the read is aborted with UnexpectedEof rather than returning torn data.","triggerScenarios":"During a read() on a mounted file, self.filesystem.read(self.path, self.position, count) returns fewer than count bytes — the file was truncated/rewritten underneath the in-progress replacement.","commonSituations":"Two writers replace the same mounted file concurrently; an external process truncates the file mid-read; a snapshot/replacement pipeline racing with a live edit.","solutions":["Take a lock or use atomic-rename replacement so a file is never mutated in place during a read","Retry the whole read/replace cycle after the concurrent writer finishes","Verify file length before and after reading and treat mismatch as retryable","Coordinate writers so only one replacement runs at a time per mounted path"],"exampleFix":"// before\nlet bytes = fs.read(path, 0, len)?;\napply(bytes);\n// after\nlet lock = mount.lock_file(path)?;\nlet bytes = fs.read(path, 0, len)?;\nif bytes.len() != len { return Err(retry); }\napply(bytes);\ndrop(lock);","handlingStrategy":"retry","validationCode":"fn file_is_stable(fs: &Mount, path: &Path, expected_len: u64) -> io::Result<bool> {\n    let before = fs.len(path)?;\n    std::thread::sleep(std::time::Duration::from_millis(5));\n    Ok(before == expected_len && fs.len(path)? == expected_len)\n}","typeGuard":null,"tryCatchPattern":"match result {\n    Err(e) if e.kind() == io::ErrorKind::UnexpectedEof\n        && e.to_string() == \"mounted file changed during replacement\" => {\n        // re-read the file's current length and retry the replacement\n        retry_with_backoff(3, || replace_mounted_file(path));\n    }\n    other => other?,\n}","preventionTips":["Serialize replacements per mounted path with a lock or single-writer discipline","Use write-to-temp + atomic rename instead of in-place truncation","Re-stat file length before and after reading to detect concurrent changes"],"tags":["concurrency","eof","filesystem","race"],"backgroundTag":"unexpected-eof","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}