From 70a339be53396c5c49e67ca0a97847aa32f6e7cb Mon Sep 17 00:00:00 2001 From: sparkzky Date: Tue, 10 Mar 2026 01:04:27 +0800 Subject: [PATCH 1/3] feat(vfs): implement pivot_root for mount namespaces Add pivot_root(2) support for mount namespaces, including mount list path rewrites, namespace-root handling, and rebinding of process fs root and cwd after namespace switches. Also fix mount-root path resolution and umount path lookup for moved mount trees, and add C unit tests covering pivot_root behavior. Signed-off-by: sparkzky --- kernel/src/filesystem/vfs/mount.rs | 77 ++- kernel/src/filesystem/vfs/syscall/mod.rs | 1 + .../filesystem/vfs/syscall/sys_pivot_root.rs | 327 ++++++++++++ .../src/filesystem/vfs/syscall/sys_umount2.rs | 77 ++- kernel/src/process/namespace/mnt.rs | 145 +++++- kernel/src/process/namespace/nsproxy.rs | 60 ++- user/apps/c_unitest/test_pivot_root.c | 476 ++++++++++++++++++ 7 files changed, 1127 insertions(+), 36 deletions(-) create mode 100644 kernel/src/filesystem/vfs/syscall/sys_pivot_root.rs create mode 100644 user/apps/c_unitest/test_pivot_root.c diff --git a/kernel/src/filesystem/vfs/mount.rs b/kernel/src/filesystem/vfs/mount.rs index fe616212a4..9c1b67601c 100644 --- a/kernel/src/filesystem/vfs/mount.rs +++ b/kernel/src/filesystem/vfs/mount.rs @@ -2,6 +2,7 @@ use core::{ any::Any, fmt::Debug, hash::Hash, + mem, sync::atomic::{compiler_fence, Ordering}, }; @@ -355,6 +356,10 @@ impl MountFS { self.namespace.init(namespace); } + pub fn namespace(&self) -> Option> { + self.namespace.try_get().and_then(|ns| ns.upgrade()) + } + pub fn fs_type(&self) -> &str { self.inner_filesystem.name() } @@ -372,6 +377,10 @@ impl MountFS { self.self_mountpoint.read().as_ref().cloned() } + pub fn set_self_mountpoint(&self, mountpoint: Option>) { + *self.self_mountpoint.write() = mountpoint; + } + /// @brief 用Arc指针包裹MountFS对象。 /// 本函数的主要功能为,初始化MountFS对象中的自引用Weak指针 /// 本函数只应在构造器中被调用 @@ -553,7 +562,14 @@ impl MountFSInode { .mountpoints .lock() .remove(&mountpoint_id) - .ok_or(SystemError::ENOENT)?; + .ok_or_else(|| { + log::warn!( + "do_umount: mountpoint id {:?} not found in parent fs '{}'", + mountpoint_id, + self.mount_fs.name() + ); + SystemError::ENOENT + })?; // Propagate umount to peers and slaves of the parent mount let parent_prop = self.mount_fs.propagation(); @@ -586,7 +602,17 @@ impl MountFSInode { // 注意:不同文件系统的 inode_id 空间可能互相独立,不能用“全局根 inode_id”作为终止条件。 // 正确做法应当按挂载树向上走,直到到达“命名空间根”(即 rootfs 的 mount,self_mountpoint 为 None)。 loop { - // 到达全局根(该 mount 没有挂载点):结束 + // 到达当前命名空间根:结束。 + if current.is_mountpoint_root()? + && current + .mount_fs + .namespace() + .is_some_and(|ns| Arc::ptr_eq(¤t.mount_fs, ns.root_mntfs())) + { + break; + } + + // 兼容旧模型:若 mount 没有挂载点,也将其视为根。 if current.is_mountpoint_root()? && current.mount_fs.self_mountpoint().is_none() { break; } @@ -644,6 +670,14 @@ impl MountFSInode { self_ref: self_ref.clone(), }) } + + pub fn mount_fs(&self) -> Arc { + self.mount_fs.clone() + } + + pub fn inode_id(&self) -> Result { + Ok(self.inner_inode.metadata()?.inode_id) + } } impl IndexNode for MountFSInode { @@ -1102,11 +1136,9 @@ impl FileSystem for MountFS { self.inner_filesystem.support_readahead() } fn root_inode(&self) -> Arc { - match self.self_mountpoint() { - Some(inode) => return inode.mount_fs.root_inode(), - // 当前文件系统是rootfs - None => self.mountpoint_root_inode(), - } + // A mounted filesystem's root inode is always its own mount root wrapper. + // Returning the parent mount's root breaks mount-root checks such as pivot_root(2). + self.mountpoint_root_inode() } fn info(&self) -> super::FsInfo { @@ -1351,6 +1383,37 @@ impl MountList { None } + pub fn rewrite_paths(&self, mut rewrite: F) + where + F: FnMut(&str) -> Option, + { + let mut inner = self.inner.write(); + let old_mounts = mem::take(&mut inner.mounts); + let mut new_mounts = HashMap::new(); + let mut new_ino2mp = HashMap::new(); + let mut new_mfs2ino = HashMap::new(); + + for (old_path, stack) in old_mounts { + let Some(new_path) = rewrite(old_path.as_str()) else { + continue; + }; + let new_path = Arc::new(MountPath::from(new_path)); + let entry = new_mounts.entry(new_path.clone()).or_insert_with(Vec::new); + + for rec in stack { + if let Some(ino) = rec.ino { + new_ino2mp.insert(ino, new_path.clone()); + new_mfs2ino.insert(rec.fs.clone(), ino); + } + entry.push(rec); + } + } + + inner.mounts = new_mounts; + inner.ino2mp = new_ino2mp; + inner.mfs2ino = new_mfs2ino; + } + /// # clone_inner - 克隆内部挂载点列表 pub fn clone_inner(&self) -> HashMap, Arc> { self.inner diff --git a/kernel/src/filesystem/vfs/syscall/mod.rs b/kernel/src/filesystem/vfs/syscall/mod.rs index b81462eedb..30c4861790 100644 --- a/kernel/src/filesystem/vfs/syscall/mod.rs +++ b/kernel/src/filesystem/vfs/syscall/mod.rs @@ -37,6 +37,7 @@ mod sys_lseek; mod sys_mkdirat; pub mod sys_mknodat; mod sys_openat; +mod sys_pivot_root; #[cfg(target_arch = "x86_64")] mod sys_poll; mod sys_ppoll; diff --git a/kernel/src/filesystem/vfs/syscall/sys_pivot_root.rs b/kernel/src/filesystem/vfs/syscall/sys_pivot_root.rs new file mode 100644 index 0000000000..f4ac382160 --- /dev/null +++ b/kernel/src/filesystem/vfs/syscall/sys_pivot_root.rs @@ -0,0 +1,327 @@ +//! System call handler for pivot_root(2). + +use alloc::{ + string::{String, ToString}, + sync::Arc, + vec::Vec, +}; +use system_error::SystemError; + +use crate::{ + arch::{interrupt::TrapFrame, syscall::nr::SYS_PIVOT_ROOT}, + filesystem::vfs::{ + mount::MountFSInode, + permission::PermissionMask, + utils::{is_ancestor, user_path_at}, + FileSystem, FileType, IndexNode, MAX_PATHLEN, VFS_MAX_FOLLOW_SYMLINK_TIMES, + }, + libs::casting::DowncastArc, + process::{all_process, cred::CAPFlags, ProcessControlBlock, ProcessManager}, + syscall::{ + table::{FormattedSyscallParam, Syscall}, + user_access::vfs_check_and_clone_cstr, + }, +}; + +pub struct SysPivotRootHandle; + +struct PivotRootTargets { + current_pcb: Arc, + current_mntns: Arc, + new_root_mntfs: Arc, + put_old_mountpoint: Arc, + old_new_root_path: String, + old_put_old_path: String, + new_put_old_path: String, +} + +impl Syscall for SysPivotRootHandle { + fn num_args(&self) -> usize { + 2 + } + + fn handle(&self, args: &[usize], _frame: &mut TrapFrame) -> Result { + let targets = resolve_pivot_root_targets(Self::new_root(args), Self::put_old(args))?; + let current_task = targets.current_pcb.clone(); + let old_root_inode = targets.current_mntns.root_inode(); + + targets.current_mntns.pivot_root( + targets.new_root_mntfs.clone(), + targets.put_old_mountpoint.clone(), + &targets.old_new_root_path, + &targets.old_put_old_path, + &targets.new_put_old_path, + )?; + + let new_root_inode = targets.current_mntns.root_inode(); + repair_same_namespace_fs_refs( + &targets.current_mntns, + ¤t_task, + &old_root_inode, + &new_root_inode, + &targets.old_new_root_path, + &targets.new_put_old_path, + )?; + + Ok(0) + } + + fn entry_format(&self, args: &[usize]) -> Vec { + vec![ + FormattedSyscallParam::new("new_root", format!("{:#x}", Self::new_root(args) as usize)), + FormattedSyscallParam::new("put_old", format!("{:#x}", Self::put_old(args) as usize)), + ] + } +} + +impl SysPivotRootHandle { + fn new_root(args: &[usize]) -> *const u8 { + args[0] as *const u8 + } + + fn put_old(args: &[usize]) -> *const u8 { + args[1] as *const u8 + } +} + +syscall_table_macros::declare_syscall!(SYS_PIVOT_ROOT, SysPivotRootHandle); + +fn resolve_pivot_root_targets( + new_root_ptr: *const u8, + put_old_ptr: *const u8, +) -> Result { + if new_root_ptr.is_null() || put_old_ptr.is_null() { + return Err(SystemError::EFAULT); + } + + let new_root_path = vfs_check_and_clone_cstr(new_root_ptr, Some(MAX_PATHLEN))? + .into_string() + .map_err(|_| SystemError::EINVAL)?; + let put_old_path = vfs_check_and_clone_cstr(put_old_ptr, Some(MAX_PATHLEN))? + .into_string() + .map_err(|_| SystemError::EINVAL)?; + + if new_root_path.is_empty() || put_old_path.is_empty() { + return Err(SystemError::ENOENT); + } + + let current_pcb = ProcessManager::current_pcb(); + if !current_pcb.cred().has_capability(CAPFlags::CAP_SYS_ADMIN) { + return Err(SystemError::EPERM); + } + + let current_mntns = ProcessManager::current_mntns(); + let namespace_root_inode = current_mntns.root_inode(); + let current_root_inode = namespace_root_inode.clone(); + + let (new_root_begin, new_root_rest) = user_path_at( + ¤t_pcb, + crate::filesystem::vfs::fcntl::AtFlags::AT_FDCWD.bits(), + &new_root_path, + )?; + let new_root_inode = + new_root_begin.lookup_follow_symlink(&new_root_rest, VFS_MAX_FOLLOW_SYMLINK_TIMES)?; + + let (put_old_begin, put_old_rest) = user_path_at( + ¤t_pcb, + crate::filesystem::vfs::fcntl::AtFlags::AT_FDCWD.bits(), + &put_old_path, + )?; + let put_old_inode = + put_old_begin.lookup_follow_symlink(&put_old_rest, VFS_MAX_FOLLOW_SYMLINK_TIMES)?; + + ensure_searchable_dir(&new_root_inode)?; + ensure_searchable_dir(&put_old_inode)?; + + let current_root_mntfs = mount_fs_from_inode(¤t_root_inode)?; + let new_root_mountpoint = inode_as_mountpoint(&new_root_inode)?; + let put_old_mountpoint = inode_as_mountpoint(&put_old_inode)?; + let new_root_mntfs = new_root_mountpoint.mount_fs(); + let put_old_mntfs = put_old_mountpoint.mount_fs(); + + if same_path_ref(&new_root_inode, ¤t_root_inode) + || same_path_ref(&put_old_inode, ¤t_root_inode) + { + return Err(SystemError::EBUSY); + } + + if !is_mount_root(&new_root_inode)? { + return Err(SystemError::EINVAL); + } + + if !is_ancestor(¤t_root_inode, &new_root_inode) { + return Err(SystemError::EINVAL); + } + + if !is_ancestor(&new_root_inode, &put_old_inode) { + return Err(SystemError::EINVAL); + } + + let new_root_parent = new_root_mntfs + .self_mountpoint() + .ok_or(SystemError::EINVAL)?; + let new_root_parent_mntfs = new_root_parent.mount_fs(); + + if current_root_mntfs.propagation().is_shared() + || new_root_mntfs.propagation().is_shared() + || new_root_parent_mntfs.propagation().is_shared() + || put_old_mntfs.propagation().is_shared() + { + return Err(SystemError::EINVAL); + } + + let old_new_root_path = new_root_inode.absolute_path()?; + let put_old_path_before = put_old_inode.absolute_path()?; + let new_put_old_path = put_old_path_before + .strip_prefix(&old_new_root_path) + .map(normalize_visible_path) + .ok_or(SystemError::EINVAL)?; + + Ok(PivotRootTargets { + current_pcb, + current_mntns, + new_root_mntfs, + put_old_mountpoint, + old_new_root_path, + old_put_old_path: put_old_path_before, + new_put_old_path, + }) +} + +fn ensure_searchable_dir(inode: &Arc) -> Result<(), SystemError> { + let metadata = inode.metadata()?; + if metadata.file_type != FileType::Dir { + return Err(SystemError::ENOTDIR); + } + + crate::filesystem::vfs::permission::check_inode_permission( + inode, + &metadata, + PermissionMask::MAY_EXEC, + ) +} + +fn mount_fs_from_inode( + inode: &Arc, +) -> Result, SystemError> { + inode + .fs() + .downcast_arc::() + .ok_or(SystemError::EINVAL) +} + +fn inode_as_mountpoint(inode: &Arc) -> Result, SystemError> { + inode + .clone() + .downcast_arc::() + .ok_or(SystemError::EINVAL) +} + +fn is_mount_root(inode: &Arc) -> Result { + let mount_fs = mount_fs_from_inode(inode)?; + let mount_root = mount_fs.root_inode(); + Ok(same_path_ref(inode, &mount_root)) +} + +fn same_path_ref(left: &Arc, right: &Arc) -> bool { + let left_meta = match left.metadata() { + Ok(meta) => meta, + Err(_) => return false, + }; + let right_meta = match right.metadata() { + Ok(meta) => meta, + Err(_) => return false, + }; + + Arc::ptr_eq(&left.fs(), &right.fs()) && left_meta.inode_id == right_meta.inode_id +} + +fn repair_same_namespace_fs_refs( + target_mntns: &Arc, + current_task: &Arc, + old_root_inode: &Arc, + new_root_inode: &Arc, + old_new_root_path: &str, + new_put_old_path: &str, +) -> Result<(), SystemError> { + let tasks: Vec> = { + let all = all_process().lock_irqsave(); + all.as_ref() + .map(|map| map.values().cloned().collect()) + .unwrap_or_default() + }; + + for task in tasks { + if !Arc::ptr_eq(task.nsproxy().mnt_namespace(), target_mntns) { + continue; + } + + let fs = task.fs_struct(); + let is_current_task = Arc::ptr_eq(&task, current_task); + let root_replaced = same_path_ref(&fs.root(), old_root_inode); + let pwd_replaced = same_path_ref(&fs.pwd(), old_root_inode); + let rewrite_cwd = is_current_task || root_replaced || pwd_replaced; + + if !root_replaced && !pwd_replaced && !rewrite_cwd { + continue; + } + + let old_cwd = task.basic().cwd(); + let mut basic = task.basic_mut(); + let fs_guard = task.fs_struct_mut(); + + if root_replaced || is_current_task { + fs_guard.set_root(new_root_inode.clone()); + } + if pwd_replaced { + fs_guard.set_pwd(new_root_inode.clone()); + } + + if rewrite_cwd { + let new_cwd = if pwd_replaced { + "/".to_string() + } else { + rewrite_visible_path(&old_cwd, old_new_root_path, new_put_old_path) + }; + basic.set_cwd(new_cwd); + } + } + + Ok(()) +} + +fn normalize_visible_path(path: &str) -> String { + if path.is_empty() || path == "/" { + "/".to_string() + } else if path.starts_with('/') { + path.to_string() + } else { + format!("/{}", path) + } +} + +fn rewrite_visible_path(path: &str, old_new_root_path: &str, new_put_old_path: &str) -> String { + if path == old_new_root_path { + return "/".to_string(); + } + + if let Some(suffix) = path.strip_prefix(old_new_root_path) { + if suffix.is_empty() { + return "/".to_string(); + } + return normalize_visible_path(suffix); + } + + if path == "/" { + return new_put_old_path.to_string(); + } + + if new_put_old_path == "/" { + return normalize_visible_path(path); + } + + let mut result = new_put_old_path.trim_end_matches('/').to_string(); + result.push('/'); + result.push_str(path.trim_start_matches('/')); + result +} diff --git a/kernel/src/filesystem/vfs/syscall/sys_umount2.rs b/kernel/src/filesystem/vfs/syscall/sys_umount2.rs index ce515fc886..5eb43384dd 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_umount2.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_umount2.rs @@ -2,14 +2,22 @@ use crate::{ arch::{interrupt::TrapFrame, syscall::nr::SYS_UMOUNT2}, - filesystem::vfs::{fcntl::AtFlags, utils::user_path_at, MountFS, MAX_PATHLEN}, + filesystem::vfs::{ + fcntl::AtFlags, utils::user_path_at, FileSystem, IndexNode, MountFS, MAX_PATHLEN, + VFS_MAX_FOLLOW_SYMLINK_TIMES, + }, + libs::casting::DowncastArc, process::ProcessManager, syscall::{ table::{FormattedSyscallParam, Syscall}, user_access, }, }; -use alloc::{sync::Arc, vec::Vec}; +use alloc::{ + string::{String, ToString}, + sync::Arc, + vec::Vec, +}; use system_error::SystemError; /// src/linux/mount.c `umount` & `umount2` @@ -86,32 +94,63 @@ pub fn do_umount2( } let (work, rest) = user_path_at(&ProcessManager::current_pcb(), dirfd, target)?; - - // user_path_at 已经保证: - // - 绝对路径:rest 以 '/' 开头 - // - 相对路径:rest 不以 '/' 开头,work 为 dirfd/cwd inode - // - // 因此这里不能无脑拼接 work.absolute_path()+rest,否则绝对路径会被重复前缀化,导致找不到挂载记录。 - let path = if rest.starts_with('/') { - rest - } else { - let mut base = work.absolute_path()?; - if !base.ends_with('/') { - base.push('/'); - } - base.push_str(&rest); - base - }; + let target_inode = work.lookup_follow_symlink(&rest, VFS_MAX_FOLLOW_SYMLINK_TIMES)?; + let path = visible_umount_path(&target_inode)?; + log::warn!( + "do_umount2: target='{}' resolved='{}' inode_fs='{}'", + target, + path, + target_inode.fs().name() + ); let result = ProcessManager::current_mntns().remove_mount(&path); if let Some(fs) = result { // Todo: 占用检测 - fs.umount()?; + if let Err(err) = fs.umount() { + log::warn!( + "do_umount2: fs.umount failed for resolved='{}', fs='{}': {:?}", + path, + fs.name(), + err + ); + return Err(err); + } return Ok(fs); } + log::warn!("do_umount2: mount_list miss for resolved='{}'", path); return Err(SystemError::EINVAL); } +fn visible_umount_path(target_inode: &Arc) -> Result { + let absolute_path = target_inode.absolute_path()?; + let Some(target_mfs) = target_inode.fs().downcast_arc::() else { + return Ok(absolute_path); + }; + + let mount_root = target_mfs.root_inode(); + if same_path_ref(target_inode, &mount_root) { + if let Some(mount_path) = ProcessManager::current_mntns() + .mount_list() + .get_mount_path_by_mountfs(&target_mfs) + { + return Ok(mount_path.as_str().to_string()); + } + } + + Ok(absolute_path) +} + +fn same_path_ref(left: &Arc, right: &Arc) -> bool { + let Ok(left_meta) = left.metadata() else { + return false; + }; + let Ok(right_meta) = right.metadata() else { + return false; + }; + + Arc::ptr_eq(&left.fs(), &right.fs()) && left_meta.inode_id == right_meta.inode_id +} + bitflags! { pub struct UmountFlag: i32 { const DEFAULT = 0; /* Default call to umount. */ diff --git a/kernel/src/process/namespace/mnt.rs b/kernel/src/process/namespace/mnt.rs index 181a48866b..4fdc4720d8 100644 --- a/kernel/src/process/namespace/mnt.rs +++ b/kernel/src/process/namespace/mnt.rs @@ -1,12 +1,12 @@ use crate::{ filesystem::vfs::{ - mount::{MountFlags, MountList, MountPath}, + mount::{MountFSInode, MountFlags, MountList, MountPath}, FileSystem, IndexNode, InodeId, MountFS, }, libs::{once::Once, spinlock::SpinLock}, process::{fork::CloneFlags, namespace::NamespaceType, ProcessManager}, }; -use alloc::string::String; +use alloc::string::{String, ToString}; use alloc::sync::{Arc, Weak}; use alloc::vec::Vec; @@ -106,6 +106,99 @@ impl MntNamespace { // update mount list ino } + pub fn pivot_root( + &self, + new_root: Arc, + put_old_mountpoint: Arc, + old_new_root_path: &str, + old_put_old_path: &str, + new_put_old_path: &str, + ) -> Result<(), SystemError> { + let old_root = self.root_mountfs.clone(); + let old_root_mountpoint = old_root.self_mountpoint(); + let new_root_mountpoint = new_root.self_mountpoint().ok_or(SystemError::EINVAL)?; + let new_root_parent = new_root_mountpoint.mount_fs(); + let put_old_parent = put_old_mountpoint.mount_fs(); + let put_old_is_new_root = old_put_old_path == old_new_root_path; + let new_root_mountpoint_id = new_root_mountpoint.inode_id()?; + let put_old_mountpoint_id = put_old_mountpoint.inode_id()?; + + { + let put_old_mounts = put_old_parent.mountpoints(); + if put_old_mounts.contains_key(&put_old_mountpoint_id) { + return Err(SystemError::EBUSY); + } + } + + { + let mut parent_mounts = new_root_parent.mountpoints(); + parent_mounts + .remove(&new_root_mountpoint_id) + .ok_or(SystemError::EINVAL)?; + } + + old_root.set_self_mountpoint(Some(put_old_mountpoint.clone())); + { + let mut put_old_mounts = put_old_parent.mountpoints(); + if put_old_mounts + .insert(put_old_mountpoint_id, old_root.clone()) + .is_some() + { + old_root.set_self_mountpoint(old_root_mountpoint); + new_root_parent + .add_mount(new_root_mountpoint_id, new_root.clone()) + .map_err(|_| SystemError::EBUSY)?; + return Err(SystemError::EBUSY); + } + } + + new_root.set_self_mountpoint(None); + + let inner_guard = self.inner.lock(); + let ptr = self as *const Self as *mut Self; + let self_mut = unsafe { (ptr).as_mut().unwrap() }; + self_mut.root_mountfs = new_root.clone(); + + inner_guard.mount_list.remove("/"); + if put_old_is_new_root { + inner_guard.mount_list.rewrite_paths(|path| { + if path == old_new_root_path || path_is_under(path, old_new_root_path) { + return Some(rewrite_pivot_path( + path, + old_new_root_path, + new_put_old_path, + )); + } + + None + }); + inner_guard.mount_list.insert( + Some(put_old_mountpoint_id), + Arc::new(MountPath::from("/")), + old_root, + ); + } else { + inner_guard.mount_list.rewrite_paths(|path| { + if path == old_put_old_path || path_is_under(path, old_put_old_path) { + return None; + } + + Some(rewrite_pivot_path( + path, + old_new_root_path, + new_put_old_path, + )) + }); + inner_guard.mount_list.insert( + Some(put_old_mountpoint_id), + Arc::new(MountPath::from(new_put_old_path)), + old_root, + ); + } + + Ok(()) + } + fn copy_with_mountfs(&self, new_root: Arc, _user_ns: Arc) -> Arc { let mut ns_common = self.ns_common.clone(); ns_common.level += 1; @@ -313,6 +406,54 @@ struct MountFSCopyInfo { mount_path: Arc, } +fn rewrite_pivot_path(path: &str, old_new_root_path: &str, new_put_old_path: &str) -> String { + if path == old_new_root_path { + return "/".to_string(); + } + + if let Some(suffix) = path.strip_prefix(old_new_root_path) { + if suffix.is_empty() { + return "/".to_string(); + } + + return normalize_pivot_path(suffix); + } + + if path == "/" { + return new_put_old_path.to_string(); + } + + join_pivot_paths(new_put_old_path, path) +} + +fn path_is_under(path: &str, prefix: &str) -> bool { + path != prefix + && path + .strip_prefix(prefix) + .is_some_and(|suffix| suffix.starts_with('/')) +} + +fn normalize_pivot_path(path: &str) -> String { + if path.is_empty() || path == "/" { + "/".to_string() + } else if path.starts_with('/') { + path.to_string() + } else { + format!("/{}", path) + } +} + +fn join_pivot_paths(prefix: &str, suffix: &str) -> String { + if prefix == "/" { + return normalize_pivot_path(suffix); + } + + let mut result = prefix.trim_end_matches('/').to_string(); + result.push('/'); + result.push_str(suffix.trim_start_matches('/')); + result +} + // impl Drop for MntNamespace { // fn drop(&mut self) { // log::warn!("mntns (level: {}) dropped", self.ns_common.level); diff --git a/kernel/src/process/namespace/nsproxy.rs b/kernel/src/process/namespace/nsproxy.rs index 37f84731de..cb495227ae 100644 --- a/kernel/src/process/namespace/nsproxy.rs +++ b/kernel/src/process/namespace/nsproxy.rs @@ -2,15 +2,18 @@ use alloc::sync::Arc; use core::sync::atomic::{AtomicUsize, Ordering}; use system_error::SystemError; -use crate::process::{ - fork::CloneFlags, - namespace::{ - cgroup_namespace::{CgroupNamespace, INIT_CGROUP_NAMESPACE}, - mnt::{root_mnt_namespace, MntNamespace}, - net_namespace::{NetNamespace, INIT_NET_NAMESPACE}, - uts_namespace::{UtsNamespace, INIT_UTS_NAMESPACE}, +use crate::{ + filesystem::vfs::{IndexNode, VFS_MAX_FOLLOW_SYMLINK_TIMES}, + process::{ + fork::CloneFlags, + namespace::{ + cgroup_namespace::{CgroupNamespace, INIT_CGROUP_NAMESPACE}, + mnt::{root_mnt_namespace, MntNamespace}, + net_namespace::{NetNamespace, INIT_NET_NAMESPACE}, + uts_namespace::{UtsNamespace, INIT_UTS_NAMESPACE}, + }, + ProcessControlBlock, ProcessManager, }, - ProcessControlBlock, ProcessManager, }; use core::{fmt::Debug, intrinsics::likely}; @@ -264,6 +267,47 @@ pub fn switch_task_namespaces( tsk: &Arc, new_nsproxy: Arc, ) -> Result<(), SystemError> { + let rebound_paths = if Arc::ptr_eq(tsk.nsproxy().mnt_namespace(), &new_nsproxy.mnt_ns) { + None + } else { + Some(resolve_fs_paths_for_new_mntns(tsk, &new_nsproxy.mnt_ns)?) + }; + tsk.set_nsproxy(new_nsproxy); + + if let Some((new_root, new_pwd)) = rebound_paths { + let fs = tsk.fs_struct_mut(); + fs.set_root(new_root); + fs.set_pwd(new_pwd); + } + Ok(()) } + +type ReboundFsPaths = (Arc, Arc); + +fn resolve_fs_paths_for_new_mntns( + tsk: &Arc, + new_mntns: &Arc, +) -> Result { + let fs = tsk.fs_struct(); + let old_root_path = fs.root().absolute_path()?; + let old_pwd_path = fs.pwd().absolute_path()?; + drop(fs); + + let new_root = resolve_from_namespace_root(new_mntns, &old_root_path)?; + let new_pwd = resolve_from_namespace_root(new_mntns, &old_pwd_path)?; + Ok((new_root, new_pwd)) +} + +fn resolve_from_namespace_root( + mntns: &Arc, + path: &str, +) -> Result, SystemError> { + let namespace_root = mntns.root_inode(); + if path.is_empty() || path == "/" { + return Ok(namespace_root); + } + + namespace_root.lookup_follow_symlink(path.trim_start_matches('/'), VFS_MAX_FOLLOW_SYMLINK_TIMES) +} diff --git a/user/apps/c_unitest/test_pivot_root.c b/user/apps/c_unitest/test_pivot_root.c new file mode 100644 index 0000000000..3f7274e601 --- /dev/null +++ b/user/apps/c_unitest/test_pivot_root.c @@ -0,0 +1,476 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef SYS_pivot_root +#ifdef __NR_pivot_root +#define SYS_pivot_root __NR_pivot_root +#elif defined(__x86_64__) +#define SYS_pivot_root 155 +#else +#define SYS_pivot_root 41 +#endif +#endif + +#ifndef CLONE_NEWNS +#define CLONE_NEWNS 0x00020000 +#endif + +#ifndef MS_REC +#define MS_REC 16384 +#endif + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST_PASS(name) \ + do { \ + printf("[PASS] %s\n", name); \ + tests_passed++; \ + } while (0) + +#define TEST_FAIL(name, reason) \ + do { \ + printf("[FAIL] %s: %s\n", name, reason); \ + tests_failed++; \ + } while (0) + +#define TEST_SKIP(name, reason) \ + do { \ + printf("[SKIP] %s: %s\n", name, reason); \ + } while (0) + +static int ensure_dir(const char *path) { + struct stat st; + if (stat(path, &st) == 0) { + return S_ISDIR(st.st_mode) ? 0 : -1; + } + return mkdir(path, 0755); +} + +static void ensure_parent_tree(void) { + ensure_dir("/tmp"); + ensure_dir("/tmp/test_pivot_root"); +} + +static void cleanup_mount(const char *path) { + umount(path); + rmdir(path); +} + +static long do_pivot_root(const char *new_root, const char *put_old) { + return syscall(SYS_pivot_root, new_root, put_old); +} + +static void test_success_path(void) { + const char *name = "pivot_root_success"; + const char *base = "/tmp/test_pivot_root/success"; + const char *new_root = "/tmp/test_pivot_root/success/newroot"; + const char *put_old = "oldroot"; + const char *oldroot_abs = "/tmp/test_pivot_root/success/newroot/oldroot"; + const char *bin_dir = "/tmp/test_pivot_root/success/newroot/bin"; + char cwd[256]; + + ensure_parent_tree(); + ensure_dir(base); + ensure_dir(new_root); + + if (unshare(CLONE_NEWNS) != 0) { + TEST_SKIP(name, strerror(errno)); + return; + } + + mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL); + + if (mount("", new_root, "ramfs", 0, NULL) != 0) { + TEST_SKIP(name, strerror(errno)); + return; + } + + if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + TEST_FAIL(name, "mkdir(oldroot) failed"); + return; + } + + if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + TEST_FAIL(name, "mkdir(bin) failed"); + return; + } + + if (chdir(new_root) != 0) { + cleanup_mount(new_root); + TEST_FAIL(name, "chdir(new_root) failed"); + return; + } + + if (do_pivot_root(".", put_old) != 0) { + cleanup_mount(new_root); + TEST_FAIL(name, strerror(errno)); + return; + } + + if (getcwd(cwd, sizeof(cwd)) == NULL) { + TEST_FAIL(name, "getcwd failed after pivot"); + return; + } + + if (strcmp(cwd, "/") != 0) { + TEST_FAIL(name, "cwd is not / after pivot"); + return; + } + + if (access("/oldroot", F_OK) != 0) { + TEST_FAIL(name, "old root not reachable under /oldroot"); + return; + } + + if (access("/bin", F_OK) != 0) { + TEST_FAIL(name, "new root is not visible via absolute path"); + return; + } + + TEST_PASS(name); +} + +static void test_dot_dot_path(void) { + const char *name = "pivot_root_dot_dot"; + const char *base = "/tmp/test_pivot_root/dotdot"; + const char *new_root = "/tmp/test_pivot_root/dotdot/newroot"; + const char *bin_dir = "/tmp/test_pivot_root/dotdot/newroot/bin"; + int oldroot_fd; + int newroot_fd; + char cwd[256]; + + ensure_parent_tree(); + ensure_dir(base); + ensure_dir(new_root); + + if (unshare(CLONE_NEWNS) != 0) { + TEST_SKIP(name, strerror(errno)); + return; + } + + mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL); + + if (mount("", new_root, "ramfs", 0, NULL) != 0) { + TEST_SKIP(name, strerror(errno)); + return; + } + + if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + TEST_FAIL(name, "mkdir(bin) failed"); + return; + } + + oldroot_fd = open("/", O_DIRECTORY | O_RDONLY); + if (oldroot_fd < 0) { + cleanup_mount(new_root); + TEST_FAIL(name, "open(oldroot) failed"); + return; + } + + newroot_fd = open(new_root, O_DIRECTORY | O_RDONLY); + if (newroot_fd < 0) { + close(oldroot_fd); + cleanup_mount(new_root); + TEST_FAIL(name, "open(newroot) failed"); + return; + } + + if (fchdir(newroot_fd) != 0) { + close(newroot_fd); + close(oldroot_fd); + cleanup_mount(new_root); + TEST_FAIL(name, "fchdir(newroot) failed"); + return; + } + + if (do_pivot_root(".", ".") != 0) { + close(newroot_fd); + close(oldroot_fd); + cleanup_mount(new_root); + TEST_FAIL(name, strerror(errno)); + return; + } + + if (fchdir(oldroot_fd) != 0) { + close(newroot_fd); + close(oldroot_fd); + TEST_FAIL(name, "fchdir(oldroot) failed after pivot"); + return; + } + + if (umount2(".", MNT_DETACH) != 0) { + close(newroot_fd); + close(oldroot_fd); + TEST_FAIL(name, "umount2(oldroot) failed"); + return; + } + + if (chdir("/") != 0) { + close(newroot_fd); + close(oldroot_fd); + TEST_FAIL(name, "chdir(/) failed after detach"); + return; + } + + if (getcwd(cwd, sizeof(cwd)) == NULL || strcmp(cwd, "/") != 0) { + close(newroot_fd); + close(oldroot_fd); + TEST_FAIL(name, "cwd is not / after dot-dot pivot"); + return; + } + + if (access("/bin", F_OK) != 0) { + close(newroot_fd); + close(oldroot_fd); + TEST_FAIL(name, "new root is not visible after dot-dot pivot"); + return; + } + + close(newroot_fd); + close(oldroot_fd); + TEST_PASS(name); +} + +static void test_dot_dot_rslave_detach(void) { + const char *name = "pivot_root_dot_dot_rslave_detach"; + const char *base = "/tmp/test_pivot_root/dotdot_rslave"; + const char *new_root = "/tmp/test_pivot_root/dotdot_rslave/newroot"; + const char *bin_dir = "/tmp/test_pivot_root/dotdot_rslave/newroot/bin"; + int oldroot_fd; + int newroot_fd; + + ensure_parent_tree(); + ensure_dir(base); + ensure_dir(new_root); + + if (unshare(CLONE_NEWNS) != 0) { + TEST_SKIP(name, strerror(errno)); + return; + } + + mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL); + + if (mount("", new_root, "ramfs", 0, NULL) != 0) { + TEST_SKIP(name, strerror(errno)); + return; + } + + if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + TEST_FAIL(name, "mkdir(bin) failed"); + return; + } + + oldroot_fd = open("/", O_DIRECTORY | O_RDONLY); + if (oldroot_fd < 0) { + cleanup_mount(new_root); + TEST_FAIL(name, "open(oldroot) failed"); + return; + } + + newroot_fd = open(new_root, O_DIRECTORY | O_RDONLY); + if (newroot_fd < 0) { + close(oldroot_fd); + cleanup_mount(new_root); + TEST_FAIL(name, "open(newroot) failed"); + return; + } + + if (fchdir(newroot_fd) != 0) { + close(newroot_fd); + close(oldroot_fd); + cleanup_mount(new_root); + TEST_FAIL(name, "fchdir(newroot) failed"); + return; + } + + if (do_pivot_root(".", ".") != 0) { + close(newroot_fd); + close(oldroot_fd); + cleanup_mount(new_root); + TEST_FAIL(name, strerror(errno)); + return; + } + + if (fchdir(oldroot_fd) != 0) { + close(newroot_fd); + close(oldroot_fd); + TEST_FAIL(name, "fchdir(oldroot) failed after pivot"); + return; + } + + if (mount(NULL, ".", NULL, MS_REC | MS_SLAVE, NULL) != 0) { + close(newroot_fd); + close(oldroot_fd); + TEST_FAIL(name, "mount(make-rslave) failed"); + return; + } + + if (umount2(".", MNT_DETACH) != 0) { + close(newroot_fd); + close(oldroot_fd); + TEST_FAIL(name, "umount2(oldroot) failed after make-rslave"); + return; + } + + close(newroot_fd); + close(oldroot_fd); + TEST_PASS(name); +} + +static void test_new_root_not_mountpoint(void) { + const char *name = "pivot_root_new_root_not_mountpoint"; + const char *base = "/tmp/test_pivot_root/not_mountpoint"; + const char *new_root = "/tmp/test_pivot_root/not_mountpoint/newroot"; + const char *oldroot_abs = "/tmp/test_pivot_root/not_mountpoint/newroot/oldroot"; + + ensure_parent_tree(); + ensure_dir(base); + ensure_dir(new_root); + ensure_dir(oldroot_abs); + + if (unshare(CLONE_NEWNS) != 0) { + TEST_SKIP(name, strerror(errno)); + return; + } + + if (do_pivot_root(new_root, oldroot_abs) == -1 && errno == EINVAL) { + TEST_PASS(name); + } else { + TEST_FAIL(name, "expected EINVAL"); + } +} + +static void test_put_old_outside_new_root(void) { + const char *name = "pivot_root_put_old_outside_new_root"; + const char *base = "/tmp/test_pivot_root/put_old_outside"; + const char *new_root = "/tmp/test_pivot_root/put_old_outside/newroot"; + const char *outside = "/tmp/test_pivot_root/put_old_outside/outside"; + const char *inside = "/tmp/test_pivot_root/put_old_outside/newroot/inside"; + + ensure_parent_tree(); + ensure_dir(base); + ensure_dir(new_root); + ensure_dir(outside); + + if (unshare(CLONE_NEWNS) != 0) { + TEST_SKIP(name, strerror(errno)); + return; + } + + if (mount("", new_root, "ramfs", 0, NULL) != 0) { + TEST_SKIP(name, strerror(errno)); + return; + } + + if (mkdir(inside, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + TEST_FAIL(name, "mkdir(inside) failed"); + return; + } + + if (do_pivot_root(new_root, outside) == -1 && errno == EINVAL) { + cleanup_mount(new_root); + TEST_PASS(name); + } else { + cleanup_mount(new_root); + TEST_FAIL(name, "expected EINVAL"); + } +} + +static void test_busy_target(void) { + const char *name = "pivot_root_busy_target"; + const char *base = "/tmp/test_pivot_root/busy"; + const char *new_root = "/tmp/test_pivot_root/busy/newroot"; + const char *oldroot_abs = "/tmp/test_pivot_root/busy/newroot/oldroot"; + + ensure_parent_tree(); + ensure_dir(base); + ensure_dir(new_root); + + if (unshare(CLONE_NEWNS) != 0) { + TEST_SKIP(name, strerror(errno)); + return; + } + + if (mount("", new_root, "ramfs", 0, NULL) != 0) { + TEST_SKIP(name, strerror(errno)); + return; + } + + if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + TEST_FAIL(name, "mkdir(oldroot) failed"); + return; + } + + if (do_pivot_root(new_root, "/") == -1 && errno == EBUSY) { + cleanup_mount(new_root); + TEST_PASS(name); + } else { + cleanup_mount(new_root); + TEST_FAIL(name, "expected EBUSY"); + } +} + +static void test_permission_failure(void) { + const char *name = "pivot_root_permission_failure"; + const char *base = "/tmp/test_pivot_root/perm"; + const char *new_root = "/tmp/test_pivot_root/perm/newroot"; + const char *oldroot_abs = "/tmp/test_pivot_root/perm/newroot/oldroot"; + + ensure_parent_tree(); + ensure_dir(base); + ensure_dir(new_root); + + if (mount("", new_root, "ramfs", 0, NULL) != 0) { + TEST_SKIP(name, strerror(errno)); + return; + } + + if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + TEST_FAIL(name, "mkdir(oldroot) failed"); + return; + } + + if (seteuid(65534) != 0) { + cleanup_mount(new_root); + TEST_SKIP(name, "seteuid failed"); + return; + } + + if (do_pivot_root(new_root, oldroot_abs) == -1 && errno == EPERM) { + TEST_PASS(name); + } else { + TEST_FAIL(name, "expected EPERM"); + } +} + +int main(void) { + test_success_path(); + test_dot_dot_path(); + test_dot_dot_rslave_detach(); + test_new_root_not_mountpoint(); + test_put_old_outside_new_root(); + test_busy_target(); + test_permission_failure(); + + printf("passed=%d failed=%d\n", tests_passed, tests_failed); + return tests_failed == 0 ? 0 : 1; +} From 6c612bf5be1eb029110a7325953d90ea37c3e7da Mon Sep 17 00:00:00 2001 From: sparkzky Date: Tue, 10 Mar 2026 12:51:16 +0800 Subject: [PATCH 2/3] refactor: move pivot-test to dunitest Signed-off-by: sparkzky --- .../{c_unitest => tests/dunitest/suites/normal}/test_pivot_root.c | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename user/apps/{c_unitest => tests/dunitest/suites/normal}/test_pivot_root.c (100%) diff --git a/user/apps/c_unitest/test_pivot_root.c b/user/apps/tests/dunitest/suites/normal/test_pivot_root.c similarity index 100% rename from user/apps/c_unitest/test_pivot_root.c rename to user/apps/tests/dunitest/suites/normal/test_pivot_root.c From acaaf3beeb0ac76c6e537e20d597a7a7de6bc4f0 Mon Sep 17 00:00:00 2001 From: sparkzky Date: Fri, 13 Mar 2026 20:30:14 +0800 Subject: [PATCH 3/3] test(dunitest): add pivot_root case --- .../dunitest/suites/normal/test_pivot_root.c | 476 ----------------- .../dunitest/suites/normal/test_pivot_root.cc | 485 ++++++++++++++++++ user/apps/tests/dunitest/whitelist.txt | 1 + 3 files changed, 486 insertions(+), 476 deletions(-) delete mode 100644 user/apps/tests/dunitest/suites/normal/test_pivot_root.c create mode 100644 user/apps/tests/dunitest/suites/normal/test_pivot_root.cc diff --git a/user/apps/tests/dunitest/suites/normal/test_pivot_root.c b/user/apps/tests/dunitest/suites/normal/test_pivot_root.c deleted file mode 100644 index 3f7274e601..0000000000 --- a/user/apps/tests/dunitest/suites/normal/test_pivot_root.c +++ /dev/null @@ -1,476 +0,0 @@ -#define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef SYS_pivot_root -#ifdef __NR_pivot_root -#define SYS_pivot_root __NR_pivot_root -#elif defined(__x86_64__) -#define SYS_pivot_root 155 -#else -#define SYS_pivot_root 41 -#endif -#endif - -#ifndef CLONE_NEWNS -#define CLONE_NEWNS 0x00020000 -#endif - -#ifndef MS_REC -#define MS_REC 16384 -#endif - -static int tests_passed = 0; -static int tests_failed = 0; - -#define TEST_PASS(name) \ - do { \ - printf("[PASS] %s\n", name); \ - tests_passed++; \ - } while (0) - -#define TEST_FAIL(name, reason) \ - do { \ - printf("[FAIL] %s: %s\n", name, reason); \ - tests_failed++; \ - } while (0) - -#define TEST_SKIP(name, reason) \ - do { \ - printf("[SKIP] %s: %s\n", name, reason); \ - } while (0) - -static int ensure_dir(const char *path) { - struct stat st; - if (stat(path, &st) == 0) { - return S_ISDIR(st.st_mode) ? 0 : -1; - } - return mkdir(path, 0755); -} - -static void ensure_parent_tree(void) { - ensure_dir("/tmp"); - ensure_dir("/tmp/test_pivot_root"); -} - -static void cleanup_mount(const char *path) { - umount(path); - rmdir(path); -} - -static long do_pivot_root(const char *new_root, const char *put_old) { - return syscall(SYS_pivot_root, new_root, put_old); -} - -static void test_success_path(void) { - const char *name = "pivot_root_success"; - const char *base = "/tmp/test_pivot_root/success"; - const char *new_root = "/tmp/test_pivot_root/success/newroot"; - const char *put_old = "oldroot"; - const char *oldroot_abs = "/tmp/test_pivot_root/success/newroot/oldroot"; - const char *bin_dir = "/tmp/test_pivot_root/success/newroot/bin"; - char cwd[256]; - - ensure_parent_tree(); - ensure_dir(base); - ensure_dir(new_root); - - if (unshare(CLONE_NEWNS) != 0) { - TEST_SKIP(name, strerror(errno)); - return; - } - - mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL); - - if (mount("", new_root, "ramfs", 0, NULL) != 0) { - TEST_SKIP(name, strerror(errno)); - return; - } - - if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { - cleanup_mount(new_root); - TEST_FAIL(name, "mkdir(oldroot) failed"); - return; - } - - if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) { - cleanup_mount(new_root); - TEST_FAIL(name, "mkdir(bin) failed"); - return; - } - - if (chdir(new_root) != 0) { - cleanup_mount(new_root); - TEST_FAIL(name, "chdir(new_root) failed"); - return; - } - - if (do_pivot_root(".", put_old) != 0) { - cleanup_mount(new_root); - TEST_FAIL(name, strerror(errno)); - return; - } - - if (getcwd(cwd, sizeof(cwd)) == NULL) { - TEST_FAIL(name, "getcwd failed after pivot"); - return; - } - - if (strcmp(cwd, "/") != 0) { - TEST_FAIL(name, "cwd is not / after pivot"); - return; - } - - if (access("/oldroot", F_OK) != 0) { - TEST_FAIL(name, "old root not reachable under /oldroot"); - return; - } - - if (access("/bin", F_OK) != 0) { - TEST_FAIL(name, "new root is not visible via absolute path"); - return; - } - - TEST_PASS(name); -} - -static void test_dot_dot_path(void) { - const char *name = "pivot_root_dot_dot"; - const char *base = "/tmp/test_pivot_root/dotdot"; - const char *new_root = "/tmp/test_pivot_root/dotdot/newroot"; - const char *bin_dir = "/tmp/test_pivot_root/dotdot/newroot/bin"; - int oldroot_fd; - int newroot_fd; - char cwd[256]; - - ensure_parent_tree(); - ensure_dir(base); - ensure_dir(new_root); - - if (unshare(CLONE_NEWNS) != 0) { - TEST_SKIP(name, strerror(errno)); - return; - } - - mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL); - - if (mount("", new_root, "ramfs", 0, NULL) != 0) { - TEST_SKIP(name, strerror(errno)); - return; - } - - if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) { - cleanup_mount(new_root); - TEST_FAIL(name, "mkdir(bin) failed"); - return; - } - - oldroot_fd = open("/", O_DIRECTORY | O_RDONLY); - if (oldroot_fd < 0) { - cleanup_mount(new_root); - TEST_FAIL(name, "open(oldroot) failed"); - return; - } - - newroot_fd = open(new_root, O_DIRECTORY | O_RDONLY); - if (newroot_fd < 0) { - close(oldroot_fd); - cleanup_mount(new_root); - TEST_FAIL(name, "open(newroot) failed"); - return; - } - - if (fchdir(newroot_fd) != 0) { - close(newroot_fd); - close(oldroot_fd); - cleanup_mount(new_root); - TEST_FAIL(name, "fchdir(newroot) failed"); - return; - } - - if (do_pivot_root(".", ".") != 0) { - close(newroot_fd); - close(oldroot_fd); - cleanup_mount(new_root); - TEST_FAIL(name, strerror(errno)); - return; - } - - if (fchdir(oldroot_fd) != 0) { - close(newroot_fd); - close(oldroot_fd); - TEST_FAIL(name, "fchdir(oldroot) failed after pivot"); - return; - } - - if (umount2(".", MNT_DETACH) != 0) { - close(newroot_fd); - close(oldroot_fd); - TEST_FAIL(name, "umount2(oldroot) failed"); - return; - } - - if (chdir("/") != 0) { - close(newroot_fd); - close(oldroot_fd); - TEST_FAIL(name, "chdir(/) failed after detach"); - return; - } - - if (getcwd(cwd, sizeof(cwd)) == NULL || strcmp(cwd, "/") != 0) { - close(newroot_fd); - close(oldroot_fd); - TEST_FAIL(name, "cwd is not / after dot-dot pivot"); - return; - } - - if (access("/bin", F_OK) != 0) { - close(newroot_fd); - close(oldroot_fd); - TEST_FAIL(name, "new root is not visible after dot-dot pivot"); - return; - } - - close(newroot_fd); - close(oldroot_fd); - TEST_PASS(name); -} - -static void test_dot_dot_rslave_detach(void) { - const char *name = "pivot_root_dot_dot_rslave_detach"; - const char *base = "/tmp/test_pivot_root/dotdot_rslave"; - const char *new_root = "/tmp/test_pivot_root/dotdot_rslave/newroot"; - const char *bin_dir = "/tmp/test_pivot_root/dotdot_rslave/newroot/bin"; - int oldroot_fd; - int newroot_fd; - - ensure_parent_tree(); - ensure_dir(base); - ensure_dir(new_root); - - if (unshare(CLONE_NEWNS) != 0) { - TEST_SKIP(name, strerror(errno)); - return; - } - - mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL); - - if (mount("", new_root, "ramfs", 0, NULL) != 0) { - TEST_SKIP(name, strerror(errno)); - return; - } - - if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) { - cleanup_mount(new_root); - TEST_FAIL(name, "mkdir(bin) failed"); - return; - } - - oldroot_fd = open("/", O_DIRECTORY | O_RDONLY); - if (oldroot_fd < 0) { - cleanup_mount(new_root); - TEST_FAIL(name, "open(oldroot) failed"); - return; - } - - newroot_fd = open(new_root, O_DIRECTORY | O_RDONLY); - if (newroot_fd < 0) { - close(oldroot_fd); - cleanup_mount(new_root); - TEST_FAIL(name, "open(newroot) failed"); - return; - } - - if (fchdir(newroot_fd) != 0) { - close(newroot_fd); - close(oldroot_fd); - cleanup_mount(new_root); - TEST_FAIL(name, "fchdir(newroot) failed"); - return; - } - - if (do_pivot_root(".", ".") != 0) { - close(newroot_fd); - close(oldroot_fd); - cleanup_mount(new_root); - TEST_FAIL(name, strerror(errno)); - return; - } - - if (fchdir(oldroot_fd) != 0) { - close(newroot_fd); - close(oldroot_fd); - TEST_FAIL(name, "fchdir(oldroot) failed after pivot"); - return; - } - - if (mount(NULL, ".", NULL, MS_REC | MS_SLAVE, NULL) != 0) { - close(newroot_fd); - close(oldroot_fd); - TEST_FAIL(name, "mount(make-rslave) failed"); - return; - } - - if (umount2(".", MNT_DETACH) != 0) { - close(newroot_fd); - close(oldroot_fd); - TEST_FAIL(name, "umount2(oldroot) failed after make-rslave"); - return; - } - - close(newroot_fd); - close(oldroot_fd); - TEST_PASS(name); -} - -static void test_new_root_not_mountpoint(void) { - const char *name = "pivot_root_new_root_not_mountpoint"; - const char *base = "/tmp/test_pivot_root/not_mountpoint"; - const char *new_root = "/tmp/test_pivot_root/not_mountpoint/newroot"; - const char *oldroot_abs = "/tmp/test_pivot_root/not_mountpoint/newroot/oldroot"; - - ensure_parent_tree(); - ensure_dir(base); - ensure_dir(new_root); - ensure_dir(oldroot_abs); - - if (unshare(CLONE_NEWNS) != 0) { - TEST_SKIP(name, strerror(errno)); - return; - } - - if (do_pivot_root(new_root, oldroot_abs) == -1 && errno == EINVAL) { - TEST_PASS(name); - } else { - TEST_FAIL(name, "expected EINVAL"); - } -} - -static void test_put_old_outside_new_root(void) { - const char *name = "pivot_root_put_old_outside_new_root"; - const char *base = "/tmp/test_pivot_root/put_old_outside"; - const char *new_root = "/tmp/test_pivot_root/put_old_outside/newroot"; - const char *outside = "/tmp/test_pivot_root/put_old_outside/outside"; - const char *inside = "/tmp/test_pivot_root/put_old_outside/newroot/inside"; - - ensure_parent_tree(); - ensure_dir(base); - ensure_dir(new_root); - ensure_dir(outside); - - if (unshare(CLONE_NEWNS) != 0) { - TEST_SKIP(name, strerror(errno)); - return; - } - - if (mount("", new_root, "ramfs", 0, NULL) != 0) { - TEST_SKIP(name, strerror(errno)); - return; - } - - if (mkdir(inside, 0755) != 0 && errno != EEXIST) { - cleanup_mount(new_root); - TEST_FAIL(name, "mkdir(inside) failed"); - return; - } - - if (do_pivot_root(new_root, outside) == -1 && errno == EINVAL) { - cleanup_mount(new_root); - TEST_PASS(name); - } else { - cleanup_mount(new_root); - TEST_FAIL(name, "expected EINVAL"); - } -} - -static void test_busy_target(void) { - const char *name = "pivot_root_busy_target"; - const char *base = "/tmp/test_pivot_root/busy"; - const char *new_root = "/tmp/test_pivot_root/busy/newroot"; - const char *oldroot_abs = "/tmp/test_pivot_root/busy/newroot/oldroot"; - - ensure_parent_tree(); - ensure_dir(base); - ensure_dir(new_root); - - if (unshare(CLONE_NEWNS) != 0) { - TEST_SKIP(name, strerror(errno)); - return; - } - - if (mount("", new_root, "ramfs", 0, NULL) != 0) { - TEST_SKIP(name, strerror(errno)); - return; - } - - if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { - cleanup_mount(new_root); - TEST_FAIL(name, "mkdir(oldroot) failed"); - return; - } - - if (do_pivot_root(new_root, "/") == -1 && errno == EBUSY) { - cleanup_mount(new_root); - TEST_PASS(name); - } else { - cleanup_mount(new_root); - TEST_FAIL(name, "expected EBUSY"); - } -} - -static void test_permission_failure(void) { - const char *name = "pivot_root_permission_failure"; - const char *base = "/tmp/test_pivot_root/perm"; - const char *new_root = "/tmp/test_pivot_root/perm/newroot"; - const char *oldroot_abs = "/tmp/test_pivot_root/perm/newroot/oldroot"; - - ensure_parent_tree(); - ensure_dir(base); - ensure_dir(new_root); - - if (mount("", new_root, "ramfs", 0, NULL) != 0) { - TEST_SKIP(name, strerror(errno)); - return; - } - - if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { - cleanup_mount(new_root); - TEST_FAIL(name, "mkdir(oldroot) failed"); - return; - } - - if (seteuid(65534) != 0) { - cleanup_mount(new_root); - TEST_SKIP(name, "seteuid failed"); - return; - } - - if (do_pivot_root(new_root, oldroot_abs) == -1 && errno == EPERM) { - TEST_PASS(name); - } else { - TEST_FAIL(name, "expected EPERM"); - } -} - -int main(void) { - test_success_path(); - test_dot_dot_path(); - test_dot_dot_rslave_detach(); - test_new_root_not_mountpoint(); - test_put_old_outside_new_root(); - test_busy_target(); - test_permission_failure(); - - printf("passed=%d failed=%d\n", tests_passed, tests_failed); - return tests_failed == 0 ? 0 : 1; -} diff --git a/user/apps/tests/dunitest/suites/normal/test_pivot_root.cc b/user/apps/tests/dunitest/suites/normal/test_pivot_root.cc new file mode 100644 index 0000000000..a749665526 --- /dev/null +++ b/user/apps/tests/dunitest/suites/normal/test_pivot_root.cc @@ -0,0 +1,485 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifndef SYS_pivot_root +#ifdef __NR_pivot_root +#define SYS_pivot_root __NR_pivot_root +#elif defined(__x86_64__) +#define SYS_pivot_root 155 +#else +#define SYS_pivot_root 41 +#endif +#endif + +#ifndef CLONE_NEWNS +#define CLONE_NEWNS 0x00020000 +#endif + +#ifndef MS_REC +#define MS_REC 16384 +#endif + +namespace { + +constexpr int kSkipExitCode = 77; +int g_child_status_fd = -1; + +using PivotRootCaseFn = void (*)(); + +void write_child_status(const char* kind, const char* reason) { + if (g_child_status_fd < 0) { + return; + } + + dprintf(g_child_status_fd, "%s:%s", kind, reason == nullptr ? "" : reason); +} + +[[noreturn]] void child_pass() { + _exit(0); +} + +[[noreturn]] void child_skip(const char* reason) { + write_child_status("SKIP", reason); + _exit(kSkipExitCode); +} + +[[noreturn]] void child_fail(const char* reason) { + write_child_status("FAIL", reason); + _exit(1); +} + +int ensure_dir(const char* path) { + struct stat st = {}; + if (stat(path, &st) == 0) { + return S_ISDIR(st.st_mode) ? 0 : -1; + } + return mkdir(path, 0755); +} + +void ensure_parent_tree() { + ensure_dir("/tmp"); + ensure_dir("/tmp/test_pivot_root"); +} + +void cleanup_mount(const char* path) { + umount(path); + rmdir(path); +} + +long do_pivot_root(const char* new_root, const char* put_old) { + return syscall(SYS_pivot_root, new_root, put_old); +} + +void prepare_private_mount_namespace() { + if (unshare(CLONE_NEWNS) != 0) { + child_skip(strerror(errno)); + } + + if (mount(nullptr, "/", nullptr, MS_REC | MS_PRIVATE, nullptr) != 0) { + child_skip(strerror(errno)); + } +} + +std::string read_all_from_fd(int fd) { + std::string out; + char buf[256]; + ssize_t n = 0; + + while ((n = read(fd, buf, sizeof(buf))) > 0) { + out.append(buf, static_cast(n)); + } + + return out; +} + +void expect_case_pass_or_skip(const char* case_name, PivotRootCaseFn fn) { + int pipefd[2] = {-1, -1}; + ASSERT_EQ(0, pipe(pipefd)) << case_name << ": pipe failed: errno=" << errno << " (" + << strerror(errno) << ")"; + + pid_t child = fork(); + ASSERT_GE(child, 0) << case_name << ": fork failed: errno=" << errno << " (" + << strerror(errno) << ")"; + + if (child == 0) { + close(pipefd[0]); + g_child_status_fd = pipefd[1]; + fn(); + child_fail("case returned without explicit status"); + } + + close(pipefd[1]); + + int status = 0; + ASSERT_EQ(child, waitpid(child, &status, 0)) << case_name << ": waitpid failed: errno=" + << errno << " (" << strerror(errno) << ")"; + + std::string detail = read_all_from_fd(pipefd[0]); + close(pipefd[0]); + + ASSERT_TRUE(WIFEXITED(status)) << case_name << ": child terminated abnormally"; + + const int exit_code = WEXITSTATUS(status); + if (exit_code == kSkipExitCode) { + GTEST_SKIP() << case_name << ": " << detail; + } + + EXPECT_EQ(0, exit_code) << case_name << ": " << detail; +} + +void case_success_path() { + const char* new_root = "/tmp/test_pivot_root/success/newroot"; + const char* put_old = "oldroot"; + const char* oldroot_abs = "/tmp/test_pivot_root/success/newroot/oldroot"; + const char* bin_dir = "/tmp/test_pivot_root/success/newroot/bin"; + char cwd[256] = {}; + + ensure_parent_tree(); + ensure_dir("/tmp/test_pivot_root/success"); + ensure_dir(new_root); + prepare_private_mount_namespace(); + + if (mount("", new_root, "ramfs", 0, nullptr) != 0) { + child_skip(strerror(errno)); + } + + if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + child_fail("mkdir(oldroot) failed"); + } + + if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + child_fail("mkdir(bin) failed"); + } + + if (chdir(new_root) != 0) { + cleanup_mount(new_root); + child_fail("chdir(new_root) failed"); + } + + if (do_pivot_root(".", put_old) != 0) { + child_fail(strerror(errno)); + } + + if (getcwd(cwd, sizeof(cwd)) == nullptr) { + child_fail("getcwd failed after pivot"); + } + + if (strcmp(cwd, "/") != 0) { + child_fail("cwd is not / after pivot"); + } + + if (access("/oldroot", F_OK) != 0) { + child_fail("old root not reachable under /oldroot"); + } + + if (access("/bin", F_OK) != 0) { + child_fail("new root is not visible via absolute path"); + } + + child_pass(); +} + +void case_dot_dot_path() { + const char* new_root = "/tmp/test_pivot_root/dotdot/newroot"; + const char* bin_dir = "/tmp/test_pivot_root/dotdot/newroot/bin"; + int oldroot_fd = -1; + int newroot_fd = -1; + char cwd[256] = {}; + + ensure_parent_tree(); + ensure_dir("/tmp/test_pivot_root/dotdot"); + ensure_dir(new_root); + prepare_private_mount_namespace(); + + if (mount("", new_root, "ramfs", 0, nullptr) != 0) { + child_skip(strerror(errno)); + } + + if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + child_fail("mkdir(bin) failed"); + } + + oldroot_fd = open("/", O_DIRECTORY | O_RDONLY); + if (oldroot_fd < 0) { + cleanup_mount(new_root); + child_fail("open(oldroot) failed"); + } + + newroot_fd = open(new_root, O_DIRECTORY | O_RDONLY); + if (newroot_fd < 0) { + close(oldroot_fd); + cleanup_mount(new_root); + child_fail("open(newroot) failed"); + } + + if (fchdir(newroot_fd) != 0) { + close(newroot_fd); + close(oldroot_fd); + cleanup_mount(new_root); + child_fail("fchdir(newroot) failed"); + } + + if (do_pivot_root(".", ".") != 0) { + close(newroot_fd); + close(oldroot_fd); + child_fail(strerror(errno)); + } + + if (fchdir(oldroot_fd) != 0) { + close(newroot_fd); + close(oldroot_fd); + child_fail("fchdir(oldroot) failed after pivot"); + } + + if (umount2(".", MNT_DETACH) != 0) { + close(newroot_fd); + close(oldroot_fd); + child_fail("umount2(oldroot) failed"); + } + + if (chdir("/") != 0) { + close(newroot_fd); + close(oldroot_fd); + child_fail("chdir(/) failed after detach"); + } + + if (getcwd(cwd, sizeof(cwd)) == nullptr || strcmp(cwd, "/") != 0) { + close(newroot_fd); + close(oldroot_fd); + child_fail("cwd is not / after dot-dot pivot"); + } + + if (access("/bin", F_OK) != 0) { + close(newroot_fd); + close(oldroot_fd); + child_fail("new root is not visible after dot-dot pivot"); + } + + close(newroot_fd); + close(oldroot_fd); + child_pass(); +} + +void case_dot_dot_rslave_detach() { + const char* new_root = "/tmp/test_pivot_root/dotdot_rslave/newroot"; + const char* bin_dir = "/tmp/test_pivot_root/dotdot_rslave/newroot/bin"; + int oldroot_fd = -1; + int newroot_fd = -1; + + ensure_parent_tree(); + ensure_dir("/tmp/test_pivot_root/dotdot_rslave"); + ensure_dir(new_root); + prepare_private_mount_namespace(); + + if (mount("", new_root, "ramfs", 0, nullptr) != 0) { + child_skip(strerror(errno)); + } + + if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + child_fail("mkdir(bin) failed"); + } + + oldroot_fd = open("/", O_DIRECTORY | O_RDONLY); + if (oldroot_fd < 0) { + cleanup_mount(new_root); + child_fail("open(oldroot) failed"); + } + + newroot_fd = open(new_root, O_DIRECTORY | O_RDONLY); + if (newroot_fd < 0) { + close(oldroot_fd); + cleanup_mount(new_root); + child_fail("open(newroot) failed"); + } + + if (fchdir(newroot_fd) != 0) { + close(newroot_fd); + close(oldroot_fd); + cleanup_mount(new_root); + child_fail("fchdir(newroot) failed"); + } + + if (do_pivot_root(".", ".") != 0) { + close(newroot_fd); + close(oldroot_fd); + child_fail(strerror(errno)); + } + + if (fchdir(oldroot_fd) != 0) { + close(newroot_fd); + close(oldroot_fd); + child_fail("fchdir(oldroot) failed after pivot"); + } + + if (mount(nullptr, ".", nullptr, MS_REC | MS_SLAVE, nullptr) != 0) { + close(newroot_fd); + close(oldroot_fd); + child_fail("mount(make-rslave) failed"); + } + + if (umount2(".", MNT_DETACH) != 0) { + close(newroot_fd); + close(oldroot_fd); + child_fail("umount2(oldroot) failed after make-rslave"); + } + + close(newroot_fd); + close(oldroot_fd); + child_pass(); +} + +void case_new_root_not_mountpoint() { + const char* new_root = "/tmp/test_pivot_root/not_mountpoint/newroot"; + const char* oldroot_abs = "/tmp/test_pivot_root/not_mountpoint/newroot/oldroot"; + + ensure_parent_tree(); + ensure_dir("/tmp/test_pivot_root/not_mountpoint"); + ensure_dir(new_root); + ensure_dir(oldroot_abs); + prepare_private_mount_namespace(); + + if (do_pivot_root(new_root, oldroot_abs) == -1 && errno == EINVAL) { + child_pass(); + } + + child_fail("expected EINVAL"); +} + +void case_put_old_outside_new_root() { + const char* new_root = "/tmp/test_pivot_root/put_old_outside/newroot"; + const char* outside = "/tmp/test_pivot_root/put_old_outside/outside"; + const char* inside = "/tmp/test_pivot_root/put_old_outside/newroot/inside"; + + ensure_parent_tree(); + ensure_dir("/tmp/test_pivot_root/put_old_outside"); + ensure_dir(new_root); + ensure_dir(outside); + prepare_private_mount_namespace(); + + if (mount("", new_root, "ramfs", 0, nullptr) != 0) { + child_skip(strerror(errno)); + } + + if (mkdir(inside, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + child_fail("mkdir(inside) failed"); + } + + if (do_pivot_root(new_root, outside) == -1 && errno == EINVAL) { + cleanup_mount(new_root); + child_pass(); + } + + cleanup_mount(new_root); + child_fail("expected EINVAL"); +} + +void case_busy_target() { + const char* new_root = "/tmp/test_pivot_root/busy/newroot"; + const char* oldroot_abs = "/tmp/test_pivot_root/busy/newroot/oldroot"; + + ensure_parent_tree(); + ensure_dir("/tmp/test_pivot_root/busy"); + ensure_dir(new_root); + prepare_private_mount_namespace(); + + if (mount("", new_root, "ramfs", 0, nullptr) != 0) { + child_skip(strerror(errno)); + } + + if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + child_fail("mkdir(oldroot) failed"); + } + + if (do_pivot_root(new_root, "/") == -1 && errno == EBUSY) { + cleanup_mount(new_root); + child_pass(); + } + + cleanup_mount(new_root); + child_fail("expected EBUSY"); +} + +void case_permission_failure() { + const char* new_root = "/tmp/test_pivot_root/perm/newroot"; + const char* oldroot_abs = "/tmp/test_pivot_root/perm/newroot/oldroot"; + + ensure_parent_tree(); + ensure_dir("/tmp/test_pivot_root/perm"); + ensure_dir(new_root); + prepare_private_mount_namespace(); + + if (mount("", new_root, "ramfs", 0, nullptr) != 0) { + child_skip(strerror(errno)); + } + + if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { + cleanup_mount(new_root); + child_fail("mkdir(oldroot) failed"); + } + + if (seteuid(65534) != 0) { + cleanup_mount(new_root); + child_skip("seteuid failed"); + } + + if (do_pivot_root(new_root, oldroot_abs) == -1 && errno == EPERM) { + child_pass(); + } + + child_fail("expected EPERM"); +} + +TEST(PivotRoot, SuccessPath) { + expect_case_pass_or_skip("pivot_root_success", case_success_path); +} + +TEST(PivotRoot, DotDotPath) { + expect_case_pass_or_skip("pivot_root_dot_dot", case_dot_dot_path); +} + +TEST(PivotRoot, DotDotRslaveDetach) { + expect_case_pass_or_skip("pivot_root_dot_dot_rslave_detach", case_dot_dot_rslave_detach); +} + +TEST(PivotRoot, NewRootNotMountpoint) { + expect_case_pass_or_skip("pivot_root_new_root_not_mountpoint", case_new_root_not_mountpoint); +} + +TEST(PivotRoot, PutOldOutsideNewRoot) { + expect_case_pass_or_skip("pivot_root_put_old_outside_new_root", case_put_old_outside_new_root); +} + +TEST(PivotRoot, BusyTarget) { + expect_case_pass_or_skip("pivot_root_busy_target", case_busy_target); +} + +TEST(PivotRoot, PermissionFailure) { + expect_case_pass_or_skip("pivot_root_permission_failure", case_permission_failure); +} + +} // namespace + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/user/apps/tests/dunitest/whitelist.txt b/user/apps/tests/dunitest/whitelist.txt index 08988551f3..356f35b809 100644 --- a/user/apps/tests/dunitest/whitelist.txt +++ b/user/apps/tests/dunitest/whitelist.txt @@ -4,5 +4,6 @@ demo/gtest_demo normal/capability normal/fcntl_lock normal/epoll_timeout_budget +normal/test_pivot_root fuse/fuse_core fuse/fuse_extended