Skip to content

Commit f7a0569

Browse files
committed
package mode: use write-alongside + atomic rename and check ESP space
Apply code review
1 parent 40f9692 commit f7a0569

File tree

3 files changed

+80
-21
lines changed

3 files changed

+80
-21
lines changed

src/efi.rs

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ use chrono::prelude::*;
1919
use fn_error_context::context;
2020
use openat_ext::OpenatDirExt;
2121
use os_release::OsRelease;
22-
use rustix::{fd::AsFd, fd::BorrowedFd, fs::StatVfsMountFlags};
23-
use walkdir::WalkDir;
2422
#[cfg(target_os = "linux")]
2523
use rustix::mount::{
2624
fsconfig_create, fsconfig_set_path, fsmount, fsopen, move_mount, FsMountFlags, FsOpenFlags,
27-
MoveMountFlags, MountAttrFlags, UnmountFlags,
25+
MountAttrFlags, MoveMountFlags, UnmountFlags,
2826
};
27+
use rustix::{fd::AsFd, fd::BorrowedFd, fs::StatVfsMountFlags};
28+
use walkdir::WalkDir;
2929
use widestring::U16CString;
3030

3131
use crate::bootupd::RootContext;
@@ -75,10 +75,15 @@ fn mount_esp(esp_device: &Path, target: &Path) -> Result<()> {
7575
use rustix::fs::CWD;
7676

7777
let fs_fd = fsopen("vfat", FsOpenFlags::empty()).context("fsopen vfat")?;
78-
fsconfig_set_path(fs_fd.as_fd(), "source", esp_device, CWD).context("fsconfig_set_path source")?;
78+
fsconfig_set_path(fs_fd.as_fd(), "source", esp_device, CWD)
79+
.context("fsconfig_set_path source")?;
7980
fsconfig_create(fs_fd.as_fd()).context("fsconfig_create")?;
80-
let mount_fd =
81-
fsmount(fs_fd.as_fd(), FsMountFlags::empty(), MountAttrFlags::empty()).context("fsmount")?;
81+
let mount_fd = fsmount(
82+
fs_fd.as_fd(),
83+
FsMountFlags::empty(),
84+
MountAttrFlags::empty(),
85+
)
86+
.context("fsmount")?;
8287
let target_dir = std::fs::File::open(target).context("open target dir for move_mount")?;
8388
let target_fd = unsafe { BorrowedFd::borrow_raw(target_dir.as_raw_fd()) };
8489
move_mount(
@@ -248,35 +253,67 @@ impl Efi {
248253
clear_efi_target(&product_name)?;
249254
create_efi_boot_entry(device, esp_part_num.trim(), &loader, &product_name)
250255
}
256+
/// Copy EFI components to ESP using the same "write alongside + atomic rename" pattern
257+
/// as bootable container updates, so the system stays bootable if any step fails.
251258
fn copy_efi_components_to_esp(
252259
&self,
253260
sysroot_dir: &openat::Dir,
254261
esp_dir: &openat::Dir,
255-
esp_path: &Path,
262+
_esp_path: &Path,
256263
efi_components: &[EFIComponent],
257264
) -> Result<()> {
258-
let dest_str = esp_path
265+
// Build a merged source tree in a temp dir (same layout as desired ESP/EFI)
266+
let temp_dir = tempfile::tempdir().context("Creating temp dir for EFI merge")?;
267+
let temp_efi_path = temp_dir.path().join("EFI");
268+
std::fs::create_dir_all(&temp_efi_path)
269+
.with_context(|| format!("Creating {}", temp_efi_path.display()))?;
270+
let temp_efi_str = temp_efi_path
259271
.to_str()
260-
.with_context(|| format!("Invalid UTF-8: {}", esp_path.display()))?;
272+
.context("Temp EFI path is not valid UTF-8")?;
261273

262274
for efi_comp in efi_components {
263275
log::info!(
264-
"Copying EFI component {} version {} to ESP at {}",
276+
"Merging EFI component {} version {} into update tree",
265277
efi_comp.name,
266-
efi_comp.version,
267-
esp_path.display()
278+
efi_comp.version
268279
);
280+
// Copy contents of component's EFI dir (e.g. fedora/) into temp_efi_path so merged
281+
// layout is EFI/fedora/..., not EFI/EFI/fedora/...
282+
let src_efi_contents = format!("{}/.", efi_comp.path);
283+
filetree::copy_dir_with_args(sysroot_dir, src_efi_contents.as_str(), temp_efi_str, OPTIONS)
284+
.with_context(|| format!("Copying {} to merge dir", efi_comp.path))?;
285+
}
269286

270-
filetree::copy_dir_with_args(sysroot_dir, efi_comp.path.as_str(), dest_str, OPTIONS)
271-
.with_context(|| {
272-
format!(
273-
"Failed to copy {} from {} to {}",
274-
efi_comp.name, efi_comp.path, dest_str
275-
)
276-
})?;
287+
// Ensure ESP/EFI exists (e.g. first install)
288+
esp_dir.ensure_dir_all(std::path::Path::new("EFI"), 0o755)?;
289+
let esp_efi_dir = esp_dir.sub_dir("EFI").context("Opening ESP EFI dir")?;
290+
291+
let source_dir =
292+
openat::Dir::open(&temp_efi_path).context("Opening merged EFI source dir")?;
293+
let source_filetree =
294+
filetree::FileTree::new_from_dir(&source_dir).context("Building source filetree")?;
295+
let current_filetree =
296+
filetree::FileTree::new_from_dir(&esp_efi_dir).context("Building current filetree")?;
297+
let diff = current_filetree
298+
.diff(&source_filetree)
299+
.context("Computing EFI diff")?;
300+
301+
// Check available space before writing to prevent partial updates when the partition is full
302+
let required_bytes = current_filetree.total_size() + source_filetree.total_size();
303+
let available_bytes = util::available_space_bytes(&esp_efi_dir)?;
304+
if available_bytes < required_bytes {
305+
anyhow::bail!(
306+
"ESP has insufficient free space for update: need {} MiB, have {} MiB",
307+
required_bytes / (1024 * 1024),
308+
available_bytes / (1024 * 1024)
309+
);
277310
}
278311

279-
// Sync the whole ESP filesystem
312+
// Same logic as bootable container: write to .btmp.* then atomic rename
313+
filetree::apply_diff(&source_dir, &esp_efi_dir, &diff, None)
314+
.context("Applying EFI update (write alongside + atomic rename)")?;
315+
316+
// Sync the whole ESP filesystem
280317
fsfreeze_thaw_cycle(esp_dir.open_file(".")?)?;
281318

282319
Ok(())

src/filetree.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,16 @@ impl FileTree {
202202
Ok(Self { children })
203203
}
204204

205+
/// Total size in bytes of all files in the tree (for space checks).
206+
#[cfg(any(
207+
target_arch = "x86_64",
208+
target_arch = "aarch64",
209+
target_arch = "riscv64"
210+
))]
211+
pub(crate) fn total_size(&self) -> u64 {
212+
self.children.values().map(|m| m.size).sum()
213+
}
214+
205215
/// Determine the changes *from* self to the updated tree
206216
#[cfg(any(
207217
target_arch = "x86_64",

src/util.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::collections::HashSet;
2+
use std::os::unix::io::AsRawFd;
23
use std::path::Path;
34
use std::process::Command;
45

56
use anyhow::{bail, Context, Result};
67
use openat_ext::OpenatDirExt;
8+
use rustix::fd::BorrowedFd;
79

810
/// Parse an environment variable as UTF-8
911
#[allow(dead_code)]
@@ -51,9 +53,18 @@ pub(crate) fn filenames(dir: &openat::Dir) -> Result<HashSet<String>> {
5153
Ok(ret)
5254
}
5355

56+
/// Return the available space in bytes on the filesystem containing the given directory.
57+
/// Uses f_bavail * f_frsize from fstatvfs to avoid partial updates when the partition is full.
58+
pub(crate) fn available_space_bytes(dir: &openat::Dir) -> Result<u64> {
59+
let fd = unsafe { BorrowedFd::borrow_raw(dir.as_raw_fd()) };
60+
let st = rustix::fs::fstatvfs(fd)?;
61+
Ok((st.f_bavail as u64) * (st.f_frsize as u64))
62+
}
63+
5464
pub(crate) fn ensure_writable_mount<P: AsRef<Path>>(p: P) -> Result<()> {
5565
let p = p.as_ref();
56-
let stat = rustix::fs::statvfs(p)?;
66+
let stat = rustix::fs::statvfs(p)
67+
.map_err(|e| std::io::Error::from_raw_os_error(e.raw_os_error()))?;
5768
if !stat.f_flag.contains(rustix::fs::StatVfsMountFlags::RDONLY) {
5869
return Ok(());
5970
}
@@ -121,3 +132,4 @@ impl Drop for SignalTerminationGuard {
121132
signal_hook_registry::unregister(self.0);
122133
}
123134
}
135+

0 commit comments

Comments
 (0)