@@ -1803,6 +1803,70 @@ 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, which is acceptable
1820+ return Ok ( ( ) ) ;
1821+ } ;
1822+
1823+ let mut found_any_entry = false ;
1824+
1825+ for entry in dir_fd. entries ( ) ? {
1826+ let entry = DirEntryUtf8 :: from_cap_std ( entry?) ;
1827+ let entry_name = entry. file_name ( ) ?;
1828+
1829+ // Skip lost+found but count it as finding an entry
1830+ if entry_name == LOST_AND_FOUND {
1831+ found_any_entry = true ;
1832+ continue ;
1833+ }
1834+
1835+ // Allow the EFI directory in /boot but count it as finding an entry
1836+ if dir_name == BOOT && entry_name == crate :: bootloader:: EFI_DIR {
1837+ found_any_entry = true ;
1838+ continue ;
1839+ }
1840+
1841+ // Mark that we found a real entry
1842+ found_any_entry = true ;
1843+
1844+ let etype = entry. file_type ( ) ?;
1845+ if etype == FileType :: dir ( ) {
1846+ // If open_dir_noxdev returns None, this is a mount point on a different filesystem
1847+ if dir_fd. open_dir_noxdev ( & entry_name) ?. is_none ( ) {
1848+ tracing:: debug!( "Found mount point: {dir_name}/{entry_name}" ) ;
1849+ continue ;
1850+ }
1851+
1852+ // Not a mount point itself, but check recursively if it contains only mounts
1853+ require_dir_contains_only_mounts ( & dir_fd, & entry_name) ?;
1854+ tracing:: debug!( "Directory {dir_name}/{entry_name} contains only mount points" ) ;
1855+ continue ;
1856+ }
1857+
1858+ // Found a non-mount, non-directory-of-mounts entry
1859+ anyhow:: bail!( "Found non-mount entry in {dir_name}: {entry_name}" ) ;
1860+ }
1861+
1862+ // Empty directories (that are not mounts themselves) are not allowed
1863+ if !found_any_entry {
1864+ anyhow:: bail!( "Empty directory (not a mount): {dir_name}" ) ;
1865+ }
1866+
1867+ Ok ( ( ) )
1868+ }
1869+
18061870#[ context( "Verifying empty rootfs" ) ]
18071871fn require_empty_rootdir ( rootfs_fd : & Dir ) -> Result < ( ) > {
18081872 for e in rootfs_fd. entries ( ) ? {
@@ -1811,20 +1875,15 @@ fn require_empty_rootdir(rootfs_fd: &Dir) -> Result<()> {
18111875 if name == LOST_AND_FOUND {
18121876 continue ;
18131877 }
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- }
1825- } else {
1826- anyhow:: bail!( "Non-empty root filesystem; found {name:?}" ) ;
1878+
1879+ // Check if this entry is a directory
1880+ let etype = e. file_type ( ) ?;
1881+ if etype == FileType :: dir ( ) {
1882+ require_dir_contains_only_mounts ( rootfs_fd, & name) ?;
18271883 }
1884+
1885+ // If we reach here, found an entry that shouldn't exist
1886+ anyhow:: bail!( "Non-empty root filesystem; found {name:?}" ) ;
18281887 }
18291888 Ok ( ( ) )
18301889}
@@ -2573,4 +2632,101 @@ UUID=boot-uuid /boot ext4 defaults 0 0
25732632
25742633 Ok ( ( ) )
25752634 }
2635+
2636+ #[ test]
2637+ fn test_require_dir_contains_only_mounts ( ) -> Result < ( ) > {
2638+ // Test 1: Empty directory should fail (not a mount point)
2639+ {
2640+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2641+ td. create_dir ( "empty" ) ?;
2642+ assert ! ( require_dir_contains_only_mounts( & td, "empty" ) . is_err( ) ) ;
2643+ }
2644+
2645+ // Test 2: Directory with only lost+found should succeed (lost+found is ignored)
2646+ {
2647+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2648+ td. create_dir_all ( "var/lost+found" ) ?;
2649+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_ok( ) ) ;
2650+ }
2651+
2652+ // Test 3: Directory with a regular file should fail
2653+ {
2654+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2655+ td. create_dir ( "var" ) ?;
2656+ td. write ( "var/test.txt" , b"content" ) ?;
2657+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2658+ }
2659+
2660+ // Test 4: Nested directory structure with a file should fail
2661+ {
2662+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2663+ td. create_dir_all ( "var/lib/containers" ) ?;
2664+ td. write ( "var/lib/containers/storage.db" , b"data" ) ?;
2665+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2666+ }
2667+
2668+ // Test 5: boot directory with efi subdirectory should succeed (efi is allowed in boot)
2669+ {
2670+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2671+ td. create_dir_all ( "boot/efi" ) ?;
2672+ assert ! ( require_dir_contains_only_mounts( & td, "boot" ) . is_ok( ) ) ;
2673+ }
2674+
2675+ // Test 6: boot directory with both efi and lost+found should succeed (both are allowed)
2676+ {
2677+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2678+ td. create_dir_all ( "boot/efi" ) ?;
2679+ td. create_dir_all ( "boot/lost+found" ) ?;
2680+ assert ! ( require_dir_contains_only_mounts( & td, "boot" ) . is_ok( ) ) ;
2681+ }
2682+
2683+ // Test 7: boot directory with grub should fail (grub2 is not a mount and contains files)
2684+ {
2685+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2686+ td. create_dir_all ( "boot/grub2" ) ?;
2687+ td. write ( "boot/grub2/grub.cfg" , b"config" ) ?;
2688+ assert ! ( require_dir_contains_only_mounts( & td, "boot" ) . is_err( ) ) ;
2689+ }
2690+
2691+ // Test 8: Nested empty directories should fail (empty directories are not mount points)
2692+ {
2693+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2694+ td. create_dir_all ( "var/lib/containers" ) ?;
2695+ td. create_dir_all ( "var/log/journal" ) ?;
2696+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2697+ }
2698+
2699+ // Test 9: Directory with lost+found and a file should fail (lost+found is ignored, but file is not allowed)
2700+ {
2701+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2702+ td. create_dir_all ( "var/lost+found" ) ?;
2703+ td. write ( "var/data.txt" , b"content" ) ?;
2704+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2705+ }
2706+
2707+ // Test 10: Directory with a symlink should fail
2708+ {
2709+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2710+ td. create_dir ( "var" ) ?;
2711+ td. symlink_contents ( "../usr/lib" , "var/lib" ) ?;
2712+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2713+ }
2714+
2715+ // Test 11: Deeply nested directory with a file should fail
2716+ {
2717+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2718+ td. create_dir_all ( "var/lib/containers/storage/overlay" ) ?;
2719+ td. write ( "var/lib/containers/storage/overlay/file.txt" , b"data" ) ?;
2720+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2721+ }
2722+
2723+ // Test 12: Non-boot directory with efi should fail (special handling only applies to boot/efi)
2724+ {
2725+ let td = cap_std_ext:: cap_tempfile:: TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
2726+ td. create_dir_all ( "var/efi" ) ?;
2727+ assert ! ( require_dir_contains_only_mounts( & td, "var" ) . is_err( ) ) ;
2728+ }
2729+
2730+ Ok ( ( ) )
2731+ }
25762732}
0 commit comments