Add global lock for mount tree topology

Add a `MountTopology` abstraction that protects mount tree topology via
a global `RwMutex`. Write side is acquired in all `Path` mutation
methods and in `Drop`, `pivot_root`. Read side is acquired in
`new_clone`, `switch_to_mnt_ns` and `collect_visible_mounts` for
consistent snapshots.

Require topology operations to take a `MountTopology` reference so that
callers must hold the corresponding read or write guard.

Closes: #3392
This commit is contained in:
Junrui Luo 2026-06-16 18:07:13 +08:00 committed by Chengjun Chen
parent cf9c093d01
commit b577872004
4 changed files with 153 additions and 47 deletions

View File

@ -6,8 +6,8 @@ use core::time::Duration;
pub(in crate::fs) use dentry::Dentry;
use inherit_methods_macro::inherit_methods;
use mount::MountNsFileCopying;
pub use mount::{Mount, MountPropType, PerMountFlags};
use mount::{MountNsFileCopying, MountTopology};
pub use mount_namespace::MountNamespace;
pub use resolver::{
AT_FDCWD, AbsPathResult, EmptyPathStr, FsPath, LookupResult, PathResolver, SplitPath,
@ -194,8 +194,12 @@ impl Path {
}
/// Finds the corresponding `Path` in the given mount namespace.
fn find_corresponding_mount(&self, mnt_ns: &Arc<MountNamespace>) -> Option<Self> {
let corresponding_mount = self.mount.find_corresponding_mount(mnt_ns)?;
fn find_corresponding_mount(
&self,
mnt_ns: &Arc<MountNamespace>,
topology: &MountTopology,
) -> Option<Self> {
let corresponding_mount = self.mount.find_corresponding_mount(mnt_ns, topology)?;
let corresponding_path = Self::new(corresponding_mount, self.dentry.clone());
Some(corresponding_path)
@ -207,7 +211,7 @@ impl Path {
/// of the `root` path. The check traverses upwards from the current path,
/// crossing mount point boundaries as necessary, until it either finds
/// the `root` path or reaches the global root.
fn is_reachable_from(&self, root: &Path) -> bool {
fn is_reachable_from(&self, root: &Path, _topology: &MountTopology) -> bool {
let mut owned;
let mut current = self;
@ -363,7 +367,10 @@ impl Path {
return_errno_with_message!(Errno::EINVAL, "the path is not in this mount namespace");
}
let child_mount = self.mount.do_mount(fs, flags, &self.dentry, source)?;
let mut topology_guard = MountTopology::write_lock();
let child_mount =
self.mount
.do_mount(fs, flags, &self.dentry, source, &mut topology_guard)?;
Ok(child_mount)
}
@ -383,7 +390,7 @@ impl Path {
return_errno_with_message!(Errno::EINVAL, "the path is not a mount root");
}
let Some(mountpoint) = self.mount.mountpoint() else {
let Some(_mountpoint) = self.mount.mountpoint() else {
return_errno_with_message!(Errno::EINVAL, "the root mount cannot be unmounted");
};
@ -395,8 +402,12 @@ impl Path {
self.mount.sync()?;
let mut topology_guard = MountTopology::write_lock();
let Some(mountpoint) = self.mount.mountpoint() else {
return_errno_with_message!(Errno::EINVAL, "the mount has been detached");
};
let parent_mount = self.mount.parent().unwrap().upgrade().unwrap();
let child_mount = parent_mount.do_unmount(&mountpoint)?;
let child_mount = parent_mount.do_unmount(&mountpoint, &mut topology_guard)?;
Ok(child_mount)
}
@ -428,7 +439,9 @@ impl Path {
return_errno_with_message!(Errno::EINVAL, "the path is not in this mount namespace");
}
self.mount.remount(mount_flags, fs_flags, data, ctx)
let mut topology_guard = MountTopology::write_lock();
self.mount
.remount(mount_flags, fs_flags, data, ctx, &mut topology_guard)
}
/// Creates a bind mount from the current path to the destination path.
@ -476,14 +489,16 @@ impl Path {
);
}
let mut topology_guard = MountTopology::write_lock();
let current_mnt_ns_weak = Arc::downgrade(current_mnt_ns);
let new_mount = self.mount.clone_mount_tree(
&self.dentry,
&current_mnt_ns_weak,
recursive,
MountNsFileCopying::Copy,
&topology_guard,
)?;
new_mount.graft_mount_tree(dst_path);
new_mount.graft_mount_tree(dst_path, &mut topology_guard);
Ok(())
}
@ -523,10 +538,11 @@ impl Path {
"the destination path is not in this mount namespace"
);
}
current_mnt_ns.check_no_mnt_ns_loop_in_tree(self.mount_node())?;
let mut topology_guard = MountTopology::write_lock();
current_mnt_ns.check_no_mnt_ns_loop_in_tree(self.mount_node(), &topology_guard)?;
if dst_path
.mount_node()
.is_equal_or_descendant_of(self.mount_node())
.is_equal_or_descendant_of(self.mount_node(), &topology_guard)
{
// Reject moves that would place a mount beneath itself, because the mount tree
// must remain acyclic.
@ -536,7 +552,7 @@ impl Path {
);
}
self.mount.graft_mount_tree(dst_path);
self.mount.graft_mount_tree(dst_path, &mut topology_guard);
Ok(())
}
@ -558,7 +574,9 @@ impl Path {
return_errno_with_message!(Errno::EINVAL, "the path is not in this mount namespace");
}
self.mount.set_propagation(prop, recursive);
let mut topology_guard = MountTopology::write_lock();
self.mount
.set_propagation(prop, recursive, &mut topology_guard);
Ok(())
}

View File

@ -5,6 +5,7 @@ use core::sync::atomic::{AtomicU32, Ordering};
use atomic_integer_wrapper::define_atomic_version_of_integer_like_type;
use hashbrown::HashMap;
use id_alloc::IdAlloc;
use ostd::sync::{RwMutexReadGuard, RwMutexWriteGuard};
use spin::Once;
use super::try_get_mnt_ns_inode;
@ -23,6 +24,35 @@ use crate::{
prelude::*,
};
/// Provides synchronized access to mount topology.
pub(super) struct MountTopology {
_private: (),
}
fn global_mount_topology() -> &'static RwMutex<MountTopology> {
static MOUNT_TOPOLOGY: RwMutex<MountTopology> = RwMutex::new(MountTopology { _private: () });
&MOUNT_TOPOLOGY
}
impl MountTopology {
/// Acquires the write side of the mount topology lock.
///
/// Use this for operations that may change the mount topology,
/// including the parent-child links, mountpoints, mount propagation state,
/// or namespace-visible mount trees.
pub(super) fn write_lock() -> RwMutexWriteGuard<'static, Self> {
global_mount_topology().write()
}
/// Acquires the read side of the mount topology lock.
///
/// Use this for operations that need a stable view of mount topology
/// without changing it.
pub(super) fn read_lock() -> RwMutexReadGuard<'static, Self> {
global_mount_topology().read()
}
}
/// Controls how recursive mount-tree cloning handles mount-namespace files.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum MountNsFileCopying {
@ -293,6 +323,7 @@ impl Mount {
flags: PerMountFlags,
mountpoint: &Arc<Dentry>,
source: Option<String>,
_topology: &mut MountTopology,
) -> Result<Arc<Self>> {
if mountpoint.type_() != InodeType::Dir {
return_errno!(Errno::ENOTDIR);
@ -315,14 +346,18 @@ impl Mount {
/// Unmounts a child mount node from the mountpoint and returns it.
///
/// The mountpoint should belong to this mount node, or an error is returned.
pub(super) fn do_unmount(&self, mountpoint: &Dentry) -> Result<Arc<Self>> {
pub(super) fn do_unmount(
&self,
mountpoint: &Dentry,
topology: &mut MountTopology,
) -> Result<Arc<Self>> {
let child_mount = self
.children
.write()
.remove(&mountpoint.key())
.ok_or_else(|| Error::with_message(Errno::ENOENT, "can not find child mount"))?;
child_mount.clear_mountpoint();
child_mount.clear_topology_link(topology);
Ok(child_mount)
}
@ -374,6 +409,7 @@ impl Mount {
new_ns: &Weak<MountNamespace>,
recursive: bool,
mnt_ns_file_copying: MountNsFileCopying,
_topology: &MountTopology,
) -> Result<Arc<Self>> {
let new_root_mount = self.clone_mount(root_dentry, new_ns)?;
if !recursive {
@ -414,7 +450,12 @@ impl Mount {
}
/// Sets the propagation type of this mount.
pub(super) fn set_propagation(&self, prop: MountPropType, recursive: bool) {
pub(super) fn set_propagation(
&self,
prop: MountPropType,
recursive: bool,
_topology: &mut MountTopology,
) {
*self.propagation.write() = prop;
if !recursive {
return;
@ -428,7 +469,7 @@ impl Mount {
}
/// Detaches the mount node from the parent mount node.
pub(super) fn detach_from_parent(&self) {
pub(super) fn detach_from_parent(&self, topology: &mut MountTopology) {
if let Some(parent) = self.parent() {
let parent = parent.upgrade().unwrap();
let child = parent
@ -437,13 +478,24 @@ impl Mount {
.remove(&self.mountpoint().unwrap().key());
if let Some(child) = child {
child.clear_mountpoint();
child.clear_topology_link(topology);
}
}
}
/// Clears this mount node's topology link.
///
/// The parent pointer and mountpoint describe the same topology edge, so
/// they must be cleared together while holding the mount topology lock.
///
/// This only mutates this mount node's own link state.
pub(super) fn clear_topology_link(&self, _topology: &mut MountTopology) {
self.set_parent(None);
self.clear_mountpoint();
}
/// Attaches the mount node to the mountpoint.
fn attach_to_path(&self, target_path: &Path) {
fn attach_to_path(&self, target_path: &Path, _topology: &mut MountTopology) {
let key = target_path.dentry.key();
target_path
.mount_node()
@ -455,9 +507,9 @@ impl Mount {
}
/// Grafts the mount node tree to the mountpoint.
pub(super) fn graft_mount_tree(&self, target_path: &Path) {
self.detach_from_parent();
self.attach_to_path(target_path);
pub(super) fn graft_mount_tree(&self, target_path: &Path, topology: &mut MountTopology) {
self.detach_from_parent(topology);
self.attach_to_path(target_path, topology);
}
/// Gets a child mount node from the mountpoint if any.
@ -476,7 +528,7 @@ impl Mount {
}
/// Sets the mountpoint.
pub(super) fn set_mountpoint(&self, dentry: &Arc<Dentry>) {
fn set_mountpoint(&self, dentry: &Arc<Dentry>) {
let mut mountpoint = self.mountpoint.write();
if let Some(mountpoint) = mountpoint.as_deref() {
mountpoint.dec_mount_count();
@ -487,7 +539,7 @@ impl Mount {
}
/// Clears the mountpoint.
pub(super) fn clear_mountpoint(&self) {
fn clear_mountpoint(&self) {
let mut mountpoint = self.mountpoint.write();
if let Some(mountpoint) = mountpoint.as_deref() {
mountpoint.dec_mount_count();
@ -508,6 +560,7 @@ impl Mount {
fs_flags: Option<FsFlags>,
data: Option<CString>,
ctx: &Context,
_topology: &mut MountTopology,
) -> Result<()> {
// TODO: This lock is a workaround to guarantee the atomicity of remount operation.
// We need to re-design the lock mechanism of `Mount` and file system in the future.
@ -547,7 +600,11 @@ impl Mount {
}
/// Returns whether `self` is `ancestor` or a descendant of it in the mount tree.
pub(super) fn is_equal_or_descendant_of(&self, ancestor: &Arc<Self>) -> bool {
pub(super) fn is_equal_or_descendant_of(
&self,
ancestor: &Arc<Self>,
_topology: &MountTopology,
) -> bool {
let mut current = self.this();
loop {
if Arc::ptr_eq(&current, ancestor) {
@ -580,7 +637,7 @@ impl Mount {
///
/// In some cases we may need to reset the parent of
/// the created Mount, such as move mount.
pub(super) fn set_parent(&self, mount: Option<&Arc<Mount>>) {
fn set_parent(&self, mount: Option<&Arc<Mount>>) {
let mut parent = self.parent.write();
*parent = mount.map(Arc::downgrade);
}
@ -589,6 +646,7 @@ impl Mount {
pub(super) fn find_corresponding_mount(
&self,
mnt_ns: &Arc<MountNamespace>,
_topology: &MountTopology,
) -> Option<Arc<Self>> {
// Collect the ancestors from self to the root mount (The root mount is not included).
let mut ancestors = VecDeque::new();

View File

@ -4,7 +4,10 @@ use alloc::sync::UniqueArc;
use spin::Once;
use super::{mount::MountNsFileCopying, try_get_mnt_ns_inode};
use super::{
mount::{MountNsFileCopying, MountTopology},
try_get_mnt_ns_inode,
};
use crate::{
fs::{
fs_impls::ramfs::RamFs,
@ -123,6 +126,8 @@ impl MountNamespace {
CapSet::SYS_ADMIN,
))?;
let topology_guard = MountTopology::read_lock();
let root_mount = self.root();
Self::new_with_root(owner, |weak_ns| {
root_mount.clone_mount_tree(
@ -130,6 +135,7 @@ impl MountNamespace {
weak_ns,
true,
MountNsFileCopying::Skip,
&topology_guard,
)
})
}
@ -174,7 +180,11 @@ impl MountNamespace {
/// Ensures that importing the mount subtree rooted at `root_mount` into this
/// mount namespace would not form a mount-namespace loop.
pub(super) fn check_no_mnt_ns_loop_in_tree(&self, root_mount: &Arc<Mount>) -> Result<()> {
pub(super) fn check_no_mnt_ns_loop_in_tree(
&self,
root_mount: &Arc<Mount>,
_topology: &MountTopology,
) -> Result<()> {
let mut worklist = VecDeque::new();
worklist.push_back(root_mount.clone());
@ -208,13 +218,15 @@ impl Drop for MountNamespace {
// and thus the subsequent cleanup logic can be skipped.
return;
};
let mut topology_guard = MountTopology::write_lock();
let mut worklist = VecDeque::new();
worklist.push_back(root.clone());
while let Some(current_mount) = worklist.pop_front() {
let mut children = current_mount.children.write();
for (_, child) in children.drain() {
child.set_parent(None);
child.clear_mountpoint();
child.clear_topology_link(&mut topology_guard);
worklist.push_back(child);
}
}

View File

@ -4,7 +4,7 @@ use alloc::str;
use ostd::task::Task;
use super::{Mount, Path};
use super::{Mount, Path, mount::MountTopology};
use crate::{
fs::{
file::{
@ -334,18 +334,26 @@ impl PathResolver {
return Ok(());
}
let new_root = self.root.find_corresponding_mount(mnt_ns).ok_or_else(|| {
Error::with_message(
Errno::EINVAL,
"the root directory does not exist in the target mount namespace",
)
})?;
let new_cwd = self.cwd.find_corresponding_mount(mnt_ns).ok_or_else(|| {
Error::with_message(
Errno::EINVAL,
"the current working directory does not exist in the target mount namespace",
)
})?;
let topology_guard = MountTopology::read_lock();
let new_root = self
.root
.find_corresponding_mount(mnt_ns, &topology_guard)
.ok_or_else(|| {
Error::with_message(
Errno::EINVAL,
"the root directory does not exist in the target mount namespace",
)
})?;
let new_cwd = self
.cwd
.find_corresponding_mount(mnt_ns, &topology_guard)
.ok_or_else(|| {
Error::with_message(
Errno::EINVAL,
"the current working directory does not exist in the target mount namespace",
)
})?;
self.root = new_root;
self.cwd = new_cwd;
@ -411,13 +419,15 @@ impl PathResolver {
"`new_root` or the current root is on the rootfs mount"
);
}
if !put_old_path.is_reachable_from(&new_root_path) {
let mut topology_guard = MountTopology::write_lock();
if !put_old_path.is_reachable_from(&new_root_path, &topology_guard) {
return_errno_with_message!(
Errno::EINVAL,
"`put_old` is not at or underneath `new_root`"
);
}
if !new_root_path.is_reachable_from(&self.root) {
if !new_root_path.is_reachable_from(&self.root, &topology_guard) {
return_errno_with_message!(
Errno::EINVAL,
"`new_root` is not at or underneath the current root"
@ -430,8 +440,15 @@ impl PathResolver {
Path::new(parent_mount, mountpoint)
};
self.root.mount.graft_mount_tree(&put_old_path);
new_root_path.mount.graft_mount_tree(&parent_path);
self.root
.mount
.graft_mount_tree(&put_old_path, &mut topology_guard);
new_root_path
.mount
.graft_mount_tree(&parent_path, &mut topology_guard);
// Release the mount topology lock before taking other threads' resolver locks.
drop(topology_guard);
// TODO: This method should only iterate threads in the current PID namespace instead of
// the whole PID table.
@ -501,6 +518,7 @@ impl PathResolver {
///
/// The mounts are collected in depth-first order.
pub(in crate::fs) fn collect_visible_mounts(&self) -> Vec<Arc<Mount>> {
let _topology_guard = MountTopology::read_lock();
let mut visible = Vec::new();
let mut stack = vec![self.root.mount.clone()];
let is_root_mount_root = self.root.is_mount_root();