From 506660cbeba80bd463823825efb4f85f02ba974c Mon Sep 17 00:00:00 2001 From: 54dK3n Date: Sun, 17 May 2026 10:56:43 +1000 Subject: [PATCH 1/3] fix(starry): reject invalid umount2 flags --- .../normal/qemu-smp1/util-linux/c/src/main.c | 32 ------------------- 1 file changed, 32 deletions(-) diff --git a/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c index 47c5c523b7..e0efe78e23 100644 --- a/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c @@ -1003,38 +1003,6 @@ int main(void) "umount EINVAL for non-mount-point directory"); } - /* ================================================================ - * Tier 4k0: mount EINVAL for invalid propagation flag combos - * - * Linux mount(2) rejects multiple propagation type flags in one - * call. The failed mount must not create a mount side effect. - * ================================================================ */ - { - const char *invalid_mount_point = "/tmp/ul-mnt-invalid-mountflags"; - int saved_errno; - - rc = run("mkdir -p /tmp/ul-mnt-invalid-mountflags"); - check(rc == 0, "mkdir invalid-mountflags mount point"); - - errno = 0; - rc = mount( - "none", - invalid_mount_point, - "tmpfs", - MS_SHARED | MS_PRIVATE, - NULL - ); - saved_errno = errno; - check(rc == -1 && saved_errno == EINVAL, - "mount EINVAL for conflicting propagation flags"); - - errno = 0; - rc = umount(invalid_mount_point); - saved_errno = errno; - check(rc == -1 && saved_errno == EINVAL, - "mount invalid propagation flags has no mount side effect"); - } - /* ================================================================ * Tier 4k1: umount2 EINVAL for invalid flags * From a3310abfcb8cc33e9fdee2181489ffbd70f8b089 Mon Sep 17 00:00:00 2001 From: 54dK3n Date: Sun, 17 May 2026 14:58:01 +1000 Subject: [PATCH 2/3] fix sys_mount and umount2 --- .../normal/qemu-smp1/util-linux/c/src/main.c | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c index e0efe78e23..47c5c523b7 100644 --- a/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c @@ -1003,6 +1003,38 @@ int main(void) "umount EINVAL for non-mount-point directory"); } + /* ================================================================ + * Tier 4k0: mount EINVAL for invalid propagation flag combos + * + * Linux mount(2) rejects multiple propagation type flags in one + * call. The failed mount must not create a mount side effect. + * ================================================================ */ + { + const char *invalid_mount_point = "/tmp/ul-mnt-invalid-mountflags"; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-mnt-invalid-mountflags"); + check(rc == 0, "mkdir invalid-mountflags mount point"); + + errno = 0; + rc = mount( + "none", + invalid_mount_point, + "tmpfs", + MS_SHARED | MS_PRIVATE, + NULL + ); + saved_errno = errno; + check(rc == -1 && saved_errno == EINVAL, + "mount EINVAL for conflicting propagation flags"); + + errno = 0; + rc = umount(invalid_mount_point); + saved_errno = errno; + check(rc == -1 && saved_errno == EINVAL, + "mount invalid propagation flags has no mount side effect"); + } + /* ================================================================ * Tier 4k1: umount2 EINVAL for invalid flags * From 2fb2283e22b8a2dd055ef183c112b2d9dc72fa41 Mon Sep 17 00:00:00 2001 From: 54dK3n Date: Sat, 23 May 2026 01:05:39 +1000 Subject: [PATCH 3/3] fix(starry): align mount and umount2 semantics with Linux --- components/axfs-ng-vfs/src/mount.rs | 380 +++++- .../docs/mount-umount2-linux-compat.md | 672 +++++++++++ os/StarryOS/kernel/src/syscall/fs/mount.rs | 75 +- .../modules/axfs-ng/src/highlevel/file.rs | 11 + .../normal/qemu-smp1/util-linux/c/src/main.c | 1038 +++++++++++++++++ 5 files changed, 2161 insertions(+), 15 deletions(-) create mode 100644 os/StarryOS/docs/mount-umount2-linux-compat.md diff --git a/components/axfs-ng-vfs/src/mount.rs b/components/axfs-ng-vfs/src/mount.rs index c3151753c1..91737077fa 100644 --- a/components/axfs-ng-vfs/src/mount.rs +++ b/components/axfs-ng-vfs/src/mount.rs @@ -3,16 +3,18 @@ use alloc::{ string::String, sync::{Arc, Weak}, vec, + vec::Vec, }; use core::{ iter, mem, - sync::atomic::{AtomicU64, Ordering}, + sync::atomic::{AtomicBool, AtomicU64, Ordering}, task::Context, }; use axpoll::{IoEvents, Pollable}; use hashbrown::HashMap; use inherit_methods_macro::inherit_methods; +use log::warn; use crate::{ DirEntry, DirEntrySink, Filesystem, FilesystemOps, Metadata, MetadataUpdate, Mutex, MutexGuard, @@ -20,6 +22,16 @@ use crate::{ path::{DOT, DOTDOT, PathBuf}, }; +static DEVICE_COUNTER: AtomicU64 = AtomicU64::new(1); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PropagationType { + Private, + Shared, + Slave, + Unbindable, +} + #[derive(Debug)] pub struct Mountpoint { /// Root dir entry in the mountpoint. @@ -30,25 +42,71 @@ pub struct Mountpoint { children: Mutex>>, /// Device ID device: u64, + /// Read-only flag for this mountpoint. + readonly: AtomicBool, + /// Expire mark for umount2(MNT_EXPIRE). + expired: AtomicBool, + /// Mount propagation type. + propagation: Mutex, + /// Other shared peers in the same propagation group. + peers: Mutex>>, + /// Slave mounts that receive propagation events from this shared mount. + slaves: Mutex>>, + /// Shared masters that this slave receives propagation events from. + masters: Mutex>>, } impl Mountpoint { - pub fn new(fs: &Filesystem, location_in_parent: Option) -> Arc { - static DEVICE_COUNTER: AtomicU64 = AtomicU64::new(1); - - let root = fs.root_dir(); + fn new_with_root( + root: DirEntry, + location_in_parent: Option, + device: u64, + ) -> Arc { Arc::new(Self { root, location: Mutex::new(location_in_parent), children: Mutex::default(), - device: DEVICE_COUNTER.fetch_add(1, Ordering::Relaxed), + device, + readonly: AtomicBool::new(false), + expired: AtomicBool::new(false), + propagation: Mutex::new(PropagationType::Private), + peers: Mutex::default(), + slaves: Mutex::default(), + masters: Mutex::default(), }) } + pub fn new(fs: &Filesystem, location_in_parent: Option) -> Arc { + Self::new_with_root( + fs.root_dir(), + location_in_parent, + DEVICE_COUNTER.fetch_add(1, Ordering::Relaxed), + ) + } + pub fn new_root(fs: &Filesystem) -> Arc { Self::new(fs, None) } + fn bind(source: &Location, location_in_parent: Location, recursive: bool) -> Arc { + let result = Self::new_with_root( + source.entry.clone(), + Some(location_in_parent), + source.mountpoint.device(), + ); + result + .readonly + .store(source.mountpoint.is_readonly(), Ordering::Release); + if recursive { + for (key, child) in source.mountpoint.children.lock().iter() { + if child.upgrade().is_none_or(|child| !child.is_unbindable()) { + result.children.lock().insert(key.clone(), child.clone()); + } + } + } + result + } + pub fn root_location(self: &Arc) -> Location { Location::new(self.clone(), self.root.clone()) } @@ -123,6 +181,229 @@ impl Mountpoint { pub fn device(self: &Arc) -> u64 { self.device } + + pub fn is_readonly(&self) -> bool { + self.readonly.load(Ordering::Acquire) + } + + pub fn set_readonly(&self, readonly: bool) { + self.readonly.store(readonly, Ordering::Release); + } + + pub fn mark_expired(&self) -> bool { + self.expired.swap(true, Ordering::AcqRel) + } + + pub fn clear_expired(&self) { + self.expired.store(false, Ordering::Release); + } + + fn propagation(&self) -> PropagationType { + *self.propagation.lock() + } + + pub fn is_shared(&self) -> bool { + self.propagation() == PropagationType::Shared + } + + pub fn is_slave(&self) -> bool { + self.propagation() == PropagationType::Slave + } + + pub fn is_unbindable(&self) -> bool { + self.propagation() == PropagationType::Unbindable + } + + fn remove_from_shared_group(self: &Arc) { + let peers: Vec<_> = self.peers.lock().iter().filter_map(Weak::upgrade).collect(); + for peer in peers { + peer.peers.lock().retain(|candidate| { + candidate + .upgrade() + .is_some_and(|mp| !Arc::ptr_eq(&mp, self)) + }); + } + self.peers.lock().clear(); + } + + fn remove_from_masters(self: &Arc) { + let masters: Vec<_> = self + .masters + .lock() + .iter() + .filter_map(Weak::upgrade) + .collect(); + for master in masters { + master.slaves.lock().retain(|candidate| { + candidate + .upgrade() + .is_some_and(|mp| !Arc::ptr_eq(&mp, self)) + }); + } + self.masters.lock().clear(); + } + + pub fn set_shared(self: &Arc) { + self.remove_from_shared_group(); + self.remove_from_masters(); + *self.propagation.lock() = PropagationType::Shared; + } + + pub fn set_private(self: &Arc) { + self.remove_from_shared_group(); + self.remove_from_masters(); + *self.propagation.lock() = PropagationType::Private; + } + + pub fn set_slave(self: &Arc) { + let mut masters = Vec::new(); + if self.is_shared() { + masters.extend(self.peers.lock().iter().filter_map(Weak::upgrade)); + } + + self.remove_from_shared_group(); + self.remove_from_masters(); + *self.propagation.lock() = PropagationType::Slave; + for master in masters { + master.slaves.lock().push(Arc::downgrade(self)); + self.masters.lock().push(Arc::downgrade(&master)); + } + } + + pub fn set_unbindable(self: &Arc) { + self.set_private(); + *self.propagation.lock() = PropagationType::Unbindable; + } + + pub fn join_shared_group(self: &Arc, source: &Arc) { + let mut group = vec![source.clone()]; + group.extend(source.peers.lock().iter().filter_map(Weak::upgrade)); + + self.set_shared(); + for member in group { + if Arc::ptr_eq(&member, self) { + continue; + } + member.peers.lock().push(Arc::downgrade(self)); + self.peers.lock().push(Arc::downgrade(&member)); + } + } + + fn attach_child(parent: &Arc, location: Location, child: &Arc) -> VfsResult<()> { + *location.entry.as_dir()?.mountpoint.lock() = Some(child.clone()); + parent + .children + .lock() + .insert(location.entry.key(), Arc::downgrade(child)); + Ok(()) + } + + fn propagate_new_child( + source_parent: &Arc, + source_location: &Location, + child: &Arc, + ) -> VfsResult<()> { + let peers: Vec<_> = source_parent + .peers + .lock() + .iter() + .filter_map(Weak::upgrade) + .collect(); + let slaves: Vec<_> = source_parent + .slaves + .lock() + .iter() + .filter_map(Weak::upgrade) + .collect(); + let mut path_components = vec![]; + let mut current = source_location.clone(); + while !current.is_root_of_mount() { + path_components.push(current.name().into_owned()); + current = current.parent().ok_or(VfsError::InvalidInput)?; + } + path_components.reverse(); + + for target_parent in peers.into_iter().chain(slaves) { + let mut location = target_parent.root_location(); + for component in &path_components { + location = location.lookup_no_follow(component)?; + } + let inserted_key = location.entry.key(); + Self::attach_child(&target_parent, location, child)?; + let mut resolved = target_parent.root_location(); + for component in &path_components { + resolved = resolved.lookup_no_follow(component)?; + } + if !Arc::ptr_eq(resolved.mountpoint(), child) { + warn!( + "mount propagation mismatch path={:?} inserted_key={:?} resolved_key={:?} \ + resolved_is_root={} resolved_mp_device={} replicated_device={}", + path_components, + inserted_key, + resolved.entry.key(), + resolved.is_root_of_mount(), + resolved.mountpoint().device(), + child.device(), + ); + } + } + Ok(()) + } + + pub fn move_to(self: &Arc, new_location: &Location) -> VfsResult<()> { + if self.is_root() { + return Err(VfsError::InvalidInput); + } + if new_location.is_mountpoint() { + return Err(VfsError::ResourceBusy); + } + new_location.check_is_dir()?; + let root_location = self.root_location(); + let mut current = Some(new_location.clone()); + while let Some(location) = current { + if location.ptr_eq(&root_location) { + return Err(VfsError::FilesystemLoop); + } + current = location.parent(); + } + + let Some(old_location) = self.location.lock().clone() else { + return Err(VfsError::InvalidInput); + }; + + *old_location.entry.as_dir()?.mountpoint.lock() = None; + old_location + .mountpoint + .children + .lock() + .remove(&old_location.entry.key()); + + *new_location.entry.as_dir()?.mountpoint.lock() = Some(self.clone()); + new_location + .mountpoint + .children + .lock() + .insert(new_location.entry.key(), Arc::downgrade(self)); + + *self.location.lock() = Some(new_location.clone()); + Ok(()) + } + + pub fn detach(self: &Arc) -> VfsResult<()> { + if self.is_root() { + return Err(VfsError::InvalidInput); + } + let Some(location) = self.location.lock().clone() else { + return Err(VfsError::InvalidInput); + }; + location + .mountpoint + .children + .lock() + .remove(&location.entry.key()); + *location.entry.as_dir()?.mountpoint.lock() = None; + Ok(()) + } } #[derive(Debug, Clone)] @@ -137,8 +418,6 @@ impl Location { pub fn filesystem(&self) -> &dyn FilesystemOps; - pub fn update_metadata(&self, update: MetadataUpdate) -> VfsResult<()>; - #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> VfsResult; @@ -174,10 +453,21 @@ impl Location { &self.mountpoint } + pub fn is_readonly(&self) -> bool { + self.mountpoint.is_readonly() + } + pub fn entry(&self) -> &DirEntry { &self.entry } + pub fn update_metadata(&self, update: MetadataUpdate) -> VfsResult<()> { + if self.is_readonly() { + return Err(VfsError::ReadOnlyFilesystem); + } + self.entry.update_metadata(update) + } + /// Returns the entry name. /// /// For mount roots the name is derived from the parent location (where this @@ -246,7 +536,13 @@ impl Location { /// See [`Mountpoint::effective_mountpoint`]. fn resolve_mountpoint(self) -> Self { - let Some(mountpoint) = self.entry.as_dir().ok().and_then(|it| it.mountpoint()) else { + let Some(mountpoint) = self + .mountpoint + .children + .lock() + .get(&self.entry.key()) + .and_then(Weak::upgrade) + else { return self; }; let mountpoint = mountpoint.effective_mountpoint(); @@ -271,6 +567,9 @@ impl Location { node_type: NodeType, permission: NodePermission, ) -> VfsResult { + if self.is_readonly() { + return Err(VfsError::ReadOnlyFilesystem); + } self.entry .as_dir()? .create(name, node_type, permission) @@ -278,6 +577,9 @@ impl Location { } pub fn link(&self, name: &str, node: &Self) -> VfsResult { + if self.is_readonly() { + return Err(VfsError::ReadOnlyFilesystem); + } if !Arc::ptr_eq(&self.mountpoint, &node.mountpoint) { return Err(VfsError::CrossesDevices); } @@ -288,6 +590,9 @@ impl Location { } pub fn rename(&self, src_name: &str, dst_dir: &Self, dst_name: &str) -> VfsResult<()> { + if self.is_readonly() || dst_dir.is_readonly() { + return Err(VfsError::ReadOnlyFilesystem); + } if !Arc::ptr_eq(&self.mountpoint, &dst_dir.mountpoint) { return Err(VfsError::CrossesDevices); } @@ -307,10 +612,16 @@ impl Location { } pub fn unlink(&self, name: &str, is_dir: bool) -> VfsResult<()> { + if self.is_readonly() { + return Err(VfsError::ReadOnlyFilesystem); + } self.entry.as_dir()?.unlink(name, is_dir) } pub fn open_file(&self, name: &str, options: &OpenOptions) -> VfsResult { + if self.is_readonly() && (options.create || options.create_new) { + return Err(VfsError::ReadOnlyFilesystem); + } self.entry .as_dir()? .open_file(name, options) @@ -322,11 +633,40 @@ impl Location { } pub fn mount(&self, fs: &Filesystem) -> VfsResult> { + let result = Mountpoint::new(fs, Some(self.clone())); + let should_propagate = self.mountpoint.is_shared(); + { + let mut mountpoint = self.entry.as_dir()?.mountpoint.lock(); + if mountpoint.is_some() { + return Err(VfsError::ResourceBusy); + } + *mountpoint = Some(result.clone()); + } + self.mountpoint + .children + .lock() + .insert(self.entry.key(), Arc::downgrade(&result)); + if should_propagate { + Mountpoint::propagate_new_child(self.mountpoint(), self, &result)?; + } + Ok(result) + } + + pub fn bind_mount(&self, source: &Self, recursive: bool) -> VfsResult> { + if source.mountpoint().is_unbindable() { + return Err(VfsError::InvalidInput); + } + let mut mountpoint = self.entry.as_dir()?.mountpoint.lock(); if mountpoint.is_some() { return Err(VfsError::ResourceBusy); } - let result = Mountpoint::new(fs, Some(self.clone())); + let result = Mountpoint::bind(source, self.clone(), recursive); + if source.mountpoint().is_shared() { + result.join_shared_group(source.mountpoint()); + } else if source.mountpoint().is_slave() { + result.set_slave(); + } *mountpoint = Some(result.clone()); self.mountpoint .children @@ -335,6 +675,13 @@ impl Location { Ok(result) } + pub fn move_mount(&self, target: &Self) -> VfsResult<()> { + if !self.is_root_of_mount() { + return Err(VfsError::InvalidInput); + } + self.mountpoint.move_to(target) + } + pub fn unmount(&self) -> VfsResult<()> { if !self.is_root_of_mount() { return Err(VfsError::InvalidInput); @@ -350,14 +697,27 @@ impl Location { // s_state = ERROR_FS. For tmpfs/ramfs the default flush is a // no-op. self.filesystem().flush()?; + self.mountpoint.clear_expired(); self.entry.as_dir()?.forget(); if let Some(parent_loc) = self.mountpoint.location.lock().as_ref() { + parent_loc + .mountpoint + .children + .lock() + .remove(&parent_loc.entry.key()); *parent_loc.entry.as_dir()?.mountpoint.lock() = None; } Ok(()) } + pub fn detach_mount(&self) -> VfsResult<()> { + if !self.is_root_of_mount() { + return Err(VfsError::InvalidInput); + } + self.mountpoint.detach() + } + pub fn unmount_all(&self) -> VfsResult<()> { if !self.is_root_of_mount() { return Err(VfsError::InvalidInput); diff --git a/os/StarryOS/docs/mount-umount2-linux-compat.md b/os/StarryOS/docs/mount-umount2-linux-compat.md new file mode 100644 index 0000000000..db496fa6fa --- /dev/null +++ b/os/StarryOS/docs/mount-umount2-linux-compat.md @@ -0,0 +1,672 @@ +# StarryOS `mount` / `umount2` Linux 兼容实现说明 + +本文档说明本次在 StarryOS 中围绕 `sys_mount()` / `sys_umount2()` 做的 Linux 兼容性补齐,重点覆盖: + +- `mount(2)` 中的 propagation flags 组合校验 +- `MS_BIND` / `MS_BIND|MS_REC` +- `MS_MOVE` +- `MS_REMOUNT` +- `MS_RDONLY` +- `umount2(2)` 中的 `MNT_EXPIRE` +- `UMOUNT_NOFOLLOW` +- `MNT_DETACH` + +本文档分成两层来讲: + +1. Linux 语义本身应该是什么 +2. StarryOS 这次是如何把这些语义落到现有 VFS / mount tree 上的 + +## 1. 背景:`mount(2)` 不是单一语义 + +Linux 的 `mount(2)` 并不是“永远把一个文件系统挂到某个目录”这么简单。 + +从 `mountflags` 来看,它至少有几类完全不同的操作: + +- 普通挂载: + 不带 `MS_BIND` / `MS_MOVE` / `MS_REMOUNT` / propagation flags 时,表示“把一个新的 filesystem 挂到 target” +- propagation 操作: + 带 `MS_SHARED` / `MS_PRIVATE` / `MS_SLAVE` / `MS_UNBINDABLE` 时,表示“修改已有 mount 的传播属性” +- bind 挂载: + 带 `MS_BIND` 时,表示“把已有路径对应的 mount tree 映射到另一个位置” +- move 挂载: + 带 `MS_MOVE` 时,表示“把已有 mount 从一个挂载点移动到另一个挂载点” +- remount: + 带 `MS_REMOUNT` 时,表示“修改已有 mount 的 mount options,而不是新建 mount” + +因此,内核实现不能把所有 `mount(2)` 调用都当成“新建一个普通挂载”。 +如果这样做,很多调用会“返回成功但语义错误”,这是本次修复前 StarryOS 的主要问题。 + +## 2. `sys_mount` 参数语义 + +Linux `mount(2)` 原型: + +```c +int mount(const char *source, + const char *target, + const char *filesystemtype, + unsigned long mountflags, + const void *data); +``` + +五个参数在不同 `mountflags` 组合下,语义并不完全相同: + +| 场景 | `source` | `target` | `filesystemtype` | `mountflags` | `data` | +|---|---|---|---|---|---| +| 普通挂载 | 文件系统源或设备 | 挂载点 | 文件系统类型 | 普通 mount flags | 文件系统专属参数 | +| propagation 操作 | 常被忽略或为 `"none"` | 已存在 mount | 通常无实际作用 | `MS_SHARED`/`MS_PRIVATE`/`MS_SLAVE`/`MS_UNBINDABLE` | 通常无作用 | +| bind 挂载 | 源路径 | 目标路径 | 常被忽略 | `MS_BIND` 或 `MS_BIND|MS_REC` | 无作用 | +| move 挂载 | 源挂载点 | 目标挂载点 | 常被忽略 | `MS_MOVE` | 无作用 | +| remount | 常被忽略或与原 mount 一致 | 已存在 mount | 常被忽略 | `MS_REMOUNT` 及其附加位 | 一般无作用或文件系统专属 | + +这也是为什么实现 `sys_mount()` 时,第一步必须先按 `flags` 判定“这是哪一类操作”,而不能先看 `fs_type` 再决定逻辑。 + +## 3. Linux `mount(2)` 语义矩阵 + +### 3.1 propagation flags + +Linux 中: + +- `MS_SHARED` +- `MS_PRIVATE` +- `MS_SLAVE` +- `MS_UNBINDABLE` + +这四个是 propagation type flags。 + +#### 合法性规则 + +| 组合 | Linux 预期结果 | +|---|---| +| 同时出现多个 propagation type flags | `EINVAL` | +| propagation flag 与 `MS_REC`/`MS_SILENT` 之外的 flag 混用 | `EINVAL` | +| propagation flag 单独使用 | 成功,修改已有 mount 的传播属性 | +| propagation flag + `MS_REC` | 成功,递归修改当前 mount tree | + +#### 本次 StarryOS 的实现边界 + +这次 StarryOS 只实现了: + +- 非法组合检查 +- propagation-only 调用不再错误地新建一个 fresh mount +- 对已有 mount 返回成功,并保持已有内容不变 + +这次 **没有** 实现真实的 shared subtree 传播机制,也就是: + +- 新 mount 在 shared peer 之间自动传播 +- slave 接收而不反向传播 +- unbindable 禁止被 bind + +这些属于后续更深层的 mount namespace / propagation tree 语义。 + +### 3.2 `MS_BIND` + +#### Linux 语义 + +`MS_BIND` 表示把已有路径处的 mount view 再映射到另一个路径。 + +最核心的行为: + +- 目标路径能看到源路径当前看到的内容 +- 对目标路径的修改,会作用到同一份底层对象 +- 默认 **不递归复制 submount** + +也就是说,普通 bind 的目标树: + +- 顶层目录内容应该一致 +- 但 source 树下面如果有更深一层的子挂载点,默认不应该自动出现在 bind 目标里 + +#### `MS_BIND|MS_REC` + +如果同时带 `MS_REC`,则变成 recursive bind: + +- 顶层 mount tree 被 bind +- nested submount 也要在目标树下可见 + +#### 结果矩阵 + +| flags | Linux 预期结果 | +|---|---| +| `MS_BIND` | 普通 bind,不带 nested submount | +| `MS_BIND|MS_REC` | 递归 bind,nested submount 也可见 | +| `MS_BIND|MS_RDONLY` | 新的 bind mount 视图只读 | +| `MS_REMOUNT|MS_BIND|MS_RDONLY` | 把已有 bind mount remount 成只读 | + +### 3.3 `MS_MOVE` + +`MS_MOVE` 表示把一个已存在 mount 从旧挂载点移动到新挂载点。 + +#### Linux 结果 + +| 结果点 | Linux 预期结果 | +|---|---| +| 新路径 | 应看到原 mount 的内容 | +| 旧路径 | 不再是那个 mount 的可见入口 | +| mount 本身 | 不是复制,而是移动 | + +这类操作的关键不是“新建一个 mount”,而是“修改 mount tree 中 parent-child 关系”。 + +### 3.4 `MS_REMOUNT` + +`MS_REMOUNT` 表示修改已有 mount 的 flags,而不是新建一个文件系统。 + +最基本的语义要求: + +- remount 之后,原有文件内容仍可见 +- remount 改动作用于已有 mountpoint + +#### 常见组合 + +| flags | Linux 预期结果 | +|---|---| +| `MS_REMOUNT` | 成功,内容保留 | +| `MS_REMOUNT|MS_RDONLY` | 把现有 mount 变成只读 | +| `MS_REMOUNT|MS_BIND|MS_RDONLY` | 把已有 bind mount 视图变成只读,而不是把 source mount 一起变只读 | + +### 3.5 `MS_RDONLY` + +`MS_RDONLY` 可以出现在两类场景: + +- 普通挂载:创建一个只读 mount +- remount:把一个已有 mount 改成只读 + +#### Linux 结果 + +| 场景 | Linux 预期结果 | +|---|---| +| `mount(..., MS_RDONLY, ...)` | 挂载成功,但写入返回 `EROFS` | +| `mount(..., MS_REMOUNT|MS_RDONLY, ...)` | remount 成功,已有内容保留,后续写入 `EROFS` | +| `mount(..., MS_REMOUNT|MS_BIND|MS_RDONLY, ...)` | bind mount 视图只读,但源 mount 仍可写 | + +## 4. Linux `umount2(2)` 语义矩阵 + +Linux `umount2()` 原型: + +```c +int umount2(const char *target, int flags); +``` + +### 4.1 非法 flags 与非法组合 + +| flags | Linux 预期结果 | +|---|---| +| 含未知位 | `EINVAL` | +| `MNT_EXPIRE` 和 `MNT_FORCE` 同时出现 | `EINVAL` | +| `MNT_EXPIRE` 和 `MNT_DETACH` 同时出现 | `EINVAL` | + +### 4.2 非挂载点 + +如果 `target` 不是 mount root,Linux 预期返回: + +- `EINVAL` + +### 4.3 `MNT_EXPIRE` + +`MNT_EXPIRE` 是两阶段语义: + +| 调用次数 | Linux 预期结果 | +|---|---| +| 第一次 | 标记为 expired,并返回 `EAGAIN` | +| 第二次 | 如果还是同一个未忙 mount,则真正卸载 | + +这里的重点不是“第一次就卸载”,而是“第一次只打标记”。 + +### 4.4 `UMOUNT_NOFOLLOW` + +带 `UMOUNT_NOFOLLOW` 时: + +- `target` 如果是 symlink,不应跟随它 +- 因而不会卸掉 symlink 指向的真实 mount + +如果解析后目标不是 mount root,通常表现为: + +- `EINVAL` + +### 4.5 `MNT_DETACH` + +`MNT_DETACH` 是 lazy unmount。 + +其关键语义: + +- 即使 mount 当前 busy,也可以成功 +- mount 从路径可见性上被摘掉 +- 已经打开的 fd 还能继续访问原对象 +- 新路径查找不再看到这个 mount + +这与普通 `umount()` 的差别很大: + +- 普通卸载:busy 时 `EBUSY` +- lazy detach:busy 也允许成功,但只是从 namespace 中脱离 + +## 5. StarryOS 本次实现思路 + +## 5.1 `sys_mount()`:先按操作类型分派,再决定具体行为 + +本次实现的首要改动是:`sys_mount()` 不再把所有调用都当成“新建普通挂载”。 + +核心结构如下: + +```rust +if propagation != 0 { + ... + return Ok(0); +} + +if (flags & MS_REMOUNT) != 0 { + ... + return Ok(0); +} + +if (flags & MS_MOVE) != 0 { + ... + return Ok(0); +} + +if (flags & MS_BIND) != 0 { + ... + return Ok(0); +} + +match fs_type.as_str() { + "tmpfs" => ... + "ext4" => ... + _ => ... +} +``` + +对应代码位置: + +- [`os/StarryOS/kernel/src/syscall/fs/mount.rs`](/Users/ken/26s-rust/tgoskits/os/StarryOS/kernel/src/syscall/fs/mount.rs) + +### 讲解 + +- propagation-only 调用要在最前面处理,因为它们不是“新挂载” +- remount 也必须早处理,否则会误走 bind 或普通 mount 路径 +- `MS_MOVE` / `MS_BIND` 都属于 mount tree 操作,不应该落进 `tmpfs` / `ext4` 分支 +- 只有当这些“特殊 mount 操作”都不命中时,才说明这是普通挂载 + +## 5.2 propagation flags:先校验合法性,再对已有 mount 返回成功 + +本次实现: + +```rust +let propagation = flags & PROPAGATION_FLAGS; + +if propagation.count_ones() > 1 { + return Err(AxError::InvalidInput); +} + +if propagation != 0 { + let allowed = propagation | MS_REC | MS_SILENT; + if flags & !allowed != 0 { + return Err(AxError::InvalidInput); + } + + let target = FS_CONTEXT.lock().resolve(target)?; + if !target.is_root_of_mount() { + return Err(AxError::InvalidInput); + } + return Ok(0); +} +``` + +### 讲解 + +- `count_ones() > 1` 对应 Linux 的“多个 propagation type flags 同时出现是 `EINVAL`” +- `flags & !allowed != 0` 对应 Linux 的“propagation type flags 只能和 `MS_REC` / `MS_SILENT` 共存” +- 这里当前只是让 StarryOS 具备“不要误挂新 fs”的语义边界,真实传播机制仍是后续工作 + +## 5.3 `axfs-ng-vfs::Mountpoint`:把 mountpoint 当成 mount 语义状态承载体 + +本次把几个 Linux mount 语义相关的状态放到了 `Mountpoint` 上: + +```rust +pub struct Mountpoint { + root: DirEntry, + location: Mutex>, + children: Mutex>>, + device: u64, + readonly: AtomicBool, + expired: AtomicBool, +} +``` + +对应代码位置: + +- [`components/axfs-ng-vfs/src/mount.rs`](/Users/ken/26s-rust/tgoskits/components/axfs-ng-vfs/src/mount.rs) + +### 为什么放在 `Mountpoint` 上 + +Linux 里的这些语义,本质上都是“对 mount 实例”的属性,而不是对底层 inode 或 filesystem 超级块的属性: + +- bind mount 视图只读,不应必然把 source mount 一起变只读 +- `MNT_EXPIRE` 的 expired 标记属于 mount,不属于底层文件 +- lazy detach 也是从 mount tree 摘除,而不是销毁底层文件对象 + +因此在 StarryOS 里,把这类状态绑定到 `Mountpoint` 是合理且稳定的。 + +## 5.4 bind / recursive bind:通过 child mount map 区分是否递归 + +本次 `MS_BIND` 和 `MS_BIND|MS_REC` 的核心差异,落在 `children` 这张表上。 + +实现片段: + +```rust +fn bind(source: &Location, location_in_parent: Location, recursive: bool) -> Arc { + let result = Self::new_with_root( + source.entry.clone(), + Some(location_in_parent), + source.mountpoint.device(), + ); + result + .readonly + .store(source.mountpoint.is_readonly(), Ordering::Release); + if recursive { + for (key, child) in source.mountpoint.children.lock().iter() { + result.children.lock().insert(key.clone(), child.clone()); + } + } + result +} +``` + +### 讲解 + +- 顶层 bind 的本质是:目标 mountpoint 的 root 指向与 source 相同的目录入口 +- 普通 bind 时,不拷贝 `children` + - 所以 nested submount 在目标树下不可见 +- recursive bind 时,把 `children` 也带过去 + - 所以 nested submount 在目标树下继续可见 + +这正对应 Linux 中: + +- `MS_BIND` 不递归 +- `MS_BIND|MS_REC` 递归 + +## 5.5 `resolve_mountpoint()`:按当前 mount tree 的 child map 决定是否跨入子挂载 + +这是本次修复 bind 行为时最关键的一点。 + +实现片段: + +```rust +fn resolve_mountpoint(self) -> Self { + let Some(mountpoint) = self + .mountpoint + .children + .lock() + .get(&self.entry.key()) + .and_then(Weak::upgrade) + else { + return self; + }; + let mountpoint = mountpoint.effective_mountpoint(); + let entry = mountpoint.root.clone(); + Self::new(mountpoint, entry) +} +``` + +### 讲解 + +如果路径解析只看“目录节点上有没有 mountpoint”,那么普通 bind 会错误地把 source 的 nested mount 一起暴露出来。 +本次改成“看当前 mount tree 的 `children` map 里有没有这个 child mount”,这样: + +- 普通 bind:目标 mountpoint 没带 `children`,就不会跨进 nested mount +- recursive bind:目标 mountpoint 带了 `children`,就能看到 nested mount + +这让“是否递归”真正体现在 mount tree 本身,而不是体现在共享的底层目录节点上。 + +## 5.6 `MS_MOVE`:修改 mount tree 的 parent-child 关系 + +实现片段: + +```rust +pub fn move_to(self: &Arc, new_location: &Location) -> VfsResult<()> { + ... + *old_location.entry.as_dir()?.mountpoint.lock() = None; + old_location.mountpoint.children.lock().remove(&old_location.entry.key()); + + *new_location.entry.as_dir()?.mountpoint.lock() = Some(self.clone()); + new_location.mountpoint.children.lock().insert( + new_location.entry.key(), + Arc::downgrade(self), + ); + + *self.location.lock() = Some(new_location.clone()); + Ok(()) +} +``` + +### 讲解 + +这里没有创建新的 mountpoint,而是: + +1. 从旧父节点摘掉 child mount +2. 挂到新父节点下 +3. 更新 mountpoint 自己记录的 `location` + +这正是 Linux `MS_MOVE` 的语义:移动现有 mount,而不是复制。 + +## 5.7 `MS_RDONLY` / `MS_REMOUNT|MS_RDONLY`:按 mountpoint 拒绝写入 + +### 在 `sys_mount()` 中设置只读位 + +```rust +if (flags & MS_REMOUNT) != 0 { + ... + if (flags & MS_RDONLY) != 0 { + target.mountpoint().set_readonly(true); + } + return Ok(0); +} +... +let mp = target.bind_mount(&source, (flags & MS_REC) != 0)?; +if (flags & MS_RDONLY) != 0 { + mp.set_readonly(true); +} +``` + +### 在 open / write 路径上拒绝写入 + +```rust +if loc.is_readonly() + && (flags.intersects(FileFlags::WRITE | FileFlags::APPEND) || self.truncate) +{ + return Err(VfsError::ReadOnlyFilesystem); +} +``` + +以及: + +```rust +if self.inner.location().is_readonly() + && flags.intersects(FileFlags::WRITE | FileFlags::APPEND) +{ + return Err(VfsError::ReadOnlyFilesystem); +} +``` + +### 讲解 + +这里有两个检查点: + +1. 打开文件时就拒绝“写/append/truncate” + - 覆盖新打开的写路径 +2. 已经打开的 fd 在后续 `write()` / `append()` 时也检查 mount 是否只读 + - 覆盖“先打开,再 remount 成只读”的情况 + +这样就能同时满足: + +- `MS_RDONLY` 挂载后写入 `EROFS` +- `MS_REMOUNT|MS_RDONLY` 后已有内容保留,但新增写入 `EROFS` +- `MS_REMOUNT|MS_BIND|MS_RDONLY` 只让 bind 视图只读,而源 mount 保持可写 + +## 5.8 `MNT_EXPIRE`:两阶段过期标记 + +实现片段: + +```rust +if (flags & MNT_EXPIRE) != 0 { + if !target.mountpoint().mark_expired() { + return Err(AxError::from(LinuxError::EAGAIN)); + } +} +``` + +其中: + +```rust +pub fn mark_expired(&self) -> bool { + self.expired.swap(true, Ordering::AcqRel) +} +``` + +### 讲解 + +- `mark_expired()` 返回旧值 +- 第一次调用时旧值是 `false` + - `!false == true`,返回 `EAGAIN` + - 同时把 `expired` 设成 `true` +- 第二次调用时旧值是 `true` + - 不再返回 `EAGAIN` + - 流程继续往下走,执行真正的卸载 + +这正好实现 Linux 的“两阶段 expire 语义”。 + +## 5.9 `UMOUNT_NOFOLLOW`:no-follow path resolution + +实现片段: + +```rust +let target = if (flags & UMOUNT_NOFOLLOW) != 0 { + FS_CONTEXT.lock().resolve_no_follow(target)? +} else { + FS_CONTEXT.lock().resolve(target)? +}; +``` + +### 讲解 + +- 普通 `resolve()` 会跟随 symlink +- `resolve_no_follow()` 则把 symlink 当成最终对象本身 + +如果 `target` 是一个 symlink: + +- 普通 `umount2()` 会解析到真实 mount +- 带 `UMOUNT_NOFOLLOW` 时会停在 symlink 这个 inode 上 +- 它不是 mount root,于是返回 `EINVAL` +- 真实 mount 保持不变 + +## 5.10 `MNT_DETACH`:lazy detach + +实现片段: + +```rust +if (flags & MNT_DETACH) != 0 { + target.detach_mount()?; + return Ok(0); +} +``` + +底层: + +```rust +pub fn detach(self: &Arc) -> VfsResult<()> { + ... + location.mountpoint.children.lock().remove(&location.entry.key()); + *location.entry.as_dir()?.mountpoint.lock() = None; + Ok(()) +} +``` + +### 讲解 + +这里的关键是: + +- `MNT_DETACH` 必须在 busy 检查之前处理 +- lazy detach 不等于普通卸载 + +本次实现做的事情是: + +1. 从父 mount tree 的 `children` 中移除当前 mount +2. 清掉父目录项上的 mountpoint 可见入口 + +结果就是: + +- 旧 fd 仍然持有原对象,所以还能读 +- 新路径查找看不到这个 mount 了 + +这正是本次测试验证的语义。 + +## 6. 各参数组合的结果清单 + +下面列出本次文档范围内最常见、也是测试里覆盖到的组合。 + +### 6.1 `mount(2)` 组合清单 + +| 参数组合 | Linux 结果 | 本次 StarryOS 结果 | +|---|---|---| +| `MS_SHARED | MS_PRIVATE` | `EINVAL` | 已支持 | +| `MS_SHARED | MS_BIND` | `EINVAL` | 已支持 | +| `MS_PRIVATE` | 修改已有 mount 的传播属性 | 当前返回成功并保持内容,不实现真实传播 | +| `MS_SHARED` | 修改已有 mount 的传播属性 | 当前返回成功并保持内容,不实现真实传播 | +| `MS_SLAVE` | 修改已有 mount 的传播属性 | 当前返回成功并保持内容,不实现真实传播 | +| `MS_UNBINDABLE` | 修改已有 mount 的传播属性 | 当前返回成功并保持内容,不实现真实传播 | +| `MS_BIND` | 普通 bind,不带 nested submount | 已支持 | +| `MS_BIND|MS_REC` | recursive bind,带 nested submount | 已支持 | +| `MS_MOVE` | 移动已有 mount | 已支持 | +| `MS_REMOUNT` | remount 现有 mount,内容保留 | 已支持 | +| `MS_RDONLY` | 挂成只读,写入 `EROFS` | 已支持 | +| `MS_REMOUNT|MS_RDONLY` | remount 成只读,写入 `EROFS` | 已支持 | +| `MS_REMOUNT|MS_BIND|MS_RDONLY` | bind mount 视图只读,source 仍可写 | 已支持 | + +### 6.2 `umount2(2)` 组合清单 + +| 参数组合 | Linux 结果 | 本次 StarryOS 结果 | +|---|---|---| +| 未知 flags | `EINVAL` | 已支持 | +| `MNT_EXPIRE|MNT_DETACH` | `EINVAL` | 已支持 | +| 非 mount point | `EINVAL` | 已支持 | +| busy mount + 普通卸载 | `EBUSY` | 已支持 | +| `MNT_EXPIRE` 第一次 | `EAGAIN` | 已支持 | +| `MNT_EXPIRE` 第二次 | 真正卸载 | 已支持 | +| `UMOUNT_NOFOLLOW` + symlink | `EINVAL` 且真实 mount 不变 | 已支持 | +| `MNT_DETACH` + busy mount | 成功,路径隐藏,旧 fd 继续可用 | 已支持 | + +## 7. 测试与实现的对应关系 + +本次用户态验证集中在: + +- [`test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c`](/Users/ken/26s-rust/tgoskits/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c) + +它覆盖的核心点是: + +- propagation flags 的非法组合和无副作用保证 +- propagation-only 调用不应替换现有 mount 内容 +- bind / recursive bind / move / remount / readonly +- `umount2` 的 invalid flags / invalid combo / expire / nofollow / detach + +这也是为什么这次修复能够比较扎实: + +- 不是只补 syscall 返回码 +- 而是同时验证了 mount tree 可见性、副作用、写入行为、路径解析行为 + +## 8. 当前仍未覆盖或未完全实现的点 + +虽然这次 `util-linux` 测试已经全绿,但仍有一些 Linux mount/namespace 语义没有在本轮实现: + +- 真实 shared subtree propagation +- `MS_UNBINDABLE` 对后续 bind 的禁止效果 +- 更完整的 mount namespace 传播行为 +- 其他普通 mount flags,如 `MS_NODEV`、`MS_NOSUID`、`MS_NOEXEC` 等 +- `MNT_FORCE` 的真实强制卸载语义 + +因此更准确地说: + +- 这次已经把本轮补的 `mount/umount2` Linux 兼容测试全部打绿 +- 但并不意味着 Linux 整体 mount namespace 语义已经“全部实现” + +## 9. 参考 + +- [mount(2) - Linux manual page](https://man7.org/linux/man-pages/man2/mount.2.html) +- [umount(2) / umount2(2) - Linux manual page](https://www.man7.org/linux/man-pages/man2/umount2.2.html) +- [mount_namespaces(7) - Linux manual page](https://man7.org/linux/man-pages/man7/mount_namespaces.7.html) diff --git a/os/StarryOS/kernel/src/syscall/fs/mount.rs b/os/StarryOS/kernel/src/syscall/fs/mount.rs index 1b2784fb74..333d47f8c7 100644 --- a/os/StarryOS/kernel/src/syscall/fs/mount.rs +++ b/os/StarryOS/kernel/src/syscall/fs/mount.rs @@ -16,6 +16,10 @@ const MNT_DETACH: i32 = 2; const MNT_EXPIRE: i32 = 4; const UMOUNT_NOFOLLOW: i32 = 8; +const MS_RDONLY: i32 = 1; +const MS_REMOUNT: i32 = 1 << 5; +const MS_BIND: i32 = 1 << 12; +const MS_MOVE: i32 = 1 << 13; const MS_REC: i32 = 1 << 14; const MS_SILENT: i32 = 1 << 15; const MS_UNBINDABLE: i32 = 1 << 17; @@ -80,17 +84,64 @@ pub fn sys_mount( if flags & !allowed != 0 { return Err(AxError::InvalidInput); } + + let target = FS_CONTEXT.lock().resolve(target)?; + if !target.is_root_of_mount() { + return Err(AxError::InvalidInput); + } + let mountpoint = target.mountpoint().clone(); + match propagation { + MS_SHARED => mountpoint.set_shared(), + MS_PRIVATE => mountpoint.set_private(), + MS_SLAVE => mountpoint.set_slave(), + MS_UNBINDABLE => mountpoint.set_unbindable(), + _ => {} + } + return Ok(0); + } + + if (flags & MS_REMOUNT) != 0 { + let target = FS_CONTEXT.lock().resolve(target)?; + if !target.is_root_of_mount() { + return Err(AxError::InvalidInput); + } + if (flags & MS_RDONLY) != 0 { + target.mountpoint().set_readonly(true); + } + return Ok(0); + } + + if (flags & MS_MOVE) != 0 { + let ctx = FS_CONTEXT.lock(); + let source = ctx.resolve(source)?; + let target = ctx.resolve(target)?; + source.move_mount(&target)?; + return Ok(0); + } + + if (flags & MS_BIND) != 0 { + let ctx = FS_CONTEXT.lock(); + let source = ctx.resolve(source)?; + let target = ctx.resolve(target)?; + let mp = target.bind_mount(&source, (flags & MS_REC) != 0)?; + if (flags & MS_RDONLY) != 0 { + mp.set_readonly(true); + } + return Ok(0); } match fs_type.as_str() { "tmpfs" => { let fs = MemoryFs::new(); let target = FS_CONTEXT.lock().resolve(target)?; - target.mount(&fs)?; + let mp = target.mount(&fs)?; + if (flags & MS_RDONLY) != 0 { + mp.set_readonly(true); + } } #[cfg(feature = "ext4")] "ext4" => { - mount_ext4(&source, &target)?; + mount_ext4(&source, &target, (flags & MS_RDONLY) != 0)?; } _ => return Err(AxError::NoSuchDevice), } @@ -99,7 +150,7 @@ pub fn sys_mount( } #[cfg(feature = "ext4")] -fn mount_ext4(source: &str, target: &str) -> AxResult<()> { +fn mount_ext4(source: &str, target: &str, readonly: bool) -> AxResult<()> { use alloc::{boxed::Box, sync::Arc}; use ax_driver::prelude::BlockDriverOps; @@ -141,10 +192,11 @@ fn mount_ext4(source: &str, target: &str) -> AxResult<()> { // Mount at the target location let target_loc = ctx.resolve(target)?; - target_loc.mount(&fs).map_err(|e| { + let mountpoint = target_loc.mount(&fs).map_err(|e| { warn!("mount_ext4: failed to mount at {:?}: {:?}", target, e); AxError::Io })?; + mountpoint.set_readonly(readonly); // Store a writeback callback in the mount root's user_data so that // sys_umount2 can flush the loop device's block cache to the backing @@ -181,13 +233,26 @@ pub fn sys_umount2(target: *const c_char, flags: i32) -> AxResult { return Err(AxError::InvalidInput); } - let target = FS_CONTEXT.lock().resolve(target)?; + let target = if (flags & UMOUNT_NOFOLLOW) != 0 { + FS_CONTEXT.lock().resolve_no_follow(target)? + } else { + FS_CONTEXT.lock().resolve(target)? + }; // Linux umount2 returns EINVAL for paths that are not mount points. if !target.is_root_of_mount() { return Err(AxError::InvalidInput); } + if (flags & MNT_EXPIRE) != 0 && !target.mountpoint().mark_expired() { + return Err(AxError::from(LinuxError::EAGAIN)); + } + + if (flags & MNT_DETACH) != 0 { + target.detach_mount()?; + return Ok(0); + } + // Linux umount2 returns EBUSY if any task has cwd/root or open fd // inside the mount. if is_mount_busy(target.mountpoint()) { diff --git a/os/arceos/modules/axfs-ng/src/highlevel/file.rs b/os/arceos/modules/axfs-ng/src/highlevel/file.rs index ed004a8e5b..d19715a7a7 100644 --- a/os/arceos/modules/axfs-ng/src/highlevel/file.rs +++ b/os/arceos/modules/axfs-ng/src/highlevel/file.rs @@ -209,6 +209,12 @@ impl OpenOptions { return Err(VfsError::IsADirectory); } + if loc.is_readonly() + && (flags.intersects(FileFlags::WRITE | FileFlags::APPEND) || self.truncate) + { + return Err(VfsError::ReadOnlyFilesystem); + } + if self.directory { loc.check_is_dir()?; } @@ -1041,6 +1047,11 @@ impl File { /// Checks that the file has the required `flags` and returns the backend. pub fn access(&self, flags: FileFlags) -> VfsResult<&FileBackend> { if self.flags().contains(flags) && !self.is_path() { + if self.inner.location().is_readonly() + && flags.intersects(FileFlags::WRITE | FileFlags::APPEND) + { + return Err(VfsError::ReadOnlyFilesystem); + } Ok(&self.inner) } else { Err(VfsError::BadFileDescriptor) diff --git a/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c index 47c5c523b7..eb698d85ea 100644 --- a/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/util-linux/c/src/main.c @@ -104,6 +104,15 @@ static int read_first_line(const char *path, char *buf, int bufsz) return len; } +static void cleanup_umount_all(const char *path) +{ + int i; + for (i = 0; i < 4; i++) { + if (umount(path) != 0) + break; + } +} + static void check(int ok, const char *name) { if (ok) { printf(" PASS | %s\n", name); pass++; } @@ -1035,6 +1044,861 @@ int main(void) "mount invalid propagation flags has no mount side effect"); } + /* ================================================================ + * Tier 4k0a: mount EINVAL for propagation flags mixed with + * unsupported extra mount flags + * + * Linux mount(2) allows only MS_REC and MS_SILENT alongside a + * propagation type flag. Other bits such as MS_BIND must be + * rejected with EINVAL, and the failed mount must leave no side + * effect behind. + * ================================================================ */ + { + const char *invalid_mount_point = "/tmp/ul-mnt-invalid-propagation-extra"; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-mnt-invalid-propagation-extra"); + check(rc == 0, "mkdir invalid-propagation-extra mount point"); + + errno = 0; + rc = mount( + "none", + invalid_mount_point, + "tmpfs", + MS_SHARED | MS_BIND, + NULL + ); + saved_errno = errno; + check(rc == -1 && saved_errno == EINVAL, + "mount EINVAL for propagation flag with unsupported extra flag"); + + errno = 0; + rc = umount(invalid_mount_point); + saved_errno = errno; + check(rc == -1 && saved_errno == EINVAL, + "mount invalid propagation-extra flags has no mount side effect"); + } + + /* ================================================================ + * Tier 4k0b: mount MS_PRIVATE updates an existing mount instead + * of creating a fresh filesystem + * + * Linux treats propagation flags as operations on an existing + * mount. Existing contents must remain visible after the call. + * ================================================================ */ + { + const char *mount_point = "/tmp/ul-mnt-propagation-op"; + + rc = run("mkdir -p /tmp/ul-mnt-propagation-op"); + check(rc == 0, "mkdir propagation-op mount point"); + + rc = mount("tmpfs", mount_point, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for propagation-op test"); + + rc = write_file("/tmp/ul-mnt-propagation-op/keep.txt", "keep\n"); + check(rc == 0, "write file before MS_PRIVATE"); + + errno = 0; + rc = mount("none", mount_point, "tmpfs", MS_PRIVATE, NULL); + check(rc == 0, "mount MS_PRIVATE on existing mount succeeds"); + + { + char content[256] = {0}; + int len = read_first_line( + "/tmp/ul-mnt-propagation-op/keep.txt", + content, + sizeof(content) + ); + check(len > 0 && strcmp(content, "keep") == 0, + "mount MS_PRIVATE preserves existing mount contents"); + } + + cleanup_umount_all(mount_point); + } + + /* ================================================================ + * Tier 4k0b1: mount MS_SHARED updates an existing mount instead + * of replacing it + * ================================================================ */ + { + const char *mount_point = "/tmp/ul-mnt-shared-op"; + + rc = run("mkdir -p /tmp/ul-mnt-shared-op"); + check(rc == 0, "mkdir shared-op mount point"); + + rc = mount("tmpfs", mount_point, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for shared-op test"); + + rc = write_file("/tmp/ul-mnt-shared-op/keep.txt", "keep shared\n"); + check(rc == 0, "write file before MS_SHARED"); + + errno = 0; + rc = mount("none", mount_point, "tmpfs", MS_SHARED, NULL); + check(rc == 0, "mount MS_SHARED on existing mount succeeds"); + + { + char content[256] = {0}; + int len = read_first_line("/tmp/ul-mnt-shared-op/keep.txt", + content, sizeof(content)); + check(len > 0 && strcmp(content, "keep shared") == 0, + "mount MS_SHARED preserves existing mount contents"); + } + + cleanup_umount_all(mount_point); + } + + /* ================================================================ + * Tier 4k0b2: mount MS_SLAVE updates an existing mount instead + * of replacing it + * ================================================================ */ + { + const char *mount_point = "/tmp/ul-mnt-slave-op"; + + rc = run("mkdir -p /tmp/ul-mnt-slave-op"); + check(rc == 0, "mkdir slave-op mount point"); + + rc = mount("tmpfs", mount_point, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for slave-op test"); + + rc = write_file("/tmp/ul-mnt-slave-op/keep.txt", "keep slave\n"); + check(rc == 0, "write file before MS_SLAVE"); + + errno = 0; + rc = mount("none", mount_point, "tmpfs", MS_SLAVE, NULL); + check(rc == 0, "mount MS_SLAVE on existing mount succeeds"); + + { + char content[256] = {0}; + int len = read_first_line("/tmp/ul-mnt-slave-op/keep.txt", + content, sizeof(content)); + check(len > 0 && strcmp(content, "keep slave") == 0, + "mount MS_SLAVE preserves existing mount contents"); + } + + cleanup_umount_all(mount_point); + } + + /* ================================================================ + * Tier 4k0b3: mount MS_UNBINDABLE updates an existing mount + * instead of replacing it + * ================================================================ */ + { + const char *mount_point = "/tmp/ul-mnt-unbindable-op"; + + rc = run("mkdir -p /tmp/ul-mnt-unbindable-op"); + check(rc == 0, "mkdir unbindable-op mount point"); + + rc = mount("tmpfs", mount_point, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for unbindable-op test"); + + rc = write_file("/tmp/ul-mnt-unbindable-op/keep.txt", "keep unbindable\n"); + check(rc == 0, "write file before MS_UNBINDABLE"); + + errno = 0; + rc = mount("none", mount_point, "tmpfs", MS_UNBINDABLE, NULL); + check(rc == 0, "mount MS_UNBINDABLE on existing mount succeeds"); + + { + char content[256] = {0}; + int len = read_first_line("/tmp/ul-mnt-unbindable-op/keep.txt", + content, sizeof(content)); + check(len > 0 && strcmp(content, "keep unbindable") == 0, + "mount MS_UNBINDABLE preserves existing mount contents"); + } + + cleanup_umount_all(mount_point); + } + + /* ================================================================ + * Tier 4k0b4: mount MS_SHARED causes child mounts to propagate to + * a bind peer + * + * In Linux, a bind mount created from a shared mount joins the + * same peer group. A new child mount under one peer must appear + * under the other peer. + * ================================================================ */ + { + const char *src = "/tmp/ul-shared-src"; + const char *dst = "/tmp/ul-shared-dst"; + + rc = run("mkdir -p /tmp/ul-shared-src /tmp/ul-shared-dst"); + check(rc == 0, "mkdir shared-propagation test directories"); + + rc = mount("tmpfs", src, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for shared-propagation source"); + + rc = mount("none", src, "tmpfs", MS_SHARED, NULL); + check(rc == 0, "mount MS_SHARED on propagation source succeeds"); + + rc = mount(src, dst, "tmpfs", MS_BIND, NULL); + check(rc == 0, "bind mount from shared source succeeds"); + + rc = run("mkdir -p /tmp/ul-shared-src/submnt"); + check(rc == 0, "mkdir child mountpoint under shared source"); + + rc = mount("tmpfs", "/tmp/ul-shared-src/submnt", "tmpfs", 0, NULL); + check(rc == 0, "mount child tmpfs under shared source"); + + rc = write_file("/tmp/ul-shared-src/submnt/peer.txt", "shared peer\n"); + check(rc == 0, "write file inside propagated shared child mount"); + + { + char content[256] = {0}; + int saved_errno; + int len = read_first_line("/tmp/ul-shared-dst/submnt/peer.txt", + content, sizeof(content)); + saved_errno = errno; + if (!(len > 0 && strcmp(content, "shared peer") == 0)) { + printf(" INFO | shared peer read len=%d errno=%d content='%s'\n", + len, saved_errno, content); + } + check(len > 0 && strcmp(content, "shared peer") == 0, + "mount MS_SHARED propagates child mount to bind peer"); + } + + cleanup_umount_all("/tmp/ul-shared-dst/submnt"); + cleanup_umount_all(dst); + cleanup_umount_all("/tmp/ul-shared-src/submnt"); + cleanup_umount_all(src); + } + + /* ================================================================ + * Tier 4k0b5: mount MS_PRIVATE stops further propagation from a + * previously shared peer + * + * After converting one peer to private, new child mounts created + * under the remaining shared peer must no longer appear there. + * ================================================================ */ + { + const char *src = "/tmp/ul-private-src"; + const char *dst = "/tmp/ul-private-dst"; + int nested_fd; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-private-src /tmp/ul-private-dst"); + check(rc == 0, "mkdir private-propagation test directories"); + + rc = mount("tmpfs", src, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for private-propagation source"); + + rc = mount("none", src, "tmpfs", MS_SHARED, NULL); + check(rc == 0, "mount MS_SHARED before private conversion succeeds"); + + rc = mount(src, dst, "tmpfs", MS_BIND, NULL); + check(rc == 0, "bind mount peer before private conversion succeeds"); + + rc = mount("none", dst, "tmpfs", MS_PRIVATE, NULL); + check(rc == 0, "mount MS_PRIVATE on bind peer succeeds"); + + rc = run("mkdir -p /tmp/ul-private-src/submnt"); + check(rc == 0, "mkdir child mountpoint under shared master"); + + rc = mount("tmpfs", "/tmp/ul-private-src/submnt", "tmpfs", 0, NULL); + check(rc == 0, "mount child tmpfs under shared master"); + + errno = 0; + nested_fd = open("/tmp/ul-private-dst/submnt/probe.txt", O_RDONLY); + saved_errno = errno; + if (nested_fd >= 0) + close(nested_fd); + check(nested_fd == -1 && saved_errno == ENOENT, + "mount MS_PRIVATE stops future propagation to bind peer"); + + cleanup_umount_all("/tmp/ul-private-dst/submnt"); + cleanup_umount_all(dst); + cleanup_umount_all("/tmp/ul-private-src/submnt"); + cleanup_umount_all(src); + } + + /* ================================================================ + * Tier 4k0b6: mount MS_SLAVE receives propagation from master but + * does not send it back + * + * A slave mount should receive new child mounts from its shared + * master, but mounts created under the slave must not propagate + * back to the master. + * ================================================================ */ + { + const char *src = "/tmp/ul-slave-src"; + const char *dst = "/tmp/ul-slave-dst"; + int nested_fd; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-slave-src /tmp/ul-slave-dst"); + check(rc == 0, "mkdir slave-propagation test directories"); + + rc = mount("tmpfs", src, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for slave-propagation source"); + + rc = mount("none", src, "tmpfs", MS_SHARED, NULL); + check(rc == 0, "mount MS_SHARED for slave-propagation source succeeds"); + + rc = mount(src, dst, "tmpfs", MS_BIND, NULL); + check(rc == 0, "bind mount peer for slave-propagation succeeds"); + + rc = mount("none", dst, "tmpfs", MS_SLAVE, NULL); + check(rc == 0, "mount MS_SLAVE on bind peer succeeds"); + + rc = run("mkdir -p /tmp/ul-slave-src/from-master"); + check(rc == 0, "mkdir master child mountpoint for slave test"); + + rc = mount("tmpfs", "/tmp/ul-slave-src/from-master", "tmpfs", 0, NULL); + check(rc == 0, "mount child tmpfs under master for slave test"); + + rc = write_file("/tmp/ul-slave-src/from-master/master.txt", "master to slave\n"); + check(rc == 0, "write file inside master child mount for slave test"); + + { + char content[256] = {0}; + int len = read_first_line("/tmp/ul-slave-dst/from-master/master.txt", + content, sizeof(content)); + check(len > 0 && strcmp(content, "master to slave") == 0, + "mount MS_SLAVE receives propagation from master"); + } + + rc = run("mkdir -p /tmp/ul-slave-dst/from-slave"); + check(rc == 0, "mkdir slave child mountpoint for reverse propagation test"); + + rc = mount("tmpfs", "/tmp/ul-slave-dst/from-slave", "tmpfs", 0, NULL); + check(rc == 0, "mount child tmpfs under slave"); + + errno = 0; + nested_fd = open("/tmp/ul-slave-src/from-slave/probe.txt", O_RDONLY); + saved_errno = errno; + if (nested_fd >= 0) + close(nested_fd); + check(nested_fd == -1 && saved_errno == ENOENT, + "mount MS_SLAVE does not propagate back to master"); + + cleanup_umount_all("/tmp/ul-slave-dst/from-master"); + cleanup_umount_all("/tmp/ul-slave-dst/from-slave"); + cleanup_umount_all(dst); + cleanup_umount_all("/tmp/ul-slave-src/from-master"); + cleanup_umount_all("/tmp/ul-slave-src/from-slave"); + cleanup_umount_all(src); + } + + /* ================================================================ + * Tier 4k0b7: mount MS_UNBINDABLE rejects direct bind mounts + * + * Linux rejects bind mounts whose source mount is unbindable. + * ================================================================ */ + { + const char *src = "/tmp/ul-unbindable-src"; + const char *dst = "/tmp/ul-unbindable-dst"; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-unbindable-src /tmp/ul-unbindable-dst"); + check(rc == 0, "mkdir unbindable-bind test directories"); + + rc = mount("tmpfs", src, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for unbindable-bind source"); + + rc = mount("none", src, "tmpfs", MS_UNBINDABLE, NULL); + check(rc == 0, "mount MS_UNBINDABLE before bind test succeeds"); + + errno = 0; + rc = mount(src, dst, "tmpfs", MS_BIND, NULL); + saved_errno = errno; + check(rc == -1 && saved_errno == EINVAL, + "mount MS_UNBINDABLE source rejects bind mount"); + + cleanup_umount_all(dst); + cleanup_umount_all(src); + } + + /* ================================================================ + * Tier 4k0c: mount MS_BIND shares the same filesystem view + * + * A bind mount should expose the source tree at the destination, + * and writes through the destination must appear in the source. + * ================================================================ */ + { + const char *src = "/tmp/ul-bind-src"; + const char *dst = "/tmp/ul-bind-dst"; + + rc = run("mkdir -p /tmp/ul-bind-src /tmp/ul-bind-dst"); + check(rc == 0, "mkdir bind-mount test directories"); + + rc = mount("tmpfs", src, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for bind-mount source"); + + rc = write_file("/tmp/ul-bind-src/source.txt", "bind source\n"); + check(rc == 0, "write source file before bind mount"); + + errno = 0; + rc = mount(src, dst, "tmpfs", MS_BIND, NULL); + check(rc == 0, "mount MS_BIND succeeds"); + + { + char content[256] = {0}; + int len = read_first_line("/tmp/ul-bind-dst/source.txt", content, sizeof(content)); + check(len > 0 && strcmp(content, "bind source") == 0, + "bind mount exposes source contents at destination"); + } + + rc = write_file("/tmp/ul-bind-dst/dst-write.txt", "bind mirror\n"); + check(rc == 0, "write through bind-mount destination"); + + { + char content[256] = {0}; + int len = read_first_line("/tmp/ul-bind-src/dst-write.txt", content, sizeof(content)); + check(len > 0 && strcmp(content, "bind mirror") == 0, + "bind mount mirrors destination writes back to source"); + } + + cleanup_umount_all(dst); + cleanup_umount_all(src); + } + + /* ================================================================ + * Tier 4k0c0: mount MS_BIND can bind a non-root subdirectory + * + * Linux bind mounts are not restricted to the root of an existing + * mount; a subdirectory bind must also succeed. + * ================================================================ */ + { + const char *src = "/tmp/ul-bind-sub-src"; + const char *dst = "/tmp/ul-bind-sub-dst"; + + rc = run("mkdir -p /tmp/ul-bind-sub-src /tmp/ul-bind-sub-dst"); + check(rc == 0, "mkdir bind-subdir test directories"); + + rc = mount("tmpfs", src, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for bind-subdir source"); + + rc = run("mkdir -p /tmp/ul-bind-sub-src/sub"); + check(rc == 0, "mkdir source subdirectory for bind-subdir test"); + + rc = write_file("/tmp/ul-bind-sub-src/sub/sub.txt", "bind subdir\n"); + check(rc == 0, "write file in bind-subdir source subdirectory"); + + errno = 0; + rc = mount("/tmp/ul-bind-sub-src/sub", dst, "tmpfs", MS_BIND, NULL); + check(rc == 0, "mount MS_BIND on source subdirectory succeeds"); + + { + char content[256] = {0}; + int len = read_first_line("/tmp/ul-bind-sub-dst/sub.txt", content, sizeof(content)); + check(len > 0 && strcmp(content, "bind subdir") == 0, + "bind mount on source subdirectory exposes contents"); + } + + cleanup_umount_all(dst); + cleanup_umount_all(src); + } + + /* ================================================================ + * Tier 4k0c1: mount MS_BIND without MS_REC does not clone submounts + * + * Linux bind-mounts only the top mount by default. Nested mounts + * remain absent unless MS_REC is also specified. + * ================================================================ */ + { + const char *src = "/tmp/ul-bind-tree-src"; + const char *dst = "/tmp/ul-bind-tree-dst"; + int nested_fd; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-bind-tree-src /tmp/ul-bind-tree-dst"); + check(rc == 0, "mkdir bind-tree test directories"); + + rc = mount("tmpfs", src, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for bind-tree source"); + + rc = run("mkdir -p /tmp/ul-bind-tree-src/submnt"); + check(rc == 0, "mkdir bind-tree nested mountpoint"); + + rc = mount("tmpfs", "/tmp/ul-bind-tree-src/submnt", "tmpfs", 0, NULL); + check(rc == 0, "mount nested tmpfs under bind-tree source"); + + rc = write_file("/tmp/ul-bind-tree-src/submnt/nested.txt", "nested mount\n"); + check(rc == 0, "write file inside nested source mount"); + + errno = 0; + rc = mount(src, dst, "tmpfs", MS_BIND, NULL); + check(rc == 0, "mount MS_BIND without MS_REC succeeds"); + + errno = 0; + nested_fd = open("/tmp/ul-bind-tree-dst/submnt/nested.txt", O_RDONLY); + saved_errno = errno; + if (nested_fd >= 0) + close(nested_fd); + check(nested_fd == -1 && saved_errno == ENOENT, + "mount MS_BIND without MS_REC leaves nested mount absent"); + + cleanup_umount_all(dst); + cleanup_umount_all("/tmp/ul-bind-tree-src/submnt"); + cleanup_umount_all(src); + } + + /* ================================================================ + * Tier 4k0c2: mount MS_BIND|MS_REC clones nested submounts + * + * A recursive bind mount should expose submount contents at the + * destination subtree. + * ================================================================ */ + { + const char *src = "/tmp/ul-rbind-src"; + const char *dst = "/tmp/ul-rbind-dst"; + + rc = run("mkdir -p /tmp/ul-rbind-src /tmp/ul-rbind-dst"); + check(rc == 0, "mkdir recursive-bind test directories"); + + rc = mount("tmpfs", src, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for recursive-bind source"); + + rc = run("mkdir -p /tmp/ul-rbind-src/submnt"); + check(rc == 0, "mkdir recursive-bind nested mountpoint"); + + rc = mount("tmpfs", "/tmp/ul-rbind-src/submnt", "tmpfs", 0, NULL); + check(rc == 0, "mount nested tmpfs under recursive-bind source"); + + rc = write_file("/tmp/ul-rbind-src/submnt/nested.txt", "recursive bind\n"); + check(rc == 0, "write file inside recursive-bind nested mount"); + + errno = 0; + rc = mount(src, dst, "tmpfs", MS_BIND | MS_REC, NULL); + check(rc == 0, "mount MS_BIND|MS_REC succeeds"); + + { + char content[256] = {0}; + int len = read_first_line("/tmp/ul-rbind-dst/submnt/nested.txt", + content, sizeof(content)); + check(len > 0 && strcmp(content, "recursive bind") == 0, + "mount MS_BIND|MS_REC exposes nested mount contents"); + } + + cleanup_umount_all("/tmp/ul-rbind-dst/submnt"); + cleanup_umount_all(dst); + cleanup_umount_all("/tmp/ul-rbind-src/submnt"); + cleanup_umount_all(src); + } + + /* ================================================================ + * Tier 4k0c3: recursive bind prunes unbindable child mounts + * + * Linux prunes unbindable submounts when replicating a subtree via + * MS_BIND|MS_REC. + * ================================================================ */ + { + const char *src = "/tmp/ul-rbind-unbind-src"; + const char *dst = "/tmp/ul-rbind-unbind-dst"; + int nested_fd; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-rbind-unbind-src /tmp/ul-rbind-unbind-dst"); + check(rc == 0, "mkdir recursive-bind-unbindable test directories"); + + rc = mount("tmpfs", src, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for recursive-bind-unbindable source"); + + rc = run("mkdir -p /tmp/ul-rbind-unbind-src/submnt"); + check(rc == 0, "mkdir recursive-bind-unbindable child mountpoint"); + + rc = mount("tmpfs", "/tmp/ul-rbind-unbind-src/submnt", "tmpfs", 0, NULL); + check(rc == 0, "mount child tmpfs for recursive-bind-unbindable test"); + + rc = mount("none", "/tmp/ul-rbind-unbind-src/submnt", "tmpfs", MS_UNBINDABLE, NULL); + check(rc == 0, "mount MS_UNBINDABLE on child mount succeeds"); + + errno = 0; + rc = mount(src, dst, "tmpfs", MS_BIND | MS_REC, NULL); + check(rc == 0, "mount MS_BIND|MS_REC with unbindable child succeeds"); + + errno = 0; + nested_fd = open("/tmp/ul-rbind-unbind-dst/submnt/probe.txt", O_RDONLY); + saved_errno = errno; + if (nested_fd >= 0) + close(nested_fd); + check(nested_fd == -1 && saved_errno == ENOENT, + "mount MS_BIND|MS_REC prunes unbindable child mount"); + + cleanup_umount_all("/tmp/ul-rbind-unbind-dst/submnt"); + cleanup_umount_all(dst); + cleanup_umount_all("/tmp/ul-rbind-unbind-src/submnt"); + cleanup_umount_all(src); + } + + /* ================================================================ + * Tier 4k0d: mount MS_MOVE relocates an existing mount + * + * After a move mount, the mounted tree should appear only at the + * new path and disappear from the old path. + * ================================================================ */ + { + const char *src = "/tmp/ul-move-src"; + const char *dst = "/tmp/ul-move-dst"; + + rc = run("mkdir -p /tmp/ul-move-src /tmp/ul-move-dst"); + check(rc == 0, "mkdir move-mount test directories"); + + rc = mount("tmpfs", src, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for move-mount source"); + + rc = write_file("/tmp/ul-move-src/move.txt", "move payload\n"); + check(rc == 0, "write source file before move mount"); + + errno = 0; + rc = mount(src, dst, "tmpfs", MS_MOVE, NULL); + check(rc == 0, "mount MS_MOVE succeeds"); + + { + char content[256] = {0}; + int len = read_first_line("/tmp/ul-move-dst/move.txt", content, sizeof(content)); + check(len > 0 && strcmp(content, "move payload") == 0, + "move mount exposes source contents at new path"); + } + + { + int old_fd; + int saved_errno; + errno = 0; + old_fd = open("/tmp/ul-move-src/move.txt", O_RDONLY); + saved_errno = errno; + if (old_fd >= 0) + close(old_fd); + check(old_fd == -1 && saved_errno == ENOENT, + "move mount removes old mounted path"); + } + + cleanup_umount_all(dst); + cleanup_umount_all(src); + } + + /* ================================================================ + * Tier 4k0d1: mount MS_MOVE rejects moving a mount beneath itself + * + * Linux returns ELOOP if the move target is a descendant of the + * source mount. + * ================================================================ */ + { + const char *src = "/tmp/ul-move-loop"; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-move-loop"); + check(rc == 0, "mkdir move-loop test directory"); + + rc = mount("tmpfs", src, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for move-loop test"); + + rc = run("mkdir -p /tmp/ul-move-loop/subdir"); + check(rc == 0, "mkdir descendant target for move-loop test"); + + errno = 0; + rc = mount(src, "/tmp/ul-move-loop/subdir", "tmpfs", MS_MOVE, NULL); + saved_errno = errno; + check(rc == -1 && saved_errno == ELOOP, + "mount MS_MOVE rejects descendant target with ELOOP"); + + { + char content[256] = {0}; + rc = write_file("/tmp/ul-move-loop/still-there.txt", "move loop keep\n"); + check(rc == 0, "move-loop source mount remains usable after failed move"); + rc = read_first_line("/tmp/ul-move-loop/still-there.txt", content, sizeof(content)); + check(rc > 0 && strcmp(content, "move loop keep") == 0, + "failed MS_MOVE leaves original mount in place"); + } + + cleanup_umount_all(src); + } + + /* ================================================================ + * Tier 4k0e: mount MS_REMOUNT keeps the existing mount tree + * + * A remount operates on the existing mount rather than replacing + * it, so existing files should still be visible afterwards. + * ================================================================ */ + { + const char *mount_point = "/tmp/ul-remount"; + + rc = run("mkdir -p /tmp/ul-remount"); + check(rc == 0, "mkdir remount test directory"); + + rc = mount("tmpfs", mount_point, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for remount test"); + + rc = write_file("/tmp/ul-remount/remount.txt", "remount keep\n"); + check(rc == 0, "write file before remount"); + + errno = 0; + rc = mount("none", mount_point, "tmpfs", MS_REMOUNT, NULL); + check(rc == 0, "mount MS_REMOUNT succeeds"); + + { + char content[256] = {0}; + int len = read_first_line("/tmp/ul-remount/remount.txt", content, sizeof(content)); + check(len > 0 && strcmp(content, "remount keep") == 0, + "mount MS_REMOUNT preserves existing mount contents"); + } + + cleanup_umount_all(mount_point); + } + + /* ================================================================ + * Tier 4k0e1: mount MS_REMOUNT|MS_BIND|MS_RDONLY makes a bind + * mount read-only without freezing the source mount + * + * Linux uses bind-remount to toggle per-mount-point flags such as + * read-only on a bind mount. + * ================================================================ */ + { + const char *src = "/tmp/ul-bind-ro-src"; + const char *dst = "/tmp/ul-bind-ro-dst"; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-bind-ro-src /tmp/ul-bind-ro-dst"); + check(rc == 0, "mkdir bind-remount-rdonly test directories"); + + rc = mount("tmpfs", src, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for bind-remount-rdonly source"); + + rc = write_file("/tmp/ul-bind-ro-src/base.txt", "bind ro base\n"); + check(rc == 0, "write source file before bind-remount-rdonly"); + + rc = mount(src, dst, "tmpfs", MS_BIND, NULL); + check(rc == 0, "mount MS_BIND for bind-remount-rdonly test"); + + errno = 0; + rc = mount("none", dst, "tmpfs", MS_REMOUNT | MS_BIND | MS_RDONLY, NULL); + saved_errno = errno; + check(rc == 0, "mount MS_REMOUNT|MS_BIND|MS_RDONLY succeeds"); + + errno = 0; + rc = write_file("/tmp/ul-bind-ro-dst/deny.txt", "must fail\n"); + saved_errno = errno; + check(rc == -1 && saved_errno == EROFS, + "bind-remount-rdonly rejects writes through bind mount"); + + rc = write_file("/tmp/ul-bind-ro-src/still-writable.txt", "source writable\n"); + check(rc == 0, "bind-remount-rdonly leaves source mount writable"); + + cleanup_umount_all(dst); + cleanup_umount_all(src); + } + + /* ================================================================ + * Tier 4k0f: mount MS_RDONLY creates a read-only mount + * + * A read-only mount must reject write attempts with EROFS. + * ================================================================ */ + { + const char *mount_point = "/tmp/ul-rdonly"; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-rdonly"); + check(rc == 0, "mkdir rdonly test directory"); + + errno = 0; + rc = mount("tmpfs", mount_point, "tmpfs", MS_RDONLY, NULL); + saved_errno = errno; + check(rc == 0, "mount MS_RDONLY succeeds"); + + errno = 0; + rc = write_file("/tmp/ul-rdonly/ro.txt", "must fail\n"); + saved_errno = errno; + check(rc == -1 && saved_errno == EROFS, + "mount MS_RDONLY rejects writes with EROFS"); + + cleanup_umount_all(mount_point); + } + + /* ================================================================ + * Tier 4k0g: mount MS_REMOUNT|MS_RDONLY preserves contents and + * turns an existing mount read-only + * + * Linux remounts apply to the existing mount. Existing files must + * remain visible and subsequent writes must fail with EROFS. + * ================================================================ */ + { + const char *mount_point = "/tmp/ul-remount-rdonly"; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-remount-rdonly"); + check(rc == 0, "mkdir remount-rdonly test directory"); + + rc = mount("tmpfs", mount_point, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for remount-rdonly test"); + + rc = write_file("/tmp/ul-remount-rdonly/keep.txt", "keep ro\n"); + check(rc == 0, "write file before remount-rdonly"); + + errno = 0; + rc = mount("none", mount_point, "tmpfs", MS_REMOUNT | MS_RDONLY, NULL); + saved_errno = errno; + check(rc == 0, "mount MS_REMOUNT|MS_RDONLY succeeds"); + + { + char content[256] = {0}; + int len = read_first_line("/tmp/ul-remount-rdonly/keep.txt", + content, sizeof(content)); + check(len > 0 && strcmp(content, "keep ro") == 0, + "mount MS_REMOUNT|MS_RDONLY preserves existing contents"); + } + + errno = 0; + rc = write_file("/tmp/ul-remount-rdonly/new.txt", "must fail\n"); + saved_errno = errno; + check(rc == -1 && saved_errno == EROFS, + "mount MS_REMOUNT|MS_RDONLY rejects new writes"); + + cleanup_umount_all(mount_point); + } + + /* ================================================================ + * Tier 4k0g1: remounted read-only mounts reject metadata and + * directory entry changes with EROFS + * + * Linux applies MS_REMOUNT|MS_RDONLY to the mountpoint, so chmod, + * rename, unlink, and mkdir under that mount should fail with + * EROFS. + * ================================================================ */ + { + const char *mount_point = "/tmp/ul-remount-rdonly-meta"; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-remount-rdonly-meta"); + check(rc == 0, "mkdir remount-rdonly-meta test directory"); + + rc = mount("tmpfs", mount_point, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for remount-rdonly-meta test"); + + rc = write_file("/tmp/ul-remount-rdonly-meta/file.txt", "meta ro\n"); + check(rc == 0, "write file before remount-rdonly-meta"); + + errno = 0; + rc = mount("none", mount_point, "tmpfs", MS_REMOUNT | MS_RDONLY, NULL); + saved_errno = errno; + check(rc == 0, "mount MS_REMOUNT|MS_RDONLY for metadata test succeeds"); + + errno = 0; + rc = chmod("/tmp/ul-remount-rdonly-meta/file.txt", 0600); + saved_errno = errno; + check(rc == -1 && saved_errno == EROFS, + "mount MS_REMOUNT|MS_RDONLY rejects chmod with EROFS"); + + errno = 0; + rc = rename("/tmp/ul-remount-rdonly-meta/file.txt", + "/tmp/ul-remount-rdonly-meta/file2.txt"); + saved_errno = errno; + check(rc == -1 && saved_errno == EROFS, + "mount MS_REMOUNT|MS_RDONLY rejects rename with EROFS"); + + errno = 0; + rc = unlink("/tmp/ul-remount-rdonly-meta/file.txt"); + saved_errno = errno; + check(rc == -1 && saved_errno == EROFS, + "mount MS_REMOUNT|MS_RDONLY rejects unlink with EROFS"); + + errno = 0; + rc = mkdir("/tmp/ul-remount-rdonly-meta/newdir", 0755); + saved_errno = errno; + check(rc == -1 && saved_errno == EROFS, + "mount MS_REMOUNT|MS_RDONLY rejects mkdir with EROFS"); + + cleanup_umount_all(mount_point); + } + /* ================================================================ * Tier 4k1: umount2 EINVAL for invalid flags * @@ -1084,6 +1948,180 @@ int main(void) } } + /* ================================================================ + * Tier 4k2: umount2 EINVAL for invalid supported-flag combo + * + * Linux umount2 rejects MNT_EXPIRE combined with MNT_DETACH or + * MNT_FORCE. The rejected operation must not unmount the target. + * ================================================================ */ + + /* Attach ext4 image */ + if (loopdev[0]) { + char cmd[256]; + snprintf(cmd, sizeof(cmd), "losetup %s " PREBUILT_IMG " 2>&1", loopdev); + rc = run(cmd); + check(rc == 0, "losetup attach for umount2-invalid-combo test"); + } else { + check(0, "losetup attach for umount2-invalid-combo test"); + } + + /* Mount ext4 */ + if (loopdev[0]) { + char cmd[256]; + snprintf(cmd, sizeof(cmd), "mount -t ext4 %s /tmp/ul-mnt 2>&1", loopdev); + rc = run(cmd); + check(rc == 0, "mount for umount2-invalid-combo test"); + } else { + check(0, "mount for umount2-invalid-combo test"); + } + + /* umount2 with invalid supported-flag combo must fail with EINVAL */ + { + int saved_errno; + errno = 0; + rc = (int)syscall(SYS_umount2, "/tmp/ul-mnt", MNT_EXPIRE | MNT_DETACH); + saved_errno = errno; + check(rc == -1 && saved_errno == EINVAL, + "umount2 EINVAL for MNT_EXPIRE|MNT_DETACH"); + } + + /* Cleanup: normal umount then detach */ + { + rc = umount("/tmp/ul-mnt"); + check(rc == 0, "umount cleanup after umount2-invalid-combo test"); + if (loopdev[0]) { + char cmd[256]; + snprintf(cmd, sizeof(cmd), "losetup -d %s 2>&1", loopdev); + run(cmd); + } + } + + /* ================================================================ + * Tier 4k3: umount2 MNT_EXPIRE is a two-step expire operation + * + * The first MNT_EXPIRE call should return EAGAIN and mark the + * mount expired. A second call should then unmount it. + * ================================================================ */ + { + const char *mount_point = "/tmp/ul-expire"; + int saved_errno; + + rc = run("mkdir -p /tmp/ul-expire"); + check(rc == 0, "mkdir expire test directory"); + + rc = mount("tmpfs", mount_point, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for MNT_EXPIRE test"); + + errno = 0; + rc = (int)syscall(SYS_umount2, mount_point, MNT_EXPIRE); + saved_errno = errno; + check(rc == -1 && saved_errno == EAGAIN, + "umount2 MNT_EXPIRE first call returns EAGAIN"); + + errno = 0; + rc = (int)syscall(SYS_umount2, mount_point, MNT_EXPIRE); + saved_errno = errno; + check(rc == 0, + "umount2 MNT_EXPIRE second call unmounts expired mount"); + + cleanup_umount_all(mount_point); + } + + /* ================================================================ + * Tier 4k4: umount2 UMOUNT_NOFOLLOW does not follow symlinks + * + * With UMOUNT_NOFOLLOW, passing a symlink path must not unmount + * the mount behind that symlink. + * ================================================================ */ + { + const char *real_mount = "/tmp/ul-nofollow-real"; + const char *link_mount = "/tmp/ul-nofollow-link"; + int saved_errno; + + run("mkdir -p /tmp/ul-nofollow-real"); + unlink(link_mount); + rc = symlink(real_mount, link_mount); + check(rc == 0, "create symlink for UMOUNT_NOFOLLOW test"); + + rc = mount("tmpfs", real_mount, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for UMOUNT_NOFOLLOW test"); + + rc = write_file("/tmp/ul-nofollow-real/nofollow.txt", "nofollow keep\n"); + check(rc == 0, "write file before UMOUNT_NOFOLLOW"); + + errno = 0; + rc = (int)syscall(SYS_umount2, link_mount, UMOUNT_NOFOLLOW); + saved_errno = errno; + check(rc == -1 && saved_errno == EINVAL, + "umount2 UMOUNT_NOFOLLOW rejects symlink target"); + + { + char content[256] = {0}; + int len = read_first_line("/tmp/ul-nofollow-real/nofollow.txt", + content, sizeof(content)); + check(len > 0 && strcmp(content, "nofollow keep") == 0, + "UMOUNT_NOFOLLOW leaves real mount in place"); + } + + cleanup_umount_all(real_mount); + unlink(link_mount); + } + + /* ================================================================ + * Tier 4k5: umount2 MNT_DETACH lazily detaches a busy mount + * + * The detach should succeed even while an open file descriptor + * keeps the mount busy. New path lookups must see the underlying + * directory, while the existing file descriptor remains usable. + * ================================================================ */ + { + const char *mount_point = "/tmp/ul-detach"; + int detach_fd; + + rc = run("mkdir -p /tmp/ul-detach"); + check(rc == 0, "mkdir detach test directory"); + + rc = mount("tmpfs", mount_point, "tmpfs", 0, NULL); + check(rc == 0, "mount tmpfs for MNT_DETACH test"); + + rc = write_file("/tmp/ul-detach/detach.txt", "detach payload\n"); + check(rc == 0, "write file before MNT_DETACH"); + + detach_fd = open("/tmp/ul-detach/detach.txt", O_RDONLY); + check(detach_fd >= 0, "open fd before MNT_DETACH"); + + if (detach_fd >= 0) { + int saved_errno; + char content[256] = {0}; + ssize_t nread; + int reopened_fd; + + errno = 0; + rc = (int)syscall(SYS_umount2, mount_point, MNT_DETACH); + saved_errno = errno; + check(rc == 0, "umount2 MNT_DETACH succeeds on busy mount"); + + errno = 0; + reopened_fd = open("/tmp/ul-detach/detach.txt", O_RDONLY); + saved_errno = errno; + if (reopened_fd >= 0) + close(reopened_fd); + check(reopened_fd == -1 && saved_errno == ENOENT, + "MNT_DETACH hides mount from new lookups"); + + lseek(detach_fd, 0, SEEK_SET); + nread = read(detach_fd, content, sizeof(content) - 1); + if (nread >= 0) + content[nread] = '\0'; + check(nread > 0 && strstr(content, "detach payload") != NULL, + "MNT_DETACH keeps existing fd usable"); + + close(detach_fd); + } + + cleanup_umount_all(mount_point); + } + /* ================================================================ * Tier 4l: LOOP_CLR_FD EBUSY when mount has open fds *