tool: fix atomic write

Atomic write works by first writing a temporary file, then syncing that
temporary file to ensure it is fully on disk before the program can
continue, and in the last step renaming the temporary file to the
target. The middle step was missing, which is likely to lead to a
truncated target file being present after power loss. Add this step.

Furthermore, even with this fix, atomicity is not fully guaranteed,
because FAT32 can become corrupted after power loss due to its design
shortcomings. Even though we cannot really do anything about this case,
adjust the comment to at least acknowledge the situation.
This commit is contained in:
Alois Wohlschlager 2023-09-24 11:27:59 +02:00
parent 4fd37670e2
commit 90a1adac54
No known key found for this signature in database
GPG Key ID: E0F59EA5E5216914
1 changed files with 22 additions and 10 deletions

View File

@ -465,17 +465,29 @@ fn assemble_kernel_cmdline(init: &Path, kernel_params: Vec<String>) -> Vec<Strin
/// Atomically copy a file. /// Atomically copy a file.
/// ///
/// The file is first written to the destination with a `.tmp` suffix and then renamed to its final /// First, the content is written to a temporary file (with a `.tmp` extension).
/// name. This is atomic, because a rename is an atomic operation on POSIX platforms. /// Then, this file is synced, to ensure its data and metadata are fully on disk before continuing.
/// In the last step, the temporary file is renamed to the final destination.
///
/// Due to the deficiencies of FAT32, it is possible for the filesystem to become corrupted after power loss.
/// It is not possible to fully defend against this situation, so this operation is not actually fully atomic.
/// However, in all other cases, the target file is either present with its correct content or not present at all.
fn atomic_copy(from: &Path, to: &Path) -> Result<()> { fn atomic_copy(from: &Path, to: &Path) -> Result<()> {
let to_tmp = to.with_extension(".tmp"); let tmp = to.with_extension(".tmp");
{
fs::copy(from, &to_tmp) let mut from_file =
.with_context(|| format!("Failed to copy from {from:?} to {to_tmp:?}",))?; File::open(from).with_context(|| format!("Failed to read the source file {from:?}"))?;
let mut tmp_file = File::create(&tmp)
fs::rename(&to_tmp, to).with_context(|| { .with_context(|| format!("Failed to create the temporary file {tmp:?}"))?;
format!("Failed to move temporary file {to_tmp:?} to final location {to:?}") std::io::copy(&mut from_file, &mut tmp_file).with_context(|| {
}) format!("Failed to copy from {from:?} to the temporary file {tmp:?}")
})?;
tmp_file
.sync_all()
.with_context(|| format!("Failed to sync the temporary file {tmp:?}"))?;
}
fs::rename(&tmp, to)
.with_context(|| format!("Failed to move temporary file {tmp:?} to target {to:?}"))
} }
/// Set the octal permission bits of the specified file. /// Set the octal permission bits of the specified file.