Skip to content

Commit 0d9b4f2

Browse files
authored
Merge commit from fork
Signed-off-by: Yusuke Sakurai <[email protected]>
1 parent b7cf453 commit 0d9b4f2

File tree

6 files changed

+397
-1
lines changed

6 files changed

+397
-1
lines changed

crates/libcontainer/src/rootfs/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub mod device;
99
pub use device::Device;
1010

1111
pub(super) mod mount;
12+
pub use mount::Mount;
13+
1214
pub(super) mod symlink;
1315

1416
pub mod utils;

crates/libcontainer/src/rootfs/mount.rs

Lines changed: 297 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
use std::fs::{canonicalize, create_dir_all, OpenOptions};
2-
use std::mem;
2+
use std::io::ErrorKind;
3+
use std::os::unix::fs::MetadataExt;
34
use std::os::unix::io::AsRawFd;
45
use std::path::{Path, PathBuf};
56
use std::time::Duration;
67
#[cfg(feature = "v1")]
78
use std::{borrow::Cow, collections::HashMap};
9+
use std::{fs, mem};
810

911
use libcgroups::common::CgroupSetup::{Hybrid, Legacy, Unified};
1012
#[cfg(feature = "v1")]
@@ -14,6 +16,7 @@ use nix::errno::Errno;
1416
use nix::fcntl::OFlag;
1517
use nix::mount::MsFlags;
1618
use nix::sys::stat::Mode;
19+
use nix::sys::statfs::{statfs, PROC_SUPER_MAGIC};
1720
use nix::NixPath;
1821
use oci_spec::runtime::{Mount as SpecMount, MountBuilder as SpecMountBuilder};
1922
use 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(
652828
mod 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
}

tests/contest/contest/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use crate::tests::process_oom_score_adj::get_process_oom_score_adj_test;
3232
use crate::tests::process_rlimits::get_process_rlimits_test;
3333
use crate::tests::process_rlimits_fail::get_process_rlimits_fail_test;
3434
use crate::tests::process_user::get_process_user_test;
35+
use crate::tests::prohibit_symlink::get_prohibit_symlink_test;
3536
use crate::tests::readonly_paths::get_ro_paths_test;
3637
use crate::tests::root_readonly_true::get_root_readonly_test;
3738
use crate::tests::rootfs_propagation::get_rootfs_propagation_test;
@@ -144,6 +145,7 @@ fn main() -> Result<()> {
144145
let process_capabilities_fail = get_process_capabilities_fail_test();
145146
let uid_mappings = get_uid_mappings_test();
146147
let exec_cpu_affinity = get_exec_cpu_affinity_test();
148+
let prohibit_symlink = get_prohibit_symlink_test();
147149

148150
tm.add_test_group(Box::new(cl));
149151
tm.add_test_group(Box::new(cc));
@@ -183,6 +185,7 @@ fn main() -> Result<()> {
183185
tm.add_test_group(Box::new(process_capabilities_fail));
184186
tm.add_test_group(Box::new(uid_mappings));
185187
tm.add_test_group(Box::new(exec_cpu_affinity));
188+
tm.add_test_group(Box::new(prohibit_symlink));
186189

187190
tm.add_test_group(Box::new(io_priority_test));
188191
tm.add_cleanup(Box::new(cgroups::cleanup_v1));

tests/contest/contest/src/tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub mod process_oom_score_adj;
2222
pub mod process_rlimits;
2323
pub mod process_rlimits_fail;
2424
pub mod process_user;
25+
pub mod prohibit_symlink;
2526
pub mod readonly_paths;
2627
pub mod root_readonly_true;
2728
pub mod rootfs_propagation;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mod prohibit_symlink_test;
2+
3+
pub use prohibit_symlink_test::get_prohibit_symlink_test;

0 commit comments

Comments
 (0)