tool: atomically write to ESP

To minimize the potential for irrecoverable errors, only atomic writes
to the ESP are performed. This is implemented by first copying the file
to the destination with a `.tmp` suffix and then renaming it to the
final desintation. This is atomic because the rename operation is atomic
on POSIX platforms.

Specifically, this means that even if the system crashes during the
operation, the final desintation path will most likely be intact if it
exists at all. There are some nuances to this however. It **cannot** be
actually guaranteed that the operation was performed on the filesystem
level. However, this is the best we can do for now.

For reference:
- POSIX rename(2): https://pubs.opengroup.org/onlinepubs/9699919799/
- Rust fs::rename corresponds to rename(2) on Unix: https://doc.rust-lang.org/std/fs/fn.rename.html
- Rust fs::rename is implemented using libc's rename: https://github.com/rust-lang/rust/blob/master/library/std/src/sys/unix/fs.rs#L1397
- Renaming in libc is atomic: https://www.gnu.org/software/libc/manual/html_node/Renaming-Files.html
This commit is contained in:
nikstur 2023-01-28 03:17:35 +01:00
parent 41c7a14a80
commit 5f28ae75ea
1 changed files with 41 additions and 18 deletions

View File

@ -143,7 +143,7 @@ impl Installer {
println!("Appending secrets to initrd..."); println!("Appending secrets to initrd...");
let initrd_location = tempdir.path().join("initrd"); let initrd_location = tempdir.path().join("initrd");
copy( fs::copy(
bootspec bootspec
.initrd .initrd
.as_ref() .as_ref()
@ -241,25 +241,39 @@ fn install_signed(key_pair: &KeyPair, from: &Path, to: &Path) -> Result<()> {
/// Sign and forcibly install a PE file. /// Sign and forcibly install a PE file.
/// ///
/// If the file already exists at the destination, it is overwritten. /// If the file already exists at the destination, it is overwritten.
///
/// This is implemented as an atomic write. The file is first written to the destination with a
/// `.tmp` suffix and then renamed to its final name. This is atomic, because a rename is an atomic
/// operation on POSIX platforms.
fn force_install_signed(key_pair: &KeyPair, from: &Path, to: &Path) -> Result<()> { fn force_install_signed(key_pair: &KeyPair, from: &Path, to: &Path) -> Result<()> {
println!("Signing and installing {}...", to.display()); println!("Signing and installing {}...", to.display());
ensure_parent_dir(to); let to_tmp = to.with_extension(".tmp");
ensure_parent_dir(&to_tmp);
key_pair key_pair
.sign_and_copy(from, to) .sign_and_copy(from, &to_tmp)
.with_context(|| format!("Failed to copy and sign file from {from:?} to {to:?}"))?; .with_context(|| format!("Failed to copy and sign file from {from:?} to {to:?}"))?;
fs::rename(&to_tmp, to).with_context(|| {
format!("Failed to move temporary file {to_tmp:?} to final location {to:?}")
})?;
Ok(()) Ok(())
} }
/// Install an arbitrary file /// Install an arbitrary file.
/// ///
/// The file is only copied if it doesn't exist at the destination /// The file is only copied if it doesn't exist at the destination.
///
/// This function is only designed to copy files to the ESP. It sets the permission bits of the
/// file at the destination to 0o755, the expected permissions for a vfat ESP. This is useful for
/// producing file systems trees which can then be converted to a file system image.
fn install(from: &Path, to: &Path) -> Result<()> { fn install(from: &Path, to: &Path) -> Result<()> {
if to.exists() { if to.exists() {
println!("{} already exists, skipping...", to.display()); println!("{} already exists, skipping...", to.display());
} else { } else {
println!("Installing {}...", to.display()); println!("Installing {}...", to.display());
ensure_parent_dir(to); ensure_parent_dir(to);
copy(from, to)?; atomic_copy(from, to)?;
set_permission_bits(to, 0o755)
.with_context(|| format!("Failed to set permission bits to 0o755 on file: {to:?}"))?;
} }
Ok(()) Ok(())
@ -293,20 +307,29 @@ fn assemble_kernel_cmdline(init: &Path, kernel_params: Vec<String>) -> Vec<Strin
kernel_cmdline kernel_cmdline
} }
fn copy(from: &Path, to: &Path) -> Result<()> { /// Atomically copy a file.
ensure_parent_dir(to); ///
fs::copy(from, to) /// The file is first written to the destination with a `.tmp` suffix and then renamed to its final
.with_context(|| format!("Failed to copy from {} to {}", from.display(), to.display()))?; /// name. This is atomic, because a rename is an atomic operation on POSIX platforms.
fn atomic_copy(from: &Path, to: &Path) -> Result<()> {
let to_tmp = to.with_extension(".tmp");
// Set permission of all files copied to 0o755 fs::copy(from, &to_tmp)
let mut perms = fs::metadata(to) .with_context(|| format!("Failed to copy from {from:?} to {to_tmp:?}",))?;
.with_context(|| format!("File {} doesn't have metadata", to.display()))?
fs::rename(&to_tmp, to).with_context(|| {
format!("Failed to move temporary file {to_tmp:?} to final location {to:?}")
})
}
/// Set the octal permission bits of the specified file.
fn set_permission_bits(path: &Path, permission_bits: u32) -> Result<()> {
let mut perms = fs::metadata(path)
.with_context(|| format!("File {path:?} doesn't have any metadata"))?
.permissions(); .permissions();
perms.set_mode(0o755); perms.set_mode(permission_bits);
fs::set_permissions(to, perms) fs::set_permissions(path, perms)
.with_context(|| format!("Failed to set permissions to: {}", to.display()))?; .with_context(|| format!("Failed to set permissions on {path:?}"))
Ok(())
} }
// Ensures the parent directory of an arbitrary path exists // Ensures the parent directory of an arbitrary path exists