@@ -1803,6 +1803,50 @@ pub(crate) async fn install_to_disk(mut opts: InstallToDiskOpts) -> Result<()> {
18031803 Ok ( ( ) )
18041804}
18051805
1806+ /// Require that a directory contains only mount points (or is empty), recursively.
1807+ /// Returns Ok(()) if all entries in the directory tree are either:
1808+ /// - Mount points (on different filesystems)
1809+ /// - Directories that themselves contain only mount points (recursively)
1810+ /// - The lost+found directory
1811+ ///
1812+ /// Returns an error if any non-mount entry is found.
1813+ ///
1814+ /// This handles cases like /var containing /var/lib (not a mount) which contains
1815+ /// /var/lib/containers (a mount point).
1816+ #[ context( "Requiring directory contains only mount points" ) ]
1817+ fn require_dir_contains_only_mounts ( parent_fd : & Dir , dir_name : & str ) -> Result < ( ) > {
1818+ let Some ( dir_fd) = parent_fd. open_dir_noxdev ( dir_name) ? else {
1819+ // The directory itself is a mount point
1820+ return Ok ( ( ) ) ;
1821+ } ;
1822+
1823+ if dir_fd. entries ( ) ?. next ( ) . is_none ( ) {
1824+ anyhow:: bail!( "Found empty directory: {dir_name}" ) ;
1825+ }
1826+
1827+ for entry in dir_fd. entries ( ) ? {
1828+ let entry = DirEntryUtf8 :: from_cap_std ( entry?) ;
1829+ let entry_name = entry. file_name ( ) ?;
1830+
1831+ if entry_name == LOST_AND_FOUND {
1832+ continue ;
1833+ }
1834+
1835+ if dir_name == BOOT && entry_name == crate :: bootloader:: EFI_DIR {
1836+ continue ;
1837+ }
1838+
1839+ let etype = entry. file_type ( ) ?;
1840+ if etype == FileType :: dir ( ) && dir_fd. open_dir_noxdev ( & entry_name) ?. is_some ( ) {
1841+ require_dir_contains_only_mounts ( & dir_fd, & entry_name) ?;
1842+ } else {
1843+ anyhow:: bail!( "Found entry in {dir_name}: {entry_name}" ) ;
1844+ }
1845+ }
1846+
1847+ Ok ( ( ) )
1848+ }
1849+
18061850#[ context( "Verifying empty rootfs" ) ]
18071851fn require_empty_rootdir ( rootfs_fd : & Dir ) -> Result < ( ) > {
18081852 for e in rootfs_fd. entries ( ) ? {
@@ -1811,17 +1855,11 @@ fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
18111855 if name == LOST_AND_FOUND {
18121856 continue ;
18131857 }
1814- // There must be a boot directory (that is empty)
1815- if name == BOOT {
1816- let mut entries = rootfs_fd. read_dir ( BOOT ) ?;
1817- if let Some ( e) = entries. next ( ) {
1818- let e = DirEntryUtf8 :: from_cap_std ( e?) ;
1819- let name = e. file_name ( ) ?;
1820- if matches ! ( name. as_str( ) , LOST_AND_FOUND | crate :: bootloader:: EFI_DIR ) {
1821- continue ;
1822- }
1823- anyhow:: bail!( "Non-empty boot directory, found {name}" ) ;
1824- }
1858+
1859+ // Check if this entry is a directory
1860+ let etype = e. file_type ( ) ?;
1861+ if etype == FileType :: dir ( ) {
1862+ require_dir_contains_only_mounts ( rootfs_fd, & name) ?;
18251863 } else {
18261864 anyhow:: bail!( "Non-empty root filesystem; found {name:?}" ) ;
18271865 }
@@ -2573,4 +2611,101 @@ UUID=boot-uuid /boot ext4 defaults 0 0
25732611
25742612 Ok ( ( ) )
25752613 }
2614+
2615+ #[ test]
2616+ fn test_require_dir_contains_only_mounts ( ) -> Result < ( ) > {
2617+ // Test 1: Empty directory should fail (not a mount point)
2618+ {
2619+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2620+ td. create_dir ( "empty" ) ?;
2621+ assert ! ( require_dir_contains_only_mounts( & td, "empty" ) . is_err( ) ) ;
2622+ }
2623+
2624+ // Test 2: Directory with only lost+found should succeed (lost+found is ignored)
2625+ {
2626+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2627+ td. create_dir_all ( "var/lost+found" ) ?;
2628+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_ok( ) ) ;
2629+ }
2630+
2631+ // Test 3: Directory with a regular file should fail
2632+ {
2633+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2634+ td. create_dir ( "var" ) ?;
2635+ td. write ( "var/test.txt" , b"content" ) ?;
2636+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2637+ }
2638+
2639+ // Test 4: Nested directory structure with a file should fail
2640+ {
2641+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2642+ td. create_dir_all ( "var/lib/containers" ) ?;
2643+ td. write ( "var/lib/containers/storage.db" , b"data" ) ?;
2644+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2645+ }
2646+
2647+ // Test 5: boot directory with efi subdirectory should succeed (efi is allowed in boot)
2648+ {
2649+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2650+ td. create_dir_all ( "boot/efi" ) ?;
2651+ assert ! ( require_dir_contains_only_mounts( & td, "boot" ) . is_ok( ) ) ;
2652+ }
2653+
2654+ // Test 6: boot directory with both efi and lost+found should succeed (both are allowed)
2655+ {
2656+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2657+ td. create_dir_all ( "boot/efi" ) ?;
2658+ td. create_dir_all ( "boot/lost+found" ) ?;
2659+ assert ! ( require_dir_contains_only_mounts( & td, "boot" ) . is_ok( ) ) ;
2660+ }
2661+
2662+ // Test 7: boot directory with grub should fail (grub2 is not a mount and contains files)
2663+ {
2664+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2665+ td. create_dir_all ( "boot/grub2" ) ?;
2666+ td. write ( "boot/grub2/grub.cfg" , b"config" ) ?;
2667+ assert ! ( require_dir_contains_only_mounts( & td, "boot" ) . is_err( ) ) ;
2668+ }
2669+
2670+ // Test 8: Nested empty directories should fail (empty directories are not mount points)
2671+ {
2672+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2673+ td. create_dir_all ( "var/lib/containers" ) ?;
2674+ td. create_dir_all ( "var/log/journal" ) ?;
2675+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2676+ }
2677+
2678+ // Test 9: Directory with lost+found and a file should fail (lost+found is ignored, but file is not allowed)
2679+ {
2680+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2681+ td. create_dir_all ( "var/lost+found" ) ?;
2682+ td. write ( "var/data.txt" , b"content" ) ?;
2683+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2684+ }
2685+
2686+ // Test 10: Directory with a symlink should fail
2687+ {
2688+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2689+ td. create_dir ( "var" ) ?;
2690+ td. symlink_contents ( "../usr/lib" , "var/lib" ) ?;
2691+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2692+ }
2693+
2694+ // Test 11: Deeply nested directory with a file should fail
2695+ {
2696+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2697+ td. create_dir_all ( "var/lib/containers/storage/overlay" ) ?;
2698+ td. write ( "var/lib/containers/storage/overlay/file.txt" , b"data" ) ?;
2699+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2700+ }
2701+
2702+ // Test 12: Non-boot directory with efi should fail (special handling only applies to boot/efi)
2703+ {
2704+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2705+ td. create_dir_all ( "var/efi" ) ?;
2706+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2707+ }
2708+
2709+ Ok ( ( ) )
2710+ }
25762711}
0 commit comments