11use std:: fs:: { canonicalize, create_dir_all, OpenOptions } ;
2- use std:: mem;
2+ use std:: io:: ErrorKind ;
3+ use std:: os:: unix:: fs:: MetadataExt ;
34use std:: os:: unix:: io:: AsRawFd ;
45use std:: path:: { Path , PathBuf } ;
56use std:: time:: Duration ;
67#[ cfg( feature = "v1" ) ]
78use std:: { borrow:: Cow , collections:: HashMap } ;
9+ use std:: { fs, mem} ;
810
911use libcgroups:: common:: CgroupSetup :: { Hybrid , Legacy , Unified } ;
1012#[ cfg( feature = "v1" ) ]
@@ -14,6 +16,7 @@ use nix::errno::Errno;
1416use nix:: fcntl:: OFlag ;
1517use nix:: mount:: MsFlags ;
1618use nix:: sys:: stat:: Mode ;
19+ use nix:: sys:: statfs:: { statfs, PROC_SUPER_MAGIC } ;
1720use nix:: NixPath ;
1821use oci_spec:: runtime:: { Mount as SpecMount , MountBuilder as SpecMountBuilder } ;
1922use procfs:: process:: { MountInfo , MountOptFields , Process } ;
@@ -119,6 +122,46 @@ impl Mount {
119122 }
120123 }
121124 }
125+ // procfs and sysfs are special because we need to ensure they are actually
126+ // mounted on a specific path in a container without any funny business.
127+ // Ref: https://github.com/opencontainers/runc/security/advisories/GHSA-fh74-hm69-rqjw
128+ Some ( typ @ ( "proc" | "sysfs" ) ) => {
129+ let dest_path = options
130+ . root
131+ . join_safely ( Path :: new ( mount. destination ( ) ) . normalize ( ) )
132+ . map_err ( |err| {
133+ tracing:: error!(
134+ "could not join rootfs path with mount destination {:?}: {}" ,
135+ mount. destination( ) ,
136+ err
137+ ) ;
138+ MountError :: Other ( err. into ( ) )
139+ } ) ?;
140+
141+ match fs:: symlink_metadata ( & dest_path) {
142+ Ok ( m) if !m. is_dir ( ) => {
143+ return Err ( MountError :: Other (
144+ format ! ( "filesystem {} must be mounted on ordinary directory" , typ)
145+ . into ( ) ,
146+ ) ) ;
147+ }
148+ Err ( e) if e. kind ( ) != ErrorKind :: NotFound => {
149+ return Err ( MountError :: Other (
150+ format ! ( "symlink_metadata failed for {}: {}" , dest_path. display( ) , e)
151+ . into ( ) ,
152+ ) ) ;
153+ }
154+ _ => { }
155+ }
156+
157+ self . check_proc_mount ( options. root , mount) ?;
158+
159+ self . mount_into_container ( mount, options. root , & mount_option_config, options. label )
160+ . map_err ( |err| {
161+ tracing:: error!( "failed to mount {:?}: {}" , mount, err) ;
162+ err
163+ } ) ?;
164+ }
122165 _ => {
123166 if * mount. destination ( ) == PathBuf :: from ( "/dev" ) {
124167 mount_option_config. flags &= !MsFlags :: MS_RDONLY ;
@@ -630,6 +673,139 @@ impl Mount {
630673
631674 Ok ( ( ) )
632675 }
676+
677+ /// check_proc_mount checks to ensure that the mount destination is not over the top of /proc.
678+ /// dest is required to be an abs path and have any symlinks resolved before calling this function.
679+ /// # Example (a valid case where `/proc` is mounted with `proc` type.)
680+ ///
681+ /// ```
682+ /// use std::path::PathBuf;
683+ /// use oci_spec::runtime::MountBuilder as SpecMountBuilder;
684+ /// use libcontainer::rootfs::Mount;
685+ ///
686+ /// let mounter = Mount::new();
687+ ///
688+ /// let rootfs = PathBuf::from("/var/lib/my-runtime/containers/abcd1234/rootfs");
689+ /// let destination = PathBuf::from("/proc");
690+ /// let source = PathBuf::from("proc");
691+ /// let typ = "proc";
692+ ///
693+ /// let mount = SpecMountBuilder::default()
694+ /// .destination(destination)
695+ /// .typ(typ)
696+ /// .source(source)
697+ /// .build()
698+ /// .expect("failed to build SpecMount");
699+ ///
700+ /// assert!(mounter.check_proc_mount(rootfs.as_path(), &mount).is_ok());
701+ /// ```
702+ /// # Example (bind mount to `/proc` that should fail)
703+ /// ```
704+ /// use std::path::PathBuf;
705+ /// use oci_spec::runtime::MountBuilder as SpecMountBuilder;
706+ /// use libcontainer::rootfs::Mount;
707+ ///
708+ /// let mounter = Mount::new();
709+ ///
710+ /// let rootfs = PathBuf::from("/var/lib/my-runtime/containers/abcd1234/rootfs");
711+ /// let destination = PathBuf::from("/proc");
712+ /// let source = PathBuf::from("/tmp");
713+ /// let typ = "bind";
714+ ///
715+ /// let mount = SpecMountBuilder::default()
716+ /// .destination(destination)
717+ /// .typ(typ)
718+ /// .source(source)
719+ /// .build()
720+ /// .expect("failed to build SpecMount");
721+ ///
722+ /// assert!(mounter.check_proc_mount(rootfs.as_path(), &mount).is_err());
723+ /// ```
724+ pub fn check_proc_mount ( & self , rootfs : & Path , mount : & SpecMount ) -> Result < ( ) > {
725+ const PROC_ROOT_INO : u64 = 1 ;
726+ const VALID_PROC_MOUNTS : & [ & str ] = & [
727+ "/proc/cpuinfo" ,
728+ "/proc/diskstats" ,
729+ "/proc/meminfo" ,
730+ "/proc/stat" ,
731+ "/proc/swaps" ,
732+ "/proc/uptime" ,
733+ "/proc/loadavg" ,
734+ "/proc/slabinfo" ,
735+ "/proc/sys/kernel/ns_last_pid" ,
736+ "/proc/sys/crypto/fips_enabled" ,
737+ ] ;
738+
739+ let dest = mount. destination ( ) ;
740+
741+ let container_proc_path = rootfs. join ( "proc" ) ;
742+ let dest_path = rootfs. join_safely ( dest) . map_err ( |err| {
743+ tracing:: error!(
744+ "could not join rootfs path with mount destination {:?}: {}" ,
745+ dest,
746+ err
747+ ) ;
748+ MountError :: Other ( err. into ( ) )
749+ } ) ?;
750+
751+ // If path is Ok, it means dest_path is under /proc.
752+ // - Ok(p) with p.is_empty(): mount target is exactly /proc.
753+ // In this case, check if the mount source is procfs.
754+ // - Ok(p) with !p.is_empty(): mount target is under /proc.
755+ // Only allow if it matches a specific whitelist of proc entries.
756+ // - Err: not under /proc, so no further checks are needed
757+ let path = dest_path. strip_prefix ( & container_proc_path) ;
758+
759+ match path {
760+ Err ( _) => Ok ( ( ) ) ,
761+ Ok ( p) if p. as_os_str ( ) . is_empty ( ) => {
762+ if mount. typ ( ) . as_deref ( ) == Some ( "proc" ) {
763+ return Ok ( ( ) ) ;
764+ }
765+
766+ if mount. typ ( ) . as_deref ( ) == Some ( "bind" ) {
767+ if let Some ( source) = mount. source ( ) {
768+ let stat = statfs ( source) . map_err ( MountError :: from) ?;
769+ if stat. filesystem_type ( ) == PROC_SUPER_MAGIC {
770+ let meta = fs:: metadata ( source) . map_err ( MountError :: from) ?;
771+ // Follow the behavior of runc's checkProcMount function.
772+ if meta. ino ( ) != PROC_ROOT_INO {
773+ tracing:: warn!(
774+ "bind-mount {} (source {:?}) is of type procfs but not the root (inode {}). \
775+ Future versions may reject this.",
776+ dest. display( ) ,
777+ mount. source( ) ,
778+ meta. ino( )
779+ ) ;
780+ }
781+ return Ok ( ( ) ) ;
782+ }
783+ }
784+ }
785+
786+ Err ( MountError :: Custom ( format ! (
787+ "{} cannot be mounted because it is not type proc" ,
788+ dest. display( )
789+ ) ) )
790+ }
791+ Ok ( _) => {
792+ // Here dest is definitely under /proc. Do not allow those,
793+ // except for a few specific entries emulated by lxcfs.
794+ let is_allowed = VALID_PROC_MOUNTS . iter ( ) . any ( |allowed_path| {
795+ let container_allowed_path = rootfs. join ( allowed_path. trim_start_matches ( '/' ) ) ;
796+ dest_path == container_allowed_path
797+ } ) ;
798+
799+ if is_allowed {
800+ Ok ( ( ) )
801+ } else {
802+ Err ( MountError :: Other (
803+ format ! ( "{} is not a valid mount under /proc" , dest. display( ) ) . into ( ) ,
804+ ) )
805+ }
806+ }
807+ }
808+ }
633809}
634810
635811/// Find parent mount of rootfs in given mount infos
@@ -652,6 +828,7 @@ pub fn find_parent_mount(
652828mod tests {
653829 #[ cfg( feature = "v1" ) ]
654830 use std:: fs;
831+ use std:: os:: unix:: fs:: symlink;
655832
656833 use anyhow:: { Context , Ok , Result } ;
657834
@@ -1167,4 +1344,123 @@ mod tests {
11671344 let res = find_parent_mount ( Path :: new ( "/path/to/rootfs" ) , mount_infos) ;
11681345 assert ! ( res. is_err( ) ) ;
11691346 }
1347+
1348+ #[ test]
1349+ fn test_check_proc_mount_proc_ok ( ) -> Result < ( ) > {
1350+ let rootfs = tempfile:: tempdir ( ) ?;
1351+ let mounter = Mount :: new ( ) ;
1352+
1353+ let mount = SpecMountBuilder :: default ( )
1354+ . destination ( PathBuf :: from ( "/proc" ) )
1355+ . typ ( "proc" . to_string ( ) )
1356+ . source ( PathBuf :: from ( "proc" ) )
1357+ . build ( ) ?;
1358+
1359+ assert ! ( mounter. check_proc_mount( rootfs. path( ) , & mount) . is_ok( ) ) ;
1360+ Ok ( ( ) )
1361+ }
1362+
1363+ #[ test]
1364+ fn test_check_proc_mount_allowed_subpath ( ) -> Result < ( ) > {
1365+ let rootfs = tempfile:: tempdir ( ) ?;
1366+ let uptime = rootfs. path ( ) . join ( "proc/uptime" ) ;
1367+ std:: fs:: create_dir_all ( uptime. parent ( ) . unwrap ( ) ) ?;
1368+
1369+ let mounter = Mount :: new ( ) ;
1370+ let mount = SpecMountBuilder :: default ( )
1371+ . destination ( PathBuf :: from ( "/proc/uptime" ) )
1372+ . typ ( "bind" . to_string ( ) )
1373+ . source ( uptime)
1374+ . build ( ) ?;
1375+
1376+ assert ! ( mounter. check_proc_mount( rootfs. path( ) , & mount) . is_ok( ) ) ;
1377+ Ok ( ( ) )
1378+ }
1379+
1380+ #[ test]
1381+ fn test_check_proc_mount_denied_subpath ( ) -> Result < ( ) > {
1382+ let rootfs = tempfile:: tempdir ( ) ?;
1383+ let custom = rootfs. path ( ) . join ( "proc/custom" ) ;
1384+ std:: fs:: create_dir_all ( custom. parent ( ) . unwrap ( ) ) ?;
1385+
1386+ let mounter = Mount :: new ( ) ;
1387+ let mount = SpecMountBuilder :: default ( )
1388+ . destination ( PathBuf :: from ( "/proc/custom" ) )
1389+ . typ ( "bind" . to_string ( ) )
1390+ . source ( custom)
1391+ . build ( ) ?;
1392+
1393+ assert ! ( mounter. check_proc_mount( rootfs. path( ) , & mount) . is_err( ) ) ;
1394+ Ok ( ( ) )
1395+ }
1396+
1397+ #[ test]
1398+ fn setup_mount_proc_fails_if_destination_is_symlink ( ) -> Result < ( ) > {
1399+ let tmp = tempfile:: tempdir ( ) ?;
1400+ let rootfs = tmp. path ( ) ;
1401+
1402+ let symlink_path = rootfs. join ( "symlink" ) ;
1403+ fs:: create_dir_all ( & symlink_path) ?;
1404+ let proc_path = rootfs. join ( "proc" ) ;
1405+
1406+ symlink ( & symlink_path, & proc_path) ?;
1407+
1408+ let mount = SpecMountBuilder :: default ( )
1409+ . destination ( PathBuf :: from ( "/proc" ) )
1410+ . typ ( "proc" )
1411+ . source ( proc_path)
1412+ . build ( ) ?;
1413+
1414+ let options = MountOptions {
1415+ root : rootfs,
1416+ label : None ,
1417+ cgroup_ns : true ,
1418+ } ;
1419+
1420+ let m = Mount :: new ( ) ;
1421+
1422+ let res = m. setup_mount ( & mount, & options) ;
1423+
1424+ // proc destination symlink should be rejected
1425+ assert ! ( res. is_err( ) ) ;
1426+ let err = format ! ( "{:?}" , res. err( ) . unwrap( ) ) ;
1427+ assert ! ( err. contains( "must be mounted on ordinary directory" ) ) ;
1428+
1429+ Ok ( ( ) )
1430+ }
1431+
1432+ #[ test]
1433+ fn setup_mount_sys_fails_if_destination_is_symlink ( ) -> Result < ( ) > {
1434+ let tmp = tempfile:: tempdir ( ) ?;
1435+ let rootfs = tmp. path ( ) ;
1436+
1437+ let symlink_path = rootfs. join ( "symlink" ) ;
1438+ fs:: create_dir_all ( & symlink_path) ?;
1439+ let sys_path = rootfs. join ( "sys" ) ;
1440+
1441+ symlink ( & symlink_path, & sys_path) ?;
1442+
1443+ let mount = SpecMountBuilder :: default ( )
1444+ . destination ( PathBuf :: from ( "/sys" ) )
1445+ . typ ( "sysfs" )
1446+ . source ( sys_path)
1447+ . build ( ) ?;
1448+
1449+ let options = MountOptions {
1450+ root : rootfs,
1451+ label : None ,
1452+ cgroup_ns : true ,
1453+ } ;
1454+
1455+ let m = Mount :: new ( ) ;
1456+
1457+ let res = m. setup_mount ( & mount, & options) ;
1458+
1459+ // sys destination symlink should be rejected
1460+ assert ! ( res. is_err( ) ) ;
1461+ let err = format ! ( "{:?}" , res. err( ) . unwrap( ) ) ;
1462+ assert ! ( err. contains( "must be mounted on ordinary directory" ) ) ;
1463+
1464+ Ok ( ( ) )
1465+ }
11701466}
0 commit comments