Skip to content

Commit 481ae40

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

3 files changed

Lines changed: 86 additions & 23 deletions

File tree

src/efi.rs

Lines changed: 64 additions & 22 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(
@@ -140,7 +145,6 @@ impl Efi {
140145
if !mnt.exists() {
141146
continue;
142147
}
143-
#[cfg(target_os = "linux")]
144148
if mount_esp(esp_device, &mnt).is_ok() {
145149
log::debug!("Mounted at {mnt:?}");
146150
mountpoint = Some(mnt);
@@ -248,35 +252,73 @@ impl Efi {
248252
clear_efi_target(&product_name)?;
249253
create_efi_boot_entry(device, esp_part_num.trim(), &loader, &product_name)
250254
}
255+
/// Copy EFI components to ESP using the same "write alongside + atomic rename" pattern
256+
/// as bootable container updates, so the system stays bootable if any step fails.
251257
fn copy_efi_components_to_esp(
252258
&self,
253259
sysroot_dir: &openat::Dir,
254260
esp_dir: &openat::Dir,
255-
esp_path: &Path,
261+
_esp_path: &Path,
256262
efi_components: &[EFIComponent],
257263
) -> Result<()> {
258-
let dest_str = esp_path
264+
// Build a merged source tree in a temp dir (same layout as desired ESP/EFI)
265+
let temp_dir = tempfile::tempdir().context("Creating temp dir for EFI merge")?;
266+
let temp_efi_path = temp_dir.path().join("EFI");
267+
std::fs::create_dir_all(&temp_efi_path)
268+
.with_context(|| format!("Creating {}", temp_efi_path.display()))?;
269+
let temp_efi_str = temp_efi_path
259270
.to_str()
260-
.with_context(|| format!("Invalid UTF-8: {}", esp_path.display()))?;
271+
.context("Temp EFI path is not valid UTF-8")?;
261272

262273
for efi_comp in efi_components {
263274
log::info!(
264-
"Copying EFI component {} version {} to ESP at {}",
275+
"Merging EFI component {} version {} into update tree",
265276
efi_comp.name,
266-
efi_comp.version,
267-
esp_path.display()
277+
efi_comp.version
268278
);
279+
// Copy contents of component's EFI dir (e.g. fedora/) into temp_efi_path so merged
280+
// layout is EFI/fedora/..., not EFI/EFI/fedora/...
281+
let src_efi_contents = format!("{}/.", efi_comp.path);
282+
filetree::copy_dir_with_args(
283+
sysroot_dir,
284+
src_efi_contents.as_str(),
285+
temp_efi_str,
286+
OPTIONS,
287+
)
288+
.with_context(|| format!("Copying {} to merge dir", efi_comp.path))?;
289+
}
269290

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-
})?;
291+
// Ensure ESP/EFI exists (e.g. first install)
292+
esp_dir.ensure_dir_all(std::path::Path::new("EFI"), 0o755)?;
293+
let esp_efi_dir = esp_dir.sub_dir("EFI").context("Opening ESP EFI dir")?;
294+
295+
let source_dir =
296+
openat::Dir::open(&temp_efi_path).context("Opening merged EFI source dir")?;
297+
let source_filetree =
298+
filetree::FileTree::new_from_dir(&source_dir).context("Building source filetree")?;
299+
let current_filetree =
300+
filetree::FileTree::new_from_dir(&esp_efi_dir).context("Building current filetree")?;
301+
let mut diff = current_filetree
302+
.diff(&source_filetree)
303+
.context("Computing EFI diff")?;
304+
diff.removals.clear();
305+
306+
// Check available space before writing to prevent partial updates when the partition is full
307+
let required_bytes = current_filetree.total_size() + source_filetree.total_size();
308+
let available_bytes = util::available_space_bytes(&esp_efi_dir)?;
309+
if available_bytes < required_bytes {
310+
anyhow::bail!(
311+
"ESP has insufficient free space for update: need {} MiB, have {} MiB",
312+
required_bytes / (1024 * 1024),
313+
available_bytes / (1024 * 1024)
314+
);
277315
}
278316

279-
// Sync the whole ESP filesystem
317+
// Same logic as bootable container: write to .btmp.* then atomic rename
318+
filetree::apply_diff(&source_dir, &esp_efi_dir, &diff, None)
319+
.context("Applying EFI update (write alongside + atomic rename)")?;
320+
321+
// Sync the whole ESP filesystem
280322
fsfreeze_thaw_cycle(esp_dir.open_file(".")?)?;
281323

282324
Ok(())
@@ -286,6 +328,7 @@ impl Efi {
286328
fn package_mode_copy_to_boot_impl(&self, sysroot: &Path) -> Result<()> {
287329
let sysroot_path = Utf8Path::from_path(sysroot)
288330
.with_context(|| format!("Invalid UTF-8: {}", sysroot.display()))?;
331+
let sysroot_dir = openat::Dir::open(sysroot).context("Opening sysroot for reading")?;
289332

290333
let efi_comps = match get_efi_component_from_usr(sysroot_path, EFILIB)? {
291334
Some(comps) if !comps.is_empty() => comps,
@@ -309,7 +352,6 @@ impl Efi {
309352
.with_context(|| format!("Opening ESP at {}", esp_path.display()))?;
310353
validate_esp_fstype(&esp_dir)?;
311354

312-
let sysroot_dir = openat::Dir::open(sysroot).context("Opening sysroot for reading")?;
313355
self.copy_efi_components_to_esp(&sysroot_dir, &esp_dir, &esp_path, &efi_comps)?;
314356

315357
log::info!(

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: 12 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 =
67+
rustix::fs::statvfs(p).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
}

0 commit comments

Comments
 (0)