Skip to content

Commit f9c076a

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

3 files changed

Lines changed: 86 additions & 28 deletions

File tree

src/efi.rs

Lines changed: 64 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,12 @@ 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;
24-
#[cfg(target_os = "linux")]
2522
use rustix::mount::{
2623
fsconfig_create, fsconfig_set_path, fsmount, fsopen, move_mount, FsMountFlags, FsOpenFlags,
27-
MoveMountFlags, MountAttrFlags, UnmountFlags,
24+
MountAttrFlags, MoveMountFlags, UnmountFlags,
2825
};
26+
use rustix::{fd::AsFd, fd::BorrowedFd, fs::StatVfsMountFlags};
27+
use walkdir::WalkDir;
2928
use widestring::U16CString;
3029

3130
use crate::bootupd::RootContext;
@@ -70,15 +69,19 @@ pub(crate) fn is_efi_booted() -> Result<bool> {
7069
.map_err(Into::into)
7170
}
7271

73-
#[cfg(target_os = "linux")]
7472
fn mount_esp(esp_device: &Path, target: &Path) -> Result<()> {
7573
use rustix::fs::CWD;
7674

7775
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")?;
76+
fsconfig_set_path(fs_fd.as_fd(), "source", esp_device, CWD)
77+
.context("fsconfig_set_path source")?;
7978
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")?;
79+
let mount_fd = fsmount(
80+
fs_fd.as_fd(),
81+
FsMountFlags::empty(),
82+
MountAttrFlags::empty(),
83+
)
84+
.context("fsmount")?;
8285
let target_dir = std::fs::File::open(target).context("open target dir for move_mount")?;
8386
let target_fd = unsafe { BorrowedFd::borrow_raw(target_dir.as_raw_fd()) };
8487
move_mount(
@@ -140,13 +143,11 @@ impl Efi {
140143
if !mnt.exists() {
141144
continue;
142145
}
143-
#[cfg(target_os = "linux")]
144146
if mount_esp(esp_device, &mnt).is_ok() {
145147
log::debug!("Mounted at {mnt:?}");
146148
mountpoint = Some(mnt);
147149
break;
148150
}
149-
#[cfg(target_os = "linux")]
150151
log::trace!("Mount failed, falling back to mount(8)");
151152
std::process::Command::new("mount")
152153
.arg(esp_device)
@@ -188,7 +189,6 @@ impl Efi {
188189
.unwrap_or(false);
189190
if should_unmount {
190191
if let Some(mount) = self.mountpoint.borrow_mut().take() {
191-
#[cfg(target_os = "linux")]
192192
if rustix::mount::unmount(&mount.path, UnmountFlags::empty()).is_ok() {
193193
log::trace!("Unmounted (new mount API)");
194194
} else {
@@ -198,7 +198,6 @@ impl Efi {
198198
.with_context(|| format!("Failed to unmount {:?}", mount.path))?;
199199
log::trace!("Unmounted");
200200
}
201-
#[cfg(not(target_os = "linux"))]
202201
{
203202
Command::new("umount")
204203
.arg(&mount.path)
@@ -248,35 +247,73 @@ impl Efi {
248247
clear_efi_target(&product_name)?;
249248
create_efi_boot_entry(device, esp_part_num.trim(), &loader, &product_name)
250249
}
250+
/// Copy EFI components to ESP using the same "write alongside + atomic rename" pattern
251+
/// as bootable container updates, so the system stays bootable if any step fails.
251252
fn copy_efi_components_to_esp(
252253
&self,
253254
sysroot_dir: &openat::Dir,
254255
esp_dir: &openat::Dir,
255-
esp_path: &Path,
256+
_esp_path: &Path,
256257
efi_components: &[EFIComponent],
257258
) -> Result<()> {
258-
let dest_str = esp_path
259+
// Build a merged source tree in a temp dir (same layout as desired ESP/EFI)
260+
let temp_dir = tempfile::tempdir().context("Creating temp dir for EFI merge")?;
261+
let temp_efi_path = temp_dir.path().join("EFI");
262+
std::fs::create_dir_all(&temp_efi_path)
263+
.with_context(|| format!("Creating {}", temp_efi_path.display()))?;
264+
let temp_efi_str = temp_efi_path
259265
.to_str()
260-
.with_context(|| format!("Invalid UTF-8: {}", esp_path.display()))?;
266+
.context("Temp EFI path is not valid UTF-8")?;
261267

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

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-
})?;
286+
// Ensure ESP/EFI exists (e.g. first install)
287+
esp_dir.ensure_dir_all(std::path::Path::new("EFI"), 0o755)?;
288+
let esp_efi_dir = esp_dir.sub_dir("EFI").context("Opening ESP EFI dir")?;
289+
290+
let source_dir =
291+
openat::Dir::open(&temp_efi_path).context("Opening merged EFI source dir")?;
292+
let source_filetree =
293+
filetree::FileTree::new_from_dir(&source_dir).context("Building source filetree")?;
294+
let current_filetree =
295+
filetree::FileTree::new_from_dir(&esp_efi_dir).context("Building current filetree")?;
296+
let mut diff = current_filetree
297+
.diff(&source_filetree)
298+
.context("Computing EFI diff")?;
299+
diff.removals.clear();
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(())
@@ -286,6 +323,7 @@ impl Efi {
286323
fn package_mode_copy_to_boot_impl(&self, sysroot: &Path) -> Result<()> {
287324
let sysroot_path = Utf8Path::from_path(sysroot)
288325
.with_context(|| format!("Invalid UTF-8: {}", sysroot.display()))?;
326+
let sysroot_dir = openat::Dir::open(sysroot).context("Opening sysroot for reading")?;
289327

290328
let efi_comps = match get_efi_component_from_usr(sysroot_path, EFILIB)? {
291329
Some(comps) if !comps.is_empty() => comps,
@@ -309,7 +347,6 @@ impl Efi {
309347
.with_context(|| format!("Opening ESP at {}", esp_path.display()))?;
310348
validate_esp_fstype(&esp_dir)?;
311349

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

315352
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)