55 */
66
77use std:: cell:: RefCell ;
8+ use std:: collections:: HashSet ;
89use std:: os:: unix:: io:: AsRawFd ;
910use std:: path:: { Path , PathBuf } ;
1011use std:: process:: Command ;
@@ -19,10 +20,7 @@ use chrono::prelude::*;
1920use fn_error_context:: context;
2021use openat_ext:: OpenatDirExt ;
2122use 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 ;
2624use rustix:: { fd:: AsFd , fd:: BorrowedFd , fs:: StatVfsMountFlags } ;
2725use walkdir:: WalkDir ;
2826use 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-
12092struct 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