Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions crates/libcontainer/src/rootfs/mount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ impl Mount {
flags: MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_NODEV,
data: vec![data.into_owned()],
rec_attr: None,
propagation_flags: vec![],
};

self.mount_into_container(
Expand Down Expand Up @@ -802,6 +803,48 @@ impl Mount {
}
}

self.apply_mount_propagation(
rootfs,
container_dest,
&mount_option_config.propagation_flags,
)?;

Ok(())
}

fn apply_mount_propagation(

@nayuta723 nayuta723 Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply_mount_propagation uses mount_setattr(2) for propagation flags (MS_SHARED, MS_SLAVE, etc.). However, mount_setattr(2) was added in Linux 5.12 (https://man7.org/linux/man-pages/man2/mount_setattr.2.html), and is not available on kernels still widely in use — notably Ubuntu 20.04 LTS (5.4), Debian 11 (5.10), Amazon Linux 2 (5.10), and RHEL 8 (4.18).

Neither runc nor crun uses mount_setattr(2) for propagation:

  • runc uses mount_setattr(2) exclusively for RecAttr (setRecAttr, rootfs_linux.go:1475) — attributes that cannot be applied recursively and atomically via mount(2). All other mount attributes (MS_RDONLY, MS_NOSUID, etc.) are set via mount(2) flags. Propagation flags are also applied via mount(2) in mountPropagate() (rootfs_linux.go:1458):

    for _, pflag := range m.PropagationFlags {
        mountViaFds("", nil, m.Destination, dstFd, "", uintptr(pflag), "")
    }
  • crun similarly retains a mount(2) path for propagation inside do_mount() (linux.c:1317-1322):

    if (mountflags & ALL_PROPAGATIONS_NO_REC)
        mount(NULL, real_target, NULL, mountflags & ALL_PROPAGATIONS, NULL);

Note that youki already uses mount_setattr(2) in other parts of the mount flow. For mount attribute setting (L671, L760), youki uses mount_setattr as part of its new mount API (open_tree → mount_setattr → move_mount), which has no equivalent in runc (runc uses mount(2) flags for attributes). For RecAttr (L681, L770), the AT_RECURSIVE usage is consistent with runc's setRecAttr. The issue is specifically that PR #3699 extends mount_setattr(2) to propagation flags, whereas both runc and crun use mount(2) for that purpose.

Since MS_REC can be passed directly to mount(2), using mount(NULL, dest, NULL, propagation_flag, NULL) would be sufficient and avoids the Linux 5.12 requirement for this step.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of avoiding the Linux 5.12 requirement, doesn’t that have little practical meaning for youki as a whole, since mount_setattr is already used in other parts of the runtime?

If the only reason is simply to align the implementation with runc and crun, I’m not particularly convinced that this is worth doing.

If we switch this back to mount(2), how do we preserve the fd-based path resolution guarantees we get from mount_setattr? As far as I can tell, youki does not currently have a helper equivalent to runc's handling for safely applying a legacy mount operation to an already-resolved fd-backed target.

Even if we decide to implement this using mount, I think it should be treated as a fallback path and addressed in a separate issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a good point. Sorry, I realize that my comment may not have made my intention clear.

My concern was not simply to align with runc or crun. I think we should separately discuss which kernel versions youki itself should support. Since youki already uses mount_setattr(2) in other parts of the runtime, switching this path to mount(2) would not meaningfully change the minimum kernel version for youki as a whole.

For now, I think it is reasonable to support kernel 5.12 and later. My main goal was to make sure we were aligned on the support policy.

For this PR, I’m fine with keeping mount_setattr(2) and considering the legacy mount(2) fallback, including safe fd-based path resolution, out of scope. We can discuss that separately in another issue.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created an issue here.

#3726

&self,
rootfs: &Path,
container_dest: &Path,
propagation_flags: &[MsFlags],
) -> Result<()> {
if propagation_flags.is_empty() {
return Ok(());
}

let root = Root::open(rootfs)?;
let dest: OwnedFd = root.resolve(container_dest)?.into();

for flags in propagation_flags {
let mut setattr_flags = linux::AT_EMPTY_PATH;
if flags.contains(MsFlags::MS_REC) {
setattr_flags |= linux::AT_RECURSIVE;
}
let mount_attr = linux::MountAttr {
attr_set: 0,
attr_clr: 0,
propagation: (*flags & !MsFlags::MS_REC).bits(),
userns_fd: 0,
};
self.syscall.mount_setattr(
dest.as_fd(),
Path::new(""),
setattr_flags,
&mount_attr,
mem::size_of::<linux::MountAttr>(),
)?;
}

Ok(())
}

Expand Down Expand Up @@ -1422,6 +1465,7 @@ mod tests {
flags,
data: vec![],
rec_attr: None,
propagation_flags: vec![],
};
mounter
.mount_cgroup_v2(&spec_cgroup_mount, &mount_opts, &mount_option_config)
Expand Down
45 changes: 36 additions & 9 deletions crates/libcontainer/src/rootfs/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ pub struct MountOptionConfig {

/// RecAttr represents mount properties to be applied recursively.
pub rec_attr: Option<linux::MountAttr>,

/// Mount propagation flags, kept separate from regular mount flags because
/// they are applied after the mount is attached.
pub propagation_flags: Vec<MsFlags>,
}

pub fn default_devices() -> Vec<LinuxDevice> {
Expand Down Expand Up @@ -87,6 +91,7 @@ pub fn to_sflag(dev_type: LinuxDeviceType) -> SFlag {

pub fn parse_mount(m: &Mount) -> std::result::Result<MountOptionConfig, MountError> {
let mut flags = MsFlags::empty();
let mut propagation_flags = Vec::new();
let mut data = Vec::new();
let mut mount_attr: Option<linux::MountAttr> = None;

Expand Down Expand Up @@ -167,14 +172,17 @@ pub fn parse_mount(m: &Mount) -> std::result::Result<MountOptionConfig, MountErr
MountOption::Nodiratime(is_clear, flag) => Some((is_clear, flag)),
MountOption::Bind(is_clear, flag) => Some((is_clear, flag)),
MountOption::Rbind(is_clear, flag) => Some((is_clear, flag)),
MountOption::Unbindable(is_clear, flag) => Some((is_clear, flag)),
MountOption::Runbindable(is_clear, flag) => Some((is_clear, flag)),
MountOption::Private(is_clear, flag) => Some((is_clear, flag)),
MountOption::Rprivate(is_clear, flag) => Some((is_clear, flag)),
MountOption::Shared(is_clear, flag) => Some((is_clear, flag)),
MountOption::Rshared(is_clear, flag) => Some((is_clear, flag)),
MountOption::Slave(is_clear, flag) => Some((is_clear, flag)),
MountOption::Rslave(is_clear, flag) => Some((is_clear, flag)),
MountOption::Unbindable(_, flag)
| MountOption::Runbindable(_, flag)
| MountOption::Private(_, flag)
| MountOption::Rprivate(_, flag)
| MountOption::Shared(_, flag)
| MountOption::Rshared(_, flag)
| MountOption::Slave(_, flag)
| MountOption::Rslave(_, flag) => {
propagation_flags.push(flag);
continue;
}
MountOption::Relatime(is_clear, flag) => Some((is_clear, flag)),
MountOption::Norelatime(is_clear, flag) => Some((is_clear, flag)),
MountOption::Strictatime(is_clear, flag) => Some((is_clear, flag)),
Expand All @@ -197,6 +205,7 @@ pub fn parse_mount(m: &Mount) -> std::result::Result<MountOptionConfig, MountErr
flags,
data: data.into_iter().map(|s| s.to_string()).collect(),
rec_attr: mount_attr,
propagation_flags,
})
}

Expand Down Expand Up @@ -245,6 +254,7 @@ mod tests {
flags: MsFlags::empty(),
data: vec![],
rec_attr: None,
propagation_flags: vec![],
},
mount_option_config
);
Expand All @@ -267,6 +277,7 @@ mod tests {
flags: MsFlags::MS_NOSUID | MsFlags::MS_STRICTATIME,
data: vec!["mode=755".to_string(), "size=65536k".to_string()],
rec_attr: None,
propagation_flags: vec![],
},
mount_option_config
);
Expand Down Expand Up @@ -297,6 +308,7 @@ mod tests {
"gid=5".to_string()
],
rec_attr: None,
propagation_flags: vec![],
},
mount_option_config
);
Expand All @@ -320,6 +332,7 @@ mod tests {
flags: MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV,
data: vec!["mode=1777".to_string(), "size=65536k".to_string()],
rec_attr: None,
propagation_flags: vec![],
},
mount_option_config
);
Expand All @@ -342,6 +355,7 @@ mod tests {
flags: MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV,
data: vec![],
rec_attr: None,
propagation_flags: vec![],
},
mount_option_config
);
Expand All @@ -367,6 +381,7 @@ mod tests {
| MsFlags::MS_RDONLY,
data: vec![],
rec_attr: None,
propagation_flags: vec![],
},
mount_option_config
);
Expand Down Expand Up @@ -394,6 +409,7 @@ mod tests {
| MsFlags::MS_RELATIME,
data: vec![],
rec_attr: None,
propagation_flags: vec![],
},
mount_option_config,
);
Expand Down Expand Up @@ -448,9 +464,19 @@ mod tests {
| MsFlags::MS_NOATIME
| MsFlags::MS_NODIRATIME
| MsFlags::MS_BIND
| MsFlags::MS_UNBINDABLE,
| MsFlags::MS_REC,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does changing this mean that the existing behavior will also change? Is this change acceptable?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MsFlags::MS_REC is included here because of rbind.

rbind reads the flags directly, so there is no issue:

.map(|v| v.iter().any(|o| o == "rbind"))

MS_REC is supposed to be included here. Previously, however, propagation flags were not handled correctly, so it was missing.

The mount_recursive tests ensure that this change does not alter the existing behavior.

https://github.com/youki-dev/youki/tree/main/tests/contest/contest/src/tests/mounts_recursive

data: vec![],
rec_attr: None,
propagation_flags: vec![
MsFlags::MS_UNBINDABLE,
MsFlags::MS_UNBINDABLE | MsFlags::MS_REC,
MsFlags::MS_PRIVATE,
MsFlags::MS_PRIVATE | MsFlags::MS_REC,
MsFlags::MS_SHARED,
MsFlags::MS_SHARED | MsFlags::MS_REC,
MsFlags::MS_SLAVE,
MsFlags::MS_SLAVE | MsFlags::MS_REC,
],
},
mount_option_config
);
Expand Down Expand Up @@ -485,6 +511,7 @@ mod tests {
flags: MsFlags::empty(),
data: vec![],
rec_attr: Some(MountAttr::all()),
propagation_flags: vec![],
},
mount_option_config
);
Expand Down
3 changes: 3 additions & 0 deletions tests/contest/contest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use crate::tests::linux_masked_paths::get_linux_masked_paths_tests;
use crate::tests::linux_ns_itype::get_ns_itype_tests;
use crate::tests::memory_policy::get_linux_memory_policy_tests;
use crate::tests::misc_props::get_misc_props_test;
use crate::tests::mount_propagation::get_mount_propagation_test;
use crate::tests::mounts_recursive::get_mounts_recursive_test;
use crate::tests::net_devices::get_net_devices_test;
use crate::tests::no_pivot::get_no_pivot_test;
Expand Down Expand Up @@ -152,6 +153,7 @@ fn main() -> Result<()> {
let ro_paths = get_ro_paths_test();
let hostname = get_hostname_test();
let misc_props = get_misc_props_test();
let mount_propagation = get_mount_propagation_test();
let mounts_recursive = get_mounts_recursive_test();
let domainname = get_domainname_tests();
let intel_rdt = get_intel_rdt_test();
Expand Down Expand Up @@ -214,6 +216,7 @@ fn main() -> Result<()> {
tm.add_test_group(Box::new(ro_paths));
tm.add_test_group(Box::new(hostname));
tm.add_test_group(Box::new(misc_props));
tm.add_test_group(Box::new(mount_propagation));
tm.add_test_group(Box::new(mounts_recursive));
tm.add_test_group(Box::new(domainname));
tm.add_test_group(Box::new(intel_rdt));
Expand Down
1 change: 1 addition & 0 deletions tests/contest/contest/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod linux_masked_paths;
pub mod linux_ns_itype;
pub mod memory_policy;
pub mod misc_props;
pub mod mount_propagation;
pub mod mounts_recursive;
pub mod net_devices;
pub mod no_pivot;
Expand Down
2 changes: 2 additions & 0 deletions tests/contest/contest/src/tests/mount_propagation/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mod mount_propagation_test;
pub use mount_propagation_test::get_mount_propagation_test;
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use std::fs::{create_dir, remove_dir_all};
use std::path::{Path, PathBuf};
use std::str::FromStr;

use anyhow::{Context, Ok, Result, anyhow};
use nix::mount::{MsFlags, mount, umount};
use oci_spec::runtime::{Mount, ProcessBuilder, Spec, SpecBuilder, get_default_mounts};
use tempfile::TempDir;
use test_framework::{Test, TestGroup, TestResult, test_result};

use crate::utils::test_inside_container;
use crate::utils::test_utils::CreateOptions;

const MOUNT_DEST: &str = "/mnt/mount_propagation";
const SUB_DIR_NAME: &str = "sub";

fn create_spec(source: &Path, propagation: &str) -> Result<Spec> {
let mut mounts = get_default_mounts();

let mut mount_spec = Mount::default();
mount_spec
.set_destination(PathBuf::from_str(MOUNT_DEST).unwrap())
.set_typ(None)
.set_source(Some(source.to_path_buf()))
.set_options(Some(vec!["rbind".to_string(), propagation.to_string()]));
mounts.push(mount_spec);

let process = ProcessBuilder::default()
.args(vec![
"runtimetest".to_string(),
"mount_propagation".to_string(),
])
.build()
.context("failed to build process")?;

let spec = SpecBuilder::default()
.mounts(mounts)
.process(process)
.build()
.context("failed to build spec")?;

Ok(spec)
}

// Make mount_dir shared and add a submount so propagation changes are
// observable, including the difference between recursive and non-recursive options.
fn setup_mount(mount_dir: &Path, sub_mount_dir: &Path) -> Result<()> {
create_dir(mount_dir)?;
mount::<Path, Path, str, str>(Some(mount_dir), mount_dir, None, MsFlags::MS_BIND, None)?;

mount::<Path, Path, str, str>(None, mount_dir, None, MsFlags::MS_SHARED, None)?;
create_dir(sub_mount_dir)?;
mount::<Path, Path, str, str>(None, sub_mount_dir, Some("tmpfs"), MsFlags::empty(), None)?;
Ok(())
}

fn clean_mount(mount_dir: &Path, sub_mount_dir: &Path) -> Result<()> {
umount(sub_mount_dir)?;
umount(mount_dir)?;
remove_dir_all(mount_dir)?;
Ok(())
}

fn check_propagation(propagation: &str) -> TestResult {
let base_dir = TempDir::new().unwrap();
let dir_path = base_dir.path().join("mount_dir");
let sub_path = dir_path.join(SUB_DIR_NAME);

let spec = test_result!(create_spec(&dir_path, propagation));

let result = test_inside_container(&spec, &CreateOptions::default(), &|_rootfs| {
setup_mount(&dir_path, &sub_path).map_err(|e| anyhow!("setup_mount failed: {e:?}"))?;
Ok(())
});

if let Err(e) = clean_mount(&dir_path, &sub_path) {
eprintln!(
"clean_mount failed (mount_dir={}, sub_dir={}): {e:?}",
dir_path.display(),
sub_path.display(),
);
}

result
}

fn shared_test() -> TestResult {
check_propagation("shared")
}

fn rshared_test() -> TestResult {
check_propagation("rshared")
}

fn slave_test() -> TestResult {
check_propagation("slave")
}

fn rslave_test() -> TestResult {
check_propagation("rslave")
}

fn private_test() -> TestResult {
check_propagation("private")
}

fn rprivate_test() -> TestResult {
check_propagation("rprivate")
}

fn unbindable_test() -> TestResult {
check_propagation("unbindable")
}

fn runbindable_test() -> TestResult {
check_propagation("runbindable")
}

pub fn get_mount_propagation_test() -> TestGroup {
let mut test_group = TestGroup::new("mount_propagation");

let shared_test = Test::new("mount_propagation_shared_test", Box::new(shared_test));
let rshared_test = Test::new("mount_propagation_rshared_test", Box::new(rshared_test));
let slave_test = Test::new("mount_propagation_slave_test", Box::new(slave_test));
let rslave_test = Test::new("mount_propagation_rslave_test", Box::new(rslave_test));
let private_test = Test::new("mount_propagation_private_test", Box::new(private_test));
let rprivate_test = Test::new("mount_propagation_rprivate_test", Box::new(rprivate_test));
let unbindable_test = Test::new(
"mount_propagation_unbindable_test",
Box::new(unbindable_test),
);
let runbindable_test = Test::new(
"mount_propagation_runbindable_test",
Box::new(runbindable_test),
);

test_group.add(vec![
Box::new(shared_test),
Box::new(rshared_test),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the test case for slave.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

Box::new(slave_test),
Box::new(rslave_test),
Box::new(private_test),
Box::new(rprivate_test),
Box::new(unbindable_test),
Box::new(runbindable_test),
]);

test_group
}
1 change: 1 addition & 0 deletions tests/contest/runtimetest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ fn main() {
"process_oom_score_adj" => tests::validate_process_oom_score_adj(&spec),
"fd_control" => tests::validate_fd_control(&spec),
"rootfs_propagation" => tests::validate_rootfs_propagation(&spec),
"mount_propagation" => tests::validate_mount_propagation(&spec),
"uid_mappings" => tests::validate_uid_mappings(&spec),
"net_devices" => tests::validate_net_devices(&spec),
"time_offsets" => tests::validate_time_offsets(&spec),
Expand Down
Loading
Loading