Skip to content

Commit 4816406

Browse files
authored
fix(namespace): lock cross-user mount copies (#2128)
Downgrade shared mounts to slaves when a mount namespace is copied into a different user namespace, and preserve Linux-style topology and per-mount attribute locks across copies and propagation. Require CAP_SYS_ADMIN in the superblock owner user namespace before ordinary remounts can reconfigure shared filesystem state. Keep bind remounts scoped to per-mount flags. Prepare peer and slave registry capacity before publishing copied mounts so allocation failures cannot expose partial propagation state. Add focused failure-injection and dunitest coverage for ordering, propagation direction, locked attributes, remount permissions, and nested copies. Closes #2103 Signed-off-by: longjin <longjin@dragonos.org>
1 parent 7d2915f commit 4816406

8 files changed

Lines changed: 665 additions & 33 deletions

File tree

kernel/src/filesystem/vfs/mount/mod.rs

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use crate::{
3030
prepare_mount_propagation_locked, propagate_umount_sources,
3131
propagation_umount_busy, MountPropagation,
3232
},
33+
user_namespace::UserNamespace,
3334
},
3435
ProcessManager,
3536
},
@@ -45,7 +46,7 @@ use core::{
4546
cell::RefCell,
4647
fmt::Debug,
4748
hash::Hash,
48-
sync::atomic::{compiler_fence, AtomicBool, AtomicUsize, Ordering},
49+
sync::atomic::{compiler_fence, AtomicBool, AtomicU32, AtomicUsize, Ordering},
4950
};
5051
use hashbrown::HashMap;
5152
use ida::IdAllocator;
@@ -282,6 +283,23 @@ impl MountFlags {
282283
}
283284
}
284285

286+
bitflags! {
287+
/// Internal per-mount locks corresponding to Linux `MNT_LOCK_*` flags.
288+
///
289+
/// These are deliberately separate from userspace-visible `MS_*` flags:
290+
/// topology locking and attribute locking have different lifetimes. In
291+
/// particular, a mount propagated across a user-namespace boundary may
292+
/// have its topology lock cleared while retaining all attribute locks.
293+
struct MountLockFlags: u32 {
294+
const TOPOLOGY = 1 << 0;
295+
const ATIME = 1 << 1;
296+
const READONLY = 1 << 2;
297+
const NODEV = 1 << 3;
298+
const NOSUID = 1 << 4;
299+
const NOEXEC = 1 << 5;
300+
}
301+
}
302+
285303
pub(crate) fn append_comma_options(base: &mut String, extra: String) {
286304
if extra.is_empty() {
287305
return;
@@ -357,8 +375,8 @@ pub struct MountFS {
357375
mount_flags: RwSem<MountFlags>,
358376
super_block_state: Arc<SuperBlockState>,
359377
mount_source: RwSem<Option<String>>,
360-
/// Internal MNT_LOCKED equivalent; never exposed as a userspace MS_* bit.
361-
locked: AtomicBool,
378+
/// Internal `MNT_LOCK_*` state; never exposed as userspace `MS_*` bits.
379+
mount_locks: AtomicU32,
362380
lifecycle: Mutex<MountLifecycle>,
363381
}
364382

@@ -520,6 +538,9 @@ unsafe impl Sync for MountExternalGuard {}
520538

521539
#[derive(Debug)]
522540
pub struct SuperBlockState {
541+
/// User namespace that owns this superblock, matching Linux `s_user_ns`.
542+
/// Bind mounts and mount-namespace copies retain the same owner.
543+
owner_user_ns: Arc<UserNamespace>,
523544
flags: RwSem<MountFlags>,
524545
write_count: AtomicUsize,
525546
wb_error: ErrSeq,
@@ -696,6 +717,7 @@ struct MountStateInit {
696717
impl SuperBlockState {
697718
pub fn new(flags: MountFlags) -> Self {
698719
Self {
720+
owner_user_ns: ProcessManager::current_user_ns(),
699721
flags: RwSem::new(flags & MountFlags::SB_SETTABLE_MASK),
700722
write_count: AtomicUsize::new(0),
701723
wb_error: ErrSeq::new(),
@@ -712,6 +734,10 @@ impl SuperBlockState {
712734
}
713735
}
714736

737+
pub fn owner_user_ns(&self) -> &Arc<UserNamespace> {
738+
&self.owner_user_ns
739+
}
740+
715741
fn activate_mount(&self, construction_reserved: bool) -> Result<(), SystemError> {
716742
let mut lifecycle = self.lifecycle.lock();
717743
if lifecycle.state != SuperBlockLifecycleState::Active {
@@ -1117,7 +1143,7 @@ impl MountFS {
11171143
mount_flags: RwSem::new(mount_flags),
11181144
super_block_state: state_init.super_block_state,
11191145
mount_source: RwSem::new(state_init.mount_source),
1120-
locked: AtomicBool::new(false),
1146+
mount_locks: AtomicU32::new(0),
11211147
lifecycle: Mutex::new(MountLifecycle {
11221148
state: MountLifecycleState::Constructing,
11231149
external_pins: 0,
@@ -1162,7 +1188,7 @@ impl MountFS {
11621188
mount_flags: RwSem::new(self.mount_flags()),
11631189
super_block_state: self.super_block_state.clone(),
11641190
mount_source: RwSem::new(mount_source),
1165-
locked: AtomicBool::new(self.locked.load(Ordering::Acquire)),
1191+
mount_locks: AtomicU32::new(self.mount_locks.load(Ordering::Acquire)),
11661192
lifecycle: Mutex::new(MountLifecycle {
11671193
state: MountLifecycleState::Constructing,
11681194
external_pins: 0,
@@ -1793,15 +1819,61 @@ impl MountFS {
17931819
}
17941820

17951821
pub(crate) fn lock_mount(&self) {
1796-
self.locked.store(true, Ordering::Release);
1822+
self.mount_locks
1823+
.fetch_or(MountLockFlags::TOPOLOGY.bits(), Ordering::Release);
17971824
}
17981825

17991826
pub(crate) fn unlock_mount(&self) {
1800-
self.locked.store(false, Ordering::Release);
1827+
self.mount_locks
1828+
.fetch_and(!MountLockFlags::TOPOLOGY.bits(), Ordering::Release);
18011829
}
18021830

18031831
pub(crate) fn is_locked(&self) -> bool {
1804-
self.locked.load(Ordering::Acquire)
1832+
self.mount_locks.load(Ordering::Acquire) & MountLockFlags::TOPOLOGY.bits() != 0
1833+
}
1834+
1835+
/// Linux `lock_mnt_tree()` semantics for mounts copied across a user
1836+
/// namespace boundary. Attribute locks snapshot per-mount flags only;
1837+
/// superblock flags have separate ownership and reconfiguration rules.
1838+
pub(crate) fn lock_cross_user_mount(&self) {
1839+
let mount_flags = self.mount_flags();
1840+
let mut locks = MountLockFlags::TOPOLOGY | MountLockFlags::ATIME;
1841+
if mount_flags.contains(MountFlags::RDONLY) {
1842+
locks.insert(MountLockFlags::READONLY);
1843+
}
1844+
if mount_flags.contains(MountFlags::NODEV) {
1845+
locks.insert(MountLockFlags::NODEV);
1846+
}
1847+
if mount_flags.contains(MountFlags::NOSUID) {
1848+
locks.insert(MountLockFlags::NOSUID);
1849+
}
1850+
if mount_flags.contains(MountFlags::NOEXEC) {
1851+
locks.insert(MountLockFlags::NOEXEC);
1852+
}
1853+
self.mount_locks.fetch_or(locks.bits(), Ordering::Release);
1854+
}
1855+
1856+
/// Linux `can_change_locked_flags()` equivalent for remount admission.
1857+
/// Locks prevent relaxing attributes that were present at lock time and
1858+
/// prevent changing the atime policy, while still allowing stricter flags
1859+
/// that were not locked to be added.
1860+
pub(crate) fn can_reconfigure_mount_flags(&self, requested: MountFlags) -> bool {
1861+
let locks = MountLockFlags::from_bits_truncate(self.mount_locks.load(Ordering::Acquire));
1862+
if locks.contains(MountLockFlags::READONLY) && !requested.contains(MountFlags::RDONLY) {
1863+
return false;
1864+
}
1865+
if locks.contains(MountLockFlags::NODEV) && !requested.contains(MountFlags::NODEV) {
1866+
return false;
1867+
}
1868+
if locks.contains(MountLockFlags::NOSUID) && !requested.contains(MountFlags::NOSUID) {
1869+
return false;
1870+
}
1871+
if locks.contains(MountLockFlags::NOEXEC) && !requested.contains(MountFlags::NOEXEC) {
1872+
return false;
1873+
}
1874+
!locks.contains(MountLockFlags::ATIME)
1875+
|| (self.mount_flags() & MountFlags::MNT_ATIME_MASK)
1876+
== (requested & MountFlags::MNT_ATIME_MASK)
18051877
}
18061878

18071879
#[inline(never)]
@@ -2021,6 +2093,11 @@ impl MountFS {
20212093
self.lifecycle.lock().external_pins != 0
20222094
}
20232095

2096+
#[cfg(test)]
2097+
pub(crate) fn superblock_external_pin_count(&self) -> usize {
2098+
self.super_block_state.lifecycle.lock().external_pins
2099+
}
2100+
20242101
pub(crate) fn subtree_has_external_pins(&self) -> bool {
20252102
let mut pending = vec![self.self_ref()];
20262103
while let Some(mount) = pending.pop() {

kernel/src/filesystem/vfs/syscall/sys_mount.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ fn do_reconfigure_bind_mount(
324324
if !target_mfs.is_live() || !target_mfs.is_belongs_to_mntns(&current_mntns) {
325325
return Err(SystemError::EINVAL);
326326
}
327+
if !target_mfs.can_reconfigure_mount_flags(requested_flags) {
328+
return Err(SystemError::EPERM);
329+
}
327330
target_mfs.update_mount_flags(|mount_flags| {
328331
let preserved = *mount_flags & !MountFlags::MNT_USER_SETTABLE_MASK;
329332
let new_settable = requested_flags & MountFlags::MNT_USER_SETTABLE_MASK;
@@ -366,9 +369,17 @@ fn do_remount(
366369
if !target_mfs.is_live() || !target_mfs.is_belongs_to_mntns(&current_mntns) {
367370
return Err(SystemError::EINVAL);
368371
}
372+
if !target_mfs.can_reconfigure_mount_flags(requested_mnt_flags) {
373+
return Err(SystemError::EPERM);
374+
}
369375
}
370376
let old_sb_flags = target_mfs.super_block_flags();
371377
let (data_sb_flags, data_sb_flags_mask, fs_private_data) = parse_remount_data(data.as_deref())?;
378+
// Linux do_remount() requires CAP_SYS_ADMIN in sb->s_user_ns before a
379+
// legacy remount may change shared superblock or filesystem-private state.
380+
if !ns_capable(super_block_state.owner_user_ns(), CAPFlags::CAP_SYS_ADMIN) {
381+
return Err(SystemError::EPERM);
382+
}
372383
let sb_flags_mask = MountFlags::RMT_MASK | data_sb_flags_mask;
373384
let requested_sb_flags = (requested_sb_flags & !data_sb_flags_mask) | data_sb_flags;
374385
let new_sb_flags = (old_sb_flags & !sb_flags_mask) | (requested_sb_flags & sb_flags_mask);

kernel/src/process/namespace/mnt.rs

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ use super::{
1717
nsproxy::NsCommon,
1818
propagation::{
1919
abort_moved_tree_propagation_locked, commit_moved_tree_propagation_locked,
20-
prepare_moved_tree_propagation_locked, register_peer, register_slave_with_master,
21-
MountPropagation,
20+
prepare_moved_tree_propagation_locked, MountPropagation, PreparedRegistrations,
2221
},
2322
user_namespace::UserNamespace,
2423
NamespaceOps,
@@ -29,6 +28,10 @@ static mut INIT_MNT_NAMESPACE: Option<Arc<MntNamespace>> = None;
2928
const DEFAULT_MOUNT_MAX: u32 = 100_000;
3029
static MOUNT_MAX: AtomicU32 = AtomicU32::new(DEFAULT_MOUNT_MAX);
3130

31+
#[cfg(test)]
32+
static FAIL_COPY_REGISTRATION_PREPARE: core::sync::atomic::AtomicBool =
33+
core::sync::atomic::AtomicBool::new(false);
34+
3235
pub fn mount_max() -> u32 {
3336
MOUNT_MAX.load(Ordering::Relaxed)
3437
}
@@ -569,6 +572,20 @@ impl MntNamespace {
569572
}
570573
};
571574

575+
#[cfg(test)]
576+
if FAIL_COPY_REGISTRATION_PREPARE.swap(false, Ordering::AcqRel) {
577+
MountFS::deactivate_disconnected_subtree(&new_root_mntfs);
578+
return Err(SystemError::ENOMEM);
579+
}
580+
let prepared_registrations =
581+
match PreparedRegistrations::prepare_iter(copied_mounts.iter().map(|(_, copy)| copy)) {
582+
Ok(registrations) => registrations,
583+
Err(error) => {
584+
MountFS::deactivate_disconnected_subtree(&new_root_mntfs);
585+
return Err(error);
586+
}
587+
};
588+
572589
let mut ns_common = self.ns_common.clone();
573590
ns_common.level += 1;
574591
let new_mntns = Arc::new_cyclic(|self_ref| Self {
@@ -594,17 +611,11 @@ impl MntNamespace {
594611
for (_old_mount, new_mount) in copied_mounts {
595612
new_mount.set_namespace(Arc::downgrade(&new_mntns));
596613
new_mount.mark_namespace_accounted(&new_mntns);
597-
let propagation = new_mount.propagation();
598-
if propagation.is_shared() {
599-
register_peer(propagation.peer_group_id(), &new_mount);
600-
}
601-
if propagation.is_slave() {
602-
register_slave_with_master(&new_mount);
603-
}
604614
new_mount
605615
.activate()
606616
.expect("a detached namespace copy is published exactly once");
607617
}
618+
prepared_registrations.commit();
608619

609620
Ok(new_mntns)
610621
}
@@ -810,7 +821,7 @@ fn restrict_cross_user_propagation(
810821
if !cross_user_namespace {
811822
return;
812823
}
813-
copy.lock_mount();
824+
copy.lock_cross_user_mount();
814825
if !source.propagation().is_shared() {
815826
return;
816827
}
@@ -830,6 +841,42 @@ impl ProcessManager {
830841
}
831842
}
832843

844+
#[cfg(test)]
845+
mod tests {
846+
use super::*;
847+
use crate::process::namespace::{
848+
propagation::{get_peers, register_peer},
849+
user_namespace::INIT_USER_NAMESPACE,
850+
};
851+
852+
#[test]
853+
fn registration_prepare_failure_cleans_namespace_copy() {
854+
let namespace = MntNamespace::new_root();
855+
let root = namespace.root_mntfs();
856+
root.propagation().set_shared().unwrap();
857+
let group_id = root.propagation().peer_group_id();
858+
register_peer(group_id, &root);
859+
let peers_before = get_peers(group_id, &root).len();
860+
let slaves_before = root.propagation().slaves().len();
861+
let pins_before = root.superblock_external_pin_count();
862+
let mount_count_before = namespace.inner.read().mount_count.mounts;
863+
864+
FAIL_COPY_REGISTRATION_PREPARE.store(true, Ordering::Release);
865+
let result = namespace.copy_mnt_ns(&CloneFlags::CLONE_NEWNS, INIT_USER_NAMESPACE.clone());
866+
867+
assert!(matches!(result, Err(SystemError::ENOMEM)));
868+
assert_eq!(root.superblock_external_pin_count(), pins_before);
869+
assert_eq!(get_peers(group_id, &root).len(), peers_before);
870+
assert_eq!(root.propagation().slaves().len(), slaves_before);
871+
assert_eq!(
872+
namespace.inner.read().mount_count.mounts,
873+
mount_count_before
874+
);
875+
assert!(root.is_live());
876+
assert!(root.is_belongs_to_mntns(&namespace));
877+
}
878+
}
879+
833880
impl Drop for MntNamespace {
834881
fn drop(&mut self) {
835882
// Namespace destruction is a topology teardown, not filesystem I/O.

0 commit comments

Comments
 (0)