Skip to content

Commit 824a5c6

Browse files
committed
mend
1 parent 42286bb commit 824a5c6

3 files changed

Lines changed: 151 additions & 107 deletions

File tree

src/bootupd.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,9 @@ pub(crate) fn client_run_migrate_static_grub_config() -> Result<()> {
802802
Ok(())
803803
}
804804

805-
/// Copy bootloader files from /usr/lib/efi to boot/ESP for package mode installations.
805+
/// Copy EFI payloads from `/usr/lib/efi` onto the ESP for package-mode installs. Removals are
806+
/// scoped to `EFI/<vendor>/` trees present in this merge; other top-level ESP dirs are untouched.
807+
/// BIOS components are no-ops.
806808
pub(crate) fn copy_to_boot(root: &Path) -> Result<()> {
807809
let all_components = get_components_impl(false);
808810
if all_components.is_empty() {

src/component.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ pub(crate) trait Component {
8686
/// Locating efi vendor dir
8787
fn get_efi_vendor(&self, sysroot: &Path) -> Result<Option<String>>;
8888

89-
/// Merge `/usr/lib/efi` onto the ESP for package-mode systems (EFI components only).
89+
/// Merge `/usr/lib/efi` onto the ESP for package-mode systems (EFI only). Deletes only under
90+
/// `EFI/<vendor>/` for vendors shipped in this merge; other ESP subtrees are left alone.
9091
fn package_mode_copy_to_boot(&self, root: &Path) -> Result<()>;
9192
}
9293

src/efi.rs

Lines changed: 146 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
use std::cell::RefCell;
8+
use std::collections::HashSet;
89
use std::os::unix::io::AsRawFd;
910
use std::path::{Path, PathBuf};
1011
use std::process::Command;
@@ -19,10 +20,7 @@ use chrono::prelude::*;
1920
use fn_error_context::context;
2021
use openat_ext::OpenatDirExt;
2122
use os_release::OsRelease;
22-
use rustix::mount::{
23-
fsconfig_create, fsconfig_set_path, fsmount, fsopen, move_mount, FsMountFlags, FsOpenFlags,
24-
MountAttrFlags, MoveMountFlags, UnmountFlags,
25-
};
23+
use rustix::mount::UnmountFlags;
2624
use rustix::{fd::AsFd, fd::BorrowedFd, fs::StatVfsMountFlags};
2725
use walkdir::WalkDir;
2826
use widestring::U16CString;
@@ -91,32 +89,6 @@ pub(crate) fn is_efi_booted() -> Result<bool> {
9189
.map_err(Into::into)
9290
}
9391

94-
fn mount_esp(esp_device: &Path, target: &Path) -> Result<()> {
95-
use rustix::fs::CWD;
96-
97-
let fs_fd = fsopen("vfat", FsOpenFlags::empty()).context("fsopen vfat")?;
98-
fsconfig_set_path(fs_fd.as_fd(), "source", esp_device, CWD)
99-
.context("fsconfig_set_path source")?;
100-
fsconfig_create(fs_fd.as_fd()).context("fsconfig_create")?;
101-
let mount_fd = fsmount(
102-
fs_fd.as_fd(),
103-
FsMountFlags::empty(),
104-
MountAttrFlags::empty(),
105-
)
106-
.context("fsmount")?;
107-
let target_dir = std::fs::File::open(target).context("open target dir for move_mount")?;
108-
let target_fd = unsafe { BorrowedFd::borrow_raw(target_dir.as_raw_fd()) };
109-
move_mount(
110-
mount_fd.as_fd(),
111-
"",
112-
target_fd,
113-
".",
114-
MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,
115-
)
116-
.context("move_mount")?;
117-
Ok(())
118-
}
119-
12092
struct Mount {
12193
path: PathBuf,
12294
owned: bool,
@@ -167,6 +139,16 @@ impl Efi {
167139
}
168140
}
169141

142+
/// Attach `esp_device` at a well-known mount under `root` when the ESP is not already there.
143+
///
144+
/// This uses **`mount(8)`** only—the same approach as the rest of bootupd’s ESP handling—not a
145+
/// second parallel stack (e.g. `fsopen`/`fsmount`/`move_mount`). Review feedback was that duplicating
146+
/// the kernel mount API without a clear win was confusing; RPM `%posttrans` also benefits from a
147+
/// single, predictable mount story.
148+
///
149+
/// A **different** enhancement would be Linux’s newer mount APIs to obtain a **`mount_fd`** and
150+
/// do all ESP I/O via **`cap_std::fs::Dir`** without attaching to the global mount namespace. That
151+
/// would require refactoring callers to work entirely through that fd; it is **not** implemented here.
170152
pub(crate) fn mount_esp_device(&self, root: &Path, esp_device: &Path) -> Result<PathBuf> {
171153
// (mount path, whether bootupd performed the mount and must unmount)
172154
let mut mountpoint: Option<(PathBuf, bool)> = None;
@@ -188,12 +170,6 @@ impl Efi {
188170
}
189171
}
190172

191-
if mount_esp(esp_device, &mnt).is_ok() {
192-
log::debug!("Mounted at {mnt:?}");
193-
mountpoint = Some((mnt, true));
194-
break;
195-
}
196-
log::trace!("Mount failed, falling back to mount(8)");
197173
std::process::Command::new("mount")
198174
.arg(esp_device)
199175
.arg(&mnt)
@@ -234,15 +210,9 @@ impl Efi {
234210
.unwrap_or(false);
235211
if should_unmount {
236212
if let Some(mount) = self.mountpoint.borrow_mut().take() {
237-
if rustix::mount::unmount(&mount.path, UnmountFlags::empty()).is_ok() {
238-
log::trace!("Unmounted (new mount API)");
239-
} else {
240-
Command::new("umount")
241-
.arg(&mount.path)
242-
.run_inherited()
243-
.with_context(|| format!("Failed to unmount {:?}", mount.path))?;
244-
log::trace!("Unmounted");
245-
}
213+
rustix::mount::unmount(&mount.path, UnmountFlags::empty())
214+
.with_context(|| format!("Failed to unmount {:?}", mount.path))?;
215+
log::trace!("Unmounted");
246216
}
247217
}
248218
Ok(())
@@ -300,31 +270,32 @@ impl Efi {
300270
&self,
301271
sysroot_dir: &openat::Dir,
302272
esp_dir: &openat::Dir,
303-
_esp_path: &Path,
273+
esp_path: &Path,
304274
efi_components: &[EFIComponent],
305275
) -> Result<()> {
306-
// Build a merged source tree in a temp dir (same layout as desired ESP/EFI)
307-
let temp_dir = tempfile::tempdir().context("Creating temp dir for EFI merge")?;
308-
let temp_efi_path = temp_dir.path().join("EFI");
309-
std::fs::create_dir_all(&temp_efi_path)
310-
.with_context(|| format!("Creating {}", temp_efi_path.display()))?;
311-
let temp_efi_str = temp_efi_path
276+
// Staging directory on the ESP (same filesystem as the destination) avoids merging under
277+
// /tmp and copying across filesystems twice. Each component path is `.../<pkg>/<ver>/EFI`;
278+
// copy that directory into the staging root so the merged tree is `EFI/<vendor>/…` without
279+
// using `{path}/.` tricks.
280+
let temp_dir = tempfile::Builder::new()
281+
.prefix(".bootupd-efi-merge-")
282+
.tempdir_in(esp_path)
283+
.with_context(|| format!("Creating EFI merge temp dir under {}", esp_path.display()))?;
284+
let temp_root_str = temp_dir
285+
.path()
312286
.to_str()
313-
.context("Temp EFI path is not valid UTF-8")?;
287+
.context("Temp merge path is not valid UTF-8")?;
314288

315289
for efi_comp in efi_components {
316290
log::info!(
317291
"Merging EFI component {} version {} into update tree",
318292
efi_comp.name,
319293
efi_comp.version
320294
);
321-
// Copy contents of component's EFI dir (e.g. fedora/) into temp_efi_path so merged
322-
// layout is EFI/fedora/..., not EFI/EFI/fedora/...
323-
let src_efi_contents = format!("{}/.", efi_comp.path);
324295
filetree::copy_dir_with_args(
325296
sysroot_dir,
326-
src_efi_contents.as_str(),
327-
temp_efi_str,
297+
efi_comp.path.as_str(),
298+
temp_root_str,
328299
OPTIONS,
329300
)
330301
.with_context(|| format!("Copying {} to merge dir", efi_comp.path))?;
@@ -334,16 +305,46 @@ impl Efi {
334305
esp_dir.ensure_dir_all(std::path::Path::new("EFI"), 0o755)?;
335306
let esp_efi_dir = esp_dir.sub_dir("EFI").context("Opening ESP EFI dir")?;
336307

337-
let source_dir =
338-
openat::Dir::open(&temp_efi_path).context("Opening merged EFI source dir")?;
308+
let source_dir = openat::Dir::open(&temp_dir.path().join("EFI"))
309+
.context("Opening merged EFI source dir")?;
339310
let source_filetree =
340311
filetree::FileTree::new_from_dir(&source_dir).context("Building source filetree")?;
341312
let current_filetree =
342313
filetree::FileTree::new_from_dir(&esp_efi_dir).context("Building current filetree")?;
343314
let mut diff = current_filetree
344315
.diff(&source_filetree)
345316
.context("Computing EFI diff")?;
346-
diff.removals.clear();
317+
318+
// Scoped removals (option 2): only delete paths under `EFI/<name>/` for `<name>` present in
319+
// this merge. Stale files under those trees are removed; other top-level ESP dirs are untouched.
320+
let mut managed_prefixes: HashSet<String> = source_filetree
321+
.children
322+
.keys()
323+
.filter_map(|k| k.split('/').next().map(str::to_string))
324+
.collect();
325+
for entry in source_dir.list_dir(".")? {
326+
let entry = entry?;
327+
if matches!(source_dir.get_file_type(&entry)?, openat::SimpleType::Dir) {
328+
if let Some(name) = entry.file_name().to_str() {
329+
managed_prefixes.insert(name.to_string());
330+
}
331+
}
332+
}
333+
let removals_before = diff.removals.len();
334+
diff.removals.retain(|path| {
335+
path.split('/')
336+
.next()
337+
.map(|first| managed_prefixes.contains(first))
338+
.unwrap_or(false)
339+
});
340+
let skipped = removals_before.saturating_sub(diff.removals.len());
341+
if skipped > 0 {
342+
log::debug!(
343+
"Skipped {} ESP removal(s) outside shipped EFI prefixes (managed: {:?})",
344+
skipped,
345+
managed_prefixes
346+
);
347+
}
347348

348349
// Check available space before writing to prevent partial updates when the partition is full
349350
let required_bytes = current_filetree.total_size() + source_filetree.total_size();
@@ -356,7 +357,7 @@ impl Efi {
356357
);
357358
}
358359

359-
// Same logic as bootable container: write to .btmp.* then atomic rename
360+
// Same logic as bootable container: write-aside + rename; removals only under managed dirs.
360361
filetree::apply_diff(&source_dir, &esp_efi_dir, &diff, None)
361362
.context("Applying EFI update (write alongside + atomic rename)")?;
362363

@@ -365,48 +366,6 @@ impl Efi {
365366

366367
Ok(())
367368
}
368-
369-
/// Copy from /usr/lib/efi to boot/ESP. Caller provides sysroot (e.g. for recovery or tests).
370-
fn package_mode_copy_to_boot_impl(&self, sysroot: &Path) -> Result<()> {
371-
let sysroot_path = Utf8Path::from_path(sysroot)
372-
.with_context(|| format!("Invalid UTF-8: {}", sysroot.display()))?;
373-
let sysroot_dir = openat::Dir::open(sysroot).context("Opening sysroot for reading")?;
374-
375-
let efi_comps = match get_efi_component_from_usr(sysroot_path, EFILIB)? {
376-
Some(comps) if !comps.is_empty() => comps,
377-
_ => anyhow::bail!("No EFI components found in /usr/lib/efi"),
378-
};
379-
380-
// First try to use an already mounted ESP
381-
let esp_path = if let Some(mounted_esp) = self.get_mounted_esp(sysroot)? {
382-
mounted_esp
383-
} else {
384-
let sysroot_cap = Dir::open_ambient_dir(sysroot, cap_std::ambient_authority())
385-
.with_context(|| format!("Opening sysroot {}", sysroot.display()))?;
386-
let device = bootc_internal_blockdev::list_dev_by_dir(&sysroot_cap)
387-
.with_context(|| format!("Resolving block device for {}", sysroot.display()))?;
388-
let Some(esp_devices) = device.find_colocated_esps()? else {
389-
anyhow::bail!("No ESP found");
390-
};
391-
let esp = esp_devices
392-
.first()
393-
.ok_or_else(|| anyhow::anyhow!("No ESP partition found"))?;
394-
self.ensure_mounted_esp(sysroot, Path::new(&esp.path()))?
395-
};
396-
397-
let esp_dir = openat::Dir::open(&esp_path)
398-
.with_context(|| format!("Opening ESP at {}", esp_path.display()))?;
399-
validate_esp_fstype(&esp_dir)?;
400-
401-
self.copy_efi_components_to_esp(&sysroot_dir, &esp_dir, &esp_path, &efi_comps)?;
402-
403-
log::info!(
404-
"Successfully copied {} EFI component(s) to ESP at {}",
405-
efi_comps.len(),
406-
esp_path.display()
407-
);
408-
Ok(())
409-
}
410369
}
411370

412371
#[context("Get product name")]
@@ -855,8 +814,46 @@ impl Component for Efi {
855814
}
856815

857816
/// Package mode: merge `/usr/lib/efi` onto the ESP (write alongside, then atomic rename).
817+
/// Prefer an ESP already mounted at a well-known path; otherwise mount via `mount(8)` only.
858818
fn package_mode_copy_to_boot(&self, root: &Path) -> Result<()> {
859-
self.package_mode_copy_to_boot_impl(root)
819+
let sysroot_path = Utf8Path::from_path(root)
820+
.with_context(|| format!("Invalid UTF-8: {}", root.display()))?;
821+
let sysroot_dir = openat::Dir::open(root).context("Opening sysroot for reading")?;
822+
823+
let efi_comps = match get_efi_component_from_usr(sysroot_path, EFILIB)? {
824+
Some(comps) if !comps.is_empty() => comps,
825+
_ => anyhow::bail!("No EFI components found in /usr/lib/efi"),
826+
};
827+
828+
// Reuse existing ESP mount when present (RPM posttrans must not rely on extra mount APIs).
829+
let esp_path = if let Some(mounted_esp) = self.get_mounted_esp(root)? {
830+
mounted_esp
831+
} else {
832+
let sysroot_cap = Dir::open_ambient_dir(root, cap_std::ambient_authority())
833+
.with_context(|| format!("Opening sysroot {}", root.display()))?;
834+
let device = bootc_internal_blockdev::list_dev_by_dir(&sysroot_cap)
835+
.with_context(|| format!("Resolving block device for {}", root.display()))?;
836+
let Some(esp_devices) = device.find_colocated_esps()? else {
837+
anyhow::bail!("No ESP found");
838+
};
839+
let esp = esp_devices
840+
.first()
841+
.ok_or_else(|| anyhow::anyhow!("No ESP partition found"))?;
842+
self.ensure_mounted_esp(root, Path::new(&esp.path()))?
843+
};
844+
845+
let esp_dir = openat::Dir::open(&esp_path)
846+
.with_context(|| format!("Opening ESP at {}", esp_path.display()))?;
847+
validate_esp_fstype(&esp_dir)?;
848+
849+
self.copy_efi_components_to_esp(&sysroot_dir, &esp_dir, &esp_path, &efi_comps)?;
850+
851+
log::info!(
852+
"Successfully copied {} EFI component(s) to ESP at {}",
853+
efi_comps.len(),
854+
esp_path.display()
855+
);
856+
Ok(())
860857
}
861858
}
862859

@@ -1373,4 +1370,48 @@ Boot0003* test";
13731370

13741371
Ok(())
13751372
}
1373+
1374+
#[test]
1375+
fn test_copy_efi_scoped_removals_under_shipped_vendor_only() -> Result<()> {
1376+
let tmpdir: &tempfile::TempDir = &tempfile::tempdir()?;
1377+
let tpath = tmpdir.path();
1378+
let efi_path = tpath.join("usr/lib/efi");
1379+
let shim_path = efi_path.join("shim/15.8-3/EFI/fedora");
1380+
std::fs::create_dir_all(&shim_path)?;
1381+
std::fs::write(shim_path.join(SHIM), b"shim v2")?;
1382+
1383+
let esp_path = tpath.join("boot/efi");
1384+
std::fs::create_dir_all(esp_path.join("EFI/fedora"))?;
1385+
std::fs::create_dir_all(esp_path.join("EFI/othervendor"))?;
1386+
std::fs::write(esp_path.join("EFI/fedora/stale.efi"), b"old")?;
1387+
std::fs::write(esp_path.join("EFI/othervendor/keep.efi"), b"keep")?;
1388+
1389+
let sysroot_dir = openat::Dir::open(tpath)?;
1390+
let utf8_tpath =
1391+
Utf8Path::from_path(tpath).ok_or_else(|| anyhow::anyhow!("Path is not valid UTF-8"))?;
1392+
let efi_comps = get_efi_component_from_usr(utf8_tpath, EFILIB)?.unwrap();
1393+
1394+
let esp_dir = openat::Dir::open(&esp_path).context("Opening ESP dir for test")?;
1395+
let efi = Efi::default();
1396+
efi.copy_efi_components_to_esp(&sysroot_dir, &esp_dir, &esp_path, &efi_comps)?;
1397+
1398+
assert!(
1399+
!esp_path.join("EFI/fedora/stale.efi").exists(),
1400+
"stale file under shipped vendor should be removed"
1401+
);
1402+
assert!(
1403+
esp_path.join("EFI/fedora").join(SHIM).exists(),
1404+
"shim should be present"
1405+
);
1406+
assert!(
1407+
esp_path.join("EFI/othervendor/keep.efi").exists(),
1408+
"unmanaged vendor tree must be untouched"
1409+
);
1410+
assert_eq!(
1411+
std::fs::read(esp_path.join("EFI/othervendor/keep.efi"))?,
1412+
b"keep"
1413+
);
1414+
1415+
Ok(())
1416+
}
13761417
}

0 commit comments

Comments
 (0)