diff --git a/.github/workflows/dunitest.yml b/.github/workflows/dunitest.yml index a966e810e8..1cbbf413bf 100644 --- a/.github/workflows/dunitest.yml +++ b/.github/workflows/dunitest.yml @@ -17,7 +17,8 @@ jobs: dunitest: name: Dunitest runs-on: ubuntu-latest - timeout-minutes: 30 + # Allow the 30-minute guest test budget plus build, cleanup, and diagnostics. + timeout-minutes: 50 container: image: dragonos/dragonos-dev:v1.23 options: --privileged -v /dev:/dev @@ -41,3 +42,11 @@ jobs: DISK_SAVE_MODE: "1" run: | make test-dunit + + - name: Preserve dunitest serial log + if: always() + uses: actions/upload-artifact@v4 + with: + name: dunitest-serial-x86_64-${{ github.run_id }}-${{ github.run_attempt }} + path: serial_opt.txt + if-no-files-found: error diff --git a/.github/workflows/test-x86.yml b/.github/workflows/test-x86.yml index 8c7ca273ba..534b289078 100644 --- a/.github/workflows/test-x86.yml +++ b/.github/workflows/test-x86.yml @@ -18,7 +18,8 @@ jobs: integration-test: name: Integration Test runs-on: ubuntu-latest - timeout-minutes: 60 + # Allow the 50-minute guest test budget plus build, cleanup, and diagnostics. + timeout-minutes: 75 container: image: dragonos/dragonos-dev:v1.23 options: --privileged -v /dev:/dev @@ -42,6 +43,14 @@ jobs: run: | make test-syscall + - name: Preserve syscall serial log + if: always() + uses: actions/upload-artifact@v4 + with: + name: gvisor-serial-x86_64-${{ github.run_id }}-${{ github.run_attempt }} + path: serial_opt.txt + if-no-files-found: error + - name: Upload test results if: always() && github.repository == 'DragonOS-Community/DragonOS' shell: bash -ileo pipefail {0} diff --git a/kernel/crates/another_ext4/src/ext4/alloc.rs b/kernel/crates/another_ext4/src/ext4/alloc.rs index b677fa3ff1..8d4183833b 100644 --- a/kernel/crates/another_ext4/src/ext4/alloc.rs +++ b/kernel/crates/another_ext4/src/ext4/alloc.rs @@ -400,9 +400,14 @@ impl Ext4 { self.transaction_stage_super_block(transaction, &sb) } - /// Create a new inode, returning the inode and its number + /// Create a new inode with its final owner, returning the inode and its number. #[inline(never)] - pub(super) fn create_inode(&self, mode: InodeMode) -> Result { + pub(super) fn create_inode_with_owner( + &self, + mode: InodeMode, + uid: u32, + gid: u32, + ) -> Result { self.ensure_mutable()?; // Allocate an inode let is_dir = mode.file_type() == FileType::Directory; @@ -413,6 +418,8 @@ impl Ext4 { let mut inode = Box::new(Inode::default()); inode.set_generation(generation); inode.set_mode(mode); + inode.set_uid(uid); + inode.set_gid(gid); inode.extent_init(); let mut inode_ref = InodeRef::new(id, inode); self.write_inode_with_csum(&mut inode_ref)?; @@ -443,6 +450,8 @@ impl Ext4 { mode: InodeMode, major: u32, minor: u32, + uid: u32, + gid: u32, ) -> Result { self.ensure_mutable()?; // Device nodes are never directories @@ -453,6 +462,8 @@ impl Ext4 { let mut inode = Box::new(Inode::default()); inode.set_generation(generation); inode.set_mode(mode); + inode.set_uid(uid); + inode.set_gid(gid); inode.set_device(major, minor); let mut inode_ref = InodeRef::new(id, inode); self.write_inode_with_csum(&mut inode_ref)?; diff --git a/kernel/crates/another_ext4/src/ext4/low_level.rs b/kernel/crates/another_ext4/src/ext4/low_level.rs index 5da18cc5df..d0611ed810 100644 --- a/kernel/crates/another_ext4/src/ext4/low_level.rs +++ b/kernel/crates/another_ext4/src/ext4/low_level.rs @@ -48,6 +48,12 @@ pub struct SetAttr { pub crtime: Option, } +#[derive(Clone, Copy)] +pub struct InodeOwner { + pub uid: u32, + pub gid: u32, +} + impl SetAttr { /// Create a new SetAttr struct with all fields set to None. pub fn new() -> Self { @@ -270,6 +276,10 @@ impl Ext4 { return_error!(ErrCode::EINVAL, "Invalid inode {}", id); } + Ok(Self::file_attr(&inode)) + } + + fn file_attr(inode: &InodeRef) -> FileAttr { // Get device number for device nodes let rdev = if inode.inode.is_device() { inode.inode.device() @@ -277,8 +287,8 @@ impl Ext4 { (0, 0) }; - Ok(FileAttr { - ino: id, + FileAttr { + ino: inode.id, size: inode.inode.size(), blocks: inode.inode.block_count(), atime: inode.inode.atime(), @@ -291,7 +301,7 @@ impl Ext4 { uid: inode.inode.uid(), gid: inode.inode.gid(), rdev, - }) + } } /// Set file attributes. @@ -486,6 +496,7 @@ impl Ext4 { &self, id: InodeId, size: Option, + atime: Option, mtime: Option, ) -> Result<()> { self.ensure_mutable()?; @@ -498,6 +509,9 @@ impl Ext4 { if let Some(size) = size { inode.inode.set_size(size); } + if let Some(atime) = atime { + inode.inode.set_atime(atime); + } if let Some(mtime) = mtime { inode.inode.set_mtime(mtime); } @@ -510,7 +524,7 @@ impl Ext4 { /// /// Call this after successful page-cache write to finalise the new file size. pub fn commit_inode_size(&self, id: InodeId, size: u64, mtime: Option) -> Result<()> { - self.commit_inode_metadata(id, Some(size), mtime) + self.commit_inode_metadata(id, Some(size), None, mtime) } /// Link a newly created inode into `parent`. @@ -557,6 +571,30 @@ impl Ext4 { /// * `ENOTDIR` - `parent` is not a directory /// * `ENOSPC` - No space left on device pub fn create(&self, parent: InodeId, name: &str, mode: InodeMode) -> Result { + self.create_with_owner(parent, name, mode, InodeOwner { uid: 0, gid: 0 }) + } + + pub fn create_with_owner( + &self, + parent: InodeId, + name: &str, + mode: InodeMode, + owner: InodeOwner, + ) -> Result { + Ok(self + .create_with_owner_and_attr(parent, name, mode, owner)? + .ino) + } + + /// Create and link a file, returning the attributes from the authoritative + /// in-memory inode used by the namespace transaction. + pub fn create_with_owner_and_attr( + &self, + parent: InodeId, + name: &str, + mode: InodeMode, + owner: InodeOwner, + ) -> Result { self.ensure_mutable()?; let _metadata_guard = self.lock_direct_metadata_mutation()?; let _namespace_guard = self.namespace_lock.lock(); @@ -567,10 +605,9 @@ impl Ext4 { return_error!(ErrCode::ENOTDIR, "Inode {} is not a directory", parent.id); } // Create child inode and link it to parent directory - let mut child = self.create_inode(mode)?; + let mut child = self.create_inode_with_owner(mode, owner.uid, owner.gid)?; self.link_new_inode_or_free(&mut parent, &mut child, name)?; - // Create file handler - Ok(child.id) + Ok(Self::file_attr(&child)) } /// Create a device node (character or block device). @@ -603,6 +640,41 @@ impl Ext4 { major: u32, minor: u32, ) -> Result { + self.mknod_with_owner( + parent, + name, + mode, + major, + minor, + InodeOwner { uid: 0, gid: 0 }, + ) + } + + pub fn mknod_with_owner( + &self, + parent: InodeId, + name: &str, + mode: InodeMode, + major: u32, + minor: u32, + owner: InodeOwner, + ) -> Result { + Ok(self + .mknod_with_owner_and_attr(parent, name, mode, major, minor, owner)? + .ino) + } + + /// Create and link a device node, returning the attributes from the + /// authoritative in-memory inode used by the namespace transaction. + pub fn mknod_with_owner_and_attr( + &self, + parent: InodeId, + name: &str, + mode: InodeMode, + major: u32, + minor: u32, + owner: InodeOwner, + ) -> Result { self.ensure_mutable()?; let _metadata_guard = self.lock_direct_metadata_mutation()?; let _namespace_guard = self.namespace_lock.lock(); @@ -619,13 +691,13 @@ impl Ext4 { } // Create device inode (uses create_device_inode which sets device number) - let mut child = self.create_device_inode(mode, major, minor)?; + let mut child = self.create_device_inode(mode, major, minor, owner.uid, owner.gid)?; // Link to parent directory self.link_new_inode_or_free(&mut parent_ref, &mut child, name)?; trace!("mknod {} ({}:{}) -> inode {}", name, major, minor, child.id); - Ok(child.id) + Ok(Self::file_attr(&child)) } /// Read data from a file. This function will read exactly `buf.len()` @@ -1404,6 +1476,30 @@ impl Ext4 { /// * `ENOTDIR` - `parent` is not a directory /// * `ENOSPC` - no space left on device pub fn mkdir(&self, parent: InodeId, name: &str, mode: InodeMode) -> Result { + self.mkdir_with_owner(parent, name, mode, InodeOwner { uid: 0, gid: 0 }) + } + + pub fn mkdir_with_owner( + &self, + parent: InodeId, + name: &str, + mode: InodeMode, + owner: InodeOwner, + ) -> Result { + Ok(self + .mkdir_with_owner_and_attr(parent, name, mode, owner)? + .ino) + } + + /// Create and link a directory, returning the attributes from the + /// authoritative in-memory inode used by the namespace transaction. + pub fn mkdir_with_owner_and_attr( + &self, + parent: InodeId, + name: &str, + mode: InodeMode, + owner: InodeOwner, + ) -> Result { self.ensure_mutable()?; let _metadata_guard = self.lock_direct_metadata_mutation()?; let _namespace_guard = self.namespace_lock.lock(); @@ -1415,7 +1511,7 @@ impl Ext4 { } // Create file/directory let mode = mode & InodeMode::PERM_MASK | InodeMode::DIRECTORY; - let mut child = self.create_inode(mode)?; + let mut child = self.create_inode_with_owner(mode, owner.uid, owner.gid)?; // Add "." entry let child_self = child.clone(); if let Err(error) = self.dir_add_entry(&mut child, &child_self, ".") { @@ -1427,7 +1523,7 @@ impl Ext4 { child.inode.set_link_count(1); // Link the new inode self.link_new_inode_or_free(&mut parent, &mut child, name)?; - Ok(child.id) + Ok(Self::file_attr(&child)) } /// Look up a directory entry by name. @@ -1705,6 +1801,39 @@ mod tests { use crate::FileType; use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + #[test] + fn file_attr_is_derived_from_the_authoritative_in_memory_inode() { + let mut inode = Box::new(Inode::default()); + inode.set_mode(InodeMode::CHARDEV | InodeMode::from_bits_retain(0o640)); + inode.set_uid(0x12345); + inode.set_gid(0x23456); + inode.set_size(0x1_0000_0020); + inode.set_block_count(17); + inode.set_atime(11); + inode.set_mtime(12); + inode.set_ctime(13); + inode.set_crtime(14); + inode.set_link_count(2); + inode.set_device(259, 0x1_0002); + let inode = InodeRef::new(42, inode); + + let attr = Ext4::file_attr(&inode); + + assert_eq!(attr.ino, 42); + assert_eq!(attr.ftype, FileType::CharacterDev); + assert_eq!(attr.perm.bits(), 0o640); + assert_eq!(attr.uid, 0x12345); + assert_eq!(attr.gid, 0x23456); + assert_eq!(attr.size, 0x1_0000_0020); + assert_eq!(attr.blocks, 17); + assert_eq!( + (attr.atime, attr.mtime, attr.ctime, attr.crtime), + (11, 12, 13, 14) + ); + assert_eq!(attr.links, 2); + assert_eq!(attr.rdev, (259, 0x1_0002)); + } + struct StubBlockDevice { sb_block: Block, } diff --git a/kernel/crates/another_ext4/src/ext4/mod.rs b/kernel/crates/another_ext4/src/ext4/mod.rs index e43a511467..899a2d8bfa 100644 --- a/kernel/crates/another_ext4/src/ext4/mod.rs +++ b/kernel/crates/another_ext4/src/ext4/mod.rs @@ -17,7 +17,7 @@ mod orphan; mod rw; mod xattr_reclaim; -pub use low_level::SetAttr; +pub use low_level::{InodeOwner, SetAttr}; /// Simple fixed-size inode cache. /// When full, the entire cache is cleared (simple but effective for common workloads). diff --git a/kernel/crates/another_ext4/src/lib.rs b/kernel/crates/another_ext4/src/lib.rs index 82311653b2..1b8b21eaa7 100644 --- a/kernel/crates/another_ext4/src/lib.rs +++ b/kernel/crates/another_ext4/src/lib.rs @@ -14,7 +14,7 @@ mod prelude; pub use constants::{BLOCK_SIZE, EXT4_ROOT_INO, INODE_BLOCK_SIZE}; pub use error::{ErrCode, Ext4Error}; -pub use ext4::{Ext4, SetAttr}; +pub use ext4::{Ext4, InodeOwner, SetAttr}; pub use ext4_defs::{ Block, BlockDevice, DirEntry, FileAttr, FileType, Inode, InodeMode, InodeReclaimError, InodeReclaimHandle, InodeRef, diff --git a/kernel/crates/rust-slabmalloc/src/pages.rs b/kernel/crates/rust-slabmalloc/src/pages.rs index e4304b79f9..bac4802633 100644 --- a/kernel/crates/rust-slabmalloc/src/pages.rs +++ b/kernel/crates/rust-slabmalloc/src/pages.rs @@ -1,5 +1,6 @@ use crate::*; use alloc::boxed::Box; +use core::mem::MaybeUninit; use core::sync::atomic::{AtomicU64, Ordering}; /// A trait defining bitfield operations we need for tracking allocated objects within a page. pub(crate) trait Bitfield { @@ -265,7 +266,7 @@ pub enum PageState { pub struct ObjectPage<'a> { /// Holds memory objects. #[allow(dead_code)] - data: [u8; OBJECT_PAGE_SIZE - OBJECT_PAGE_METADATA_OVERHEAD], + data: MaybeUninit<[u8; OBJECT_PAGE_SIZE - OBJECT_PAGE_METADATA_OVERHEAD]>, /// Next element in list (used by `PageList`). next: Rawlink>, @@ -282,7 +283,22 @@ pub struct ObjectPage<'a> { } impl<'a> ObjectPage<'a> { pub fn new() -> Box> { - unsafe { Box::new_uninit().assume_init() } + let mut page = Box::>::new_uninit(); + unsafe { + // The data area is intentionally uninitialized object storage. It + // is wrapped in MaybeUninit so constructing ObjectPage is sound; + // initialize only the allocator metadata that refill() consumes. + // Doing this in place avoids both a 4 KiB stack temporary and a + // redundant full-page clear on the slab refill hot path. + let raw = page.as_mut_ptr(); + core::ptr::addr_of_mut!((*raw).next).write(Rawlink::none()); + core::ptr::addr_of_mut!((*raw).prev).write(Rawlink::none()); + core::ptr::addr_of_mut!((*raw).state).write(PageState::Empty); + core::ptr::addr_of_mut!((*raw)._state_pad).write([0; 7]); + core::ptr::addr_of_mut!((*raw).bitfield) + .write(core::array::from_fn(|_| AtomicU64::new(0))); + page.assume_init() + } } } diff --git a/kernel/src/driver/tty/tty_device.rs b/kernel/src/driver/tty/tty_device.rs index fefab48d32..0fb192f914 100644 --- a/kernel/src/driver/tty/tty_device.rs +++ b/kernel/src/driver/tty/tty_device.rs @@ -41,6 +41,7 @@ use crate::{ mm::VirtAddr, process::ProcessManager, syscall::user_access::{UserBufferReader, UserBufferWriter}, + time::PosixTimeSpec, }; use super::{ @@ -457,6 +458,12 @@ impl IndexNode for TtyDevice { Ok(()) } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let mut guard = self.inner_write(); + crate::filesystem::vfs::update_atime_locked(&mut guard.metadata, now, relatime); + Ok(()) + } + fn page_cache(&self) -> Option> { // TTY设备是字符设备,不需要页面缓存 None diff --git a/kernel/src/filesystem/cgroup2/inode.rs b/kernel/src/filesystem/cgroup2/inode.rs index 5ea8fb126e..a5b69cda89 100644 --- a/kernel/src/filesystem/cgroup2/inode.rs +++ b/kernel/src/filesystem/cgroup2/inode.rs @@ -608,6 +608,12 @@ impl IndexNode for Cgroup2Inode { Ok(()) } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let mut inner = self.inner.lock(); + crate::filesystem::vfs::update_atime_locked(&mut inner.metadata, now, relatime); + Ok(()) + } + fn create( &self, name: &str, diff --git a/kernel/src/filesystem/devfs/full_dev.rs b/kernel/src/filesystem/devfs/full_dev.rs index 8fc4f6304a..3d3a3dd987 100644 --- a/kernel/src/filesystem/devfs/full_dev.rs +++ b/kernel/src/filesystem/devfs/full_dev.rs @@ -111,6 +111,12 @@ impl IndexNode for LockedFullInode { return Ok(()); } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let mut inode = self.0.lock(); + crate::filesystem::vfs::update_atime_locked(&mut inode.metadata, now, relatime); + Ok(()) + } + fn read_at( &self, _offset: usize, diff --git a/kernel/src/filesystem/devfs/mod.rs b/kernel/src/filesystem/devfs/mod.rs index 74eb66c7b8..394243505b 100644 --- a/kernel/src/filesystem/devfs/mod.rs +++ b/kernel/src/filesystem/devfs/mod.rs @@ -903,6 +903,12 @@ impl IndexNode for LockedDevFSInode { return Ok(()); } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let mut inode = self.0.lock(); + crate::filesystem::vfs::update_atime_locked(&mut inode.metadata, now, relatime); + Ok(()) + } + /// 读设备 - 应该调用设备的函数读写,而不是通过文件系统读写,仅支持符号链接的读取 fn read_at( &self, diff --git a/kernel/src/filesystem/devfs/null_dev.rs b/kernel/src/filesystem/devfs/null_dev.rs index a11f5bc035..9c514cb41d 100644 --- a/kernel/src/filesystem/devfs/null_dev.rs +++ b/kernel/src/filesystem/devfs/null_dev.rs @@ -119,6 +119,12 @@ impl IndexNode for LockedNullInode { return Ok(()); } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let mut inode = self.0.lock(); + crate::filesystem::vfs::update_atime_locked(&mut inode.metadata, now, relatime); + Ok(()) + } + /// 读设备 - 应该调用设备的函数读写,而不是通过文件系统读写 fn read_at( &self, diff --git a/kernel/src/filesystem/devfs/random_dev.rs b/kernel/src/filesystem/devfs/random_dev.rs index 592246bcf0..35aa23c5bc 100644 --- a/kernel/src/filesystem/devfs/random_dev.rs +++ b/kernel/src/filesystem/devfs/random_dev.rs @@ -115,6 +115,12 @@ impl IndexNode for LockedRandomInode { Ok(()) } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let mut inode = self.0.lock(); + crate::filesystem::vfs::update_atime_locked(&mut inode.metadata, now, relatime); + Ok(()) + } + fn mmap(&self, _start: usize, _len: usize, _offset: usize) -> Result<(), SystemError> { Err(SystemError::ENODEV) } diff --git a/kernel/src/filesystem/devfs/zero_dev.rs b/kernel/src/filesystem/devfs/zero_dev.rs index dfd0a3607a..5cb3cba2e9 100644 --- a/kernel/src/filesystem/devfs/zero_dev.rs +++ b/kernel/src/filesystem/devfs/zero_dev.rs @@ -119,6 +119,12 @@ impl IndexNode for LockedZeroInode { return Ok(()); } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let mut inode = self.0.lock(); + crate::filesystem::vfs::update_atime_locked(&mut inode.metadata, now, relatime); + Ok(()) + } + /// 读设备 - 应该调用设备的函数读写,而不是通过文件系统读写 fn read_at( &self, diff --git a/kernel/src/filesystem/ext4/filesystem.rs b/kernel/src/filesystem/ext4/filesystem.rs index fbd8f0d24b..fc05997286 100644 --- a/kernel/src/filesystem/ext4/filesystem.rs +++ b/kernel/src/filesystem/ext4/filesystem.rs @@ -2,7 +2,9 @@ use crate::{ driver::base::{block::gendisk::GenDisk, device::device_number::DeviceNumber}, exception::workqueue::{Work, WorkQueue}, filesystem::{ - ext4::inode::{Ext4Inode, Ext4InodeLifecycle, Ext4InodeLifecycleState, InodeDirtyState}, + ext4::inode::{ + Ext4Inode, Ext4InodeLifecycle, Ext4InodeLifecycleState, Ext4InodeTimes, InodeDirtyState, + }, vfs::{ self, fcntl::AtFlags, @@ -412,13 +414,12 @@ impl Ext4FileSystem { pub(super) fn publish_allocated_inode( self: &Arc, - inode_num: u32, + attr: another_ext4::FileAttr, dname: DName, parent: Option>, - file_type: another_ext4::FileType, _reuse_guard: &RwSemReadGuard<'_, ()>, ) -> Result, SystemError> { - self.get_or_create_inode_inner(inode_num, dname, parent, true, Some(file_type)) + self.get_or_create_inode_inner(attr.ino, dname, parent, true, Some(attr)) } fn get_or_create_inode_inner( @@ -427,7 +428,7 @@ impl Ext4FileSystem { dname: DName, parent: Option>, reuse_guard_held: bool, - known_file_type: Option, + allocated_attr: Option, ) -> Result, SystemError> { let mut candidate = None; let mut admission_guard = None; @@ -463,13 +464,22 @@ impl Ext4FileSystem { if !reuse_guard_held { admission_guard = Some(self.inode_reuse_barrier.read()); } - candidate = Some(LockedExt4Inode::new( - inode_num, - Arc::downgrade(self), - dname.clone(), - parent.clone(), - known_file_type, - )?); + candidate = Some(if let Some(attr) = allocated_attr.as_ref() { + LockedExt4Inode::new_with_attr( + inode_num, + Arc::downgrade(self), + dname.clone(), + parent.clone(), + attr, + )? + } else { + LockedExt4Inode::new( + inode_num, + Arc::downgrade(self), + dname.clone(), + parent.clone(), + )? + }); continue; } let inode = candidate.take().expect("candidate checked above"); @@ -582,33 +592,91 @@ impl Ext4FileSystem { inode: &Arc, dirty: InodeDirtyState, ) -> Result<(), SystemError> { + Self::mark_inode_dirty_with(inode, dirty, |_| {}) + } + + pub(super) fn mark_inode_atime_dirty( + inode: &Arc, + atime: u32, + relatime: bool, + ) -> Result<(), SystemError> { + let now = crate::time::PosixTimeSpec::new(atime.into(), 0); + Self::mark_inode_dirty_if( + inode, + InodeDirtyState::ATIME_DIRTY, + |guard| { + let times = guard.cached_times; + vfs::should_update_atime( + crate::time::PosixTimeSpec::new(times.atime.into(), 0), + crate::time::PosixTimeSpec::new(times.mtime.into(), 0), + crate::time::PosixTimeSpec::new(times.ctime.into(), 0), + now, + relatime, + ) + }, + |guard| { + guard.cached_times.atime = atime; + guard.cached_atime_version = guard.cached_atime_version.wrapping_add(1); + }, + ) + } + + fn mark_inode_dirty_with( + inode: &Arc, + dirty: InodeDirtyState, + update_cached_metadata: F, + ) -> Result<(), SystemError> + where + F: FnOnce(&mut super::inode::Ext4Inode), + { + Self::mark_inode_dirty_if(inode, dirty, |_| true, update_cached_metadata) + } + + fn mark_inode_dirty_if( + inode: &Arc, + dirty: InodeDirtyState, + should_update: P, + update_cached_metadata: F, + ) -> Result<(), SystemError> + where + P: FnOnce(&super::inode::Ext4Inode) -> bool, + F: FnOnce(&mut super::inode::Ext4Inode), + { let _operation = inode.lifecycle().begin_operation()?; // Acquire a prospective queue ownership before publishing dirty state. // If the inode is already queued, the existing AsyncWork owner prevents // this temporary retain/release pair from creating a false-zero edge. inode.retain(InodeRetentionKind::AsyncWork)?; - let (fs, should_queue) = { + let queue_fs = { let mut guard = inode.inner.lock(); - guard.dirty_state.insert(dirty); + if !should_update(&guard) { + drop(guard); + inode.release(InodeRetentionKind::AsyncWork); + return Ok(()); + } let should_queue = !guard .dirty_state .intersects(InodeDirtyState::QUEUED | InodeDirtyState::WRITEBACK); + let queue_fs = if should_queue { + let Some(fs) = guard.fs_ptr.upgrade() else { + drop(guard); + inode.release(InodeRetentionKind::AsyncWork); + return Err(SystemError::EIO); + }; + Some(fs) + } else { + None + }; + update_cached_metadata(&mut guard); + guard.dirty_state.insert(dirty); if should_queue { guard.dirty_state.insert(InodeDirtyState::QUEUED); } - (guard.fs_ptr.upgrade(), should_queue) + queue_fs }; - if should_queue { - if let Some(fs) = fs { - fs.dirty_inodes.lock().push(inode.clone()); - } else { - let mut guard = inode.inner.lock(); - guard.dirty_state.remove(InodeDirtyState::QUEUED); - drop(guard); - inode.release(InodeRetentionKind::AsyncWork); - return Err(SystemError::EIO); - } + if let Some(fs) = queue_fs { + fs.dirty_inodes.lock().push(inode.clone()); } else { inode.release(InodeRetentionKind::AsyncWork); } @@ -629,11 +697,9 @@ impl Ext4FileSystem { let mut guard = inode.inner.lock(); if !guard.dirty_state.contains(InodeDirtyState::QUEUED) - || guard.dirty_state.intersects( - InodeDirtyState::SIZE_DIRTY - | InodeDirtyState::MTIME_DIRTY - | InodeDirtyState::WRITEBACK, - ) + || guard + .dirty_state + .intersects(InodeDirtyState::PERSISTENT_DIRTY | InodeDirtyState::WRITEBACK) { return; } @@ -653,7 +719,7 @@ impl Ext4FileSystem { let mut guard = inode.inner.lock(); if guard .dirty_state - .intersects(InodeDirtyState::SIZE_DIRTY | InodeDirtyState::MTIME_DIRTY) + .intersects(InodeDirtyState::PERSISTENT_DIRTY) { guard.dirty_state.insert(InodeDirtyState::QUEUED); drop(guard); @@ -689,7 +755,7 @@ impl Ext4FileSystem { let mut guard = inode.inner.lock(); let has_dirty = guard .dirty_state - .intersects(InodeDirtyState::SIZE_DIRTY | InodeDirtyState::MTIME_DIRTY); + .intersects(InodeDirtyState::PERSISTENT_DIRTY); if !has_dirty { guard .dirty_state @@ -716,8 +782,7 @@ impl Ext4FileSystem { let page_cache = { let mut guard = inode.inner.lock(); guard.dirty_state.remove( - InodeDirtyState::SIZE_DIRTY - | InodeDirtyState::MTIME_DIRTY + InodeDirtyState::PERSISTENT_DIRTY | InodeDirtyState::QUEUED | InodeDirtyState::WRITEBACK, ); @@ -740,12 +805,20 @@ impl Ext4FileSystem { let result = { let _operation = operation; let _io_guard = inode.io_lock.lock(); - let (fs, inode_num, snapshot_dirty, cached_size, cached_mtime) = { + let ( + fs, + inode_num, + snapshot_dirty, + cached_size, + cached_times, + cached_atime_version, + cached_mtime_version, + ) = { let mut guard = inode.inner.lock(); guard.dirty_state.remove(InodeDirtyState::QUEUED); let snapshot_dirty = guard .dirty_state - .intersection(InodeDirtyState::SIZE_DIRTY | InodeDirtyState::MTIME_DIRTY); + .intersection(InodeDirtyState::PERSISTENT_DIRTY); if snapshot_dirty.is_empty() { guard.dirty_state.remove(InodeDirtyState::WRITEBACK); release_async_owner = true; @@ -758,7 +831,9 @@ impl Ext4FileSystem { guard.inner_inode_num, snapshot_dirty, guard.cached_file_size, - guard.cached_mtime, + guard.cached_times, + guard.cached_atime_version, + guard.cached_mtime_version, ) }; @@ -780,12 +855,17 @@ impl Ext4FileSystem { }; size.and_then(|size| { let mtime = if snapshot_dirty.contains(InodeDirtyState::MTIME_DIRTY) { - cached_mtime + Some(cached_times.mtime) + } else { + None + }; + let atime = if snapshot_dirty.contains(InodeDirtyState::ATIME_DIRTY) { + Some(cached_times.atime) } else { None }; LockedExt4Inode::retry_metadata_contention(|| { - fs.fs.commit_inode_metadata(inode_num, size, mtime) + fs.fs.commit_inode_metadata(inode_num, size, atime, mtime) }) }) } else { @@ -800,15 +880,20 @@ impl Ext4FileSystem { guard.dirty_state.remove(InodeDirtyState::SIZE_DIRTY); } if snapshot_dirty.contains(InodeDirtyState::MTIME_DIRTY) - && guard.cached_mtime == cached_mtime + && guard.cached_mtime_version == cached_mtime_version { guard.dirty_state.remove(InodeDirtyState::MTIME_DIRTY); } + if snapshot_dirty.contains(InodeDirtyState::ATIME_DIRTY) + && guard.cached_atime_version == cached_atime_version + { + guard.dirty_state.remove(InodeDirtyState::ATIME_DIRTY); + } } guard.dirty_state.remove(InodeDirtyState::WRITEBACK); if guard .dirty_state - .intersects(InodeDirtyState::SIZE_DIRTY | InodeDirtyState::MTIME_DIRTY) + .intersects(InodeDirtyState::PERSISTENT_DIRTY) { guard.dirty_state.insert(InodeDirtyState::QUEUED); should_requeue = true; @@ -882,6 +967,7 @@ impl Ext4FileSystem { mount_options.write_barrier, )? }; + let root_attr = fs.getattr(another_ext4::EXT4_ROOT_INO)?; let root_inode: Arc = Arc::new_cyclic(|self_ref: &Weak| LockedExt4Inode { inner: Mutex::new(Ext4Inode { @@ -895,7 +981,9 @@ impl Ext4FileSystem { self_ref: self_ref.clone(), special_node: None, cached_file_size: None, - cached_mtime: None, + cached_times: Ext4InodeTimes::from(&root_attr), + cached_atime_version: 0, + cached_mtime_version: 0, dirty_state: super::inode::InodeDirtyState::empty(), }), io_lock: Mutex::new(()), diff --git a/kernel/src/filesystem/ext4/inode.rs b/kernel/src/filesystem/ext4/inode.rs index cc6b09425d..a0f3a62ca9 100644 --- a/kernel/src/filesystem/ext4/inode.rs +++ b/kernel/src/filesystem/ext4/inode.rs @@ -5,8 +5,8 @@ use crate::{ page_cache::{AsyncPageCacheBackend, PageCache}, vfs::{ self, syscall::RenameFlags, utils::DName, vcore::generate_inode_id, FilePrivateData, - IndexNode, InodeFlags, InodeId, InodeMode, InodeRetentionState, SpecialNodeData, - XattrFlags, + IndexNode, InodeFlags, InodeId, InodeMode, InodeRetentionState, SetMetadataMask, + SpecialNodeData, XattrFlags, }, }, ipc::pipe::LockedPipeInode, @@ -46,13 +46,16 @@ bitflags! { const SIZE_DIRTY = 1 << 0; /// mtime 变更未刷盘,对应 I_DIRTY_DATASYNC (1 << 1) const MTIME_DIRTY = 1 << 1; + /// atime 变更未刷盘。读路径仅更新缓存,由 inode writeback 持久化。 + const ATIME_DIRTY = 1 << 2; /// 该 inode 已在文件系统 dirty_inodes 队列中。 const QUEUED = 1 << 3; /// 该 inode 正在执行元数据写回。 const WRITEBACK = 1 << 4; - /// 仅时间戳脏(lazytime),对应 I_DIRTY_TIME (1 << 11) - #[allow(dead_code)] - const TIME_DIRTY = 1 << 11; + /// 需要持久化的缓存元数据集合。 + const PERSISTENT_DIRTY = Self::SIZE_DIRTY.bits() + | Self::MTIME_DIRTY.bits() + | Self::ATIME_DIRTY.bits(); } } @@ -204,6 +207,23 @@ impl Drop for Ext4InodeOperation { type PrivateData<'a> = crate::libs::mutex::MutexGuard<'a, vfs::FilePrivateData>; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct Ext4InodeTimes { + pub(super) atime: u32, + pub(super) mtime: u32, + pub(super) ctime: u32, +} + +impl From<&another_ext4::FileAttr> for Ext4InodeTimes { + fn from(attr: &another_ext4::FileAttr) -> Self { + Self { + atime: attr.atime, + mtime: attr.mtime, + ctime: attr.ctime, + } + } +} + pub struct Ext4Inode { // 对应another_ext4里面的inode号,用于在ext4文件系统中查找相应的inode pub(super) inner_inode_num: u32, @@ -227,8 +247,16 @@ pub struct Ext4Inode { /// 缓存的文件大小,避免频繁调用 getattr/setattr。 /// None 表示未初始化(第一次写时从磁盘读取并缓存)。 pub(super) cached_file_size: Option, - /// 缓存的 mtime。普通 buffered write 先更新内存态,fsync/O_SYNC 再刷到磁盘。 - pub(super) cached_mtime: Option, + /// Linux inode-style authoritative in-memory timestamps. They are loaded + /// before the canonical inode is published; atime/mtime writeback is lazy. + pub(super) cached_times: Ext4InodeTimes, + /// Monotonic sequence for atime cache mutations. Disk commits compare the + /// sequence, not only the value, so A->B->A updates cannot be lost. + pub(super) cached_atime_version: u64, + /// Monotonic sequence for mtime cache mutations; mmap write preparation + /// updates mtime after releasing io_lock, so setters/writeback use this + /// sequence to avoid same-second ABA and lost dirty state. + pub(super) cached_mtime_version: u64, /// 脏状态标志位,对应 Linux `inode->i_state & I_DIRTY_*`。 pub(super) dirty_state: InodeDirtyState, } @@ -282,29 +310,52 @@ impl IndexNode for LockedExt4Inode { mode: vfs::InodeMode, ) -> Result, SystemError> { let _operation = self.begin_operation()?; + let _io = self.io_lock.lock(); let _namespace = self.namespace_lock.lock(); + let parent_metadata = self.metadata()?; + let init = vfs::permission::child_inode_init(&parent_metadata, file_type, mode); let mut guard = self.inner.lock(); // another_ext4的高4位是文件类型,低12位是权限 - let file_mode = InodeMode::from(file_type).union(mode); + let file_mode = InodeMode::from(file_type).union(init.mode); let file_mode = another_ext4::InodeMode::from_bits_truncate(file_mode.bits() as u16); let fs = guard.concret_fs(); let _reuse = fs.begin_allocation()?; let ext4 = &fs.fs; + // Resolve the parent lifetime before publishing the on-disk name so + // no fallible parent lookup remains after the namespace transaction. + let self_arc = guard.self_ref.upgrade().ok_or(SystemError::ENOENT)?; - let id = if file_type == vfs::FileType::Dir { - Self::retry_metadata_contention(|| ext4.mkdir(guard.inner_inode_num, name, file_mode))? + let attr = if file_type == vfs::FileType::Dir { + Self::retry_metadata_contention(|| { + ext4.mkdir_with_owner_and_attr( + guard.inner_inode_num, + name, + file_mode, + another_ext4::InodeOwner { + uid: init.uid as u32, + gid: init.gid as u32, + }, + ) + })? } else { - Self::retry_metadata_contention(|| ext4.create(guard.inner_inode_num, name, file_mode))? + Self::retry_metadata_contention(|| { + ext4.create_with_owner_and_attr( + guard.inner_inode_num, + name, + file_mode, + another_ext4::InodeOwner { + uid: init.uid as u32, + gid: init.gid as u32, + }, + ) + })? }; let dname = DName::from(name); - // 通过self_ref获取Arc,然后转换为Arc - let self_arc = guard.self_ref.upgrade().ok_or(SystemError::ENOENT)?; let inode = fs.publish_allocated_inode( - id, + attr, dname.clone(), Some(Arc::downgrade(&self_arc)), - Self::disk_file_type(file_type), &_reuse, )?; // 更新 children 缓存 @@ -466,7 +517,8 @@ impl IndexNode for LockedExt4Inode { let self_arc = { let mut guard = self.inner.lock(); guard.cached_file_size = Some(current_file_size); - guard.cached_mtime = Some(time); + guard.cached_times.mtime = time; + guard.cached_mtime_version = guard.cached_mtime_version.wrapping_add(1); guard.self_ref.upgrade().ok_or(SystemError::ENOENT)? }; Ext4FileSystem::mark_inode_dirty( @@ -658,19 +710,21 @@ impl IndexNode for LockedExt4Inode { fn metadata(&self) -> Result { let _operation = self.begin_operation()?; - let (fs, inode_num, vfs_inode_id, cached_size, cached_mtime) = { + let (fs, inode_num, vfs_inode_id, cached_size) = { let guard = self.inner.lock(); ( guard.concret_fs(), guard.inner_inode_num, guard.vfs_inode_id, guard.cached_file_size, - guard.cached_mtime, ) }; let attr = fs.fs.getattr(inode_num)?; + // Disk attributes provide non-cached fields. Read the authoritative + // in-memory values afterwards so a concurrent atime update cannot be + // hidden by a stale pre-getattr snapshot. + let cached_times = self.inner.lock().cached_times; let size = cached_size.unwrap_or(attr.size); - let mtime = cached_mtime.unwrap_or(attr.mtime); // dev_id: filesystem device number (st_dev) let dev_id = fs.raw_dev.data() as usize; @@ -691,10 +745,10 @@ impl IndexNode for LockedExt4Inode { size: size as i64, blk_size: another_ext4::BLOCK_SIZE, blocks: attr.blocks as usize, - atime: PosixTimeSpec::new(attr.atime.into(), 0), + atime: PosixTimeSpec::new(cached_times.atime.into(), 0), btime: PosixTimeSpec::new(attr.atime.into(), 0), - mtime: PosixTimeSpec::new(mtime.into(), 0), - ctime: PosixTimeSpec::new(attr.ctime.into(), 0), + mtime: PosixTimeSpec::new(cached_times.mtime.into(), 0), + ctime: PosixTimeSpec::new(cached_times.ctime.into(), 0), file_type: Self::file_type(attr.ftype), mode: InodeMode::from_bits_truncate(attr.perm.bits() as u32), flags: InodeFlags::empty(), @@ -768,14 +822,20 @@ impl IndexNode for LockedExt4Inode { fn set_metadata(&self, metadata: &vfs::Metadata) -> Result<(), SystemError> { let _operation = self.begin_operation()?; + let _io_guard = self.io_lock.lock(); let mode = metadata.mode.union(InodeMode::from(metadata.file_type)); let to_ext4_time = |time: &PosixTimeSpec| -> u32 { time.tv_sec.max(0).min(u32::MAX as i64) as u32 }; - let (fs, inode_num) = { + let (fs, inode_num, before_atime_version, before_mtime_version) = { let guard = self.inner.lock(); - (guard.concret_fs(), guard.inner_inode_num) + ( + guard.concret_fs(), + guard.inner_inode_num, + guard.cached_atime_version, + guard.cached_mtime_version, + ) }; let ext4 = &fs.fs; Self::retry_metadata_contention(|| { @@ -798,16 +858,125 @@ impl IndexNode for LockedExt4Inode { { let mut guard = self.inner.lock(); guard.cached_file_size = Some(metadata.size as u64); - guard.cached_mtime = Some(to_ext4_time(&metadata.mtime)); - guard - .dirty_state - .remove(InodeDirtyState::SIZE_DIRTY | InodeDirtyState::MTIME_DIRTY); + if guard.cached_atime_version == before_atime_version { + guard.cached_times.atime = to_ext4_time(&metadata.atime); + guard.cached_atime_version = guard.cached_atime_version.wrapping_add(1); + guard.dirty_state.remove(InodeDirtyState::ATIME_DIRTY); + } + if guard.cached_mtime_version == before_mtime_version { + guard.cached_times.mtime = to_ext4_time(&metadata.mtime); + guard.cached_mtime_version = guard.cached_mtime_version.wrapping_add(1); + guard.dirty_state.remove(InodeDirtyState::MTIME_DIRTY); + } + guard.cached_times.ctime = to_ext4_time(&metadata.ctime); + guard.dirty_state.remove(InodeDirtyState::SIZE_DIRTY); } self.release_clean_metadata_queue_owner(&fs); Ok(()) } + fn set_metadata_masked( + &self, + metadata: &vfs::Metadata, + mask: SetMetadataMask, + ) -> Result<(), SystemError> { + if mask.is_empty() { + return Ok(()); + } + + let _operation = self.begin_operation()?; + let _io_guard = self.io_lock.lock(); + let to_ext4_time = + |time: &PosixTimeSpec| -> u32 { time.tv_sec.max(0).min(u32::MAX as i64) as u32 }; + let (fs, inode_num, before_atime_version, before_mtime_version) = { + let guard = self.inner.lock(); + ( + guard.concret_fs(), + guard.inner_inode_num, + guard.cached_atime_version, + guard.cached_mtime_version, + ) + }; + let mode = metadata.mode.union(InodeMode::from(metadata.file_type)); + let atime = mask + .contains(SetMetadataMask::ATIME) + .then(|| to_ext4_time(&metadata.atime)); + let mtime = mask + .contains(SetMetadataMask::MTIME) + .then(|| to_ext4_time(&metadata.mtime)); + let ctime = mask + .contains(SetMetadataMask::CTIME) + .then(|| to_ext4_time(&metadata.ctime)); + + Self::retry_metadata_contention(|| { + fs.fs.setattr( + inode_num, + another_ext4::SetAttr { + mode: mask + .contains(SetMetadataMask::MODE) + .then(|| another_ext4::InodeMode::from_bits_truncate(mode.bits() as u16)), + uid: mask + .contains(SetMetadataMask::UID) + .then_some(metadata.uid as u32), + gid: mask + .contains(SetMetadataMask::GID) + .then_some(metadata.gid as u32), + atime, + mtime, + ctime, + ..Default::default() + }, + ) + })?; + + { + let mut guard = self.inner.lock(); + // Buffered reads/writes can update cached times without io_lock. + // Preserve and leave dirty any value that changed while setattr + // was in flight; writeback will then persist the newer value. + if let Some(atime) = atime { + if guard.cached_atime_version == before_atime_version { + guard.cached_times.atime = atime; + guard.cached_atime_version = guard.cached_atime_version.wrapping_add(1); + guard.dirty_state.remove(InodeDirtyState::ATIME_DIRTY); + } + } + if let Some(mtime) = mtime { + if guard.cached_mtime_version == before_mtime_version { + guard.cached_times.mtime = mtime; + guard.cached_mtime_version = guard.cached_mtime_version.wrapping_add(1); + guard.dirty_state.remove(InodeDirtyState::MTIME_DIRTY); + } + } + if let Some(ctime) = ctime { + guard.cached_times.ctime = ctime; + } + } + self.release_clean_metadata_queue_owner(&fs); + Ok(()) + } + + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let atime = now.tv_sec.max(0).min(u32::MAX as i64) as u32; + let now = PosixTimeSpec::new(atime.into(), 0); + let self_arc = { + let guard = self.inner.lock(); + let times = guard.cached_times; + if !vfs::should_update_atime( + PosixTimeSpec::new(times.atime.into(), 0), + PosixTimeSpec::new(times.mtime.into(), 0), + PosixTimeSpec::new(times.ctime.into(), 0), + now, + relatime, + ) { + return Ok(()); + } + guard.self_ref.upgrade().ok_or(SystemError::ENOENT)? + }; + Ext4FileSystem::mark_inode_atime_dirty(&self_arc, atime, relatime) + } + fn resize(&self, len: usize) -> Result<(), SystemError> { let _operation = self.begin_operation()?; let (fs, inode_num, page_cache) = { @@ -841,9 +1010,7 @@ impl IndexNode for LockedExt4Inode { { let mut guard = self.inner.lock(); guard.cached_file_size = Some(len as u64); - guard - .dirty_state - .remove(InodeDirtyState::SIZE_DIRTY | InodeDirtyState::MTIME_DIRTY); + guard.dirty_state.remove(InodeDirtyState::SIZE_DIRTY); } self.release_clean_metadata_queue_owner(&fs); Ok(()) @@ -1079,49 +1246,67 @@ impl IndexNode for LockedExt4Inode { return self.create(filename, vfs::FileType::File, mode); } let _operation = self.begin_operation()?; + let _io = self.io_lock.lock(); let _namespace = self.namespace_lock.lock(); + let parent_metadata = self.metadata()?; + let init = vfs::permission::child_inode_init(&parent_metadata, file_type, mode); let mut guard = self.inner.lock(); let fs = guard.concret_fs(); let _reuse = fs.begin_allocation()?; let ext4 = &fs.fs; let inode_num = guard.inner_inode_num; + // Resolve the parent lifetime before publishing the on-disk name so + // no fallible parent lookup remains after the namespace transaction. + let self_arc = guard.self_ref.upgrade().ok_or(SystemError::ENOENT)?; if ext4.getattr(inode_num)?.ftype != FileType::Directory { return Err(SystemError::ENOTDIR); } // VFS InodeMode(u32) → another_ext4 InodeMode(u16) - let file_mode = another_ext4::InodeMode::from_bits_truncate(mode.bits() as u16); + let file_mode = another_ext4::InodeMode::from_bits_truncate(init.mode.bits() as u16); // Create inode based on file type - let id = if matches!( + let attr = if matches!( file_type, vfs::FileType::CharDevice | vfs::FileType::BlockDevice ) { // Character/block device: use mknod to store device number in i_block Self::retry_metadata_contention(|| { - ext4.mknod( + ext4.mknod_with_owner_and_attr( inode_num, filename, file_mode, dev_t.major().data(), dev_t.minor(), + another_ext4::InodeOwner { + uid: init.uid as u32, + gid: init.gid as u32, + }, ) })? } else { // FIFO, Socket, etc.: use regular create (no device number needed) - Self::retry_metadata_contention(|| ext4.create(inode_num, filename, file_mode))? + Self::retry_metadata_contention(|| { + ext4.create_with_owner_and_attr( + inode_num, + filename, + file_mode, + another_ext4::InodeOwner { + uid: init.uid as u32, + gid: init.gid as u32, + }, + ) + })? }; // Wrap as VFS inode and cache let dname = DName::from(filename); - let self_arc = guard.self_ref.upgrade().ok_or(SystemError::ENOENT)?; let inode = fs.publish_allocated_inode( - id, + attr, dname.clone(), Some(Arc::downgrade(&self_arc)), - Self::disk_file_type(file_type), &_reuse, )?; guard.children.insert(dname, inode.clone()); @@ -1141,6 +1326,16 @@ impl IndexNode for LockedExt4Inode { flags: RenameFlags, ) -> Result<(), SystemError> { let _operation = self.begin_operation()?; + let _source_io = self.io_lock.lock(); + let whiteout_init = if flags.contains(RenameFlags::WHITEOUT) { + Some(vfs::permission::child_inode_init( + &self.metadata()?, + vfs::FileType::CharDevice, + InodeMode::S_IFCHR | InodeMode::from_bits_truncate(0o600), + )) + } else { + None + }; let target_locked = target .clone() .downcast_arc::() @@ -1247,6 +1442,7 @@ impl IndexNode for LockedExt4Inode { let mut resulting_whiteout = None; if flags.contains(RenameFlags::WHITEOUT) { + let whiteout_init = whiteout_init.as_ref().ok_or(SystemError::EIO)?; let mut temp_name = String::new(); let mut whiteout_inode = None; let source_parent = self @@ -1261,21 +1457,24 @@ impl IndexNode for LockedExt4Inode { continue; } let allocation = ext4_fs.begin_allocation()?; - let whiteout_num = Self::retry_metadata_contention(|| { - ext4.mknod( + let whiteout_attr = Self::retry_metadata_contention(|| { + ext4.mknod_with_owner_and_attr( src_inode_num, &candidate, another_ext4::InodeMode::CHARDEV | another_ext4::InodeMode::from_bits_retain(0o600), WHITEOUT_DEV.major().data(), WHITEOUT_DEV.minor(), + another_ext4::InodeOwner { + uid: whiteout_init.uid as u32, + gid: whiteout_init.gid as u32, + }, ) })?; whiteout_inode = match ext4_fs.publish_allocated_inode( - whiteout_num, + whiteout_attr, DName::from(candidate.as_str()), Some(Arc::downgrade(&source_parent)), - FileType::CharacterDev, &allocation, ) { Ok(inode) => Some(inode), @@ -1492,20 +1691,6 @@ impl LockedExt4Inode { fs.complete_freeing(tombstone) } - fn disk_file_type(file_type: vfs::FileType) -> FileType { - match file_type { - vfs::FileType::Dir => FileType::Directory, - vfs::FileType::BlockDevice => FileType::BlockDev, - vfs::FileType::CharDevice - | vfs::FileType::FramebufferDevice - | vfs::FileType::KvmDevice => FileType::CharacterDev, - vfs::FileType::Pipe => FileType::Fifo, - vfs::FileType::SymLink => FileType::SymLink, - vfs::FileType::Socket => FileType::Socket, - vfs::FileType::File => FileType::RegularFile, - } - } - #[inline] fn begin_operation(&self) -> Result { self.lifecycle.begin_operation() @@ -1632,11 +1817,29 @@ impl LockedExt4Inode { fs_ptr: Weak, dname: DName, parent: Option>, - known_file_type: Option, ) -> Result, SystemError> { + let fs = fs_ptr.upgrade().ok_or(SystemError::EIO)?; + let attr = fs.fs.getattr(inode_num)?; + Self::new_with_attr(inode_num, fs_ptr, dname, parent, &attr) + } + + pub(super) fn new_with_attr( + inode_num: u32, + fs_ptr: Weak, + dname: DName, + parent: Option>, + attr: &another_ext4::FileAttr, + ) -> Result, SystemError> { + debug_assert_eq!(inode_num, attr.ino); let lifecycle = Ext4InodeLifecycle::new(); let inode = Arc::new_cyclic(|self_ref| LockedExt4Inode { - inner: Mutex::new(Ext4Inode::new(inode_num, fs_ptr.clone(), dname, parent)), + inner: Mutex::new(Ext4Inode::new( + inode_num, + fs_ptr.clone(), + dname, + parent, + Ext4InodeTimes::from(attr), + )), io_lock: Mutex::new(()), size_lock: RwSem::new(()), namespace_lock: Mutex::new(()), @@ -1662,16 +1865,10 @@ impl LockedExt4Inode { guard.page_cache = Some(page_cache); // 对于 FIFO,创建 pipe inode - if let Some(fs) = fs_ptr.upgrade() { - let file_type = match known_file_type { - Some(file_type) => file_type, - None => fs.fs.getattr(inode_num)?.ftype, - }; - if file_type == FileType::Fifo { - let pipe_inode = LockedPipeInode::new(); - pipe_inode.set_fifo(); - guard.special_node = Some(SpecialNodeData::Pipe(pipe_inode)); - } + if attr.ftype == FileType::Fifo { + let pipe_inode = LockedPipeInode::new(); + pipe_inode.set_fifo(); + guard.special_node = Some(SpecialNodeData::Pipe(pipe_inode)); } drop(guard); @@ -1702,11 +1899,12 @@ impl Ext4Inode { .expect("Ext4FileSystem should be alive") } - pub fn new( + pub(super) fn new( inode_num: u32, fs_ptr: Weak, dname: DName, parent: Option>, + times: Ext4InodeTimes, ) -> Self { Self { inner_inode_num: inode_num, @@ -1719,7 +1917,9 @@ impl Ext4Inode { self_ref: Weak::new(), // 将在LockedExt4Inode::new()中设置 special_node: None, cached_file_size: None, - cached_mtime: None, + cached_times: times, + cached_atime_version: 0, + cached_mtime_version: 0, dirty_state: InodeDirtyState::empty(), } } @@ -1849,21 +2049,32 @@ impl LockedExt4Inode { pub(super) fn flush_metadata(&self, datasync: bool) -> Result<(), SystemError> { let _operation = self.begin_operation()?; let _io_guard = self.io_lock.lock(); - let (fs, inode_num, dirty, cached_size, cached_mtime) = { + let ( + fs, + inode_num, + dirty, + cached_size, + cached_times, + cached_atime_version, + cached_mtime_version, + ) = { let guard = self.inner.lock(); ( guard.concret_fs(), guard.inner_inode_num, guard.dirty_state, guard.cached_file_size, - guard.cached_mtime, + guard.cached_times, + guard.cached_atime_version, + guard.cached_mtime_version, ) }; let size_dirty = dirty.contains(InodeDirtyState::SIZE_DIRTY); + let atime_dirty = dirty.contains(InodeDirtyState::ATIME_DIRTY); let mtime_dirty = dirty.contains(InodeDirtyState::MTIME_DIRTY); - if !size_dirty && (!mtime_dirty || datasync) { + if !size_dirty && (datasync || (!atime_dirty && !mtime_dirty)) { self.release_clean_metadata_queue_owner(&fs); return Ok(()); } @@ -1876,18 +2087,28 @@ impl LockedExt4Inode { } else { None }; + let atime = if !datasync && atime_dirty { + Some(cached_times.atime) + } else { + None + }; let mtime = if !datasync && mtime_dirty { - cached_mtime + Some(cached_times.mtime) } else { None }; - Self::retry_metadata_contention(|| fs.fs.commit_inode_metadata(inode_num, size, mtime))?; + Self::retry_metadata_contention(|| { + fs.fs.commit_inode_metadata(inode_num, size, atime, mtime) + })?; let mut guard = self.inner.lock(); if size_dirty && guard.cached_file_size == cached_size { guard.dirty_state.remove(InodeDirtyState::SIZE_DIRTY); } - if !datasync && mtime_dirty && guard.cached_mtime == cached_mtime { + if !datasync && atime_dirty && guard.cached_atime_version == cached_atime_version { + guard.dirty_state.remove(InodeDirtyState::ATIME_DIRTY); + } + if !datasync && mtime_dirty && guard.cached_mtime_version == cached_mtime_version { guard.dirty_state.remove(InodeDirtyState::MTIME_DIRTY); } drop(guard); @@ -1944,7 +2165,8 @@ impl LockedExt4Inode { let self_arc = { let mut guard = self.inner.lock(); - guard.cached_mtime = Some(time); + guard.cached_times.mtime = time; + guard.cached_mtime_version = guard.cached_mtime_version.wrapping_add(1); guard.self_ref.upgrade().ok_or(SystemError::ENOENT)? }; Ext4FileSystem::mark_inode_dirty(&self_arc, InodeDirtyState::MTIME_DIRTY)?; diff --git a/kernel/src/filesystem/fat/fs.rs b/kernel/src/filesystem/fat/fs.rs index 7291bfded0..f2c0fc0325 100644 --- a/kernel/src/filesystem/fat/fs.rs +++ b/kernel/src/filesystem/fat/fs.rs @@ -2104,6 +2104,11 @@ impl IndexNode for LockedFATInode { inode.metadata.gid = metadata.gid; Ok(()) } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let inode = &mut self.0.lock(); + crate::filesystem::vfs::update_atime_locked(&mut inode.metadata, now, relatime); + Ok(()) + } fn resize(&self, len: usize) -> Result<(), SystemError> { let _size_guard = self.1.write(); //检查是否超过fat支持的最大容量 diff --git a/kernel/src/filesystem/fuse/inode.rs b/kernel/src/filesystem/fuse/inode.rs index ae2e943f2f..783c598c64 100644 --- a/kernel/src/filesystem/fuse/inode.rs +++ b/kernel/src/filesystem/fuse/inode.rs @@ -306,6 +306,10 @@ pub struct FuseNode { dax_pte_epoch: AtomicU64, cached_metadata_deadline_ticks: AtomicU64, attr_version: AtomicU64, + /// Serializes SETATTR requests and publication of their returned attrs. + /// FUSE daemons may process requests concurrently, so without this lock an + /// older reply can overwrite a newer inode metadata snapshot in the cache. + setattr_lock: Mutex<()>, /// Version chain produced while short READ replies from one metadata /// snapshot monotonically converge on the lowest observed EOF. short_read_source_attr_version: AtomicU64, @@ -388,6 +392,7 @@ impl FuseNode { dax_pte_epoch: AtomicU64::new(0), cached_metadata_deadline_ticks: AtomicU64::new(if has_cached { u64::MAX } else { 0 }), attr_version: AtomicU64::new(initial_attr_epoch), + setattr_lock: Mutex::new(()), short_read_source_attr_version: AtomicU64::new(0), short_read_chain_attr_version: AtomicU64::new(0), pending_short_read_eof: AtomicU64::new(u64::MAX), diff --git a/kernel/src/filesystem/fuse/inode/file.rs b/kernel/src/filesystem/fuse/inode/file.rs index 0980e8ab20..e4db53b94e 100644 --- a/kernel/src/filesystem/fuse/inode/file.rs +++ b/kernel/src/filesystem/fuse/inode/file.rs @@ -618,6 +618,7 @@ impl FuseNode { truncate_metadata: Option<(&Metadata, SetMetadataMask)>, ) -> Result<(), SystemError> { self.check_not_stale()?; + let _setattr_guard = self.setattr_lock.lock(); let old_size = self.cached_or_fetch_metadata()?.size.max(0) as usize; self.resolve_pending_short_read_truncate(len)?; // Drain once before taking the exclusive admission barrier to reduce diff --git a/kernel/src/filesystem/fuse/inode/vfs.rs b/kernel/src/filesystem/fuse/inode/vfs.rs index ec000e5064..ba01bbf484 100644 --- a/kernel/src/filesystem/fuse/inode/vfs.rs +++ b/kernel/src/filesystem/fuse/inode/vfs.rs @@ -19,6 +19,7 @@ use crate::{ }, libs::{casting::DowncastArc, mutex::MutexGuard}, mm::MemoryManagementArch, + time::PosixTimeSpec, }; use super::super::{ @@ -606,6 +607,7 @@ impl IndexNode for FuseNode { fn set_metadata(&self, metadata: &Metadata) -> Result<(), SystemError> { self.check_not_stale()?; + let _setattr_guard = self.setattr_lock.lock(); let old = self.cached_or_fetch_metadata()?; let writeback_cache = self.conn().has_init_flag(FUSE_WRITEBACK_CACHE); if writeback_cache { @@ -690,6 +692,45 @@ impl IndexNode for FuseNode { Ok(()) } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + self.check_not_stale()?; + let _setattr_guard = self.setattr_lock.lock(); + let mut metadata = self.cached_or_fetch_metadata()?; + if !crate::filesystem::vfs::update_atime_locked(&mut metadata, now, relatime) { + return Ok(()); + } + + let inarg = FuseSetattrIn { + valid: FATTR_ATIME, + padding: 0, + fh: 0, + size: 0, + lock_owner: 0, + atime: now.tv_sec as u64, + mtime: 0, + ctime: 0, + atimensec: now.tv_nsec as u32, + mtimensec: 0, + ctimensec: 0, + mode: 0, + unused4: 0, + uid: 0, + gid: 0, + unused5: 0, + }; + let payload = self + .conn() + .request(FUSE_SETATTR, self.nodeid, fuse_pack_struct(&inarg))?; + let out: FuseAttrOut = fuse_read_struct(&payload)?; + self.set_cached_metadata_with_valid( + Self::attr_to_metadata(&out.attr), + out.attr_valid, + out.attr_valid_nsec, + out.attr.flags, + ); + Ok(()) + } + fn resize(&self, len: usize) -> Result<(), SystemError> { self.setattr_size(len, None, None, None) } diff --git a/kernel/src/filesystem/overlayfs/inode.rs b/kernel/src/filesystem/overlayfs/inode.rs index 01419606eb..a423646d4f 100644 --- a/kernel/src/filesystem/overlayfs/inode.rs +++ b/kernel/src/filesystem/overlayfs/inode.rs @@ -434,6 +434,17 @@ impl IndexNode for OvlInode { super::metadata::set_metadata_masked(self, metadata, mask) } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + // Match ovl_update_time(): reading a lower-only inode must not copy it + // up merely to persist atime. If an upper inode already exists, let its + // native synchronization domain evaluate and update the timestamp. + let upper = self.upper_inode.lock().clone(); + if let Some(upper) = upper { + upper.update_atime(now, relatime)?; + } + Ok(()) + } + fn resize(&self, len: usize) -> Result<(), SystemError> { super::metadata::resize_with_lock_owner(self, len, 0) } diff --git a/kernel/src/filesystem/page_cache.rs b/kernel/src/filesystem/page_cache.rs index cb17ffc9ec..200bb78c3c 100644 --- a/kernel/src/filesystem/page_cache.rs +++ b/kernel/src/filesystem/page_cache.rs @@ -118,6 +118,35 @@ impl PageCacheCompletionSelftestState { } } +#[derive(Debug, Default)] +struct PageCacheQuotaSelftestBackend { + reserved: AtomicUsize, + released: AtomicUsize, +} + +impl PageCacheBackend for PageCacheQuotaSelftestBackend { + fn read_page(&self, _index: usize, _buf: &mut [u8]) -> Result { + Ok(0) + } + + fn write_page(&self, _index: usize, buf: &[u8]) -> Result { + Ok(buf.len()) + } + + fn npages(&self) -> usize { + 0 + } + + fn reserve_page(&self) -> Result<(), SystemError> { + self.reserved.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + + fn release_page(&self) { + self.released.fetch_add(1, Ordering::Relaxed); + } +} + #[derive(Debug, Default)] struct FileVmaIndex { vmas: HashMap>, @@ -337,6 +366,21 @@ pub(crate) fn run_accounting_debug_selftest() -> Result Result Result Result Result; fn npages(&self) -> usize; + /// Reserve one filesystem block before publishing a new cache page. + fn reserve_page(&self) -> Result<(), SystemError> { + Ok(()) + } + + /// Release the reservation owned by one removed cache page. + fn release_page(&self) {} + /// Maximum number of consecutive pages which are useful in one backend /// write request. Backends are single-page by default. fn write_batch_pages(&self) -> Result { @@ -946,6 +1022,7 @@ pub struct InnerPageCache { dirty_preparations: usize, kind: PageCacheKind, page_cache_ref: Weak, + accounting_backend: Option>, } /// 描述一次从页缓存到目标缓冲区的拷贝 @@ -1544,7 +1621,11 @@ impl PageCacheManager { return Err(SystemError::EEXIST); } for item in &items { - inner.insert_entry(item.descriptor.page_index, item.entry.clone()); + if let Err(error) = inner.insert_entry(item.descriptor.page_index, item.entry.clone()) { + drop(inner); + PageCacheReadDmaReservation::cleanup_unsubmitted_items(&cache, &items); + return Err(error); + } } drop(inner); for item in &items { @@ -1604,6 +1685,27 @@ impl PageCacheManager { self.upgrade()?.get_or_create_page_zero_pinned(page_index) } + pub fn commit_overwrite_pinned_with_status( + &self, + page_index: usize, + ) -> Result<(PageCachePagePin, bool), SystemError> { + self.upgrade()? + .get_or_create_page_zero_pinned_with_status(page_index) + } + + /// Count cache holes in an inclusive page-index range from one locked + /// membership snapshot. This is used only for fallible transaction + /// metadata sizing; the later per-page insertion remains authoritative. + pub fn missing_pages_in_range(&self, first: usize, last: usize) -> Result { + let cache = self.upgrade()?; + let total = last + .checked_sub(first) + .and_then(|count| count.checked_add(1)) + .ok_or(SystemError::ENOMEM)?; + let present = cache.lock().page_indices.range(first..=last).count(); + total.checked_sub(present).ok_or(SystemError::EIO) + } + pub fn prefetch_page(&self, page_index: usize) -> Result<(), SystemError> { self.upgrade()?.start_async_read(page_index) } @@ -2915,6 +3017,78 @@ impl PageCacheManager { Ok(removed) } + /// Roll back a page newly inserted by a transactional cache operation. + /// + /// Identity and liveness are checked while the cache membership lock is + /// held. If another user has acquired or mapped the page, it is no longer + /// safe to treat it as transaction-private and the rollback leaves it in + /// place. A successful removal also retires the page from the global page + /// manager and reclaimer; callers must not reproduce that lifecycle logic. + pub fn discard_created_page( + &self, + page_index: usize, + expected_page: &Arc, + ) -> Result { + let cache = self.upgrade()?; + let (entry, state) = { + let guard = cache.lock(); + let Some(entry) = guard.get_entry(page_index) else { + return Ok(false); + }; + if !Arc::ptr_eq(&entry.page, expected_page) + || entry.active_users() != 0 + || guard.dirty_pages.contains(&page_index) + || guard.writeback_pages.contains(&page_index) + { + return Ok(false); + } + let state = entry.state(); + if matches!( + state, + PageState::Loading | PageState::Dirty | PageState::Writeback | PageState::Error + ) { + return Ok(false); + } + (entry, state) + }; + + // Dirty publication uses the page -> cache lock order. Keep the page + // guard through the final membership check so rollback cannot slip + // between setting PG_DIRTY and publishing the dirty tag/state. + let page_guard = entry.page.read(); + let page_discardable = !page_guard + .flags() + .intersects(PageFlags::PG_DIRTY | PageFlags::PG_WRITEBACK) + && page_guard.map_count() == 0; + if !page_discardable { + return Ok(false); + } + + let removed = { + let mut guard = cache.lock(); + let Some(current) = guard.get_entry(page_index) else { + return Ok(false); + }; + if !Arc::ptr_eq(¤t, &entry) + || !Arc::ptr_eq(¤t.page, expected_page) + || current.active_users() != 0 + || current.state() != state + || guard.dirty_pages.contains(&page_index) + || guard.writeback_pages.contains(&page_index) + { + return Ok(false); + } + guard.remove_page(page_index) + }; + drop(page_guard); + let Some(page) = removed else { + return Ok(false); + }; + cache.discard_unlinked_page(&page); + drop(cache.detach_dirty_retention_if_idle()); + Ok(true) + } + pub fn remove_clean_page_for_reclaim( &self, page_index: usize, @@ -3271,7 +3445,12 @@ impl PageCachePagePin { } impl InnerPageCache { - fn new(page_cache_ref: Weak, id: usize, kind: PageCacheKind) -> InnerPageCache { + fn new( + page_cache_ref: Weak, + id: usize, + kind: PageCacheKind, + accounting_backend: Option>, + ) -> InnerPageCache { Self { id, pages: HashMap::new(), @@ -3282,6 +3461,7 @@ impl InnerPageCache { dirty_preparations: 0, kind, page_cache_ref, + accounting_backend, } } @@ -3295,6 +3475,9 @@ impl InnerPageCache { self.dirty_pages.remove(&offset); self.writeback_pages.remove(&offset); entry.account_remove(); + if let Some(backend) = self.accounting_backend.as_ref() { + backend.release_page(); + } Some(entry.page.clone()) } @@ -3302,19 +3485,23 @@ impl InnerPageCache { self.pages.get(&offset).cloned() } - fn insert_entry(&mut self, offset: usize, entry: Arc) { + fn insert_entry(&mut self, offset: usize, entry: Arc) -> Result<(), SystemError> { let mapping_unevictable = self .page_cache_ref .upgrade() .is_some_and(|cache| cache.mapping_unevictable()); match self.pages.entry(offset) { Entry::Vacant(slot) => { + if let Some(backend) = self.accounting_backend.as_ref() { + backend.reserve_page()?; + } entry.account_insert(self.kind, mapping_unevictable); slot.insert(entry); } Entry::Occupied(_) => panic!("page-cache insert requires a vacant slot"), } self.page_indices.insert(offset); + Ok(()) } fn is_page_ready(&self, offset: usize) -> bool { @@ -3340,6 +3527,9 @@ impl Drop for InnerPageCache { let mut page_manager = page_manager_lock(); for entry in self.pages.values() { entry.account_remove(); + if let Some(backend) = self.accounting_backend.as_ref() { + backend.release_page(); + } page_manager.remove_page(&entry.page.phys_address()); } drop(page_manager); @@ -3374,9 +3564,23 @@ impl PageCache { kind: PageCacheKind, ) -> Arc { let id = PAGE_CACHE_ID.fetch_add(1, Ordering::SeqCst); + // Quota accounting is a shmem concern. Regular file backends use the + // same trait for I/O, but its accounting hooks are the default no-ops; + // retaining them here would add an unnecessary indirect call to every + // file-page insertion and removal while the cache lock is held. + let accounting_backend = if kind == PageCacheKind::Shmem { + backend.clone() + } else { + None + }; let cache = Arc::new_cyclic(|weak| Self { id, - inner: Mutex::new(InnerPageCache::new(weak.clone(), id, kind)), + inner: Mutex::new(InnerPageCache::new( + weak.clone(), + id, + kind, + accounting_backend, + )), inode: { let v: Lazy> = Lazy::new(); if let Some(inode) = inode { @@ -4166,6 +4370,15 @@ impl PageCache { page_index: usize, populate_backend: bool, ) -> Result, SystemError> { + self.get_or_create_entry_with_status(page_index, populate_backend) + .map(|(entry, _created)| entry) + } + + fn get_or_create_entry_with_status( + &self, + page_index: usize, + populate_backend: bool, + ) -> Result<(Arc, bool), SystemError> { let mut page_cache_ref = None; let mut existing_entry = None; { @@ -4180,13 +4393,13 @@ impl PageCache { if let Some(entry) = existing_entry { let state = entry.state(); if state.is_ready() { - return Ok(entry); + return Ok((entry, false)); } if state == PageState::Error { return Err(SystemError::EIO); } let _ = entry.wait_ready()?; - return Ok(entry); + return Ok((entry, false)); } let (entry, need_populate) = { @@ -4205,7 +4418,11 @@ impl PageCache { (entry, false) } else { let entry = Arc::new(PageEntry::new(page, PageState::Loading)); - guard.insert_entry(page_index, entry.clone()); + if let Err(error) = guard.insert_entry(page_index, entry.clone()) { + drop(guard); + self.discard_unlinked_page(&entry.page); + return Err(error); + } (entry, true) } } @@ -4214,13 +4431,13 @@ impl PageCache { if !need_populate { let state = entry.state(); if state.is_ready() { - return Ok(entry); + return Ok((entry, false)); } if state == PageState::Error { return Err(SystemError::EIO); } let _ = entry.wait_ready()?; - return Ok(entry); + return Ok((entry, false)); } self.reconcile_entry_unevictable_for_insert(&entry); @@ -4234,7 +4451,7 @@ impl PageCache { Ok(()) => { entry.set_state(PageState::UpToDate); entry.wait_queue.wake_all(); - Ok(entry) + Ok((entry, true)) } Err(e) => { entry.set_state(PageState::Error); @@ -4362,7 +4579,11 @@ impl PageCache { return Ok(()); } let entry = Arc::new(PageEntry::new(page, PageState::Loading)); - guard.insert_entry(page_index, entry.clone()); + if let Err(error) = guard.insert_entry(page_index, entry.clone()) { + drop(guard); + self.discard_unlinked_page(&entry.page); + return Err(error); + } entry }; self.reconcile_entry_unevictable_for_insert(&entry); @@ -4516,7 +4737,11 @@ impl PageCache { (entry, false) } else { let entry = Arc::new(PageEntry::new(page, PageState::Loading)); - guard.insert_entry(page_index, entry.clone()); + if let Err(error) = guard.insert_entry(page_index, entry.clone()) { + drop(guard); + self.discard_unlinked_page(&entry.page); + return Err(error); + } (entry, true) } } @@ -4618,7 +4843,11 @@ impl PageCache { if guard.get_entry(page_index).is_some() { false } else { - guard.insert_entry(page_index, entry.clone()); + if let Err(error) = guard.insert_entry(page_index, entry.clone()) { + drop(guard); + self.discard_unlinked_page(&entry.page); + return Err(error); + } true } }; @@ -4670,6 +4899,24 @@ impl PageCache { self.get_or_create_page_pinned(page_index, false) } + pub fn get_or_create_page_zero_pinned_with_status( + &self, + page_index: usize, + ) -> Result<(PageCachePagePin, bool), SystemError> { + loop { + let (entry, created) = self.get_or_create_entry_with_status(page_index, false)?; + let guard = self.inner.lock(); + let Some(current) = guard.get_entry(page_index) else { + continue; + }; + if !Arc::ptr_eq(¤t, &entry) || !entry.state().is_ready() { + continue; + } + let pin = entry.pin(); + return Ok((PageCachePagePin::new(entry.page.clone(), pin), created)); + } + } + fn get_or_create_page_pinned( &self, page_index: usize, @@ -4690,6 +4937,15 @@ impl PageCache { } fn ensure_dirty_retention_locked(&self, inner: &mut InnerPageCache) -> Result<(), SystemError> { + // Shmem pages have no asynchronous backing-store writeback. The inode + // already owns the mapping, so retaining the inode from the mapping's + // dirty state would form a permanent inode -> page cache -> inode cycle. + // In particular, an unlinked tmpfs inode would never release its page + // reservations. Ordinary file mappings still need this guard while + // dirty/writeback work can outlive the caller. + if self.is_shmem() { + return Ok(()); + } if inner.dirty_retention.is_some() { return Ok(()); } @@ -4852,8 +5108,14 @@ impl PageCache { self.discard_unlinked_page(&entry.page); return Err(SystemError::EEXIST); } - guard.insert_entry(page_index, entry); - Ok(()) + match guard.insert_entry(page_index, entry.clone()) { + Ok(()) => Ok(()), + Err(error) => { + drop(guard); + self.discard_unlinked_page(&entry.page); + Err(error) + } + } } pub fn read_pages(&self, start_page_index: usize, page_num: usize) -> Result<(), SystemError> { diff --git a/kernel/src/filesystem/procfs/mount/collect.rs b/kernel/src/filesystem/procfs/mount/collect.rs index 7fe448137f..4826e19c1e 100644 --- a/kernel/src/filesystem/procfs/mount/collect.rs +++ b/kernel/src/filesystem/procfs/mount/collect.rs @@ -39,6 +39,7 @@ pub(crate) fn collect_visible_mounts( return Ok((Vec::new(), "/".to_string())); } let root_mount = root.mount_fs(); + let mount_namespace = target.nsproxy().mnt_ns.clone(); let mut mounts = Vec::new(); let mount_root = root_mount .root_inode() @@ -52,6 +53,11 @@ pub(crate) fn collect_visible_mounts( let parent_mount_id = root_mount .self_mountpoint() .map(|mountpoint| mountpoint.mount_fs().mount_id().into()) + .or_else(|| { + Arc::ptr_eq(&root_mount, &mount_namespace.root_mntfs()) + .then(|| mount_namespace.root_parent_mount_id().map(|id| id.data())) + .flatten() + }) .unwrap_or_else(|| root_mount.mount_id().into()); mounts.push(( mountpoint_display, diff --git a/kernel/src/filesystem/procfs/root.rs b/kernel/src/filesystem/procfs/root.rs index 9f504638f9..91809ef2f7 100644 --- a/kernel/src/filesystem/procfs/root.rs +++ b/kernel/src/filesystem/procfs/root.rs @@ -254,7 +254,7 @@ impl FileSystem for ProcFS { } fn name(&self) -> &str { - "procfs" + "proc" } fn super_block(&self) -> SuperBlock { diff --git a/kernel/src/filesystem/ramfs/mod.rs b/kernel/src/filesystem/ramfs/mod.rs index e2d327380b..8d040c20b8 100644 --- a/kernel/src/filesystem/ramfs/mod.rs +++ b/kernel/src/filesystem/ramfs/mod.rs @@ -510,6 +510,12 @@ impl IndexNode for LockedRamFSInode { return Ok(()); } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let mut inode = self.0.lock(); + crate::filesystem::vfs::update_atime_locked(&mut inode.metadata, now, relatime); + Ok(()) + } + fn resize(&self, len: usize) -> Result<(), SystemError> { let mut inode = self.0.lock(); if inode.metadata.file_type == FileType::File { @@ -550,6 +556,8 @@ impl IndexNode for LockedRamFSInode { if inode.children.contains_key(&name) { return Err(SystemError::EEXIST); } + let init = + crate::filesystem::vfs::permission::child_inode_init(&inode.metadata, file_type, mode); // 创建inode let result: Arc = Arc::new(LockedRamFSInode(Mutex::new(RamFSInode { @@ -568,12 +576,12 @@ impl IndexNode for LockedRamFSInode { ctime: PosixTimeSpec::default(), btime: PosixTimeSpec::default(), file_type, - mode, + mode: init.mode, flags: InodeFlags::empty(), // 目录需要包含 "." 自引用,因此初始为2 nlinks: if file_type == FileType::Dir { 2 } else { 1 }, - uid: 0, - gid: 0, + uid: init.uid, + gid: init.gid, raw_dev: DeviceNumber::from(data as u32), }, fs: inode.fs.clone(), @@ -892,6 +900,8 @@ impl IndexNode for LockedRamFSInode { FileType::Socket => FileType::Socket, _ => return Err(SystemError::EINVAL), }; + let init = + crate::filesystem::vfs::permission::child_inode_init(&inode.metadata, file_type, mode); let nod = Arc::new(LockedRamFSInode(Mutex::new(RamFSInode { parent: inode.self_ref.clone(), @@ -909,10 +919,10 @@ impl IndexNode for LockedRamFSInode { ctime: PosixTimeSpec::default(), btime: PosixTimeSpec::default(), file_type, - mode, + mode: init.mode, nlinks: 1, - uid: 0, - gid: 0, + uid: init.uid, + gid: init.gid, raw_dev: dev_t, flags: InodeFlags::empty(), }, diff --git a/kernel/src/filesystem/tmpfs/mod.rs b/kernel/src/filesystem/tmpfs/mod.rs index 9bedb19ed6..9d93d91d28 100644 --- a/kernel/src/filesystem/tmpfs/mod.rs +++ b/kernel/src/filesystem/tmpfs/mod.rs @@ -1,8 +1,9 @@ use core::any::Any; +use core::fmt::Write; use core::intrinsics::unlikely; use core::sync::atomic::{AtomicU64, Ordering}; -use crate::filesystem::page_cache::{PageCache, PageCacheBackend, PageCachePagePin}; +use crate::filesystem::page_cache::{PageCache, PageCacheBackend}; use crate::filesystem::vfs::syscall::RenameFlags; use crate::filesystem::vfs::{FileSystemMakerData, FSMAKER}; use crate::libs::rwsem::RwSem; @@ -19,6 +20,7 @@ use crate::{ libs::casting::DowncastArc, libs::mutex::{Mutex, MutexGuard}, mm::MemoryManagementArch, + process::ProcessManager, time::PosixTimeSpec, }; @@ -51,11 +53,12 @@ const WHITEOUT_DEV: DeviceNumber = DeviceNumber::new(Major::UNNAMED_MAJOR, 0); #[derive(Debug)] struct TmpfsPageCacheBackend { inode: Weak, + fs: Weak, } impl TmpfsPageCacheBackend { - fn new(inode: Weak) -> Self { - Self { inode } + fn new(inode: Weak, fs: Weak) -> Self { + Self { inode, fs } } } @@ -85,6 +88,19 @@ impl PageCacheBackend for TmpfsPageCacheBackend { Err(_) => 0, } } + + fn reserve_page(&self) -> Result<(), SystemError> { + self.fs + .upgrade() + .ok_or(SystemError::EIO)? + .increase_size(MMArch::PAGE_SIZE as u64) + } + + fn release_page(&self) { + if let Some(fs) = self.fs.upgrade() { + fs.decrease_size(MMArch::PAGE_SIZE); + } + } } fn tmpfs_move_entry_between_dirs( @@ -276,6 +292,7 @@ fn tmpfs_insert_whiteout(dir: &mut TmpfsInode, name: &DName) -> Result<(), Syste }, fs: dir.fs.clone(), special_node: None, + inline_symlink: None, name: name.clone(), })); whiteout.0.lock().self_ref = Arc::downgrade(&whiteout); @@ -298,6 +315,7 @@ pub struct Tmpfs { super_block: RwSem, size_limit: RwSem>, current_size: AtomicU64, + mount_mode: RwSem, } #[derive(Debug)] @@ -344,6 +362,7 @@ pub struct TmpfsInode { metadata: Metadata, fs: Weak, special_node: Option, + inline_symlink: Option, name: DName, } @@ -374,6 +393,7 @@ impl TmpfsInode { }, fs: Weak::default(), special_node: None, + inline_symlink: None, name: Default::default(), } } @@ -409,7 +429,14 @@ impl TmpfsMountData { (&v_lower[..], 1u64) }; let base = num_str.parse::().map_err(|_| SystemError::EINVAL)?; - size_bytes = Some(base.saturating_mul(mul)); + let bytes = base.checked_mul(mul).ok_or(SystemError::EINVAL)?; + let rounded = bytes + .checked_add(MMArch::PAGE_SIZE as u64 - 1) + .ok_or(SystemError::EINVAL)? + & !(MMArch::PAGE_SIZE as u64 - 1); + size_bytes = Some(rounded); + } else { + return Err(SystemError::EINVAL); } } } @@ -474,8 +501,32 @@ impl FileSystem for Tmpfs { "tmpfs" } + fn proc_show_mount_options( + &self, + _mount: &super::vfs::mount::MountFS, + out: &mut dyn Write, + ) -> Result<(), SystemError> { + let mode = *self.mount_mode.read(); + if mode != InodeMode::S_IRWXUGO { + write!(out, "mode={:03o}", mode.bits() & 0o7777).map_err(|_| SystemError::EINVAL)?; + } + Ok(()) + } + fn super_block(&self) -> SuperBlock { - self.super_block.read().clone() + let limit = self.size_limit.read(); + let mut sb = self.super_block.read().clone(); + if let Some(limit) = *limit { + let current = self.current_size.load(Ordering::Acquire); + let total_blocks = limit / TMPFS_BLOCK_SIZE; + let used_blocks = Self::bytes_to_blocks_ceil(current); + let free_blocks = total_blocks.saturating_sub(used_blocks); + sb.blocks = total_blocks; + sb.bfree = free_blocks; + sb.bavail = free_blocks; + sb.frsize = TMPFS_BLOCK_SIZE; + } + sb } fn support_readahead(&self) -> bool { @@ -487,17 +538,18 @@ impl FileSystem for Tmpfs { let parsed = TmpfsMountData::parse(request.raw_data)?; if let Some(new_limit) = parsed.size_bytes { + let mut limit = self.size_limit.write(); let current = self.current_size.load(Ordering::Acquire); if new_limit < current { return Err(SystemError::EINVAL); } - *self.size_limit.write() = Some(new_limit); - self.update_superblock_free(current); + *limit = Some(new_limit); } if let Some(mode) = parsed.mode { let mut root = self.root_inode.0.lock(); root.metadata.mode = mode; + *self.mount_mode.write() = mode; } Ok(request.sb_flags & request.sb_flags_mask) @@ -518,21 +570,6 @@ impl Tmpfs { bytes.div_ceil(TMPFS_BLOCK_SIZE) } - fn update_superblock_free(&self, current_bytes: u64) { - // 只在启用 size_limit 时输出可用容量;否则保持现有行为(0-sized)。 - let Some(limit) = *self.size_limit.read() else { - return; - }; - let total_blocks = limit / TMPFS_BLOCK_SIZE; - let used_blocks = Self::bytes_to_blocks_ceil(current_bytes); - let free_blocks = total_blocks.saturating_sub(used_blocks); - let mut sb = self.super_block.write(); - sb.blocks = total_blocks; - sb.bfree = free_blocks; - sb.bavail = free_blocks; - sb.frsize = TMPFS_BLOCK_SIZE; - } - pub fn new(mount_data: &TmpfsMountData) -> Arc { // 若未指定 size=,使用默认容量策略(通常为物理内存的一半)。 // 这样 busybox df -h(默认过滤 f_blocks==0)就能显示 /tmp。 @@ -563,6 +600,7 @@ impl Tmpfs { super_block: RwSem::new(sb), size_limit: RwSem::new(size_limit), current_size: AtomicU64::new(0), + mount_mode: RwSem::new(mode.unwrap_or(InodeMode::S_IRWXUGO)), }); let mut root_guard: MutexGuard = result.root_inode.0.lock(); @@ -583,7 +621,8 @@ impl Tmpfs { /// 返回Ok(())如果更新成功,Err(SystemError::ENOSPC)如果超过限制 /// 使用compare_exchange_weak循环确保并发安全 fn increase_size(&self, size_diff: u64) -> Result<(), SystemError> { - if let Some(limit) = *self.size_limit.read() { + let size_limit = self.size_limit.read(); + if let Some(limit) = *size_limit { // 使用compare_exchange_weak循环确保原子性 loop { let current = self.current_size.load(Ordering::Acquire); @@ -600,11 +639,7 @@ impl Tmpfs { Ordering::Release, Ordering::Acquire, ) { - Ok(_) => { - // 同步更新 superblock 的 free 统计,供 statfs/df 使用 - self.update_superblock_free(new_total); - break; - } // 更新成功 + Ok(_) => break, // 更新成功 Err(_) => continue, // 被其他线程修改,重试 } } @@ -615,17 +650,31 @@ impl Tmpfs { /// 原子地减少文件系统当前使用的大小(用于文件删除或缩小) /// 使用fetch_sub确保并发安全 fn decrease_size(&self, size: usize) { - if self.size_limit.read().is_some() { + let size_limit = self.size_limit.read(); + if size_limit.is_some() { let size_to_decrease = size as u64; - // 使用fetch_sub原子地减少大小 - let prev = self - .current_size - .fetch_sub(size_to_decrease, Ordering::Release); - let new = prev.saturating_sub(size_to_decrease); - self.update_superblock_free(new); + loop { + let current = self.current_size.load(Ordering::Acquire); + let new = current.saturating_sub(size_to_decrease); + if self + .current_size + .compare_exchange_weak(current, new, Ordering::Release, Ordering::Acquire) + .is_ok() + { + break; + } + } } } + fn available_pages(&self) -> Option { + let size_limit = self.size_limit.read(); + (*size_limit).map(|limit| { + let current = self.current_size.load(Ordering::Acquire); + (limit.saturating_sub(current) / MMArch::PAGE_SIZE as u64) as usize + }) + } + fn create_unlinked_shmem_inode( self: &Arc, name: DName, @@ -672,12 +721,16 @@ impl Tmpfs { }, fs: Arc::downgrade(self), special_node: None, + inline_symlink: None, name, })); result.0.lock().self_ref = Arc::downgrade(&result); let inode_dyn: Arc = result.clone(); - let backend = Arc::new(TmpfsPageCacheBackend::new(Arc::downgrade(&inode_dyn))); + let backend = Arc::new(TmpfsPageCacheBackend::new( + Arc::downgrade(&inode_dyn), + Arc::downgrade(self), + )); let pc = PageCache::new_shmem(Some(Arc::downgrade(&inode_dyn)), Some(backend)); result.0.lock().page_cache = Some(pc.clone()); @@ -817,6 +870,18 @@ impl IndexNode for LockedTmpfsInode { return Err(SystemError::EISDIR); } let file_size = inode.metadata.size as usize; + if let Some(target) = inode.inline_symlink.clone() { + drop(inode); + let read_len = if offset < target.len() { + core::cmp::min(target.len() - offset, len) + } else { + 0 + }; + if read_len > 0 { + buf[..read_len].copy_from_slice(&target.as_bytes()[offset..offset + read_len]); + } + return Ok(read_len); + } let page_cache = inode.page_cache.clone().ok_or(SystemError::EIO)?; drop(inode); @@ -837,7 +902,7 @@ impl IndexNode for LockedTmpfsInode { // 1) 持有 page_cache 锁:只做“取页/建页 + 收集引用”,绝不触碰用户缓冲区 // 2) 释放 page_cache 锁:再把页内容拷贝到用户缓冲区(并做 prefault) struct ReadItem { - page: Arc, + page: Option>, page_offset: usize, sub_len: usize, } @@ -854,8 +919,9 @@ impl IndexNode for LockedTmpfsInode { continue; } - // tmpfs: 缺页即创建零页 - let page = page_cache.manager().commit_overwrite(page_index)?; + // Reading a sparse tmpfs hole returns zeroes without allocating a + // page or consuming the mount's block quota. + let page = page_cache.manager().peek_page(page_index); items.push(ReadItem { page, @@ -876,11 +942,15 @@ impl IndexNode for LockedTmpfsInode { let v = volatile_read!(buf[dst_off + it.sub_len - 1]); volatile_write!(buf[dst_off + it.sub_len - 1], v); - let page_guard = it.page.read(); - unsafe { - buf[dst_off..dst_off + it.sub_len].copy_from_slice( - &page_guard.as_slice()[it.page_offset..it.page_offset + it.sub_len], - ); + if let Some(page) = it.page { + let page_guard = page.read(); + unsafe { + buf[dst_off..dst_off + it.sub_len].copy_from_slice( + &page_guard.as_slice()[it.page_offset..it.page_offset + it.sub_len], + ); + } + } else { + buf[dst_off..dst_off + it.sub_len].fill(0); } dst_off += it.sub_len; } @@ -910,36 +980,12 @@ impl IndexNode for LockedTmpfsInode { return Err(SystemError::EISDIR); } let page_cache = inode.page_cache.clone().ok_or(SystemError::EIO)?; - let old_size = inode.metadata.size as usize; let write_end = offset.checked_add(len).ok_or(SystemError::EFBIG)?; - let new_size = write_end.max(old_size); - let size_diff = new_size.saturating_sub(old_size) as u64; - - // 获取文件系统引用 - let fs = inode.fs.upgrade().ok_or(SystemError::EIO)?; - let tmpfs = fs - .as_any_ref() - .downcast_ref::() - .ok_or(SystemError::EIO)?; - - // 先预留空间,失败直接返回;后续 page-cache/拷贝失败时必须回滚本次预留。 - if size_diff > 0 { - tmpfs.increase_size(size_diff)?; - } - drop(inode); let start_page_index = offset >> MMArch::PAGE_SHIFT; let end_page_index = (write_end - 1) >> MMArch::PAGE_SHIFT; - // 两阶段写入:同样避免在持有 page_cache 锁时触碰用户缓冲区(SelfRead)。 - struct WriteItem { - pin: PageCachePagePin, - page_index: usize, - page_offset: usize, - sub_len: usize, - } - - let mut items: Vec = Vec::new(); + let mut written = 0usize; for page_index in start_page_index..=end_page_index { let page_start = page_index * MMArch::PAGE_SIZE; let page_end = page_start + MMArch::PAGE_SIZE; @@ -954,66 +1000,43 @@ impl IndexNode for LockedTmpfsInode { let pin = match page_cache.manager().commit_overwrite_pinned(page_index) { Ok(pin) => pin, Err(err) => { - if size_diff > 0 { - tmpfs.decrease_size(size_diff as usize); + if written == 0 { + return Err(err); } - return Err(err); + break; } }; - items.push(WriteItem { - pin, - page_index, - page_offset: write_start - page_start, - sub_len: page_write_len, - }); - } - - let mut src_off = 0usize; - for it in items { - if it.sub_len == 0 { - continue; - } - // prefault 用户缓冲区,避免后续在持页锁时缺页 - volatile_read!(buf[src_off]); - volatile_read!(buf[src_off + it.sub_len - 1]); + volatile_read!(buf[written]); + volatile_read!(buf[written + page_write_len - 1]); - let page = it.pin.page(); + let page = pin.page(); let mut page_guard = page.write(); unsafe { - page_guard.as_slice_mut()[it.page_offset..it.page_offset + it.sub_len] - .copy_from_slice(&buf[src_off..src_off + it.sub_len]); + let page_offset = write_start - page_start; + page_guard.as_slice_mut()[page_offset..page_offset + page_write_len] + .copy_from_slice(&buf[written..written + page_write_len]); } page_guard.add_flags(crate::mm::page::PageFlags::PG_DIRTY); - if let Err(err) = page_cache.manager().update_page(it.page_index) { - if size_diff > 0 { - tmpfs.decrease_size(size_diff as usize); + drop(page_guard); + if let Err(err) = page_cache.manager().update_page(page_index) { + if written == 0 { + return Err(err); } - return Err(err); + break; } - src_off += it.sub_len; + written += page_write_len; } - // 更新文件大小并按当前 inode size 结算本次预留,避免并发扩容写重复 charge。 + // Quota is charged by page-cache membership. Logical size advances + // only through the prefix which was actually copied. let mut inode = self.0.lock(); - let committed_size = inode.metadata.size as usize; - let actual_growth = new_size.saturating_sub(committed_size); - if actual_growth > size_diff as usize { - let extra = actual_growth - size_diff as usize; - if let Err(err) = tmpfs.increase_size(extra as u64) { - if size_diff > 0 { - tmpfs.decrease_size(size_diff as usize); - } - return Err(err); - } - } else if (size_diff as usize) > actual_growth { - tmpfs.decrease_size(size_diff as usize - actual_growth); - } - if new_size > committed_size { - inode.metadata.size = new_size as i64; + let committed_end = offset + written; + if committed_end > inode.metadata.size as usize { + inode.metadata.size = committed_end as i64; } - Ok(len) + Ok(written) } fn fs(&self) -> Arc { @@ -1041,9 +1064,15 @@ impl IndexNode for LockedTmpfsInode { Ok(()) } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let mut inode = self.0.lock(); + crate::filesystem::vfs::update_atime_locked(&mut inode.metadata, now, relatime); + Ok(()) + } + fn resize(&self, len: usize) -> Result<(), SystemError> { let _size_guard = self.1.write(); - let (old_size, new_size, page_cache, fs) = { + let (old_size, new_size, page_cache) = { let mut inode = self.0.lock(); if inode.metadata.file_type != FileType::File { return Err(SystemError::EINVAL); @@ -1052,35 +1081,17 @@ impl IndexNode for LockedTmpfsInode { let old_size = inode.metadata.size as usize; let new_size = len; - // 获取文件系统引用 - let fs = inode.fs.upgrade().ok_or(SystemError::EIO)?; - let tmpfs = fs - .as_any_ref() - .downcast_ref::() - .ok_or(SystemError::EIO)?; - - let growth = new_size.saturating_sub(old_size); - if growth > 0 { - tmpfs.increase_size(growth as u64)?; - } - // Linux truncate_setsize() writes the new i_size before truncating page cache. // Drop the inode lock before page-cache unmap/truncate so page faults do not // form an inode-lock/MM-lock ABBA with the truncate path. inode.metadata.size = len as i64; - (old_size, new_size, inode.page_cache.clone(), fs) + (old_size, new_size, inode.page_cache.clone()) }; if new_size < old_size { if let Some(pc) = page_cache { pc.manager().resize(len)?; } - - let tmpfs = fs - .as_any_ref() - .downcast_ref::() - .ok_or(SystemError::EIO)?; - tmpfs.decrease_size(old_size - new_size); } Ok(()) @@ -1095,7 +1106,186 @@ impl IndexNode for LockedTmpfsInode { data: MutexGuard, ) -> Result<(), SystemError> { drop(data); - crate::filesystem::vfs::vcore::resize_based_fallocate(self, mode, offset, len, lock_owner) + if mode != 0 { + return Err(SystemError::EOPNOTSUPP_OR_ENOTSUP); + } + if len == 0 { + return Err(SystemError::EINVAL); + } + let end = offset.checked_add(len).ok_or(SystemError::EFBIG)?; + if end > isize::MAX as usize { + return Err(SystemError::EFBIG); + } + crate::filesystem::vfs::vcore::check_file_size_limit(end)?; + + let _size_guard = self.1.write(); + let (page_cache, fs) = { + let inode = self.0.lock(); + ( + inode.page_cache.clone().ok_or(SystemError::EIO)?, + inode.fs.upgrade().ok_or(SystemError::EIO)?, + ) + }; + let first = offset >> MMArch::PAGE_SHIFT; + let last = (end - 1) >> MMArch::PAGE_SHIFT; + let mut created: Vec<(usize, Arc)> = Vec::new(); + let missing_pages = page_cache.manager().missing_pages_in_range(first, last)?; + if fs + .available_pages() + .is_some_and(|available| missing_pages > available) + { + return Err(SystemError::ENOSPC); + } + created + .try_reserve_exact(missing_pages) + .map_err(|_| SystemError::ENOMEM)?; + + for page_index in first..=last { + match page_cache + .manager() + .commit_overwrite_pinned_with_status(page_index) + { + Ok((pin, was_created)) => { + if was_created { + if created.len() == created.capacity() && created.try_reserve(1).is_err() { + let current_page = pin.page(); + drop(pin); + let _ = page_cache + .manager() + .discard_created_page(page_index, ¤t_page); + for (created_index, created_page) in created.into_iter().rev() { + let _ = page_cache + .manager() + .discard_created_page(created_index, &created_page); + } + return Err(SystemError::ENOMEM); + } + created.push((page_index, pin.page())); + } + } + Err(error) => { + for (created_index, created_page) in created.into_iter().rev() { + let _ = page_cache + .manager() + .discard_created_page(created_index, &created_page); + } + return Err(error); + } + } + } + + // Match Linux shmem_fallocate(): only after every page has been + // allocated successfully, apply the same write-side metadata effects + // as a regular file modification. Build the update from the latest + // metadata while holding the inode lock so concurrent chmod/chown + // changes cannot be overwritten by a stale pre-allocation snapshot. + let cred = ProcessManager::current_pcb().cred(); + let mut inode = self.0.lock(); + let new_size = core::cmp::max(inode.metadata.size.max(0) as usize, end); + let (metadata, mask) = + crate::filesystem::vfs::vcore::prepare_write_side_effect_metadata_with_cred( + inode.metadata.clone(), + new_size, + &cred, + ); + inode.metadata.size = metadata.size; + crate::filesystem::vfs::merge_metadata_masked(&mut inode.metadata, &metadata, mask); + let _ = lock_owner; + Ok(()) + } + + fn symlink(&self, name: &str, target: &str) -> Result, SystemError> { + const SHORT_SYMLINK_LEN: usize = 128; + + if target + .len() + .checked_add(1) + .ok_or(SystemError::ENAMETOOLONG)? + > MMArch::PAGE_SIZE + { + return Err(SystemError::ENAMETOOLONG); + } + + let name = DName::from(name); + let mut parent = self.0.lock(); + tmpfs_require_live_dir(&parent)?; + if parent.children.contains_key(&name) { + return Err(SystemError::EEXIST); + } + // Revalidate local DAC while holding the same lock that protects the + // parent metadata and publishes the child. This is the tmpfs analogue + // of Linux holding the parent inode lock across may_create()+symlink. + if parent.metadata.flags.contains(InodeFlags::S_IMMUTABLE) { + return Err(SystemError::EPERM); + } + if ProcessManager::initialized() { + let cred = ProcessManager::current_pcb().cred(); + cred.inode_permission( + &parent.metadata, + (crate::filesystem::vfs::permission::PermissionMask::MAY_WRITE + | crate::filesystem::vfs::permission::PermissionMask::MAY_EXEC) + .bits(), + )?; + } + let init = crate::filesystem::vfs::permission::child_inode_init( + &parent.metadata, + FileType::SymLink, + InodeMode::S_IRWXUGO, + ); + + let now = PosixTimeSpec::now(); + let inline = target.len() < SHORT_SYMLINK_LEN; + let result = Arc::new(LockedTmpfsInode::new(TmpfsInode { + parent: parent.self_ref.clone(), + self_ref: Weak::default(), + children: BTreeMap::new(), + page_cache: None, + metadata: Metadata { + dev_id: 0, + inode_id: generate_inode_id(), + size: target.len() as i64, + blk_size: TMPFS_BLOCK_SIZE as usize, + blocks: if inline { 0 } else { 1 }, + atime: now, + mtime: now, + ctime: now, + btime: now, + file_type: FileType::SymLink, + mode: init.mode, + flags: InodeFlags::empty(), + nlinks: 1, + uid: init.uid, + gid: init.gid, + raw_dev: DeviceNumber::default(), + }, + fs: parent.fs.clone(), + special_node: None, + inline_symlink: inline.then(|| target.to_string()), + name: name.clone(), + })); + result.0.lock().self_ref = Arc::downgrade(&result); + + if !inline { + let inode_dyn: Arc = result.clone(); + let backend = Arc::new(TmpfsPageCacheBackend::new( + Arc::downgrade(&inode_dyn), + parent.fs.clone(), + )); + let page_cache = PageCache::new_shmem(Some(Arc::downgrade(&inode_dyn)), Some(backend)); + result.0.lock().page_cache = Some(page_cache.clone()); + let page = page_cache.manager().commit_overwrite(0)?; + let mut page_guard = page.write(); + unsafe { + page_guard.as_slice_mut()[..target.len()].copy_from_slice(target.as_bytes()); + } + page_guard.add_flags(crate::mm::page::PageFlags::PG_DIRTY); + drop(page_guard); + page_cache.manager().update_page(0)?; + } + + parent.children.insert(name, result.clone()); + tmpfs_touch_dir(&mut parent, now); + Ok(result) } fn create_with_data( @@ -1111,6 +1301,8 @@ impl IndexNode for LockedTmpfsInode { if inode.children.contains_key(&name) { return Err(SystemError::EEXIST); } + let init = + crate::filesystem::vfs::permission::child_inode_init(&inode.metadata, file_type, mode); let now = PosixTimeSpec::now(); let result: Arc = Arc::new(LockedTmpfsInode::new(TmpfsInode { @@ -1129,15 +1321,16 @@ impl IndexNode for LockedTmpfsInode { ctime: now, btime: now, file_type, - mode, + mode: init.mode, flags: InodeFlags::empty(), nlinks: if file_type == FileType::Dir { 2 } else { 1 }, - uid: 0, - gid: 0, + uid: init.uid, + gid: init.gid, raw_dev: DeviceNumber::from(data as u32), }, fs: inode.fs.clone(), special_node: None, + inline_symlink: None, name: name.clone(), })); @@ -1148,7 +1341,8 @@ impl IndexNode for LockedTmpfsInode { // 因此 symlink 也必须有 page_cache 后端,否则会在 write_at/read_at 返回 EIO。 if file_type == FileType::File || file_type == FileType::SymLink { let backend = Arc::new(TmpfsPageCacheBackend::new( - Arc::downgrade(&result) as Weak + Arc::downgrade(&result) as Weak, + inode.fs.clone(), )); let pc = PageCache::new_shmem( Some(Arc::downgrade(&result) as Weak), @@ -1206,14 +1400,6 @@ impl IndexNode for LockedTmpfsInode { return Err(SystemError::EPERM); } - // 获取文件大小,用于减少current_size - let file_size = deleted_inode.metadata.size as usize; - let fs = deleted_inode.fs.upgrade().ok_or(SystemError::EIO)?; - let tmpfs = fs - .as_any_ref() - .downcast_ref::() - .ok_or(SystemError::EIO)?; - drop(deleted_inode); let mut deleted_guard = to_delete.0.lock(); @@ -1223,7 +1409,6 @@ impl IndexNode for LockedTmpfsInode { .checked_sub(1) .expect("tempfs nlinks underflow: filesystem corruption detected"); - let should_free = deleted_guard.metadata.nlinks == 0; let now = PosixTimeSpec::now(); deleted_guard.metadata.ctime = now; drop(deleted_guard); @@ -1231,10 +1416,6 @@ impl IndexNode for LockedTmpfsInode { inode.children.remove(&name); tmpfs_touch_dir(&mut inode, now); - if should_free { - tmpfs.decrease_size(file_size); - } - Ok(()) } @@ -1261,14 +1442,6 @@ impl IndexNode for LockedTmpfsInode { return Err(SystemError::ENOTEMPTY); } - // 目录的大小通常是0(不包含数据),但为了完整性,我们也处理 - let dir_size = deleted_inode.metadata.size as usize; - let fs = deleted_inode.fs.upgrade().ok_or(SystemError::EIO)?; - let tmpfs = fs - .as_any_ref() - .downcast_ref::() - .ok_or(SystemError::EIO)?; - drop(deleted_inode); let now = PosixTimeSpec::now(); let mut deleted_inode = to_delete.0.lock(); @@ -1279,9 +1452,6 @@ impl IndexNode for LockedTmpfsInode { inode.metadata.nlinks -= 1; tmpfs_touch_dir(&mut inode, now); - // 减少文件系统使用的大小(目录通常大小为0) - tmpfs.decrease_size(dir_size); - Ok(()) } @@ -1522,6 +1692,8 @@ impl IndexNode for LockedTmpfsInode { FileType::Socket => FileType::Socket, _ => return Err(SystemError::EINVAL), }; + let init = + crate::filesystem::vfs::permission::child_inode_init(&inode.metadata, file_type, mode); let now = PosixTimeSpec::now(); let nod = Arc::new(LockedTmpfsInode::new(TmpfsInode { @@ -1540,15 +1712,16 @@ impl IndexNode for LockedTmpfsInode { ctime: now, btime: now, file_type, - mode, + mode: init.mode, nlinks: 1, - uid: 0, - gid: 0, + uid: init.uid, + gid: init.gid, raw_dev: dev_t, flags: InodeFlags::empty(), }, fs: inode.fs.clone(), special_node: None, + inline_symlink: None, name: filename.clone(), })); diff --git a/kernel/src/filesystem/vfs/file.rs b/kernel/src/filesystem/vfs/file.rs index d4e6fd0100..626acda291 100644 --- a/kernel/src/filesystem/vfs/file.rs +++ b/kernel/src/filesystem/vfs/file.rs @@ -1398,9 +1398,46 @@ impl File { self.offset .fetch_add(len, core::sync::atomic::Ordering::SeqCst); } + // Linux file_accessed() runs for every non-zero regular-file read + // request, including EOF. Zero-count reads returned above are exempt. + if len > 0 || self.file_type == FileType::File { + self.touch_atime_after_access(); + } Ok(len) } + /// Best-effort equivalent of Linux file_accessed()/touch_atime(). + fn touch_atime_after_access(&self) { + use super::mount::{MountFS, MountFlags}; + + if self.flags().contains(FileFlags::O_NOATIME) { + return; + } + let mount_flags = self.inode.mount_flags(); + if mount_flags.contains(MountFlags::NOATIME) + || (self.file_type == FileType::Dir && mount_flags.contains(MountFlags::NODIRATIME)) + { + return; + } + let Some(fs) = self.inode.try_fs() else { + // Anonymous kernel objects such as sockets are represented by an + // IndexNode but are not backed by a filesystem or mount. Linux + // does not apply file_accessed() timestamp updates to their I/O. + return; + }; + if fs + .downcast_arc::() + .is_some_and(|mount| mount.is_readonly()) + { + return; + } + + let now = crate::time::PosixTimeSpec::now(); + let _ = self + .inode + .update_atime(now, mount_flags.contains(MountFlags::RELATIME)); + } + pub fn do_write( &self, offset: usize, @@ -1687,17 +1724,25 @@ impl File { /// ## 参数 /// - ctx 填充目录项的上下文 pub fn read_dir(&self, ctx: &mut FilldirContext) -> Result<(), SystemError> { - // O_PATH 文件描述符只能用于有限的操作,getdents/getdents64 - // 在 Linux 中会返回 EBADF。提前检测并返回相同语义。 + // O_PATH file descriptors cannot be used by getdents/getdents64. if self.flags().contains(FileFlags::O_PATH) { return Err(SystemError::EBADF); } - // 仅目录允许读取目录项,其它类型遵循 POSIX 语义返回 ENOTDIR。 if self.file_type() != FileType::Dir { return Err(SystemError::ENOTDIR); } + let result = self.read_dir_impl(ctx); + // Match Linux iterate_dir(): once filesystem iteration has started, + // reaching EOF or returning an error both count as directory access. + // read_dir_impl has released readdir_state before this metadata update, + // avoiding a cross-filesystem lock-order dependency. + self.touch_atime_after_access(); + result + } + + fn read_dir_impl(&self, ctx: &mut FilldirContext) -> Result<(), SystemError> { let inode: &Arc = &self.inode; // POSIX 标准要求readdir应该返回. 和 .. // 但是观察到在现有的子目录中已经包含,不做处理也能正常返回. 和 .. 这里先不做处理 diff --git a/kernel/src/filesystem/vfs/mod.rs b/kernel/src/filesystem/vfs/mod.rs index 6d6bfc456c..da1ec9214e 100644 --- a/kernel/src/filesystem/vfs/mod.rs +++ b/kernel/src/filesystem/vfs/mod.rs @@ -164,6 +164,49 @@ pub fn merge_metadata_masked(target: &mut Metadata, requested: &Metadata, mask: } } +/// Apply Linux relatime rules and update only the inode access time. +/// +/// Local filesystems call this while holding the lock that protects their +/// metadata, so the decision and the single-field update form one atomic +/// operation with respect to chmod, chown, writes, and other timestamp updates. +pub fn update_atime_locked(metadata: &mut Metadata, now: PosixTimeSpec, relatime: bool) -> bool { + if metadata.flags.contains(InodeFlags::S_NOATIME) { + return false; + } + if !should_update_atime( + metadata.atime, + metadata.mtime, + metadata.ctime, + now, + relatime, + ) { + return false; + } + metadata.atime = now; + true +} + +/// Evaluate Linux strictatime/relatime policy from authoritative in-memory +/// inode timestamps. Filesystems with a native inode cache can use this helper +/// without constructing a full metadata snapshot or reading the disk inode. +pub fn should_update_atime( + atime: PosixTimeSpec, + mtime: PosixTimeSpec, + ctime: PosixTimeSpec, + now: PosixTimeSpec, + relatime: bool, +) -> bool { + if relatime { + let recent = now.tv_sec.saturating_sub(atime.tv_sec) < 24 * 60 * 60; + let atime_after_mtime = (atime.tv_sec, atime.tv_nsec) > (mtime.tv_sec, mtime.tv_nsec); + let atime_after_ctime = (atime.tv_sec, atime.tv_nsec) > (ctime.tv_sec, ctime.tv_nsec); + if atime_after_mtime && atime_after_ctime && recent { + return false; + } + } + atime != now +} + impl From for InodeMode { fn from(val: FileType) -> Self { match val { @@ -721,6 +764,15 @@ pub trait IndexNode: Any + Sync + Send + Debug + CastFromSync { self.set_metadata(metadata) } + /// Atomically evaluate atime policy and update only atime. + /// + /// Pseudo filesystems that do not maintain access timestamps may keep the + /// no-op default. Mutable local and protocol filesystems must override this + /// instead of feeding a lockless full metadata snapshot to `set_metadata`. + fn update_atime(&self, _now: PosixTimeSpec, _relatime: bool) -> Result<(), SystemError> { + Ok(()) + } + /// @brief 重新设置文件的大小 /// /// 如果文件大小增加,则文件内容不变,但是文件的空洞部分会被填充为0 @@ -997,10 +1049,12 @@ pub trait IndexNode: Any + Sync + Send + Debug + CastFromSync { /// @brief 获取inode所在的文件系统的指针 fn fs(&self) -> Arc; - /// 获取 inode 所在的文件系统;供异步回收等可能与卸载并发的路径使用。 + /// 获取 inode 所在的文件系统;供可选文件系统能力检查,以及异步回收等 + /// 可能与卸载并发的路径使用。 /// - /// 默认实现适用于 inode 强持有文件系统的实现。仅当 inode 与文件系统之间 - /// 使用弱引用、且调用方允许文件系统已完成销毁时才应覆盖此方法。 + /// `None` 表示 inode 天生不属于任何文件系统(例如匿名 socket),或 inode + /// 仅弱持有文件系统且文件系统已经销毁。默认实现仅适用于 `fs()` 始终有效、 + /// 且 inode 强持有文件系统的实现。 fn try_fs(&self) -> Option> { Some(self.fs()) } @@ -1397,6 +1451,15 @@ impl DowncastArc for dyn IndexNode { } } +enum PathWalkOutcome { + Found(Arc, Option), + MissingFinal { + ownership: Option, + name: String, + must_be_dir: bool, + }, +} + impl dyn IndexNode { /// @brief 将当前Inode转换为一个具体的结构体(类型由T指定) /// 如果类型正确,则返回Some,否则返回None @@ -1452,8 +1515,16 @@ impl dyn IndexNode { max_follow_times: usize, follow_final_symlink: bool, ) -> Result, SystemError> { - self.do_lookup_follow_symlink_owned(path, max_follow_times, follow_final_symlink, None) - .map(|(inode, _)| inode) + match self.do_lookup_follow_symlink_owned( + path, + max_follow_times, + follow_final_symlink, + None, + false, + )? { + PathWalkOutcome::Found(inode, _) => Ok(inode), + PathWalkOutcome::MissingFinal { .. } => Err(SystemError::ENOENT), + } } /// Path walk variant that transfers a mount/operation pin at every mount @@ -1465,14 +1536,50 @@ impl dyn IndexNode { max_follow_times: usize, follow_final_symlink: bool, ) -> Result { - self.do_lookup_follow_symlink_owned( + match self.do_lookup_follow_symlink_owned( path, max_follow_times, follow_final_symlink, Some(start.derive()?), - )? - .1 - .ok_or(SystemError::ESTALE) + false, + )? { + PathWalkOutcome::Found(_, ownership) => ownership.ok_or(SystemError::ESTALE), + PathWalkOutcome::MissingFinal { .. } => Err(SystemError::ENOENT), + } + } + + /// Resolve a path while retaining the real parent of a missing final + /// component after all required symlinks have been expanded. + pub fn lookup_follow_symlink_or_missing_owned( + &self, + start: &utils::ResolvedPath, + path: &str, + max_follow_times: usize, + follow_final_symlink: bool, + ) -> Result { + match self.do_lookup_follow_symlink_owned( + path, + max_follow_times, + follow_final_symlink, + Some(start.derive()?), + true, + )? { + PathWalkOutcome::Found(_, ownership) => ownership + .map(utils::OwnedLookupOutcome::Found) + .ok_or(SystemError::ESTALE), + PathWalkOutcome::MissingFinal { + ownership, + name, + must_be_dir, + .. + } => ownership + .map(|parent| utils::OwnedLookupOutcome::MissingFinal { + parent, + name, + must_be_dir, + }) + .ok_or(SystemError::ESTALE), + } } fn do_lookup_follow_symlink_owned( @@ -1481,7 +1588,8 @@ impl dyn IndexNode { max_follow_times: usize, follow_final_symlink: bool, mut ownership: Option, - ) -> Result<(Arc, Option), SystemError> { + return_missing_final: bool, + ) -> Result { if self.metadata()?.file_type != FileType::Dir { return Err(SystemError::ENOTDIR); } @@ -1497,7 +1605,7 @@ impl dyn IndexNode { .as_ref() .map(|path| path.inode()) .unwrap_or_else(|| fs_struct.root()); - let trailing_slash = path.ends_with('/'); + let mut trailing_slash = path.ends_with('/'); // 处理绝对路径 // result: 上一个被找到的inode @@ -1518,6 +1626,9 @@ impl dyn IndexNode { }; let mut symlink_follows_remaining = max_follow_times; + // Allocate the PATH_MAX read buffer lazily and reuse it for the whole + // walk. A symlink chain must not amplify allocator work per hop. + let mut symlink_buffer: Option> = None; // 逐级查找文件 while !rest_path.is_empty() { @@ -1559,18 +1670,38 @@ impl dyn IndexNode { } } - let inode = result.find(&name)?; - if ownership.is_some() { + let has_more_components = rest_path.split('/').any(|component| !component.is_empty()); + // Keep the current directory owner aside until we know whether + // this component exists. It is both the owner returned for a + // missing final component and the base restored for a relative + // symlink target. + let parent_ownership = ownership.take(); + let inode = match result.find(&name) { + Ok(inode) => inode, + Err(error) + if return_missing_final + && error == SystemError::ENOENT + && !has_more_components => + { + return Ok(PathWalkOutcome::MissingFinal { + ownership: parent_ownership, + name, + must_be_dir: trailing_slash, + }); + } + Err(error) => return Err(error), + }; + if parent_ownership.is_some() { ownership = Some(utils::ResolvedPath::new(inode.clone())?); } let file_type = inode.metadata()?.file_type; // 如果已经是路径的最后一个部分,并且不希望跟随最后的符号链接 - if rest_path.is_empty() && !follow_final_symlink && file_type == FileType::SymLink { + if !has_more_components && !follow_final_symlink && file_type == FileType::SymLink { // Linux 语义:若 pathname 以 '/' 结尾,则必须解析为目录, // 此时即使请求"不跟随最终 symlink",也不能返回 symlink 本身。 if !trailing_slash { // 返回符号链接本身 - return Ok((inode, ownership)); + return Ok(PathWalkOutcome::Found(inode, ownership)); } } @@ -1580,9 +1711,7 @@ impl dyn IndexNode { // - symlink 位于路径中间(rest_path 非空) // - 需要跟随最终 symlink(follow_final_symlink=true) // - 或者 pathname 以 '/' 结尾(trailing_slash=true) - let need_follow = !rest_path.is_empty() - || follow_final_symlink - || (trailing_slash && rest_path.is_empty()); + let need_follow = has_more_components || follow_final_symlink || trailing_slash; // 兼容旧语义:symlink_follows_remaining==0 表示完全不跟随 symlink。 // 在这种模式下,如果路径解析"需要跟随"(例如 symlink 位于中间,或末尾带 '/'), @@ -1615,7 +1744,7 @@ impl dyn IndexNode { ownership = Some(utils::ResolvedPath::new(target_inode.clone())?); } if rest_path.is_empty() { - return Ok((target_inode, ownership)); + return Ok(PathWalkOutcome::Found(target_inode, ownership)); } else { // 将 result 设为 magic link 的目标 inode,继续迭代 result = target_inode; @@ -1623,15 +1752,31 @@ impl dyn IndexNode { } } - let mut content = [0u8; 256]; + // Filesystems such as procfs expose dynamic symlink targets + // with i_size == 0, so the inode size cannot be used as the + // read bound. Read once into a PATH_MAX-sized buffer: some of + // those implementations intentionally ignore the offset and + // therefore cannot be consumed in chunks. + if symlink_buffer.is_none() { + let mut buffer = Vec::new(); + buffer + .try_reserve_exact(MAX_PATHLEN) + .map_err(|_| SystemError::ENOMEM)?; + buffer.resize(MAX_PATHLEN, 0); + symlink_buffer = Some(buffer); + } + let content = symlink_buffer.as_mut().expect("symlink buffer initialized"); // 读取符号链接 // TODO:We need to clarify which interfaces require private data and which do not let len = inode.read_at( 0, - 256, - &mut content, + MAX_PATHLEN, + content, Mutex::new(FilePrivateData::Unused).lock(), )?; + if len >= MAX_PATHLEN { + return Err(SystemError::ENAMETOOLONG); + } // 将读到的数据转换为utf8字符串(先转为str,再转为String) let link_path = String::from( @@ -1644,6 +1789,10 @@ impl dyn IndexNode { } else { link_path + "/" + &rest_path }; + // A slash required by the unresolved suffix (for example the + // original `link/`) remains authoritative after expansion; + // the symlink target may add the same requirement as well. + trailing_slash |= new_path.ends_with('/'); // 处理 symlink 目标为绝对路径或相对路径 // 绝对路径:从进程 root 开始 @@ -1660,6 +1809,7 @@ impl dyn IndexNode { } rest_path = String::from(rest); } else { + ownership = parent_ownership; rest_path = new_path; } @@ -1674,7 +1824,7 @@ impl dyn IndexNode { return Err(SystemError::ENOTDIR); } - return Ok((result, ownership)); + return Ok(PathWalkOutcome::Found(result, ownership)); } } @@ -2254,7 +2404,9 @@ pub fn produce_fs( } None => { log::error!("mismatch filesystem type : {}", filesystem); - Err(SystemError::EINVAL) + // Linux get_fs_type() reports ENODEV when no filesystem driver is + // registered for the requested type. + Err(SystemError::ENODEV) } } } diff --git a/kernel/src/filesystem/vfs/mount/mod.rs b/kernel/src/filesystem/vfs/mount/mod.rs index ac3e4d98b0..7cc21bdb92 100644 --- a/kernel/src/filesystem/vfs/mount/mod.rs +++ b/kernel/src/filesystem/vfs/mount/mod.rs @@ -436,8 +436,7 @@ pub(crate) fn append_comma_options(base: &mut String, extra: String) { // MountId type int_like!(MountId, usize); -static MOUNT_ID_ALLOCATOR: Mutex = - Mutex::new(IdAllocator::new(0, usize::MAX).unwrap()); +static NEXT_MOUNT_ID: AtomicUsize = AtomicUsize::new(0); static NEXT_DENTRY_ID: AtomicUsize = AtomicUsize::new(1); @@ -452,13 +451,16 @@ lazy_static! { impl MountId { fn alloc() -> Self { - let id = MOUNT_ID_ALLOCATOR.lock().alloc().unwrap(); - + let id = NEXT_MOUNT_ID + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1)) + .expect("mount ID space exhausted"); MountId(id) } - unsafe fn free(&mut self) { - MOUNT_ID_ALLOCATOR.lock().free(self.0); + /// Allocate an ID for a mount object represented only by namespace + /// metadata, such as DragonOS's hidden initial-root anchor. + pub(crate) fn alloc_conceptual() -> Self { + Self::alloc() } } @@ -1486,7 +1488,27 @@ impl MountFS { { return Err(SystemError::EINVAL); } - let _gate = mountpoint.dentry.mount_gate.lock(); + with_dentry_mount_gate_set( + [ + Some(mountpoint.dentry.clone()), + Some(cover_mountpoint.dentry.clone()), + None, + ], + |gates| self.attach_beneath_prelocked(mountpoint, mount_fs, cover_mountpoint, gates), + ) + } + + fn attach_beneath_prelocked( + &self, + mountpoint: &Arc, + mount_fs: Arc, + cover_mountpoint: &Arc, + gates: &MountEdgeCommitToken, + ) -> Result<(), SystemError> { + assert!( + gates.covers(mountpoint) && gates.covers(cover_mountpoint), + "tuck-under commit token must cover both mount edges" + ); let mut mountpoints = self.mountpoints.lock(); let stack = mountpoints.entry(mountpoint.dentry.id).or_default(); if stack.iter().any(|child| Arc::ptr_eq(child, &mount_fs)) { @@ -1506,7 +1528,8 @@ impl MountFS { // Linux mnt_set_mountpoint_beneath(): the propagated mount takes the // original edge and the previous topper is reparented onto its root. covered.relocate_mountpoint(Some(cover_mountpoint.clone())); - if let Err(error) = mount_fs.attach_top(cover_mountpoint, covered.clone()) { + if let Err(error) = mount_fs.attach_top_prelocked(cover_mountpoint, covered.clone(), false) + { covered.relocate_mountpoint(Some(mountpoint.clone())); let mut mountpoints = self.mountpoints.lock(); let stack = mountpoints @@ -2891,15 +2914,6 @@ pub fn record_writeback_error_for_fs(inner_fs: &Arc, error: Syst } } -impl Drop for MountFS { - fn drop(&mut self) { - // Release MountId - unsafe { - self.mount_id.free(); - } - } -} - impl MountExternalGuard { pub fn mount(&self) -> Arc { self.mount.clone() @@ -3310,20 +3324,70 @@ impl MountFSInode { /// @return Arc pub(crate) fn overlaid_inode(&self) -> Arc { let mut current = self.self_ref.upgrade().unwrap(); + // Nearly every lookup crosses at most a handful of mount layers. Keep + // that hot path single-pass and reserve Floyd's extra topology reads + // for an already pathological depth. This is not a semantic limit: + // the cycle-safe walk below still follows every valid layer. for _ in 0..1024 { let Some(sub_mountfs) = current.mount_fs.lookup_top(¤t) else { return current; }; + let next = sub_mountfs.mountpoint_root_inode(); + if Arc::ptr_eq(&next, ¤t) { + return current; + } + current = next; + } + Self::overlaid_inode_cycle_safe(current) + } + + fn overlaid_inode_cycle_safe(mut current: Arc) -> Arc { + let mut fast = Some(current.clone()); + loop { + let Some(sub_mountfs) = current.mount_fs.lookup_top(¤t) else { + return current; + }; let next = sub_mountfs.mountpoint_root_inode(); if Arc::ptr_eq(&next, ¤t) { return current; } current = next; + + // mount-max is an admission limit and may be lowered below the + // depth of an existing namespace. Use structural cycle detection + // instead of truncating valid topology traversal at that value. + if let Some(mut cursor) = fast.take() { + for _ in 0..2 { + let Some(sub_mountfs) = cursor.mount_fs.lookup_top(&cursor) else { + return Self::finish_overlay_walk(current); + }; + let next = sub_mountfs.mountpoint_root_inode(); + if Arc::ptr_eq(&next, &cursor) { + return Self::finish_overlay_walk(current); + } + cursor = next; + } + if Arc::ptr_eq(&cursor, ¤t) { + log::warn!("MountFSInode::overlaid_inode: mount topology cycle detected"); + return current; + } + fast = Some(cursor); + } } + } - log::warn!("MountFSInode::overlaid_inode: overlay depth exceeds 1024"); - current + fn finish_overlay_walk(mut current: Arc) -> Arc { + loop { + let Some(sub_mountfs) = current.mount_fs.lookup_top(¤t) else { + return current; + }; + let next = sub_mountfs.mountpoint_root_inode(); + if Arc::ptr_eq(&next, ¤t) { + return current; + } + current = next; + } } fn do_find(&self, name: &str) -> Result, SystemError> { @@ -3350,27 +3414,50 @@ impl MountFSInode { } pub(super) fn do_parent(&self) -> Result, SystemError> { - if self.is_mountpoint_root()? { - // The current inode is the root inode of its filesystem - match self.mount_fs.self_mountpoint() { - Some(inode) => { - // `inode` is the mount point inode in the “parent mount tree”. - // Linux semantics: going up (..) from the root of a mounted filesystem should - // return to the parent directory of the mount point, and subsequent path traversal - // should occur on the parent mount (inode.mount_fs). - // - // Here we directly reuse the mount point inode's do_parent() to ensure mount_fs is switched correctly. - return inode.do_parent(); - } - None => { - return Ok(self.self_ref.upgrade().unwrap()); + // A propagation transaction can construct a very deep stack of mounts. + // Resolve `..` iteratively so a userspace directory walk cannot consume + // one kernel stack frame per mount layer. + let mut current = self.self_ref.upgrade().unwrap(); + let mut fast = Some(current.clone()); + loop { + if current.is_mountpoint_root()? { + // Linux crosses from a mounted root to the parent of the mount + // point. The mount point may itself be another mounted root, so + // keep walking until a real dentry parent is reached. + let Some(mountpoint) = current.mount_fs.self_mountpoint() else { + return Ok(current); + }; + current = mountpoint; + + if let Some(mut cursor) = fast.take() { + let mut exhausted = false; + for _ in 0..2 { + if !cursor.is_mountpoint_root()? { + exhausted = true; + break; + } + let Some(mountpoint) = cursor.mount_fs.self_mountpoint() else { + exhausted = true; + break; + }; + cursor = mountpoint; + } + if !exhausted { + if Arc::ptr_eq(&cursor, ¤t) { + return Err(SystemError::ELOOP); + } + fast = Some(cursor); + } } + continue; } + + let parent = current.dentry.state.lock().parent.clone(); + return match parent { + Some(parent) => current.mount_fs.wrapper_for_dentry(parent), + None => Ok(current), + }; } - if let Some(parent) = self.dentry.state.lock().parent.clone() { - return self.mount_fs.wrapper_for_dentry(parent); - } - Ok(self.self_ref.upgrade().unwrap()) } fn do_absolute_path(&self) -> Result { @@ -3744,6 +3831,16 @@ impl IndexNode for MountFSInode { self.dentry.inode.set_metadata_masked(metadata, mask) } + #[inline] + fn update_atime( + &self, + now: crate::time::PosixTimeSpec, + relatime: bool, + ) -> Result<(), SystemError> { + self.ensure_mount_writable()?; + self.dentry.inode.update_atime(now, relatime) + } + #[inline] fn resize(&self, len: usize) -> Result<(), SystemError> { self.ensure_mount_writable()?; diff --git a/kernel/src/filesystem/vfs/open.rs b/kernel/src/filesystem/vfs/open.rs index ec832e336d..acb8272a87 100644 --- a/kernel/src/filesystem/vfs/open.rs +++ b/kernel/src/filesystem/vfs/open.rs @@ -8,7 +8,8 @@ use super::{ permission::PermissionMask, syscall::{OpenHow, OpenHowResolve}, utils::{ - rsplit_path, should_remove_sgid_on_chown, user_path_at, user_resolved_path_at, ResolvedPath, + should_remove_sgid_on_chown, user_path_at, user_resolved_path_at, OwnedLookupOutcome, + ResolvedPath, }, vcore::{check_parent_dir_permission_inode, vfs_truncate}, FileType, FsPermissionPolicy, IndexNode, InodeMode, SetMetadataMask, MAX_PATHLEN, @@ -275,8 +276,15 @@ pub fn do_sys_open( fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result { // log::debug!("openat2: dirfd: {}, path: {}, how: {:?}",dirfd, path, how); + // Linux 6.6 rejects this contradictory creation request before lookup, so + // the result must not depend on whether the pathname already exists. + if how.o_flags.contains(FileFlags::O_CREAT) && how.o_flags.contains(FileFlags::O_DIRECTORY) { + return Err(SystemError::EINVAL); + } let path = path.trim(); - let follow_symlink = !how.o_flags.contains(FileFlags::O_NOFOLLOW); + // Linux makes O_CREAT|O_EXCL imply O_NOFOLLOW for the final component. + let follow_symlink = !(how.o_flags.contains(FileFlags::O_NOFOLLOW) + || how.o_flags.contains(FileFlags::O_CREAT) && how.o_flags.contains(FileFlags::O_EXCL)); // 检查空字符串路径 if path.is_empty() { return Err(SystemError::ENOENT); @@ -287,7 +295,7 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result Result = None; let resolved = match resolved { - Ok(resolved) => resolved, - Err(errno) => { + Ok(OwnedLookupOutcome::Found(resolved)) => resolved, + Ok(OwnedLookupOutcome::MissingFinal { + parent: parent_resolved, + name: filename, + must_be_dir, + }) => { // 文件不存在,且需要创建 if how.o_flags.contains(FileFlags::O_CREAT) && !how.o_flags.contains(FileFlags::O_DIRECTORY) - && errno == SystemError::ENOENT { - // 如果路径以斜杠结尾,不能创建普通文件 - if path_ends_with_slash { + // A trailing slash may come from the expanded symlink target, + // not only from the original userspace pathname. + if must_be_dir { return Err(SystemError::EISDIR); } - let (filename, parent_path) = rsplit_path(&path); // 检查文件名长度 if filename.len() > crate::filesystem::vfs::NAME_MAX { return Err(SystemError::ENAMETOOLONG); } - // Resolve and retain the actual parent mount across permission - // checks and create. The original start_path alone is not - // sufficient when parent_path crosses a mount boundary. - let parent_resolved = inode_begin.lookup_follow_symlink_owned( - &start_path, - parent_path.unwrap_or(""), - VFS_MAX_FOLLOW_SYMLINK_TIMES, - true, - )?; let parent_inode = parent_resolved.inode(); let parent_md = parent_inode.metadata()?; // 父节点必须是目录 @@ -343,14 +345,14 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result = - match parent_inode.create_and_open(filename, create_mode, &create_flags) { + match parent_inode.create_and_open(&filename, create_mode, &create_flags) { Ok(opened) => { let inode = opened.inode(); preopened = Some(opened); inode } Err(SystemError::ENOSYS) => { - parent_inode.create(filename, FileType::File, create_mode)? + parent_inode.create(&filename, FileType::File, create_mode)? } Err(err) => return Err(err), }; @@ -359,10 +361,10 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result return Err(errno), }; drop(start_path); let inode = resolved.inode(); @@ -376,18 +378,6 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result Result Result Result ChildInodeInit { + // Filesystems such as procfs create backing ramfs nodes before the process + // manager has installed an idle task. Those kernel-owned bootstrap + // creations run with the initial root credential rather than a current + // task credential. + if !ProcessManager::initialized() { + let gid = if parent.mode.contains(InodeMode::S_ISGID) { + if file_type == FileType::Dir { + mode.insert(InodeMode::S_ISGID); + } + parent.gid + } else { + 0 + }; + return ChildInodeInit { uid: 0, gid, mode }; + } + + let cred = ProcessManager::current_pcb().cred(); + let gid = if parent.mode.contains(InodeMode::S_ISGID) { + if file_type == FileType::Dir { + mode.insert(InodeMode::S_ISGID); + } + parent.gid + } else { + cred.fsgid.data() + }; + // Linux vfs_prepare_mode()/mode_strip_sgid(): a caller must not create an + // executable setgid non-directory for a group it does not belong to. + let in_group = cred.fsgid.data() == gid || cred.groups.iter().any(|group| group.data() == gid); + if file_type != FileType::Dir + && mode.contains(InodeMode::S_ISGID | InodeMode::S_IXGRP) + && !in_group + && !cred.has_capability(CAPFlags::CAP_FSETID) + { + mode.remove(InodeMode::S_ISGID); + } + ChildInodeInit { + uid: cred.fsuid.data(), + gid, + mode, + } +} + /// VFS permission check wrapper that respects per-filesystem policy. /// /// This is the single entry point that should be used by VFS/pathwalk/syscalls @@ -49,10 +104,30 @@ pub fn check_inode_permission( metadata: &Metadata, mask: PermissionMask, ) -> Result<(), SystemError> { + // Match Linux sb_permission(): a read-only mount rejects write access to + // filesystem objects before DAC/capability overrides are considered. + if mask.contains(PermissionMask::MAY_WRITE) + && matches!( + metadata.file_type, + FileType::File | FileType::Dir | FileType::SymLink + ) + && inode + .try_fs() + .and_then(|fs| fs.downcast_arc::()) + .is_some_and(|mount| mount.is_readonly()) + { + return Err(SystemError::EROFS); + } + if mask.contains(PermissionMask::MAY_WRITE) + && metadata.flags.contains(super::InodeFlags::S_IMMUTABLE) + { + return Err(SystemError::EPERM); + } + let cred = ProcessManager::current_pcb().cred(); - match inode.fs().permission_policy() { - FsPermissionPolicy::Dac => cred.inode_permission(metadata, mask.bits()), - FsPermissionPolicy::Remote => { + match inode.try_fs().map(|fs| fs.permission_policy()) { + None | Some(FsPermissionPolicy::Dac) => cred.inode_permission(metadata, mask.bits()), + Some(FsPermissionPolicy::Remote) => { if mask.contains(PermissionMask::MAY_EXEC) && metadata.file_type == FileType::File && (metadata.mode.bits() & InodeMode::S_IXUGO.bits()) == 0 @@ -64,6 +139,21 @@ pub fn check_inode_permission( } } +/// Check whether the current task may suppress access-time updates for an inode. +/// +/// Linux restricts enabling `O_NOATIME` to the inode owner or a task with +/// `CAP_FOWNER`. This check is separate from read permission: being allowed to +/// read another user's file does not imply permission to hide that access from +/// its timestamp metadata. +pub fn check_noatime_permission(metadata: &Metadata) -> Result<(), SystemError> { + let cred = ProcessManager::current_pcb().cred(); + if cred.fsuid.data() == metadata.uid || cred.has_capability(CAPFlags::CAP_FOWNER) { + Ok(()) + } else { + Err(SystemError::EPERM) + } +} + impl Cred { /// 检查具有给定凭证的进程是否有权限访问 inode。 /// diff --git a/kernel/src/filesystem/vfs/syscall/symlink_utils.rs b/kernel/src/filesystem/vfs/syscall/symlink_utils.rs index de12f5987e..07984ab572 100644 --- a/kernel/src/filesystem/vfs/syscall/symlink_utils.rs +++ b/kernel/src/filesystem/vfs/syscall/symlink_utils.rs @@ -3,6 +3,7 @@ use system_error::SystemError; use crate::{ filesystem::vfs::{ fcntl::AtFlags, + permission::{check_inode_permission, PermissionMask}, utils::{rsplit_path, user_path_at}, FileType, NAME_MAX, VFS_MAX_FOLLOW_SYMLINK_TIMES, }, @@ -20,10 +21,10 @@ pub fn do_symlinkat(from: &str, newdfd: Option, to: &str) -> Result, to: &str) -> Result new_begin_inode, }; - if new_parent.metadata()?.file_type != FileType::Dir { + let parent_metadata = new_parent.metadata()?; + if parent_metadata.file_type != FileType::Dir { return Err(SystemError::ENOTDIR); } + // Linux filename_create()/may_create() checks search permission before + // exposing whether the destination exists, but an existing destination + // takes precedence over a missing write permission. + check_inode_permission(&new_parent, &parent_metadata, PermissionMask::MAY_EXEC)?; + match new_parent.find(new_name) { + Ok(_) => return Err(SystemError::EEXIST), + Err(SystemError::ENOENT) => {} + Err(error) => return Err(error), + } + if trailing_slash { + return Err(SystemError::ENOENT); + } + check_inode_permission(&new_parent, &parent_metadata, PermissionMask::MAY_WRITE)?; + new_parent.symlink(new_name, from)?; return Ok(0); diff --git a/kernel/src/filesystem/vfs/syscall/sys_fcntl.rs b/kernel/src/filesystem/vfs/syscall/sys_fcntl.rs index 352c446198..2febdafced 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_fcntl.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_fcntl.rs @@ -187,6 +187,12 @@ impl SysFcntlHandle { { return Err(SystemError::EPERM); } + if new_flags.contains(FileFlags::O_NOATIME) + && !current_flags.contains(FileFlags::O_NOATIME) + { + let metadata = file.inode().metadata()?; + crate::filesystem::vfs::permission::check_noatime_permission(&metadata)?; + } let old_fasync = current_flags.contains(FileFlags::FASYNC); let new_fasync = new_flags.contains(FileFlags::FASYNC); file.set_flags(new_flags)?; diff --git a/kernel/src/filesystem/vfs/syscall/sys_mount.rs b/kernel/src/filesystem/vfs/syscall/sys_mount.rs index 23f23d5164..48f094ebce 100644 --- a/kernel/src/filesystem/vfs/syscall/sys_mount.rs +++ b/kernel/src/filesystem/vfs/syscall/sys_mount.rs @@ -485,10 +485,18 @@ fn do_new_mount( mount_flags: MountFlags, ) -> Result, SystemError> { let fs_type_str = filesystemtype.ok_or(SystemError::EINVAL)?; - let source = source.ok_or(SystemError::EINVAL)?; - let fs = produce_fs(&fs_type_str, data.as_deref(), &source, mount_flags).inspect_err(|e| { + // Linux accepts a NULL source for nodev filesystems and reports it as + // "none" in mountinfo. Keep an explicitly supplied empty string distinct. + let fs = produce_fs( + &fs_type_str, + data.as_deref(), + source.as_deref().unwrap_or(""), + mount_flags, + ) + .inspect_err(|e| { log::warn!("Failed to produce filesystem: {:?}", e); })?; + let source = source.unwrap_or_else(|| String::from("none")); let new_mount_res: Result, SystemError> = if let Some(mnt_inode) = target_inode.clone().downcast_arc::() { @@ -516,7 +524,11 @@ fn do_new_mount( } Ok(prepared) } else { - target_inode.mount(fs, mount_flags) + // A pathname usable as a mountpoint in this namespace resolves to + // a MountFSInode. A bare inode can be reached through an internal + // pseudo-filesystem magic link (for example /proc/self/fd/N), but + // it is not attached to the caller's mount namespace. + Err(SystemError::EINVAL) }; let new_mount = new_mount_res?; diff --git a/kernel/src/filesystem/vfs/utils.rs b/kernel/src/filesystem/vfs/utils.rs index ff358e2357..d691a64acf 100644 --- a/kernel/src/filesystem/vfs/utils.rs +++ b/kernel/src/filesystem/vfs/utils.rs @@ -25,6 +25,21 @@ pub struct ResolvedPath { _operation_guard: InodeRetentionGuard, } +/// Result of a pathname walk that may stop at a missing final component. +/// +/// `MissingFinal` retains the resolved parent, including its mount ownership, +/// so create-style operations act on the final pathname after symlink +/// expansion instead of reparsing the original userspace string. +#[derive(Debug)] +pub enum OwnedLookupOutcome { + Found(ResolvedPath), + MissingFinal { + parent: ResolvedPath, + name: String, + must_be_dir: bool, + }, +} + impl ResolvedPath { pub fn new(inode: Arc) -> Result { let mount_guard = inode diff --git a/kernel/src/filesystem/vfs/vcore.rs b/kernel/src/filesystem/vfs/vcore.rs index 93374e10c1..a6a0221e57 100644 --- a/kernel/src/filesystem/vfs/vcore.rs +++ b/kernel/src/filesystem/vfs/vcore.rs @@ -27,7 +27,7 @@ use crate::{ ipc::kill::send_signal_to_pid, libs::mutex::MutexGuard, process::{ - cred::CAPFlags, + cred::{CAPFlags, Cred}, namespace::mnt::{mnt_namespace_init, RootMountAttachment}, resource::RLimitID, ProcessManager, @@ -783,11 +783,19 @@ where do_resize(&inode, &md, mask) } -fn prepare_write_side_effect_metadata( - mut md: Metadata, +pub(crate) fn prepare_write_side_effect_metadata( + md: Metadata, new_size: usize, ) -> (Metadata, SetMetadataMask) { let cred = ProcessManager::current_pcb().cred(); + prepare_write_side_effect_metadata_with_cred(md, new_size, &cred) +} + +pub(crate) fn prepare_write_side_effect_metadata_with_cred( + mut md: Metadata, + new_size: usize, + cred: &Arc, +) -> (Metadata, SetMetadataMask) { let now = crate::time::PosixTimeSpec::now(); md.size = new_size as i64; md.mtime = now; @@ -802,7 +810,7 @@ fn prepare_write_side_effect_metadata( let original_mode = md.mode; md.mode.remove(InodeMode::S_ISUID); - if should_remove_sgid(md.mode, md.gid, &cred) { + if should_remove_sgid(md.mode, md.gid, cred) { md.mode.remove(InodeMode::S_ISGID); } diff --git a/kernel/src/mm/ucontext/address_space.rs b/kernel/src/mm/ucontext/address_space.rs index b672cc8613..8c5df309f2 100644 --- a/kernel/src/mm/ucontext/address_space.rs +++ b/kernel/src/mm/ucontext/address_space.rs @@ -516,7 +516,7 @@ impl AddressSpace { len, prot_flags, map_flags, - may_exec, + mut may_exec, offset, round_to_min, allocate_at_once, @@ -527,6 +527,14 @@ impl AddressSpace { if len == 0 { return Err(SystemError::EINVAL); } + let noexec = file + .inode() + .mount_flags() + .contains(crate::filesystem::vfs::mount::MountFlags::NOEXEC); + if noexec && prot_flags.contains(ProtFlags::PROT_EXEC) { + return Err(SystemError::EPERM); + } + may_exec &= !noexec; let _force_lazy_on_page_fault_arch = allocate_at_once && MMArch::PAGE_FAULT_ENABLED; diff --git a/kernel/src/net/socket/inode.rs b/kernel/src/net/socket/inode.rs index 880d039d92..6850b1bf34 100644 --- a/kernel/src/net/socket/inode.rs +++ b/kernel/src/net/socket/inode.rs @@ -358,6 +358,10 @@ impl IndexNode for T { unreachable!("Socket does not have a file system") } + fn try_fs(&self) -> Option> { + None + } + fn as_any_ref(&self) -> &dyn core::any::Any { self } diff --git a/kernel/src/process/namespace/mnt.rs b/kernel/src/process/namespace/mnt.rs index 1f95e4cb5b..ee073d0ee9 100644 --- a/kernel/src/process/namespace/mnt.rs +++ b/kernel/src/process/namespace/mnt.rs @@ -1,7 +1,7 @@ use crate::{ filesystem::vfs::{ mount::{ - lock_mount_lifecycle, MountFSInode, MountFlags, MountTopologyGuard, + lock_mount_lifecycle, MountFSInode, MountFlags, MountId, MountTopologyGuard, MOUNT_LIFECYCLE_LOCK, }, FileSystem, IndexNode, MountFS, @@ -64,7 +64,7 @@ struct MountCountState { } impl MountCountState { - fn reserve(&mut self, amount: u32, limit: u32) -> Result<(), SystemError> { + fn ensure_capacity(&self, amount: u32, limit: u32) -> Result<(), SystemError> { let used = self .mounts .checked_add(self.pending_mounts) @@ -73,6 +73,11 @@ impl MountCountState { if amount > remaining { return Err(SystemError::ENOSPC); } + Ok(()) + } + + fn reserve(&mut self, amount: u32, limit: u32) -> Result<(), SystemError> { + self.ensure_capacity(amount, limit)?; self.pending_mounts = self .pending_mounts .checked_add(amount) @@ -142,14 +147,15 @@ pub(crate) enum RootMountAttachment { pub struct InnerMntNamespace { _dead: bool, root_mountfs: Arc, - /// Whether the current root has Linux's parent mount attachment. + /// Identity of the hidden parent attachment of the namespace root. /// /// DragonOS removes the boot rootfs object when the real root is /// installed, so that hidden parent edge cannot be represented by /// `MountFS::self_mountpoint`. Keep its semantic state explicitly: the /// initial rootfs is unattached and cannot be pivoted, while the real boot - /// root and namespace copies remain attached to that conceptual boundary. - root_attached: bool, + /// root and namespace copies retain the old rootfs mount ID as the + /// conceptual parent exposed by mountinfo. + root_parent_mount_id: Option, mount_count: MountCountState, /// Exact old-mount to copied-mount projection used to rebind fs_struct /// root/pwd after CLONE_NEWNS. This is object identity, never a pathname. @@ -232,7 +238,7 @@ impl MntNamespace { _user_ns: super::user_namespace::INIT_USER_NAMESPACE.clone(), inner: RwSem::new(InnerMntNamespace { root_mountfs: ramfs.clone(), - root_attached: false, + root_parent_mount_id: None, mount_count: MountCountState::default(), copy_sources: Vec::new(), _dead: false, @@ -264,7 +270,8 @@ impl MntNamespace { let mut inner_guard = self.inner.write(); new_root.set_namespace(self.self_ref.clone()); let old_root = core::mem::replace(&mut inner_guard.root_mountfs, new_root); - inner_guard.root_attached = attachment == RootMountAttachment::Attached; + inner_guard.root_parent_mount_id = + (attachment == RootMountAttachment::Attached).then_some(old_root.mount_id()); assert!( old_root.take_namespace_accounted(&self.self_ref), "the old namespace root must own one committed mount slot" @@ -353,7 +360,7 @@ impl MntNamespace { if Arc::ptr_eq(&new_root, &old_root) || Arc::ptr_eq(&put_old_mnt, &old_root) { return Err(SystemError::EBUSY); } - if old_is_namespace_root && !namespace_inner.root_attached { + if old_is_namespace_root && namespace_inner.root_parent_mount_id.is_none() { return Err(SystemError::EINVAL); } let old_root_mountpoint = old_root_mountpoint.as_ref(); @@ -709,7 +716,12 @@ impl MntNamespace { inner: RwSem::new(InnerMntNamespace { _dead: false, root_mountfs: new_root_mntfs, - root_attached: inner.root_attached, + // Linux copy_tree() clones the hidden parent together with the + // visible root, so the copied namespace must not expose the + // source namespace's parent mount identity in mountinfo. + root_parent_mount_id: inner + .root_parent_mount_id + .map(|_| MountId::alloc_conceptual()), // Linux copy_mnt_ns() initializes the copied namespace's // existing mount count directly. mount-max only admits new // mount trees; it must not reject cloning existing topology. @@ -743,6 +755,13 @@ impl MntNamespace { Self::root_mntfs_locked(&self.inner.read()) } + /// Return the (possibly invisible) parent mount ID of an attached namespace + /// root. Linux exposes this ID in mountinfo even when the parent is outside + /// the process's visible root. + pub(crate) fn root_parent_mount_id(&self) -> Option { + self.inner.read().root_parent_mount_id + } + /// Get the root inode of this mount namespace pub fn root_inode(&self) -> Arc { let root = self.root_mntfs(); @@ -900,6 +919,18 @@ impl MntNamespace { Some(mntfs.clone()) } + /// Fail early when a known lower bound cannot fit in this namespace. + /// + /// This is only a preflight optimization; the later reservation remains + /// authoritative and closes any race with a concurrent limit change. + pub(crate) fn ensure_mount_capacity(&self, amount: usize) -> Result<(), SystemError> { + let amount = u32::try_from(amount).map_err(|_| SystemError::ENOSPC)?; + self.inner + .read() + .mount_count + .ensure_capacity(amount, mount_max()) + } + pub(crate) fn reserve_mounts( &self, mut mounts: Vec>, @@ -1110,7 +1141,7 @@ mod tests { .self_mountpoint() .as_ref() .is_some_and(|mountpoint| Arc::ptr_eq(mountpoint, &new_root.mountpoint_root_inode()))); - assert!(namespace.inner.read().root_attached); + assert!(namespace.inner.read().root_parent_mount_id.is_some()); } #[test] @@ -1119,14 +1150,17 @@ mod tests { let initial_copy = initial .copy_mnt_ns(&CloneFlags::CLONE_NEWNS, INIT_USER_NAMESPACE.clone()) .unwrap(); - assert!(!initial_copy.inner.read().root_attached); + assert!(initial_copy.inner.read().root_parent_mount_id.is_none()); let attached = MntNamespace::new_root(); install_attached_root(&attached); let attached_copy = attached .copy_mnt_ns(&CloneFlags::CLONE_NEWNS, INIT_USER_NAMESPACE.clone()) .unwrap(); - assert!(attached_copy.inner.read().root_attached); + let attached_parent = attached.inner.read().root_parent_mount_id.unwrap(); + let copied_parent = attached_copy.inner.read().root_parent_mount_id.unwrap(); + assert_ne!(attached_parent, copied_parent); + assert_ne!(copied_parent, attached_copy.root_mntfs().mount_id()); } #[test] diff --git a/kernel/src/process/namespace/propagation/change.rs b/kernel/src/process/namespace/propagation/change.rs index 47920c4b08..230ed6c232 100644 --- a/kernel/src/process/namespace/propagation/change.rs +++ b/kernel/src/process/namespace/propagation/change.rs @@ -3,7 +3,7 @@ use alloc::sync::Arc; use alloc::vec::Vec; -use hashbrown::HashMap; +use hashbrown::{HashMap, HashSet}; use system_error::SystemError; use crate::filesystem::vfs::{ @@ -419,10 +419,17 @@ impl PreparedPropagationRemoval { let mut graph = PropagationGraph::new(targets.len(), &mut before_reserve)?; let mut target_ids = Vec::new(); reserve_vec(&mut target_ids, targets.len(), &mut before_reserve)?; + let mut target_id_set = HashSet::new(); + if !targets.is_empty() { + before_reserve()?; + target_id_set + .try_reserve(targets.len()) + .map_err(|_| SystemError::ENOMEM)?; + } let mut removals = Vec::new(); reserve_vec(&mut removals, targets.len(), &mut before_reserve)?; for target in targets { - if target_ids.contains(&target.mount_id()) { + if !target_id_set.insert(target.mount_id()) { continue; } graph.capture_component(target.clone(), &mut before_reserve)?; @@ -435,9 +442,73 @@ impl PreparedPropagationRemoval { graph.capture_group(propagation.peer_group_id(), target, &mut before_reserve)?; } } + + // Removing an entire standalone peer group has no make-slave + // reparenting work: every member ends private and the registry group + // disappears. Running the general one-at-a-time simulation would + // repeatedly search and retain the shrinking member vector, which is + // quadratic for mount-max sized groups. Keep groups with an external + // master/slave edge, or with any surviving peer, on the general path. + let mut standalone_groups = Vec::new(); + reserve_vec( + &mut standalone_groups, + graph.groups.len(), + &mut before_reserve, + )?; + for (group_id, group) in &graph.groups { + if !group.members.is_empty() + && group.members.iter().all(|member| { + target_id_set.contains(member) + && graph + .nodes + .get(member) + .is_some_and(|node| node.master.is_none() && node.slaves.is_empty()) + }) + { + standalone_groups.push(*group_id); + } + } + let mut handled = HashSet::new(); + if !target_ids.is_empty() { + before_reserve()?; + handled + .try_reserve(target_ids.len()) + .map_err(|_| SystemError::ENOMEM)?; + } + for group_id in standalone_groups { + let members = core::mem::take( + &mut graph + .groups + .get_mut(&group_id) + .expect("captured standalone peer group") + .members, + ); + for member in members { + let node = graph + .nodes + .get_mut(&member) + .expect("captured peer group member"); + node.flags + .remove(PropagationFlags::SHARED | PropagationFlags::UNBINDABLE); + node.peer_group = None; + handled.insert(member); + } + } + let mut complex_targets = Vec::new(); + reserve_vec( + &mut complex_targets, + target_ids.len().saturating_sub(handled.len()), + &mut before_reserve, + )?; + complex_targets.extend( + target_ids + .iter() + .copied() + .filter(|target| !handled.contains(target)), + ); let mut alloc_group = PropagationGroup::alloc; graph.simulate_change( - &target_ids, + &complex_targets, PropagationType::Private, &mut alloc_group, &mut before_reserve, diff --git a/kernel/src/process/namespace/propagation/event.rs b/kernel/src/process/namespace/propagation/event.rs index 4627efb05e..9305325b7f 100644 --- a/kernel/src/process/namespace/propagation/event.rs +++ b/kernel/src/process/namespace/propagation/event.rs @@ -68,15 +68,15 @@ fn try_collect_subtree(root: &Arc) -> Result>, SystemE } pub(super) struct CorrespondingSources { - mounts: BTreeMap>, - peer_groups: BTreeMap>, + mounts: HashMap>, + peer_groups: HashMap>, } impl CorrespondingSources { pub(super) fn new() -> Self { Self { - mounts: BTreeMap::new(), - peer_groups: BTreeMap::new(), + mounts: HashMap::new(), + peer_groups: HashMap::new(), } } @@ -93,19 +93,25 @@ impl CorrespondingSources { } } + fn try_reserve(&mut self, additional: usize) -> Result<(), SystemError> { + self.mounts + .try_reserve(additional) + .map_err(|_| SystemError::ENOMEM)?; + self.peer_groups + .try_reserve(additional) + .map_err(|_| SystemError::ENOMEM) + } + pub(super) fn nearest( &self, target: &Arc, ) -> Result>, SystemError> { // Linux `propagate_one()` retains `last_source` when a narrow peer is // uncovered. Match that layer before walking to the next master. - let mut visited = HashSet::new(); - visited.insert(target.mount_id().data()); let mut master = target.propagation().master(); + let mut tortoise = master.clone(); + let mut hare = master.clone(); while let Some(candidate) = master { - if !visited.insert(candidate.mount_id().data()) { - return Err(SystemError::ELOOP); - } if let Some(source) = self.mounts.get(&candidate.mount_id().data()) { return Ok(Some(source.clone())); } @@ -117,11 +123,89 @@ impl CorrespondingSources { } } master = propagation.master(); + tortoise = tortoise.and_then(|mount| mount.propagation().master()); + hare = hare + .and_then(|mount| mount.propagation().master()) + .and_then(|mount| mount.propagation().master()); + if tortoise + .as_ref() + .zip(hare.as_ref()) + .is_some_and(|(slow, fast)| Arc::ptr_eq(slow, fast)) + { + return Err(SystemError::ELOOP); + } } Ok(None) } } +/// Allocation-light mirror of CorrespondingSources used only for admission. +/// It records reachability identities rather than retaining one Arc child per +/// propagation target. +struct CorrespondingPresence { + mounts: HashSet, + peer_groups: HashSet, +} + +impl CorrespondingPresence { + fn new(target_count: usize) -> Result { + let capacity = target_count.checked_add(1).ok_or(SystemError::ENOMEM)?; + let mut mounts = HashSet::new(); + mounts + .try_reserve(capacity) + .map_err(|_| SystemError::ENOMEM)?; + let mut peer_groups = HashSet::new(); + peer_groups + .try_reserve(capacity) + .map_err(|_| SystemError::ENOMEM)?; + Ok(Self { + mounts, + peer_groups, + }) + } + + fn insert(&mut self, parent: &Arc) { + self.mounts.insert(parent.mount_id().data()); + let propagation = parent.propagation(); + let group_id = propagation.peer_group_id(); + if propagation.is_shared() && group_id.is_valid() { + self.peer_groups.insert(group_id.data()); + } + } + + fn has_nearest(&self, target: &Arc) -> Result { + let mut master = target.propagation().master(); + let mut tortoise = master.clone(); + let mut hare = master.clone(); + while let Some(candidate) = master { + if self.mounts.contains(&candidate.mount_id().data()) { + return Ok(true); + } + let propagation = candidate.propagation(); + let group_id = propagation.peer_group_id(); + if propagation.is_shared() + && group_id.is_valid() + && self.peer_groups.contains(&group_id.data()) + { + return Ok(true); + } + master = propagation.master(); + tortoise = tortoise.and_then(|mount| mount.propagation().master()); + hare = hare + .and_then(|mount| mount.propagation().master()) + .and_then(|mount| mount.propagation().master()); + if tortoise + .as_ref() + .zip(hare.as_ref()) + .is_some_and(|(slow, fast)| Arc::ptr_eq(slow, fast)) + { + return Err(SystemError::ELOOP); + } + } + Ok(false) + } +} + pub(crate) struct PreparedRegistrations { peer_groups: Vec, slaves: Vec>, @@ -430,7 +514,7 @@ pub(crate) fn prepare_mount_propagation_locked( mountpoint: &Arc, new_child: &Arc, ) -> Result, SystemError> { - prepare_mount_propagation_with_counting_locked(source_mnt, mountpoint, new_child, true) + prepare_mount_propagation_with_counting_locked(source_mnt, mountpoint, new_child, true, None) } fn prepare_mount_propagation_with_counting_locked( @@ -438,6 +522,7 @@ fn prepare_mount_propagation_with_counting_locked( mountpoint: &Arc, new_child: &Arc, count_local_tree: bool, + precollected_child_tree: Option>>, ) -> Result, SystemError> { let source_prop = source_mnt.propagation(); let canonical_mountpoint = source_mnt.wrapper_for_dentry(mountpoint.shared_dentry())?; @@ -452,21 +537,77 @@ fn prepare_mount_propagation_with_counting_locked( let mut slave_groups = BTreeMap::new(); let mut mounts = Vec::new(); let mut count_reservations = Vec::new(); + let mut new_child_tree = Some(match precollected_child_tree { + Some(tree) => tree, + None => try_collect_subtree(new_child)?, + }); + let new_child_mount_count = new_child_tree + .as_ref() + .expect("new child tree remains available before reservation") + .len(); if count_local_tree { if let Some(namespace) = source_mnt.namespace() { count_reservations .try_reserve(1) .map_err(|_| SystemError::ENOMEM)?; - count_reservations.push(namespace.reserve_mounts(try_collect_subtree(new_child)?)?); + count_reservations.push( + namespace.reserve_mounts( + new_child_tree + .take() + .expect("new child tree is reserved at most once"), + )?, + ); } } - let mut propagated_sources = CorrespondingSources::new(); - propagated_sources.insert(source_mnt, new_child.clone()); let targets = if source_prop.is_shared() { propagation_targets(source_mnt) } else { Vec::new() }; + let mut propagated_sources = CorrespondingSources::new(); + propagated_sources.try_reserve(targets.len().saturating_add(1))?; + propagated_sources.insert(source_mnt, new_child.clone()); + + // Compute the complete admission cost before allocating detached clones. + // Simulate exactly which slave targets have a nearest propagated source; + // insertion order, peer-group fallback and coverage checks match the + // commit loop below, without retaining one Arc child per target. + // This is Linux count_mounts()'s pre-clone admission principle and avoids + // constructing tens of thousands of doomed peer/slave copies at ENOSPC. + let mut planned_sources = CorrespondingPresence::new(targets.len())?; + planned_sources.insert(source_mnt); + let mut propagation_capacity = HashMap::new(); + propagation_capacity + .try_reserve(targets.len()) + .map_err(|_| SystemError::ENOMEM)?; + for target in &targets { + if matches!(target.kind, PropagationTargetKind::Slave) + && !planned_sources.has_nearest(&target.mount)? + { + continue; + } + match target.mount.wrapper_for_dentry(source_dentry.clone()) { + Ok(_) => {} + Err(SystemError::EXDEV) => continue, + Err(error) => return Err(error), + } + planned_sources.insert(&target.mount); + let Some(namespace) = target.mount.namespace() else { + continue; + }; + let key = Arc::as_ptr(&namespace) as usize; + let entry = propagation_capacity + .entry(key) + .or_insert((namespace, 0usize)); + entry.1 = entry + .1 + .checked_add(new_child_mount_count) + .ok_or(SystemError::ENOSPC)?; + } + for (_, (namespace, amount)) in propagation_capacity { + namespace.ensure_mount_capacity(amount)?; + } + for target in targets { let PropagationTarget { mount: target_parent, @@ -563,7 +704,22 @@ fn prepare_mount_propagation_with_counting_locked( cover_reservation, }); } - let mut registration_mounts = collect_subtree(new_child); + // MS_MOVE already collected this subtree while inventing propagation + // groups. Reuse that exact snapshot for registration instead of walking + // and allocating for the same tree again under the lifecycle lock. + let mut registration_mounts = if let Some(tree) = new_child_tree.take() { + tree + } else { + // The local namespace reservation owns the first snapshot on ordinary + // mount/bind, so its registration plan needs a separate collection. + match try_collect_subtree(new_child) { + Ok(tree) => tree, + Err(error) => { + abandon_prepared(&mounts); + return Err(error); + } + } + }; for item in &mounts { registration_mounts.extend(collect_subtree(&item.clone)); } @@ -730,7 +886,7 @@ pub(crate) fn prepare_moved_tree_propagation_locked( moved_root: &Arc, moved_root_mountpoint: &Arc, ) -> Result { - let subtree = collect_subtree(moved_root); + let subtree = try_collect_subtree(moved_root)?; let mut invented = Vec::new(); for mount in &subtree { let prop = mount.propagation(); @@ -749,6 +905,7 @@ pub(crate) fn prepare_moved_tree_propagation_locked( moved_root_mountpoint, moved_root, false, + Some(subtree), ) { Ok(propagation) => propagation, Err(error) => { @@ -816,7 +973,12 @@ pub(crate) fn propagate_umount_sources( sources: &[Arc], lazy: bool, ) -> Result<(), SystemError> { + let mut source_ids = HashSet::new(); + source_ids + .try_reserve(sources.len()) + .map_err(|_| SystemError::ENOMEM)?; for source in sources { + source_ids.insert(source.mount_id().data()); let mountpoint = source.self_mountpoint().ok_or(SystemError::EINVAL)?; if !mountpoint .mount_fs() @@ -849,7 +1011,12 @@ pub(crate) fn propagate_umount_sources( // whole set before the first mutation, then every detach below is an // invariant-preserving exact-object operation. for target in &prepared { - if !target.parent.is_live() + // A complete subtree detach marks every local source Detaching before + // the propagation transaction starts. A propagated edge may therefore + // be parented by another source in this same batch. That is stable and + // expected; only a non-live parent outside the batch proves that the + // prepared topology changed concurrently. + if (!target.parent.is_live() && !source_ids.contains(&target.parent.mount_id().data())) || target .parent .lookup_top(&target.mountpoint) @@ -868,6 +1035,69 @@ pub(crate) fn propagate_umount_sources( return Err(SystemError::EBUSY); } } + // Source scan depth is not a topology order: propagation can discover a + // child and its parent from the same source layer. Build the exact commit + // dependency graph from MountFS parent edges so every removable child is + // detached before its removable parent. This is Linux umount_list()'s + // children-first property expressed without relying on discovery order. + let mut disconnect_by_id = HashMap::new(); + disconnect_by_id + .try_reserve(prepared.len()) + .map_err(|_| SystemError::ENOMEM)?; + for (index, target) in prepared.iter().enumerate() { + if target.disconnect { + disconnect_by_id.insert(target.child.mount_id().data(), index); + } + } + let mut remaining_children = Vec::new(); + remaining_children + .try_reserve(prepared.len()) + .map_err(|_| SystemError::ENOMEM)?; + remaining_children.resize(prepared.len(), 0usize); + let mut parent_of = Vec::new(); + parent_of + .try_reserve(prepared.len()) + .map_err(|_| SystemError::ENOMEM)?; + parent_of.resize(prepared.len(), None); + for (index, target) in prepared.iter().enumerate() { + if !target.disconnect { + continue; + } + if let Some(parent_index) = disconnect_by_id + .get(&target.parent.mount_id().data()) + .copied() + { + remaining_children[parent_index] = remaining_children[parent_index] + .checked_add(1) + .ok_or(SystemError::ENOMEM)?; + parent_of[index] = Some(parent_index); + } + } + let mut ready = VecDeque::new(); + ready + .try_reserve(disconnect_by_id.len()) + .map_err(|_| SystemError::ENOMEM)?; + for index in disconnect_by_id.values().copied() { + if remaining_children[index] == 0 { + ready.push_back(index); + } + } + let mut commit_order = Vec::new(); + commit_order + .try_reserve(disconnect_by_id.len()) + .map_err(|_| SystemError::ENOMEM)?; + while let Some(index) = ready.pop_front() { + commit_order.push(index); + if let Some(parent_index) = parent_of[index] { + remaining_children[parent_index] -= 1; + if remaining_children[parent_index] == 0 { + ready.push_back(parent_index); + } + } + } + if commit_order.len() != disconnect_by_id.len() { + return Err(SystemError::ELOOP); + } // Linux propagate_mount_unlock() clears MNT_LOCKED only from the exact // corresponding propagation roots before gathering them for umount. The // protected descendants remain locked and, for lazy detach, connected to @@ -877,40 +1107,27 @@ pub(crate) fn propagate_umount_sources( target.child.unlock_mount(); } } - let max_depth = prepared - .iter() - .map(|target| target.depth) - .max() - .unwrap_or(0); - for depth in (0..=max_depth).rev() { - for target in prepared - .iter_mut() - .filter(|target| target.disconnect && target.depth == depth) - { - let reservation = target - .reservation - .as_ref() - .expect("removable propagated umount target has a reservation"); - target - .parent - .detach_exact_restoring_root_children( - &target.child, - core::mem::take(&mut target.root_children_commit), - reservation, - ) - .expect("validated propagated umount edge commit cannot fail"); - } + for index in commit_order.iter().copied() { + let target = &mut prepared[index]; + let reservation = target + .reservation + .as_ref() + .expect("removable propagated umount target has a reservation"); + target + .parent + .detach_exact_restoring_root_children( + &target.child, + core::mem::take(&mut target.root_children_commit), + reservation, + ) + .expect("validated propagated umount edge commit cannot fail"); } graph.commit_locked(); - for depth in (0..=max_depth).rev() { - for target in prepared - .iter() - .filter(|target| target.disconnect && target.depth == depth) - { - target.child.set_self_mountpoint(None); - MountFS::finish_disconnected_umount(&target.child, lazy) - .expect("detached prepared propagation root has valid teardown topology"); - } + for index in commit_order { + let target = &prepared[index]; + target.child.set_self_mountpoint(None); + MountFS::finish_disconnected_umount(&target.child, lazy) + .expect("detached prepared propagation root has valid teardown topology"); } Ok(()) } @@ -942,6 +1159,18 @@ fn prepare_propagated_umount_targets( .try_reserve(sources.len()) .map_err(|_| SystemError::ENOMEM)?; for (depth, source) in sources.iter().rev().enumerate() { + // The first source at one propagated edge discovers the corresponding + // child on every peer/slave parent. A subtree umount then encounters + // those children again in `sources`; repeating propagation_targets() + // for each one rescans the same peer group quadratically. Preserve the + // per-source root/depth contribution while reusing the object-identity + // candidate established by the first discovery. + if let Some(index) = candidate_by_id.get(&source.mount_id().data()).copied() { + let existing = &mut result[index]; + existing.unlock_root |= depth == 0; + existing.depth = existing.depth.min(depth); + continue; + } let source_mountpoint = source.self_mountpoint().ok_or(SystemError::EINVAL)?; let parent = source_mountpoint.mount_fs(); if !parent.propagation().is_shared() { diff --git a/kernel/src/process/namespace/propagation/group.rs b/kernel/src/process/namespace/propagation/group.rs index 4297c5ea3f..3a940cad46 100644 --- a/kernel/src/process/namespace/propagation/group.rs +++ b/kernel/src/process/namespace/propagation/group.rs @@ -3,7 +3,7 @@ use alloc::sync::{Arc, Weak}; use alloc::vec::Vec; -use hashbrown::HashMap; +use hashbrown::{HashMap, HashSet}; use ida::IdAllocator; use system_error::SystemError; @@ -490,18 +490,27 @@ pub(super) fn prepare_peer_registrations( let mut before_reserve = || Ok(()); let current = try_snapshot_peer_group(group_id, &mut before_reserve)?; let mut members = Vec::new(); + let final_capacity = current + .len() + .checked_add(index - start) + .ok_or(SystemError::ENOMEM)?; members - .try_reserve(current.len() + index - start) + .try_reserve(final_capacity) + .map_err(|_| SystemError::ENOMEM)?; + // A shared bind can add tens of thousands of peers in one + // transaction. Deduplicating by scanning the growing vector makes + // this preflight quadratic and can hold the mount lifecycle lock for + // minutes. Mount IDs are the stable object identity within a boot, so + // keep publication order while using an O(1) membership index. + let mut member_ids = HashSet::new(); + member_ids + .try_reserve(final_capacity) .map_err(|_| SystemError::ENOMEM)?; for member in current .iter() .chain(additions[start..index].iter().map(|(_, mount)| mount)) { - if members.iter().any(|existing: &Weak| { - existing - .upgrade() - .is_some_and(|existing| Arc::ptr_eq(&existing, member)) - }) { + if !member_ids.insert(member.mount_id().data()) { continue; } members.push(Arc::downgrade(member)); diff --git a/user/apps/c_unitest/Makefile b/user/apps/c_unitest/Makefile index 909d977488..00e095f965 100644 --- a/user/apps/c_unitest/Makefile +++ b/user/apps/c_unitest/Makefile @@ -8,7 +8,10 @@ endif CC=$(CROSS_COMPILE)gcc CFLAGS := -Wall -O2 -static -lpthread -SRCS := $(wildcard *.c) +# These two legacy programs contain unsupported-as-PASS paths. Their Linux +# conformance coverage is maintained by dunitest and the upstream gVisor suite. +RETIRED_FALSE_POSITIVE_SRCS := test_mount_propagation.c test_pivot_root.c +SRCS := $(filter-out $(RETIRED_FALSE_POSITIVE_SRCS),$(wildcard *.c)) BINS := $(SRCS:.c=) all: $(BINS) @@ -24,6 +27,6 @@ install: all mv $(BINS) $(DADK_CURRENT_BUILD_DIR)/ clean: - rm -f $(BINS) + rm -f $(BINS) $(RETIRED_FALSE_POSITIVE_SRCS:.c=) .PHONY: all install clean diff --git a/user/apps/tests/dunitest/Makefile b/user/apps/tests/dunitest/Makefile index 19a85a6a16..bc6d96442d 100644 --- a/user/apps/tests/dunitest/Makefile +++ b/user/apps/tests/dunitest/Makefile @@ -101,6 +101,7 @@ install: build @mkdir -p $(INSTALL_DIR)/fixtures @cp --sparse=always -f $(EXT4_FIXTURE) $(INSTALL_DIR)/fixtures/ @cp -f whitelist.txt $(INSTALL_DIR)/whitelist.txt + @cp -f no_skip.txt $(INSTALL_DIR)/no_skip.txt @cp -f scripts/run_tests.sh $(INSTALL_DIR)/run_tests.sh @chmod +x $(INSTALL_DIR)/run_tests.sh @echo "安装完成" diff --git a/user/apps/tests/dunitest/default.nix b/user/apps/tests/dunitest/default.nix index fd5023c549..72accf0f5a 100644 --- a/user/apps/tests/dunitest/default.nix +++ b/user/apps/tests/dunitest/default.nix @@ -61,6 +61,7 @@ let "^suites/.*" "^Makefile$" "^whitelist\\.txt$" + "^no_skip\\.txt$" "^scripts$" "^scripts/run_tests\\.sh$" ]; @@ -90,6 +91,7 @@ let cp -r bin $out/${installDir}/ cp -r build/fixtures $out/${installDir}/ install -m644 whitelist.txt $out/${installDir}/ + install -m644 no_skip.txt $out/${installDir}/ install -m755 scripts/run_tests.sh $out/${installDir}/ runHook postInstall diff --git a/user/apps/tests/dunitest/no_skip.txt b/user/apps/tests/dunitest/no_skip.txt new file mode 100644 index 0000000000..b9b2ebedac --- /dev/null +++ b/user/apps/tests/dunitest/no_skip.txt @@ -0,0 +1,6 @@ +# Focused conformance binaries that must never report a skipped gtest case. +normal/test_pivot_root +normal/mount_propagation +normal/mount_object_topology +normal/mount_limit +normal/open_dangling_symlink diff --git a/user/apps/tests/dunitest/runner/src/main.rs b/user/apps/tests/dunitest/runner/src/main.rs index 28dcf56ce1..9bb934cb15 100644 --- a/user/apps/tests/dunitest/runner/src/main.rs +++ b/user/apps/tests/dunitest/runner/src/main.rs @@ -28,6 +28,8 @@ struct Cli { whitelist: PathBuf, #[arg(long, default_value = "blocklist.txt")] blocklist: PathBuf, + #[arg(long, default_value = "no_skip.txt")] + no_skip: PathBuf, #[arg(long, default_value = "results")] results_dir: PathBuf, #[arg(long)] @@ -45,11 +47,14 @@ fn main() -> Result<()> { let bin_dir = abs_or_join(&cwd, &cli.bin_dir.display().to_string()); let whitelist_path = abs_or_join(&cwd, &cli.whitelist.display().to_string()); let blocklist_path = abs_or_join(&cwd, &cli.blocklist.display().to_string()); + let no_skip_path = abs_or_join(&cwd, &cli.no_skip.display().to_string()); let results_dir = abs_or_join(&cwd, &cli.results_dir.display().to_string()); let manifest = Manifest::discover(&bin_dir, cli.timeout_sec)?; let whitelist = read_name_list(&whitelist_path); let blocklist = read_name_list(&blocklist_path); + let no_skip = read_strict_name_list(&no_skip_path)?; + validate_no_skip_config(&manifest, whitelist.as_ref(), &no_skip)?; if cli.list { for t in &manifest.tests { @@ -92,7 +97,8 @@ fn main() -> Result<()> { let mut t = test.clone(); t.path = abs_or_join(&cwd, &t.path).display().to_string(); - let one = run_test(&t, &results_dir, cli.verbose)?; + let mut one = run_test(&t, &results_dir, cli.verbose)?; + enforce_no_skip(&mut one, &no_skip); println!( "[RUNNER] END: {} status={} duration_ms={} log={}", one.name, @@ -158,6 +164,79 @@ fn read_name_list(path: &Path) -> Option> { Some(set) } +fn read_strict_name_list(path: &Path) -> Result> { + let content = fs::read_to_string(path) + .with_context(|| format!("读取 no-skip 配置失败: {}", path.display()))?; + let mut set = HashSet::new(); + for (index, line) in content.lines().enumerate() { + let name = line.trim(); + if name.is_empty() || name.starts_with('#') { + continue; + } + if name.starts_with('/') + || name + .split('/') + .any(|part| part.is_empty() || part == "." || part == "..") + { + anyhow::bail!("no-skip 第 {} 行包含非法测试名: {}", index + 1, name); + } + if !set.insert(name.to_string()) { + anyhow::bail!("no-skip 配置包含重复测试: {}", name); + } + } + if set.is_empty() { + anyhow::bail!("no-skip 配置为空: {}", path.display()); + } + Ok(set) +} + +fn validate_no_skip_config( + manifest: &Manifest, + whitelist: Option<&HashSet>, + no_skip: &HashSet, +) -> Result<()> { + let discovered: HashSet<_> = manifest + .tests + .iter() + .map(|test| test.name.as_str()) + .collect(); + for name in no_skip { + if !discovered.contains(name.as_str()) { + anyhow::bail!("no-skip 测试二进制不存在: {}", name); + } + if let Some(whitelist) = whitelist { + if !whitelist.contains(name) { + anyhow::bail!("no-skip 测试未进入 dunitest 白名单: {}", name); + } + } + } + Ok(()) +} + +fn enforce_no_skip(result: &mut CaseResult, no_skip: &HashSet) { + if !no_skip.contains(&result.name) { + return; + } + + let accounted = result + .gtest_passed + .checked_add(result.gtest_failed) + .and_then(|count| count.checked_add(result.gtest_skipped)); + if result.gtest_total == 0 || accounted != Some(result.gtest_total) { + result.status = CaseStatus::Failed; + result.message = format!( + "严格一致性测试缺少完整 gtest 汇总: total={}, passed={}, failed={}, skipped={}", + result.gtest_total, result.gtest_passed, result.gtest_failed, result.gtest_skipped + ); + } else if result.gtest_skipped > 0 { + result.status = CaseStatus::Failed; + result.message = format!( + "严格一致性测试不允许 skip,实际跳过 {} 个 gtest case", + result.gtest_skipped + ); + } +} + fn show_summary(summary: &report::Summary, results_dir: &Path) { println!(); println!("================ dunitest ================"); @@ -179,3 +258,48 @@ fn case_status_text(status: &CaseStatus) -> &'static str { CaseStatus::Timeout => "TIMEOUT", } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn no_skip_policy_converts_success_to_failure() { + let mut result = CaseResult { + name: "normal/test_pivot_root".to_string(), + status: CaseStatus::Passed, + duration_ms: 1, + exit_code: Some(0), + message: "ok".to_string(), + log_file: String::new(), + gtest_total: 19, + gtest_passed: 18, + gtest_failed: 0, + gtest_skipped: 1, + }; + let no_skip = HashSet::from(["normal/test_pivot_root".to_string()]); + enforce_no_skip(&mut result, &no_skip); + assert!(matches!(result.status, CaseStatus::Failed)); + assert!(result.message.contains("不允许 skip")); + } + + #[test] + fn no_skip_policy_rejects_truncated_gtest_summary() { + let mut result = CaseResult { + name: "normal/test_pivot_root".to_string(), + status: CaseStatus::Passed, + duration_ms: 1, + exit_code: Some(0), + message: "ok".to_string(), + log_file: String::new(), + gtest_total: 19, + gtest_passed: 0, + gtest_failed: 0, + gtest_skipped: 0, + }; + let no_skip = HashSet::from(["normal/test_pivot_root".to_string()]); + enforce_no_skip(&mut result, &no_skip); + assert!(matches!(result.status, CaseStatus::Failed)); + assert!(result.message.contains("缺少完整 gtest 汇总")); + } +} diff --git a/user/apps/tests/dunitest/runner/src/report.rs b/user/apps/tests/dunitest/runner/src/report.rs index fd49ee7e05..c5c93c1c2d 100644 --- a/user/apps/tests/dunitest/runner/src/report.rs +++ b/user/apps/tests/dunitest/runner/src/report.rs @@ -114,6 +114,28 @@ fn write_text_report(results_dir: &Path, summary: &Summary) -> Result<()> { Ok(()) } +fn write_json_report(results_dir: &Path, summary: &Summary) -> Result<()> { + let file = results_dir.join("summary.json"); + let content = + serde_json::to_string_pretty(summary).with_context(|| "序列化 summary.json 失败")?; + fs::write(&file, content).with_context(|| format!("写入失败: {}", file.display()))?; + Ok(()) +} + +fn write_failed_cases(results_dir: &Path, summary: &Summary) -> Result<()> { + let file = results_dir.join("failed_cases.txt"); + let mut f = File::create(&file).with_context(|| format!("创建文件失败: {}", file.display()))?; + for c in &summary.cases { + match c.status { + CaseStatus::Failed | CaseStatus::Timeout => { + writeln!(f, "{}", c.name)?; + } + _ => {} + } + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -221,25 +243,3 @@ mod tests { assert_eq!(summary.timeout, 1); } } - -fn write_json_report(results_dir: &Path, summary: &Summary) -> Result<()> { - let file = results_dir.join("summary.json"); - let content = - serde_json::to_string_pretty(summary).with_context(|| "序列化 summary.json 失败")?; - fs::write(&file, content).with_context(|| format!("写入失败: {}", file.display()))?; - Ok(()) -} - -fn write_failed_cases(results_dir: &Path, summary: &Summary) -> Result<()> { - let file = results_dir.join("failed_cases.txt"); - let mut f = File::create(&file).with_context(|| format!("创建文件失败: {}", file.display()))?; - for c in &summary.cases { - match c.status { - CaseStatus::Failed | CaseStatus::Timeout => { - writeln!(f, "{}", c.name)?; - } - _ => {} - } - } - Ok(()) -} diff --git a/user/apps/tests/dunitest/scripts/run_tests.sh b/user/apps/tests/dunitest/scripts/run_tests.sh index 30f98dca00..0edac181fe 100644 --- a/user/apps/tests/dunitest/scripts/run_tests.sh +++ b/user/apps/tests/dunitest/scripts/run_tests.sh @@ -11,12 +11,16 @@ fi RUNNER="$BASE_DIR/dunitest-runner" BIN_DIR="$BASE_DIR/bin" RESULTS="$BASE_DIR/results" +WHITELIST="$BASE_DIR/whitelist.txt" +NO_SKIP="$BASE_DIR/no_skip.txt" + +RUNNER_ARGS="--bin-dir $BIN_DIR --results-dir $RESULTS --whitelist $WHITELIST --no-skip $NO_SKIP" echo "[dunit] start running tests..." if [ "${DUNITEST_PATTERN:-}" != "" ]; then - "$RUNNER" --bin-dir "$BIN_DIR" --results-dir "$RESULTS" --pattern "$DUNITEST_PATTERN" + "$RUNNER" $RUNNER_ARGS --pattern "$DUNITEST_PATTERN" else - "$RUNNER" --bin-dir "$BIN_DIR" --results-dir "$RESULTS" + "$RUNNER" $RUNNER_ARGS fi status=$? echo "[dunit] 测试完成, status=$status" diff --git a/user/apps/tests/dunitest/suites/normal/ext4_inode_identity.cc b/user/apps/tests/dunitest/suites/normal/ext4_inode_identity.cc index f4566fcac4..e3f0ec5962 100644 --- a/user/apps/tests/dunitest/suites/normal/ext4_inode_identity.cc +++ b/user/apps/tests/dunitest/suites/normal/ext4_inode_identity.cc @@ -355,6 +355,66 @@ TEST(Ext4InodeIdentity, DeletedFdSupportsTruncateAndFallocate) { ASSERT_NO_FATAL_FAILURE(fs.Unmount()); } +TEST(Ext4InodeIdentity, ReadAtimeIsImmediatelyVisibleAndPersistsAfterFsync) { + LoopExt4 fs; + ASSERT_NO_FATAL_FAILURE(fs.SetUp()); + ASSERT_NO_FATAL_FAILURE(fs.Mount()); + + const std::string path = fs.mount_point() + "/read_atime"; + int fd = open(path.c_str(), O_CREAT | O_EXCL | O_RDWR, 0644); + ASSERT_GE(fd, 0) << strerror(errno); + constexpr char kData[] = "atime"; + ASSERT_NO_FATAL_FAILURE(WriteAll(fd, kData, sizeof(kData) - 1)); + const timespec old_times[2] = {{1, 0}, {2, 0}}; + ASSERT_EQ(0, futimens(fd, old_times)) << strerror(errno); + + char byte = 0; + ASSERT_EQ(1, pread(fd, &byte, 1, 0)) << strerror(errno); + EXPECT_EQ(kData[0], byte); + + struct stat cached {}; + ASSERT_EQ(0, fstat(fd, &cached)) << strerror(errno); + EXPECT_GT(cached.st_atim.tv_sec, old_times[0].tv_sec); + ASSERT_EQ(0, fsync(fd)) << strerror(errno); + ASSERT_EQ(0, close(fd)) << strerror(errno); + ASSERT_NO_FATAL_FAILURE(fs.Unmount()); + + ASSERT_NO_FATAL_FAILURE(fs.Mount()); + struct stat persisted {}; + ASSERT_EQ(0, stat(path.c_str(), &persisted)) << strerror(errno); + EXPECT_EQ(cached.st_atim.tv_sec, persisted.st_atim.tv_sec); + EXPECT_EQ(cached.st_atim.tv_nsec, persisted.st_atim.tv_nsec); + ASSERT_NO_FATAL_FAILURE(fs.Unmount()); +} + +TEST(Ext4InodeIdentity, RelatimeSkipsRecentAtimeNewerThanMtimeAndCtime) { + LoopExt4 fs; + ASSERT_NO_FATAL_FAILURE(fs.SetUp()); + ASSERT_NO_FATAL_FAILURE(fs.Mount()); + + const std::string path = fs.mount_point() + "/relatime_skip"; + int fd = open(path.c_str(), O_CREAT | O_EXCL | O_RDWR, 0644); + ASSERT_GE(fd, 0) << strerror(errno); + ASSERT_NO_FATAL_FAILURE(WriteAll(fd, "x", 1)); + + timespec now = {}; + ASSERT_EQ(0, clock_gettime(CLOCK_REALTIME, &now)) << strerror(errno); + const timespec times[2] = {{now.tv_sec + 3600, 0}, {now.tv_sec - 3600, 0}}; + ASSERT_EQ(0, futimens(fd, times)) << strerror(errno); + + struct stat before = {}; + ASSERT_EQ(0, fstat(fd, &before)) << strerror(errno); + char byte = 0; + ASSERT_EQ(1, pread(fd, &byte, 1, 0)) << strerror(errno); + struct stat after = {}; + ASSERT_EQ(0, fstat(fd, &after)) << strerror(errno); + EXPECT_EQ(before.st_atim.tv_sec, after.st_atim.tv_sec); + EXPECT_EQ(before.st_atim.tv_nsec, after.st_atim.tv_nsec); + + ASSERT_EQ(0, close(fd)) << strerror(errno); + ASSERT_NO_FATAL_FAILURE(fs.Unmount()); +} + TEST(Ext4InodeIdentity, DirtySharedMappingSurvivesUnlinkAndFdClose) { LoopExt4 fs; ASSERT_NO_FATAL_FAILURE(fs.SetUp()); diff --git a/user/apps/tests/dunitest/suites/normal/fallocate_semantics.cc b/user/apps/tests/dunitest/suites/normal/fallocate_semantics.cc index 3e855bc7b2..728f6e1b8e 100644 --- a/user/apps/tests/dunitest/suites/normal/fallocate_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/fallocate_semantics.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -92,6 +93,10 @@ off_t FileSize(int fd) { return st.st_size; } +bool TimespecAfter(const timespec& lhs, const timespec& rhs) { + return lhs.tv_sec > rhs.tv_sec || (lhs.tv_sec == rhs.tv_sec && lhs.tv_nsec > rhs.tv_nsec); +} + class ScopedSignalIgnore { public: explicit ScopedSignalIgnore(int signal) : signal_(signal) { @@ -165,6 +170,55 @@ TEST(FallocateSemantics, Mode0DoesNotShrink) { EXPECT_EQ(4096, FileSize(file.fd())); } +TEST(FallocateSemantics, SuccessfulMode0UpdatesWriteSideMetadata) { + TempFile file; + ASSERT_TRUE(file.valid()) << "mkstemp failed: " << strerror(errno); + + const timespec old_times[2] = {{1, 0}, {1, 0}}; + ASSERT_EQ(0, futimens(file.fd(), old_times)) << strerror(errno); + + struct stat before {}; + ASSERT_EQ(0, fstat(file.fd(), &before)) << strerror(errno); + sleep(1); + + ASSERT_EQ(0, RawFallocate(file.fd(), 0, 0, 4096)) << strerror(errno); + + struct stat after {}; + ASSERT_EQ(0, fstat(file.fd(), &after)) << strerror(errno); + EXPECT_EQ(4096, after.st_size); + EXPECT_TRUE(TimespecAfter(after.st_mtim, before.st_mtim)); + EXPECT_TRUE(TimespecAfter(after.st_ctim, before.st_ctim)); +} + +TEST(FallocateSemantics, SuccessfulMode0ClearsSetidForUnprivilegedCaller) { + if (geteuid() != 0) { + GTEST_SKIP() << "requires root to create an unprivileged child"; + } + + TempFile file; + ASSERT_TRUE(file.valid()) << "mkstemp failed: " << strerror(errno); + ASSERT_EQ(0, fchmod(file.fd(), 06755)) << strerror(errno); + + const pid_t child = fork(); + ASSERT_GE(child, 0) << strerror(errno); + if (child == 0) { + if (setgid(1000) != 0 || setuid(1000) != 0 || + RawFallocate(file.fd(), 0, 0, 4096) != 0) { + _exit(1); + } + _exit(0); + } + + int status = 0; + ASSERT_EQ(child, waitpid(child, &status, 0)) << strerror(errno); + ASSERT_TRUE(WIFEXITED(status)); + ASSERT_EQ(0, WEXITSTATUS(status)); + + struct stat after {}; + ASSERT_EQ(0, fstat(file.fd(), &after)) << strerror(errno); + EXPECT_EQ(static_cast(0), after.st_mode & (S_ISUID | S_ISGID)); +} + TEST(FallocateSemantics, UnsupportedKeepSizeRemainsUnsupported) { TempFile file; ASSERT_TRUE(file.valid()) << "mkstemp failed: " << strerror(errno); @@ -197,10 +251,20 @@ TEST(FallocateSemantics, RlimitFsizeBlocksGrowth) { ScopedRlimit limit(RLIMIT_FSIZE, 64); ASSERT_TRUE(limit.valid()) << "setrlimit failed: " << strerror(errno); + struct stat before {}; + ASSERT_EQ(0, fstat(file.fd(), &before)) << strerror(errno); + errno = 0; EXPECT_EQ(-1, RawFallocate(file.fd(), 0, 0, 128)); EXPECT_EQ(EFBIG, errno) << "unexpected errno=" << errno << " (" << strerror(errno) << ")"; - EXPECT_EQ(0, FileSize(file.fd())); + + struct stat after {}; + ASSERT_EQ(0, fstat(file.fd(), &after)) << strerror(errno); + EXPECT_EQ(before.st_size, after.st_size); + EXPECT_EQ(before.st_mtim.tv_sec, after.st_mtim.tv_sec); + EXPECT_EQ(before.st_mtim.tv_nsec, after.st_mtim.tv_nsec); + EXPECT_EQ(before.st_ctim.tv_sec, after.st_ctim.tv_sec); + EXPECT_EQ(before.st_ctim.tv_nsec, after.st_ctim.tv_nsec); } int main(int argc, char** argv) { diff --git a/user/apps/tests/dunitest/suites/normal/mount_limit.cc b/user/apps/tests/dunitest/suites/normal/mount_limit.cc index d36532eb21..82fc67b28c 100644 --- a/user/apps/tests/dunitest/suites/normal/mount_limit.cc +++ b/user/apps/tests/dunitest/suites/normal/mount_limit.cc @@ -400,6 +400,30 @@ TEST_F(MountLimitTest, NamespaceCopyIgnoresLoweredLimitAndCreatorsRespectIt) { EXPECT_EQ(before + 1, mount_count()); } +TEST_F(MountLimitTest, LoweredLimitDoesNotTruncateExistingTopologyWalk) { + const std::string target = dir("/a"); + for (int i = 0; i < 3; ++i) { + ASSERT_EQ(0, mount("none", target.c_str(), "ramfs", 0, nullptr)) << strerror(errno); + } + const std::string marker = target + "/top"; + int fd = open(marker.c_str(), O_CREAT | O_WRONLY, 0600); + ASSERT_GE(fd, 0) << strerror(errno); + close(fd); + + ASSERT_EQ(0, write_mount_max(1)) << strerror(errno); + struct stat st = {}; + ASSERT_EQ(0, stat(marker.c_str(), &st)) << strerror(errno); + + char old_cwd[PATH_MAX] = {}; + ASSERT_NE(nullptr, getcwd(old_cwd, sizeof(old_cwd))) << strerror(errno); + ASSERT_EQ(0, chdir(target.c_str())) << strerror(errno); + ASSERT_EQ(0, chdir("..")) << strerror(errno); + char parent[PATH_MAX] = {}; + ASSERT_NE(nullptr, getcwd(parent, sizeof(parent))) << strerror(errno); + EXPECT_EQ(root_, parent); + ASSERT_EQ(0, chdir(old_cwd)) << strerror(errno); +} + } // namespace int main(int argc, char** argv) { diff --git a/user/apps/tests/dunitest/suites/normal/mount_propagation.cc b/user/apps/tests/dunitest/suites/normal/mount_propagation.cc index 5f881a9f38..7773b0fef3 100644 --- a/user/apps/tests/dunitest/suites/normal/mount_propagation.cc +++ b/user/apps/tests/dunitest/suites/normal/mount_propagation.cc @@ -342,7 +342,19 @@ bool wait_children_until(pid_t* children, size_t count, int timeout_seconds) { int status = 0; const pid_t result = waitpid(children[i], &status, WNOHANG); if (result == children[i]) { - ok = ok && WIFEXITED(status) && WEXITSTATUS(status) == 0; + const bool child_ok = WIFEXITED(status) && WEXITSTATUS(status) == 0; + if (!child_ok) { + if (WIFEXITED(status)) { + fprintf(stderr, "child[%zu] exited with status %d\n", i, + WEXITSTATUS(status)); + } else if (WIFSIGNALED(status)) { + fprintf(stderr, "child[%zu] terminated by signal %d\n", i, + WTERMSIG(status)); + } else { + fprintf(stderr, "child[%zu] returned wait status %#x\n", i, status); + } + } + ok = ok && child_ok; children[i] = -1; --remaining; } else if (result < 0 && errno != EINTR) { @@ -380,9 +392,7 @@ class MountPropagationTest : public ::testing::Test { snprintf(root_, sizeof(root_), "/tmp/mount_propagation_%d", getpid()); ASSERT_EQ(0, ensure_dir(root_)) << strerror(errno); - if (unshare(CLONE_NEWNS) != 0) { - GTEST_SKIP() << "unshare(CLONE_NEWNS): " << strerror(errno); - } + ASSERT_EQ(0, unshare(CLONE_NEWNS)) << strerror(errno); ASSERT_EQ(0, mount(nullptr, "/", nullptr, MS_REC | MS_PRIVATE, nullptr)) << strerror(errno); } @@ -1633,8 +1643,19 @@ TEST_F(MountPropagationTest, RecursiveChangesAreAtomicAgainstSnapshotsAndNamespa !write_exact(activity_pipe[1], "A", 1)) { _exit(2); } - if (!read_exact(ready_pipe[0], &token, 1) || - !write_exact(worker_activity_pipe[1], "AA", 2)) { + if (!read_exact(ready_pipe[0], &token, 1) || token != 'O') { + _exit(3); + } + const unsigned long phases[] = {MS_SHARED, MS_PRIVATE}; + const char phase_tokens[] = {'S', 'P'}; + for (size_t i = 0; i < 2; ++i) { + if (mount(nullptr, base, nullptr, MS_REC | phases[i], nullptr) != 0 || + !write_exact(activity_pipe[1], &phase_tokens[i], 1) || + !read_exact(ready_pipe[0], &token, 1) || token != phase_tokens[i]) { + _exit(3); + } + } + if (!write_exact(worker_activity_pipe[1], "AA", 2)) { _exit(3); } for (int i = 0; i < 1024; ++i) { @@ -1645,6 +1666,15 @@ TEST_F(MountPropagationTest, RecursiveChangesAreAtomicAgainstSnapshotsAndNamespa if ((i & 15) == 15) { sched_yield(); } + // Do not let the complete stress phase outrun the observer. The + // explicit midpoint token prevents an early, pre-stress snapshot + // from satisfying this hand-off. + if (i == 511) { + if (!write_exact(activity_pipe[1], "M", 1) || + !read_exact(ready_pipe[0], &token, 1) || token != 'X') { + _exit(3); + } + } } char done[2] = {}; if (!read_exact(worker_done_pipe[0], done, sizeof(done)) || @@ -1676,17 +1706,37 @@ TEST_F(MountPropagationTest, RecursiveChangesAreAtomicAgainstSnapshotsAndNamespa if (!read_exact(activity_pipe[0], &token, 1) || token != 'A') { _exit(6); } - const int flags = fcntl(activity_pipe[0], F_GETFL, 0); - if (flags < 0 || fcntl(activity_pipe[0], F_SETFL, flags | O_NONBLOCK) != 0) { - _exit(7); - } if (!write_exact(ready_pipe[1], "O", 1)) { _exit(8); } const char* paths[] = {base, child, grandchild}; - int samples = 0; - bool saw_shared = false; - bool saw_private = false; + // Prove both allowed states deterministically before the unsynchronised + // stress phase. sched_yield() is not a hand-off guarantee, so relying + // on the observer to happen to see both transient states is flaky. + const char phase_tokens[] = {'S', 'P'}; + const bool phase_is_shared[] = {true, false}; + for (size_t i = 0; i < 2; ++i) { + PropagationTags tags[3] = {}; + if (!read_exact(activity_pipe[0], &token, 1) || token != phase_tokens[i] || + !read_propagation_snapshot(paths, 3, tags) || !snapshot_is_uniform(tags, 3) || + (tags[0].shared > 0) != phase_is_shared[i] || + !write_exact(ready_pipe[1], &token, 1)) { + _exit(10); + } + } + if (!read_exact(activity_pipe[0], &token, 1) || token != 'M') { + _exit(9); + } + PropagationTags midpoint_tags[3] = {}; + if (!read_propagation_snapshot(paths, 3, midpoint_tags) || + !snapshot_is_uniform(midpoint_tags, 3) || + !write_exact(ready_pipe[1], "X", 1)) { + _exit(10); + } + const int flags = fcntl(activity_pipe[0], F_GETFL, 0); + if (flags < 0 || fcntl(activity_pipe[0], F_SETFL, flags | O_NONBLOCK) != 0) { + _exit(7); + } for (;;) { const ssize_t status = read(activity_pipe[0], &token, 1); if (status == 1 && token == 'D') { @@ -1699,12 +1749,6 @@ TEST_F(MountPropagationTest, RecursiveChangesAreAtomicAgainstSnapshotsAndNamespa if (!read_propagation_snapshot(paths, 3, tags) || !snapshot_is_uniform(tags, 3)) { _exit(10); } - ++samples; - saw_shared = saw_shared || tags[0].shared > 0; - saw_private = saw_private || tags[0].shared < 0; - } - if (samples == 0 || !saw_shared || !saw_private) { - _exit(11); } _exit(0); } diff --git a/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc b/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc index e6cd289bc5..e5b8c4a3ce 100644 --- a/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc +++ b/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc @@ -1,9 +1,13 @@ #include +#include +#include #include #include +#include #include #include +#include #include #include #include @@ -11,7 +15,9 @@ #include #include #include +#include #include +#include #include #ifndef CLONE_NEWNS @@ -105,6 +111,22 @@ static bool mount_has_option(const char *mountpoint, const char *option) { return found; } +struct AtimeReaderContext { + int fd; + std::atomic stop; +}; + +static void *atime_reader(void *opaque) { + auto *context = static_cast(opaque); + char byte; + while (!context->stop.load(std::memory_order_relaxed)) { + if (pread(context->fd, &byte, 1, 0) != 1) { + return reinterpret_cast(1); + } + } + return nullptr; +} + } // namespace TEST(MountReconfigure, BindRemountReadonly) { @@ -150,6 +172,10 @@ TEST(MountReconfigure, BindRemountReadonly) { FAIL() << strerror(errno); } + errno = 0; + EXPECT_EQ(-1, access(target, W_OK)); + EXPECT_EQ(EROFS, errno); + snprintf(dst_file, sizeof(dst_file), "%s/readonly.txt", target); fd = open(dst_file, O_CREAT | O_WRONLY | O_TRUNC, 0644); if (fd >= 0) { @@ -350,6 +376,26 @@ TEST(MountReconfigure, NoexecRejectsExec) { FAIL() << strerror(errno); } + int fd = open(target_file, O_RDONLY); + ASSERT_GE(fd, 0) << strerror(errno); + errno = 0; + void *mapping = mmap(NULL, 0, PROT_EXEC, MAP_PRIVATE, fd, 0); + EXPECT_EQ(MAP_FAILED, mapping); + EXPECT_EQ(EINVAL, errno); + + errno = 0; + mapping = mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE, fd, 0); + EXPECT_EQ(MAP_FAILED, mapping); + EXPECT_EQ(EPERM, errno); + + mapping = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd, 0); + ASSERT_NE(MAP_FAILED, mapping) << strerror(errno); + errno = 0; + EXPECT_EQ(-1, mprotect(mapping, 4096, PROT_READ | PROT_EXEC)); + EXPECT_EQ(EACCES, errno); + EXPECT_EQ(0, munmap(mapping, 4096)) << strerror(errno); + close(fd); + char *const argv[] = {target_file, nullptr}; char *const envp[] = {nullptr}; errno = 0; @@ -363,6 +409,406 @@ TEST(MountReconfigure, NoexecRejectsExec) { rmdir(root); } +TEST(MountReconfigure, StrictAtimeReadUpdatesAccessTime) { + const char *target = "/tmp/test_mount_strictatime_read"; + const char *file = "/tmp/test_mount_strictatime_read/file"; + + ensure_dir("/tmp"); + ensure_dir(target); + ASSERT_EQ(0, unshare(CLONE_NEWNS)) << strerror(errno); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) << strerror(errno); + ASSERT_EQ(0, mount("tmpfs", target, "tmpfs", MS_STRICTATIME, NULL)) << strerror(errno); + ASSERT_EQ(0, write_file(file)) << strerror(errno); + + struct timespec times[2] = {{1, 0}, {2, 0}}; + ASSERT_EQ(0, utimensat(AT_FDCWD, file, times, 0)) << strerror(errno); + + int fd = open(file, O_RDONLY); + ASSERT_GE(fd, 0) << strerror(errno); + char byte; + ASSERT_EQ(1, read(fd, &byte, 1)) << strerror(errno); + close(fd); + + struct stat st = {}; + ASSERT_EQ(0, stat(file, &st)) << strerror(errno); + EXPECT_GT(st.st_atim.tv_sec, 1); + cleanup_mount(target); +} + +TEST(MountReconfigure, StrictAtimeEofReadUpdatesAccessTime) { + const char *target = "/tmp/test_mount_strictatime_eof"; + const char *file = "/tmp/test_mount_strictatime_eof/empty"; + + ensure_dir("/tmp"); + ensure_dir(target); + ASSERT_EQ(0, unshare(CLONE_NEWNS)) << strerror(errno); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) << strerror(errno); + ASSERT_EQ(0, mount("tmpfs", target, "tmpfs", MS_STRICTATIME, NULL)) << strerror(errno); + + int fd = open(file, O_CREAT | O_RDONLY, 0600); + ASSERT_GE(fd, 0) << strerror(errno); + struct timespec times[2] = {{1, 0}, {2, 0}}; + ASSERT_EQ(0, utimensat(AT_FDCWD, file, times, 0)) << strerror(errno); + + char byte = 0; + ASSERT_EQ(0, read(fd, &byte, 1)) << strerror(errno); + struct stat st = {}; + ASSERT_EQ(0, fstat(fd, &st)) << strerror(errno); + EXPECT_GT(st.st_atim.tv_sec, 1); + + ASSERT_EQ(0, utimensat(AT_FDCWD, file, times, 0)) << strerror(errno); + ASSERT_EQ(0, read(fd, &byte, 0)) << strerror(errno); + ASSERT_EQ(0, fstat(fd, &st)) << strerror(errno); + EXPECT_EQ(1, st.st_atim.tv_sec); + close(fd); + unlink(file); + cleanup_mount(target); +} + +TEST(MountReconfigure, DirectoryReadHonorsAtimeFlags) { + const char *strict_target = "/tmp/test_mount_dir_strictatime"; + const char *strict_file = "/tmp/test_mount_dir_strictatime/file"; + const char *nodir_target = "/tmp/test_mount_dir_nodiratime"; + const char *nodir_file = "/tmp/test_mount_dir_nodiratime/file"; + + ensure_dir("/tmp"); + ensure_dir(strict_target); + ensure_dir(nodir_target); + ASSERT_EQ(0, unshare(CLONE_NEWNS)) << strerror(errno); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) << strerror(errno); + ASSERT_EQ(0, mount("tmpfs", strict_target, "tmpfs", MS_STRICTATIME, NULL)) + << strerror(errno); + ASSERT_EQ(0, + mount("tmpfs", nodir_target, "tmpfs", MS_STRICTATIME | MS_NODIRATIME, NULL)) + << strerror(errno); + ASSERT_EQ(0, write_file(strict_file)) << strerror(errno); + ASSERT_EQ(0, write_file(nodir_file)) << strerror(errno); + + struct timespec times[2] = {{1, 0}, {2, 0}}; + ASSERT_EQ(0, utimensat(AT_FDCWD, strict_target, times, 0)) << strerror(errno); + ASSERT_EQ(0, utimensat(AT_FDCWD, nodir_target, times, 0)) << strerror(errno); + + auto read_directory = [](const char *path) { + DIR *dir = opendir(path); + if (dir == nullptr) { + return -1; + } + while (readdir(dir) != nullptr) { + } + return closedir(dir); + }; + ASSERT_EQ(0, read_directory(strict_target)) << strerror(errno); + ASSERT_EQ(0, read_directory(nodir_target)) << strerror(errno); + + struct stat strict_st = {}; + struct stat nodir_st = {}; + ASSERT_EQ(0, stat(strict_target, &strict_st)) << strerror(errno); + ASSERT_EQ(0, stat(nodir_target, &nodir_st)) << strerror(errno); + EXPECT_GT(strict_st.st_atim.tv_sec, 1); + EXPECT_EQ(1, nodir_st.st_atim.tv_sec); + + unlink(strict_file); + unlink(nodir_file); + cleanup_mount(strict_target); + cleanup_mount(nodir_target); +} + +TEST(MountReconfigure, FollowsTmpfsSymlinkLongerThan256Bytes) { + const char *target = "/tmp/test_mount_long_symlink"; + const char *file = "/tmp/test_mount_long_symlink/file"; + const char *link = "/tmp/test_mount_long_symlink/link"; + + ensure_dir("/tmp"); + ensure_dir(target); + ASSERT_EQ(0, unshare(CLONE_NEWNS)) << strerror(errno); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) << strerror(errno); + ASSERT_EQ(0, mount("tmpfs", target, "tmpfs", 0, NULL)) << strerror(errno); + ASSERT_EQ(0, write_file(file)) << strerror(errno); + + std::string link_target; + for (int i = 0; i < 130; ++i) { + link_target += "./"; + } + link_target += "file"; + ASSERT_GT(link_target.size(), 256UL); + ASSERT_EQ(0, symlink(link_target.c_str(), link)) << strerror(errno); + + int fd = open(link, O_RDONLY); + ASSERT_GE(fd, 0) << strerror(errno); + char byte = 0; + ASSERT_EQ(1, read(fd, &byte, 1)) << strerror(errno); + EXPECT_EQ('x', byte); + close(fd); + unlink(link); + unlink(file); + cleanup_mount(target); +} + +TEST(MountReconfigure, CreatDirectoryIsAlwaysInvalid) { + const char *target = "/tmp/test_mount_creat_directory"; + const char *missing = "/tmp/test_mount_creat_directory/missing"; + + ensure_dir("/tmp"); + ensure_dir(target); + + errno = 0; + EXPECT_EQ(-1, open(target, O_RDONLY | O_CREAT | O_DIRECTORY, 0600)); + EXPECT_EQ(EINVAL, errno); + errno = 0; + EXPECT_EQ(-1, open(missing, O_RDONLY | O_CREAT | O_DIRECTORY, 0600)); + EXPECT_EQ(EINVAL, errno); + rmdir(target); +} + +TEST(MountReconfigure, NoatimeRequiresOwnerOrCapFowner) { + const char *target = "/tmp/test_mount_noatime_permission"; + const char *file = "/tmp/test_mount_noatime_permission/file"; + const char *link = "/tmp/test_mount_noatime_permission/link"; + + ensure_dir("/tmp"); + ensure_dir(target); + ASSERT_EQ(0, unshare(CLONE_NEWNS)) << strerror(errno); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) << strerror(errno); + ASSERT_EQ(0, mount("tmpfs", target, "tmpfs", MS_STRICTATIME, NULL)) << strerror(errno); + ASSERT_EQ(0, write_file(file)) << strerror(errno); + ASSERT_EQ(0, symlink("file", link)) << strerror(errno); + + pid_t child = fork(); + ASSERT_GE(child, 0) << strerror(errno); + if (child == 0) { + if (setuid(1000) != 0) { + _exit(10); + } + + errno = 0; + int fd = open(file, O_RDONLY | O_NOATIME); + if (fd >= 0 || errno != EPERM) { + if (fd >= 0) { + close(fd); + } + _exit(11); + } + + errno = 0; + fd = open(link, O_RDONLY | O_NOFOLLOW | O_NOATIME); + if (fd >= 0 || errno != ELOOP) { + if (fd >= 0) { + close(fd); + } + _exit(14); + } + + errno = 0; + fd = open(target, O_RDONLY | O_DIRECT | O_NOATIME); + if (fd >= 0 || errno != EPERM) { + if (fd >= 0) { + close(fd); + } + _exit(15); + } + + fd = open(file, O_RDONLY); + if (fd < 0) { + _exit(12); + } + errno = 0; + if (fcntl(fd, F_SETFL, O_NOATIME) != -1 || errno != EPERM) { + close(fd); + _exit(13); + } + close(fd); + _exit(0); + } + + int status = 0; + ASSERT_EQ(child, waitpid(child, &status, 0)) << strerror(errno); + ASSERT_TRUE(WIFEXITED(status)); + EXPECT_EQ(0, WEXITSTATUS(status)); + unlink(link); + cleanup_mount(target); +} + +TEST(MountReconfigure, NoatimeCreateUsesCallerOwnership) { + const char *target = "/tmp/test_mount_noatime_create_owner"; + const char *file = "/tmp/test_mount_noatime_create_owner/file"; + + ensure_dir("/tmp"); + ensure_dir(target); + ASSERT_EQ(0, unshare(CLONE_NEWNS)) << strerror(errno); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) << strerror(errno); + ASSERT_EQ(0, mount("tmpfs", target, "tmpfs", MS_STRICTATIME, "mode=0777")) + << strerror(errno); + + pid_t child = fork(); + ASSERT_GE(child, 0) << strerror(errno); + if (child == 0) { + if (setgid(1000) != 0 || setuid(1000) != 0) { + _exit(20); + } + int fd = open(file, O_CREAT | O_EXCL | O_RDONLY | O_NOATIME, 0600); + if (fd < 0) { + _exit(21); + } + struct stat st = {}; + if (fstat(fd, &st) != 0 || st.st_uid != 1000 || st.st_gid != 1000) { + close(fd); + _exit(22); + } + close(fd); + fd = open(file, O_RDONLY | O_NOATIME); + if (fd < 0) { + _exit(23); + } + close(fd); + _exit(0); + } + + int status = 0; + ASSERT_EQ(child, waitpid(child, &status, 0)) << strerror(errno); + ASSERT_TRUE(WIFEXITED(status)); + EXPECT_EQ(0, WEXITSTATUS(status)); + unlink(file); + cleanup_mount(target); +} + +TEST(MountReconfigure, SetgidDirectoryCreationSemantics) { + const char *target = "/tmp/test_mount_setgid_create"; + const char *parent = "/tmp/test_mount_setgid_create/parent"; + const char *file = "/tmp/test_mount_setgid_create/parent/file"; + const char *dir = "/tmp/test_mount_setgid_create/parent/dir"; + + ensure_dir("/tmp"); + ensure_dir(target); + ASSERT_EQ(0, unshare(CLONE_NEWNS)) << strerror(errno); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) << strerror(errno); + ASSERT_EQ(0, mount("tmpfs", target, "tmpfs", 0, "mode=0777")) << strerror(errno); + ASSERT_EQ(0, mkdir(parent, 0777)) << strerror(errno); + ASSERT_EQ(0, chown(parent, 0, 2000)) << strerror(errno); + ASSERT_EQ(0, chmod(parent, 02777)) << strerror(errno); + + pid_t child = fork(); + ASSERT_GE(child, 0) << strerror(errno); + if (child == 0) { + if (setgid(1000) != 0 || setuid(1000) != 0) { + _exit(30); + } + umask(0); + int fd = open(file, O_CREAT | O_EXCL | O_WRONLY, 02770); + if (fd < 0) { + _exit(31); + } + close(fd); + struct stat st = {}; + if (stat(file, &st) != 0 || st.st_gid != 2000 || (st.st_mode & S_ISGID) != 0) { + _exit(32); + } + if (mkdir(dir, 0770) != 0) { + _exit(33); + } + if (stat(dir, &st) != 0 || st.st_gid != 2000 || (st.st_mode & S_ISGID) == 0) { + _exit(34); + } + _exit(0); + } + + int status = 0; + ASSERT_EQ(child, waitpid(child, &status, 0)) << strerror(errno); + ASSERT_TRUE(WIFEXITED(status)); + EXPECT_EQ(0, WEXITSTATUS(status)); + unlink(file); + rmdir(dir); + rmdir(parent); + cleanup_mount(target); +} + +TEST(MountReconfigure, AtimeUpdateDoesNotOverwriteConcurrentMetadata) { + const char *target = "/tmp/test_mount_atime_atomic"; + const char *file = "/tmp/test_mount_atime_atomic/file"; + + ensure_dir("/tmp"); + ensure_dir(target); + ASSERT_EQ(0, unshare(CLONE_NEWNS)) << strerror(errno); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) << strerror(errno); + ASSERT_EQ(0, mount("tmpfs", target, "tmpfs", MS_STRICTATIME, "size=1m")) + << strerror(errno); + ASSERT_EQ(0, write_file(file)) << strerror(errno); + + int fd = open(file, O_RDONLY); + ASSERT_GE(fd, 0) << strerror(errno); + AtimeReaderContext context = {fd, false}; + pthread_t reader; + ASSERT_EQ(0, pthread_create(&reader, nullptr, atime_reader, &context)); + + for (int i = 0; i < 2000; ++i) { + ASSERT_EQ(0, chmod(file, (i & 1) ? 0613 : 0642)) << strerror(errno); + struct timespec times[2] = {{100 + i, 0}, {200 + i, 0}}; + ASSERT_EQ(0, utimensat(AT_FDCWD, file, times, 0)) << strerror(errno); + } + + const mode_t final_mode = 0613; + const time_t final_mtime = 123456; + ASSERT_EQ(0, chmod(file, final_mode)) << strerror(errno); + struct timespec final_times[2] = {{1, 0}, {final_mtime, 0}}; + ASSERT_EQ(0, utimensat(AT_FDCWD, file, final_times, 0)) << strerror(errno); + usleep(20000); + context.stop.store(true, std::memory_order_relaxed); + void *thread_result = nullptr; + ASSERT_EQ(0, pthread_join(reader, &thread_result)); + ASSERT_EQ(nullptr, thread_result); + + struct stat st = {}; + ASSERT_EQ(0, stat(file, &st)) << strerror(errno); + EXPECT_EQ(final_mode, st.st_mode & 0777); + EXPECT_EQ(final_mtime, st.st_mtim.tv_sec); + EXPECT_GT(st.st_atim.tv_sec, 1); + + close(fd); + cleanup_mount(target); +} + +TEST(MountReconfigure, TmpfsStatfsTracksPageQuotaWithoutCachedFreeBlocks) { + const char *target = "/tmp/test_tmpfs_statfs_quota"; + const char *file = "/tmp/test_tmpfs_statfs_quota/file"; + + ensure_dir("/tmp"); + ensure_dir(target); + ASSERT_EQ(0, unshare(CLONE_NEWNS)) << strerror(errno); + ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) << strerror(errno); + ASSERT_EQ(0, mount("tmpfs", target, "tmpfs", 0, "size=16k")) << strerror(errno); + + struct statfs before = {}; + ASSERT_EQ(0, statfs(target, &before)) << strerror(errno); + ASSERT_EQ(4UL, before.f_blocks); + ASSERT_EQ(4UL, before.f_bfree); + + int fd = open(file, O_CREAT | O_RDWR | O_TRUNC, 0600); + ASSERT_GE(fd, 0) << strerror(errno); + ASSERT_EQ(0, posix_fallocate(fd, 0, 8192)); + + struct statfs allocated = {}; + ASSERT_EQ(0, statfs(target, &allocated)) << strerror(errno); + EXPECT_EQ(4UL, allocated.f_blocks); + EXPECT_EQ(2UL, allocated.f_bfree); + + errno = 0; + EXPECT_EQ(-1, mount("tmpfs", target, "tmpfs", MS_REMOUNT, "size=4k")); + EXPECT_EQ(EINVAL, errno); + + struct statfs rejected = {}; + ASSERT_EQ(0, statfs(target, &rejected)) << strerror(errno); + EXPECT_EQ(4UL, rejected.f_blocks); + EXPECT_EQ(2UL, rejected.f_bfree); + + ASSERT_EQ(0, ftruncate(fd, 0)) << strerror(errno); + struct statfs released = {}; + ASSERT_EQ(0, statfs(target, &released)) << strerror(errno); + EXPECT_EQ(4UL, released.f_bfree); + + close(fd); + unlink(file); + cleanup_mount(target); +} + TEST(MountReconfigure, BindRemountReadonlyDoesNotChangeSourceSuperblock) { const char *source = "/tmp/test_bind_remount_scope/source"; const char *target = "/tmp/test_bind_remount_scope/target"; diff --git a/user/apps/tests/dunitest/suites/normal/open_dangling_symlink.cc b/user/apps/tests/dunitest/suites/normal/open_dangling_symlink.cc new file mode 100644 index 0000000000..3afc1b1169 --- /dev/null +++ b/user/apps/tests/dunitest/suites/normal/open_dangling_symlink.cc @@ -0,0 +1,185 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class OpenDanglingSymlinkTest : public ::testing::Test { +protected: + void SetUp() override { + char path[] = "/tmp/dunitest_open_symlink_XXXXXX"; + char* created = mkdtemp(path); + ASSERT_NE(nullptr, created) << std::strerror(errno); + dir_ = created; + } + + void TearDown() override { + unlink(Path("link").c_str()); + unlink(Path("target").c_str()); + unlink(Path("sub/l2").c_str()); + rmdir(Path("sub").c_str()); + rmdir(dir_.c_str()); + } + + std::string Path(const char* name) const { return dir_ + "/" + name; } + + void ExpectRegularFile(const std::string& path) { + struct stat st = {}; + ASSERT_EQ(0, stat(path.c_str(), &st)) << std::strerror(errno); + EXPECT_TRUE(S_ISREG(st.st_mode)); + } + + std::string dir_; +}; + +TEST_F(OpenDanglingSymlinkTest, CreatesRelativeTargetAfterFollowingFinalSymlink) { + ASSERT_EQ(0, symlink("target", Path("link").c_str())) << std::strerror(errno); + + int fd = open(Path("link").c_str(), O_CREAT | O_RDWR, 0600); + ASSERT_GE(fd, 0) << std::strerror(errno); + EXPECT_EQ(0, close(fd)); + + struct stat link_st = {}; + ASSERT_EQ(0, lstat(Path("link").c_str(), &link_st)); + EXPECT_TRUE(S_ISLNK(link_st.st_mode)); + ExpectRegularFile(Path("target")); +} + +TEST_F(OpenDanglingSymlinkTest, EmptyTargetIsRejectedByBothSyscalls) { + errno = 0; + EXPECT_EQ(-1, symlink("", Path("link").c_str())); + EXPECT_EQ(ENOENT, errno); + + int dirfd = open(dir_.c_str(), O_RDONLY | O_DIRECTORY); + ASSERT_GE(dirfd, 0) << std::strerror(errno); + errno = 0; + EXPECT_EQ(-1, symlinkat("", dirfd, "link")); + EXPECT_EQ(ENOENT, errno); + EXPECT_EQ(0, close(dirfd)); + + struct stat st = {}; + errno = 0; + EXPECT_EQ(-1, lstat(Path("link").c_str(), &st)); + EXPECT_EQ(ENOENT, errno); +} + +TEST_F(OpenDanglingSymlinkTest, TrailingSlashDoesNotCreateFinalComponent) { + errno = 0; + EXPECT_EQ(-1, symlink("target", (Path("link") + "/").c_str())); + EXPECT_EQ(ENOENT, errno); + + struct stat st = {}; + errno = 0; + EXPECT_EQ(-1, lstat(Path("link").c_str(), &st)); + EXPECT_EQ(ENOENT, errno); +} + +TEST_F(OpenDanglingSymlinkTest, CreationRequiresParentWritePermission) { + ASSERT_EQ(0, chmod(dir_.c_str(), 0555)) << std::strerror(errno); + pid_t child = fork(); + ASSERT_GE(child, 0) << std::strerror(errno); + if (child == 0) { + if (setgid(1000) != 0 || setuid(1000) != 0) { + _exit(2); + } + errno = 0; + int result = symlink("target", Path("link").c_str()); + _exit(result == -1 && errno == EACCES ? 0 : 1); + } + + int status = 0; + ASSERT_EQ(child, waitpid(child, &status, 0)) << std::strerror(errno); + EXPECT_TRUE(WIFEXITED(status)); + EXPECT_EQ(0, WEXITSTATUS(status)); + ASSERT_EQ(0, chmod(dir_.c_str(), 0700)) << std::strerror(errno); + + struct stat st = {}; + errno = 0; + EXPECT_EQ(-1, lstat(Path("link").c_str(), &st)); + EXPECT_EQ(ENOENT, errno); +} + +TEST_F(OpenDanglingSymlinkTest, CreatesAbsoluteTarget) { + const std::string target = Path("target"); + ASSERT_EQ(0, symlink(target.c_str(), Path("link").c_str())) << std::strerror(errno); + + int fd = open(Path("link").c_str(), O_CREAT | O_RDWR, 0600); + ASSERT_GE(fd, 0) << std::strerror(errno); + EXPECT_EQ(0, close(fd)); + ExpectRegularFile(target); +} + +TEST_F(OpenDanglingSymlinkTest, CreatesTargetAfterNestedRelativeSymlinks) { + ASSERT_EQ(0, mkdir(Path("sub").c_str(), 0700)) << std::strerror(errno); + ASSERT_EQ(0, symlink("../target", Path("sub/l2").c_str())) << std::strerror(errno); + ASSERT_EQ(0, symlink("sub/l2", Path("link").c_str())) << std::strerror(errno); + + int fd = open(Path("link").c_str(), O_CREAT | O_RDWR, 0600); + ASSERT_GE(fd, 0) << std::strerror(errno); + EXPECT_EQ(0, close(fd)); + ExpectRegularFile(Path("target")); +} + +TEST_F(OpenDanglingSymlinkTest, ExclusiveCreateDoesNotFollowFinalSymlink) { + ASSERT_EQ(0, symlink("target", Path("link").c_str())) << std::strerror(errno); + + errno = 0; + EXPECT_EQ(-1, open(Path("link").c_str(), O_CREAT | O_EXCL | O_RDWR, 0600)); + EXPECT_EQ(EEXIST, errno); + EXPECT_EQ(-1, access(Path("target").c_str(), F_OK)); + EXPECT_EQ(ENOENT, errno); +} + +TEST_F(OpenDanglingSymlinkTest, NoFollowRejectsFinalSymlink) { + ASSERT_EQ(0, symlink("target", Path("link").c_str())) << std::strerror(errno); + + errno = 0; + EXPECT_EQ(-1, open(Path("link").c_str(), O_CREAT | O_NOFOLLOW | O_RDWR, 0600)); + EXPECT_EQ(ELOOP, errno); + EXPECT_EQ(-1, access(Path("target").c_str(), F_OK)); + EXPECT_EQ(ENOENT, errno); +} + +TEST_F(OpenDanglingSymlinkTest, TrailingSlashInTargetPreventsRegularCreate) { + ASSERT_EQ(0, symlink("target/", Path("link").c_str())) << std::strerror(errno); + + errno = 0; + EXPECT_EQ(-1, open(Path("link").c_str(), O_CREAT | O_RDWR, 0600)); + EXPECT_EQ(EISDIR, errno); + EXPECT_EQ(-1, access(Path("target").c_str(), F_OK)); + EXPECT_EQ(ENOENT, errno); +} + +TEST_F(OpenDanglingSymlinkTest, TrailingSlashInOriginalPathPreventsRegularCreate) { + ASSERT_EQ(0, symlink("target", Path("link").c_str())) << std::strerror(errno); + + errno = 0; + EXPECT_EQ(-1, open((Path("link") + "/").c_str(), O_CREAT | O_RDWR, 0600)); + EXPECT_EQ(EISDIR, errno); + EXPECT_EQ(-1, access(Path("target").c_str(), F_OK)); + EXPECT_EQ(ENOENT, errno); +} + +TEST_F(OpenDanglingSymlinkTest, MissingIntermediateDirectoryIsNotCreated) { + ASSERT_EQ(0, symlink("missing/target", Path("link").c_str())) << std::strerror(errno); + + errno = 0; + EXPECT_EQ(-1, open(Path("link").c_str(), O_CREAT | O_RDWR, 0600)); + EXPECT_EQ(ENOENT, errno); + EXPECT_EQ(-1, access(Path("missing").c_str(), F_OK)); + EXPECT_EQ(ENOENT, errno); +} + +} // namespace + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/user/apps/tests/dunitest/suites/normal/test_pivot_root.cc b/user/apps/tests/dunitest/suites/normal/test_pivot_root.cc index 9d425af453..1b02b318b0 100644 --- a/user/apps/tests/dunitest/suites/normal/test_pivot_root.cc +++ b/user/apps/tests/dunitest/suites/normal/test_pivot_root.cc @@ -10,7 +10,6 @@ #include #include #include -#include #include #include @@ -38,13 +37,8 @@ #define MS_REC 16384 #endif -#ifndef RAMFS_MAGIC -#define RAMFS_MAGIC 0x858458f6 -#endif - namespace { -constexpr int kSkipExitCode = 77; int g_child_status_fd = -1; using PivotRootCaseFn = void (*)(); @@ -61,11 +55,6 @@ void write_child_status(const char* kind, const char* reason) { _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); @@ -95,11 +84,11 @@ long do_pivot_root(const char* new_root, const char* put_old) { void prepare_private_mount_namespace() { if (unshare(CLONE_NEWNS) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mount(nullptr, "/", nullptr, MS_REC | MS_PRIVATE, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } } @@ -115,7 +104,7 @@ std::string read_all_from_fd(int fd) { return out; } -void expect_case_pass_or_skip(const char* case_name, PivotRootCaseFn fn) { +void expect_case_pass(const char* case_name, PivotRootCaseFn fn) { int pipefd[2] = {-1, -1}; ASSERT_EQ(0, pipe(pipefd)) << case_name << ": pipe failed: errno=" << errno << " (" << strerror(errno) << ")"; @@ -142,12 +131,7 @@ void expect_case_pass_or_skip(const char* case_name, PivotRootCaseFn fn) { 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; + EXPECT_EQ(0, WEXITSTATUS(status)) << case_name << ": " << detail; } void case_success_path() { @@ -164,13 +148,13 @@ void case_success_path() { prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(newroot) failed"); } if (mount("", new_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { @@ -224,13 +208,13 @@ void case_dot_dot_path() { prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(newroot) failed"); } if (mount("", new_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) { @@ -316,13 +300,13 @@ void case_dot_dot_rslave_detach() { prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(newroot) failed"); } if (mount("", new_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(bin_dir, 0755) != 0 && errno != EEXIST) { @@ -396,13 +380,13 @@ void case_new_root_not_mountpoint() { prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(containing_mount, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(containing mount) failed"); } if (mount("", containing_mount, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if ((mkdir(new_root, 0755) != 0 && errno != EEXIST) || (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST)) { @@ -431,17 +415,17 @@ void case_put_old_outside_new_root() { prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if ((mkdir(new_root, 0755) != 0 && errno != EEXIST) || (mkdir(outside, 0755) != 0 && errno != EEXIST)) { child_fail("mkdir reachability layout failed"); } if (mount("", new_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mount("", outside, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(inside, 0755) != 0 && errno != EEXIST) { @@ -468,7 +452,7 @@ void case_busy_target() { prepare_private_mount_namespace(); if (mount("", new_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { @@ -495,7 +479,7 @@ void case_permission_failure() { prepare_private_mount_namespace(); if (mount("", new_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { @@ -505,7 +489,7 @@ void case_permission_failure() { if (seteuid(65534) != 0) { cleanup_mount(new_root); - child_skip("seteuid failed"); + child_fail("seteuid failed"); } if (do_pivot_root(new_root, oldroot_abs) == -1 && errno == EPERM) { @@ -566,14 +550,14 @@ void case_shared_mount_rejection() { ensure_dir(old_root); prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(newroot) failed"); } if (mount("", new_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { @@ -583,7 +567,7 @@ void case_shared_mount_rejection() { // put_old resolves on new_root, so making this mount shared exercises // Linux's IS_MNT_SHARED(old_mnt) rejection without an unattached root. if (mount(nullptr, new_root, nullptr, MS_SHARED, nullptr) != 0) { - child_skip("cannot make put_old mount shared"); + child_fail("cannot make put_old mount shared"); } if (chroot(old_root) != 0 || chdir("/") != 0) { child_fail("chroot(old_root) failed"); @@ -608,7 +592,7 @@ void case_shared_root_parent_rejected() { ensure_dir(outer); prepare_private_mount_namespace(); if (mount("", outer, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if ((mkdir(old_root, 0755) != 0 && errno != EEXIST) || mount("", old_root, "ramfs", 0, nullptr) != 0 || @@ -620,7 +604,7 @@ void case_shared_root_parent_rejected() { // Use an explicit mount above the caller root, so this isolates Linux's // root_mnt->mnt_parent check from the namespace-root representation. if (mount(nullptr, outer, nullptr, MS_SHARED, nullptr) != 0) { - child_skip("cannot make caller-root parent shared"); + child_fail("cannot make caller-root parent shared"); } if (chroot(old_root) != 0 || chdir("/") != 0) { child_fail("chroot(old_root) failed"); @@ -642,7 +626,7 @@ void case_shared_new_root_parent_rejected() { ensure_dir(old_root); prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if ((mkdir(new_root, 0755) != 0 && errno != EEXIST) || mount("", new_root, "ramfs", 0, nullptr) != 0 || @@ -651,7 +635,7 @@ void case_shared_new_root_parent_rejected() { } // new_mnt->parent is the caller's old-root mount. if (mount(nullptr, old_root, nullptr, MS_SHARED, nullptr) != 0) { - child_skip("cannot make new-root parent shared"); + child_fail("cannot make new-root parent shared"); } if (chroot(old_root) != 0 || chdir("/") != 0) { child_fail("chroot(old_root) failed"); @@ -673,7 +657,7 @@ void case_shared_mounted_put_old_rejected() { ensure_dir(old_root); prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if ((mkdir(new_root, 0755) != 0 && errno != EEXIST) || mount("", new_root, "ramfs", 0, nullptr) != 0 || @@ -746,7 +730,7 @@ void case_mount_namespace_isolation() { int code = WEXITSTATUS(status); if (code == 77) { - child_skip("child could not set up namespace"); + child_fail("child could not set up namespace"); } if (code == 2) { child_fail("child could not see its own marker"); @@ -779,13 +763,13 @@ void case_double_pivot() { prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(newroot) failed"); } if (mount("", new_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(oldroot_abs, 0755) != 0 && errno != EEXIST) { @@ -842,7 +826,7 @@ void case_chroot_mount_root() { ensure_dir(sandbox); prepare_private_mount_namespace(); if (mount("", sandbox, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(newroot) failed"); @@ -875,7 +859,7 @@ void case_chroot_ordinary_directory_rejected() { ensure_dir(sandbox); prepare_private_mount_namespace(); if (mount("", sandbox, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(jail, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(jail) failed"); @@ -906,25 +890,17 @@ void case_namespace_root_pivot() { const char* old_marker = "/tmp/test_pivot_root/namespace_root_old_marker"; const char* new_marker = "/tmp/test_pivot_root/namespace_root/newroot/new_marker"; - struct statfs current_root = {}; - if (statfs("/", ¤t_root) != 0) { - child_fail("statfs current root failed"); - } - if (current_root.f_type == RAMFS_MAGIC) { - child_skip("initial ramfs root is intentionally unattached"); - } - ensure_parent_tree(); ensure_dir(base); prepare_private_mount_namespace(); if (mount("", base, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir namespace-root newroot failed"); } if (mount("", new_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if ((mkdir(put_old, 0755) != 0 && errno != EEXIST) || (mkdir(old_marker, 0755) != 0 && errno != EEXIST) || @@ -961,7 +937,7 @@ void case_locked_new_root_rejected() { ensure_dir(old_root); prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if ((mkdir(new_root, 0755) != 0 && errno != EEXIST) || mount("", new_root, "ramfs", 0, nullptr) != 0 || @@ -994,7 +970,7 @@ void case_old_root_lock_transferred() { ensure_dir(old_root); prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(newroot) before namespace copy failed"); @@ -1036,7 +1012,7 @@ void case_new_root_on_root_mount() { ensure_dir(old_root); prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(newroot) failed"); @@ -1066,7 +1042,7 @@ void case_unreachable_new_root() { ensure_dir(outside_root); prepare_private_mount_namespace(); if (mount("", outside_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(old_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(root) failed"); @@ -1111,7 +1087,7 @@ void case_nested_pivot_preserves_upper_sibling() { prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0 || mount("", sibling, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(newroot) failed"); @@ -1163,7 +1139,7 @@ void case_private_stacked_put_old() { ensure_dir(old_root); prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(newroot) failed"); @@ -1175,7 +1151,7 @@ void case_private_stacked_put_old() { child_fail("mkdir(oldroot) failed"); } if (mount("", put_old, "ramfs", 0, nullptr) != 0) { - child_skip("private put_old mount is unavailable"); + child_fail("private put_old mount is unavailable"); } if (mkdir(covered_marker, 0755) != 0 && errno != EEXIST) { child_fail("mkdir covered marker failed"); @@ -1215,7 +1191,7 @@ void case_cross_process_exact_fs_refs() { prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0 || mount("", sibling, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(newroot) failed"); @@ -1319,7 +1295,7 @@ void case_symlink_to_mounted_new_root() { ensure_dir(old_root); prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(new_root, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(newroot) failed"); @@ -1357,7 +1333,7 @@ void case_self_bind_alias_new_root() { ensure_dir(old_root); prepare_private_mount_namespace(); if (mount("", old_root, "ramfs", 0, nullptr) != 0) { - child_skip(strerror(errno)); + child_fail(strerror(errno)); } if (mkdir(candidate, 0755) != 0 && errno != EEXIST) { child_fail("mkdir(bind candidate) failed"); @@ -1369,7 +1345,7 @@ void case_self_bind_alias_new_root() { // A self-bind gives the same backing dentry a distinct mount identity, // making an ordinary directory a legal pivot_root new_root. if (mount(candidate, candidate, nullptr, MS_BIND, nullptr) != 0) { - child_skip("self-bind mount is unavailable"); + child_fail("self-bind mount is unavailable"); } if (chroot(old_root) != 0 || chdir("/") != 0) { child_fail("chroot(old_root) failed"); @@ -1384,125 +1360,125 @@ void case_self_bind_alias_new_root() { } TEST(PivotRoot, SuccessPath) { - expect_case_pass_or_skip("pivot_root_success", case_success_path); + expect_case_pass("pivot_root_success", case_success_path); } TEST(PivotRoot, DotDotPath) { - expect_case_pass_or_skip("pivot_root_dot_dot", case_dot_dot_path); + expect_case_pass("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); + expect_case_pass("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); + expect_case_pass("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); + expect_case_pass("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); + expect_case_pass("pivot_root_busy_target", case_busy_target); } TEST(PivotRoot, PermissionFailure) { - expect_case_pass_or_skip("pivot_root_permission_failure", case_permission_failure); + expect_case_pass("pivot_root_permission_failure", case_permission_failure); } TEST(PivotRoot, NotDir) { - expect_case_pass_or_skip("pivot_root_not_dir", case_not_dir); + expect_case_pass("pivot_root_not_dir", case_not_dir); } TEST(PivotRoot, NotExist) { - expect_case_pass_or_skip("pivot_root_not_exist", case_not_exist); + expect_case_pass("pivot_root_not_exist", case_not_exist); } TEST(PivotRoot, SharedMountRejection) { - expect_case_pass_or_skip("pivot_root_shared_mount_rejection", case_shared_mount_rejection); + expect_case_pass("pivot_root_shared_mount_rejection", case_shared_mount_rejection); } TEST(PivotRoot, SharedRootParentRejected) { - expect_case_pass_or_skip("pivot_root_shared_root_parent_rejected", + expect_case_pass("pivot_root_shared_root_parent_rejected", case_shared_root_parent_rejected); } TEST(PivotRoot, SharedNewRootParentRejected) { - expect_case_pass_or_skip("pivot_root_shared_new_root_parent_rejected", + expect_case_pass("pivot_root_shared_new_root_parent_rejected", case_shared_new_root_parent_rejected); } TEST(PivotRoot, SharedMountedPutOldRejected) { - expect_case_pass_or_skip("pivot_root_shared_mounted_put_old_rejected", + expect_case_pass("pivot_root_shared_mounted_put_old_rejected", case_shared_mounted_put_old_rejected); } TEST(PivotRoot, MountNamespaceIsolation) { - expect_case_pass_or_skip("pivot_root_mount_namespace_isolation", + expect_case_pass("pivot_root_mount_namespace_isolation", case_mount_namespace_isolation); } TEST(PivotRoot, DoublePivot) { - expect_case_pass_or_skip("pivot_root_double_pivot", case_double_pivot); + expect_case_pass("pivot_root_double_pivot", case_double_pivot); } TEST(PivotRoot, ChrootMountRoot) { - expect_case_pass_or_skip("pivot_root_chroot_mount_root", case_chroot_mount_root); + expect_case_pass("pivot_root_chroot_mount_root", case_chroot_mount_root); } TEST(PivotRoot, ChrootOrdinaryDirectoryRejected) { - expect_case_pass_or_skip("pivot_root_chroot_ordinary_directory_rejected", + expect_case_pass("pivot_root_chroot_ordinary_directory_rejected", case_chroot_ordinary_directory_rejected); } TEST(PivotRoot, NamespaceRootPivot) { - expect_case_pass_or_skip("pivot_root_namespace_root", case_namespace_root_pivot); + expect_case_pass("pivot_root_namespace_root", case_namespace_root_pivot); } TEST(PivotRoot, NamespaceRootIdentityBusy) { - expect_case_pass_or_skip("pivot_root_namespace_root_identity_busy", + expect_case_pass("pivot_root_namespace_root_identity_busy", case_namespace_root_identity_busy); } TEST(PivotRoot, LockedNewRootRejected) { - expect_case_pass_or_skip("pivot_root_locked_new_root_rejected", case_locked_new_root_rejected); + expect_case_pass("pivot_root_locked_new_root_rejected", case_locked_new_root_rejected); } TEST(PivotRoot, OldRootLockTransferred) { - expect_case_pass_or_skip("pivot_root_old_root_lock_transferred", + expect_case_pass("pivot_root_old_root_lock_transferred", case_old_root_lock_transferred); } TEST(PivotRoot, NewRootOnRootMount) { - expect_case_pass_or_skip("pivot_root_new_root_on_root_mount", case_new_root_on_root_mount); + expect_case_pass("pivot_root_new_root_on_root_mount", case_new_root_on_root_mount); } TEST(PivotRoot, UnreachableNewRoot) { - expect_case_pass_or_skip("pivot_root_unreachable_new_root", case_unreachable_new_root); + expect_case_pass("pivot_root_unreachable_new_root", case_unreachable_new_root); } TEST(PivotRoot, NestedPivotPreservesUpperSibling) { - expect_case_pass_or_skip("pivot_root_nested_preserves_upper_sibling", + expect_case_pass("pivot_root_nested_preserves_upper_sibling", case_nested_pivot_preserves_upper_sibling); } TEST(PivotRoot, PrivateStackedPutOld) { - expect_case_pass_or_skip("pivot_root_private_stacked_put_old", case_private_stacked_put_old); + expect_case_pass("pivot_root_private_stacked_put_old", case_private_stacked_put_old); } TEST(PivotRoot, CrossProcessExactFsRefs) { - expect_case_pass_or_skip("pivot_root_cross_process_exact_fs_refs", + expect_case_pass("pivot_root_cross_process_exact_fs_refs", case_cross_process_exact_fs_refs); } TEST(PivotRoot, SymlinkToMountedNewRoot) { - expect_case_pass_or_skip("pivot_root_symlink_to_mounted_new_root", + expect_case_pass("pivot_root_symlink_to_mounted_new_root", case_symlink_to_mounted_new_root); } TEST(PivotRoot, SelfBindAliasNewRoot) { - expect_case_pass_or_skip("pivot_root_self_bind_alias_new_root", + expect_case_pass("pivot_root_self_bind_alias_new_root", case_self_bind_alias_new_root); } diff --git a/user/apps/tests/dunitest/whitelist.txt b/user/apps/tests/dunitest/whitelist.txt index f5846bf604..df7b41c271 100644 --- a/user/apps/tests/dunitest/whitelist.txt +++ b/user/apps/tests/dunitest/whitelist.txt @@ -18,6 +18,7 @@ normal/fuse_stats_debugfs normal/test_pivot_root normal/mount_reconfigure normal/mount_propagation +normal/open_dangling_symlink normal/mount_move normal/mount_limit normal/mount_object_topology diff --git a/user/apps/tests/syscall/gvisor/Makefile b/user/apps/tests/syscall/gvisor/Makefile index 57845c350f..2ac4396c33 100644 --- a/user/apps/tests/syscall/gvisor/Makefile +++ b/user/apps/tests/syscall/gvisor/Makefile @@ -60,7 +60,7 @@ download: BLOCKLIST_FILES := $(shell find blocklists -type f 2>/dev/null) # 安装到目标目录 -install: build download whitelist.txt $(BLOCKLIST_FILES) +install: build download whitelist.txt required_tests.txt $(BLOCKLIST_FILES) @echo "安装gvisor测试套件到 $(INSTALL_DIR)" @mkdir -p $(INSTALL_DIR) # 安装Rust测试运行器二进制文件 @@ -68,6 +68,7 @@ install: build download whitelist.txt $(BLOCKLIST_FILES) @chmod +x $(INSTALL_DIR)/gvisor-test-runner # 安装测试配置文件 @cp -f whitelist.txt $(INSTALL_DIR)/ + @cp -f required_tests.txt $(INSTALL_DIR)/ @cp -rf blocklists $(INSTALL_DIR)/ @cp -rf tests $(INSTALL_DIR)/ @cp -f run_tests.sh $(INSTALL_DIR)/ diff --git a/user/apps/tests/syscall/gvisor/default.nix b/user/apps/tests/syscall/gvisor/default.nix index a7040a6581..f85c749a6d 100644 --- a/user/apps/tests/syscall/gvisor/default.nix +++ b/user/apps/tests/syscall/gvisor/default.nix @@ -58,6 +58,7 @@ let # This prevents rebuilds when files in ./runner change. src = lib.sourceByRegex ./. [ "^whitelist\.txt$" + "^required_tests\.txt$" "^blocklists" "^blocklists/.*" "^run_tests\.sh$" @@ -71,6 +72,7 @@ let mkdir -p $out/${installDir} install -m644 whitelist.txt $out/${installDir}/ + install -m644 required_tests.txt $out/${installDir}/ cp -r blocklists $out/${installDir}/ install -m755 run_tests.sh $out/${installDir}/ diff --git a/user/apps/tests/syscall/gvisor/monitor_test_results.sh b/user/apps/tests/syscall/gvisor/monitor_test_results.sh index 2d484e56ba..2bc8c4cfb1 100755 --- a/user/apps/tests/syscall/gvisor/monitor_test_results.sh +++ b/user/apps/tests/syscall/gvisor/monitor_test_results.sh @@ -18,9 +18,9 @@ SERIAL_FILE="serial_opt.txt" # 超时配置(秒) BOOT_TIMEOUT=300 # DragonOS开机超时(5分钟) TEST_START_TIMEOUT=600 # 测试程序启动超时(10分钟) -TEST_TIMEOUT=2100 # 整个测试超时(35分钟) -IDLE_TIMEOUT=120 # 无输出超时(5分钟) -SINGLE_TEST_TIMEOUT=60 # 单个测试用例超时(1分钟) +TEST_TIMEOUT=3000 # 整个测试超时(50分钟,CI job 保留10分钟收尾) +IDLE_TIMEOUT=300 # 无输出超时(5分钟) +SINGLE_TEST_TIMEOUT=300 # mount 压力用例允许5分钟,仍对卡死保持有界 # 从PID文件读取QEMU进程 get_qemu_pid() { @@ -318,4 +318,4 @@ else echo "[监控] 测试未完全通过,总用时: $(($(date +%s) - START_TIME)) 秒" clean_up exit 1 -fi \ No newline at end of file +fi diff --git a/user/apps/tests/syscall/gvisor/required_tests.txt b/user/apps/tests/syscall/gvisor/required_tests.txt new file mode 100644 index 0000000000..cd6a6f37d3 --- /dev/null +++ b/user/apps/tests/syscall/gvisor/required_tests.txt @@ -0,0 +1,4 @@ +# Required default binaries, exact case counts, and environment-dependent skips. +# Keep in sync with the pinned gVisor test archive in default.nix. +mount_test 87 +pivot_root_test 19 PivotRootTest.OnRootFS diff --git a/user/apps/tests/syscall/gvisor/runner/Cargo.lock b/user/apps/tests/syscall/gvisor/runner/Cargo.lock index 9ceab9af78..9bd7e5c3e6 100644 --- a/user/apps/tests/syscall/gvisor/runner/Cargo.lock +++ b/user/apps/tests/syscall/gvisor/runner/Cargo.lock @@ -199,6 +199,7 @@ dependencies = [ "env_logger", "libc", "log", + "quick-xml", "regex", ] @@ -319,6 +320,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.42" diff --git a/user/apps/tests/syscall/gvisor/runner/Cargo.toml b/user/apps/tests/syscall/gvisor/runner/Cargo.toml index 309ed6f721..71c2a0d291 100644 --- a/user/apps/tests/syscall/gvisor/runner/Cargo.toml +++ b/user/apps/tests/syscall/gvisor/runner/Cargo.toml @@ -15,6 +15,7 @@ chrono = { version = "0.4", features = ["serde"] } log = "0.4" env_logger = "0.10" libc = "0.2" +quick-xml = "0.39" [profile.release] lto = true diff --git a/user/apps/tests/syscall/gvisor/runner/src/gtest_xml.rs b/user/apps/tests/syscall/gvisor/runner/src/gtest_xml.rs new file mode 100644 index 0000000000..c1274a1244 --- /dev/null +++ b/user/apps/tests/syscall/gvisor/runner/src/gtest_xml.rs @@ -0,0 +1,458 @@ +use anyhow::{bail, Context, Result}; +use quick_xml::{ + events::{BytesStart, Event}, + Reader, +}; +use std::{ + collections::{HashMap, HashSet}, + fs::File, + io::BufReader, + path::Path, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GtestCase { + pub name: String, + pub skipped: bool, + pub failed: bool, + pub error: bool, + pub disabled: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GtestReport { + pub total: usize, + pub failures: usize, + pub errors: usize, + pub disabled: usize, + pub skipped: usize, + pub cases: Vec, +} + +#[derive(Debug)] +struct PendingCase { + name: String, + status: String, + result: String, + has_skipped: bool, + has_failure: bool, + has_error: bool, +} + +pub fn parse_gtest_xml(path: &Path) -> Result { + let file = + File::open(path).with_context(|| format!("无法打开 gtest XML: {}", path.display()))?; + let mut reader = Reader::from_reader(BufReader::new(file)); + reader.config_mut().trim_text(true); + + let mut buf = Vec::new(); + let mut aggregate: Option<(usize, usize, usize, usize, Option)> = None; + let mut pending: Option = None; + let mut cases = Vec::new(); + let mut names = HashSet::new(); + let mut depth = 0usize; + let mut root_open = false; + let mut root_closed = false; + + loop { + let event = reader + .read_event_into(&mut buf) + .context("解析 gtest XML 失败")?; + + match &event { + Event::Start(element) => { + let name = element.name(); + if name.as_ref() == b"testsuites" { + if depth != 0 || root_open || root_closed { + bail!("gtest XML testsuites 根节点位置非法"); + } + root_open = true; + } else if depth == 0 { + bail!("gtest XML 根节点必须是 testsuites"); + } + if name.as_ref() == b"testcase" && (!root_open || root_closed || depth != 2) { + bail!("gtest XML testcase 不在 testsuite 内"); + } + depth += 1; + } + Event::Empty(element) => { + if element.name().as_ref() == b"testsuites" || depth == 0 { + bail!("gtest XML 根节点结构非法"); + } + if element.name().as_ref() == b"testcase" + && (!root_open || root_closed || depth != 2) + { + bail!("gtest XML testcase 不在 testsuite 内"); + } + } + Event::End(element) => { + if depth == 0 { + bail!("gtest XML 出现多余结束标签"); + } + if element.name().as_ref() == b"testcase" && depth != 3 { + bail!("gtest XML testcase 结束标签层级非法"); + } + depth -= 1; + if element.name().as_ref() == b"testsuites" { + if depth != 0 || !root_open || root_closed { + bail!("gtest XML testsuites 结束标签位置非法"); + } + root_open = false; + root_closed = true; + } + } + _ => {} + } + + match event { + Event::Start(ref event) if event.name().as_ref() == b"testsuites" => { + if aggregate.is_some() { + bail!("gtest XML 包含多个 testsuites 根节点"); + } + let attrs = attributes(event)?; + aggregate = Some(( + required_usize(&attrs, "tests")?, + required_usize(&attrs, "failures")?, + required_usize(&attrs, "errors")?, + required_usize(&attrs, "disabled")?, + optional_usize(&attrs, "skipped")?, + )); + } + Event::Start(ref event) if event.name().as_ref() == b"testcase" => { + if pending.is_some() { + bail!("gtest XML testcase 非法嵌套"); + } + pending = Some(begin_case(event)?); + } + Event::Empty(ref event) if event.name().as_ref() == b"testcase" => { + let case = finish_case(begin_case(event)?)?; + insert_case(&mut cases, &mut names, case)?; + } + Event::Start(ref event) | Event::Empty(ref event) + if event.name().as_ref() == b"skipped" => + { + pending + .as_mut() + .context("skipped 元素不在 testcase 内")? + .has_skipped = true; + } + Event::Start(ref event) | Event::Empty(ref event) + if event.name().as_ref() == b"failure" => + { + pending + .as_mut() + .context("failure 元素不在 testcase 内")? + .has_failure = true; + } + Event::Start(ref event) | Event::Empty(ref event) + if event.name().as_ref() == b"error" => + { + pending + .as_mut() + .context("error 元素不在 testcase 内")? + .has_error = true; + } + Event::End(ref event) if event.name().as_ref() == b"testcase" => { + let case = finish_case(pending.take().context("testcase 结束标签无起始标签")?)?; + insert_case(&mut cases, &mut names, case)?; + } + Event::Eof => break, + _ => {} + } + buf.clear(); + } + + if pending.is_some() { + bail!("gtest XML testcase 未闭合"); + } + if depth != 0 || root_open || !root_closed { + bail!("gtest XML testsuites 根节点未闭合"); + } + let (total, failures, errors, disabled, root_skipped) = + aggregate.context("缺少 testsuites 根节点")?; + let case_skipped = cases.iter().filter(|case| case.skipped).count(); + let skipped = root_skipped.unwrap_or(case_skipped); + let report = GtestReport { + total, + failures, + errors, + disabled, + skipped, + cases, + }; + validate_aggregate(&report)?; + Ok(report) +} + +impl GtestReport { + pub fn validate_required( + &self, + test_name: &str, + expected: usize, + allowed_skips: &HashSet, + ) -> Result<()> { + if self.total != expected { + bail!( + "{} 实际执行 {} 个用例,required 清单要求 {} 个", + test_name, + self.total, + expected + ); + } + if self.failures != 0 || self.errors != 0 || self.disabled != 0 { + bail!( + "{} required 门禁失败: total={}, failures={}, errors={}, disabled={}, skipped={}", + test_name, + self.total, + self.failures, + self.errors, + self.disabled, + self.skipped + ); + } + let unexpected_skips: Vec<_> = self + .cases + .iter() + .filter(|case| case.skipped && !allowed_skips.contains(&case.name)) + .map(|case| case.name.as_str()) + .collect(); + if !unexpected_skips.is_empty() { + bail!( + "{} required 测试出现未授权 skip: {}", + test_name, + unexpected_skips.join(", ") + ); + } + Ok(()) + } +} + +fn attributes(event: &BytesStart<'_>) -> Result> { + let mut values = HashMap::new(); + for attr in event.attributes() { + let attr = attr.context("读取 XML 属性失败")?; + let key = std::str::from_utf8(attr.key.as_ref()).context("XML 属性名不是 UTF-8")?; + let value = attr + .unescape_value() + .context("XML 属性值转义非法")? + .into_owned(); + if values.insert(key.to_string(), value).is_some() { + bail!("XML 属性重复: {}", key); + } + } + Ok(values) +} + +fn required_usize(attrs: &HashMap, name: &str) -> Result { + attrs + .get(name) + .with_context(|| format!("testsuites 缺少 {} 属性", name))? + .parse::() + .with_context(|| format!("testsuites 的 {} 不是非负整数", name)) +} + +fn optional_usize(attrs: &HashMap, name: &str) -> Result> { + attrs + .get(name) + .map(|value| { + value + .parse::() + .with_context(|| format!("testsuites 的 {} 不是非负整数", name)) + }) + .transpose() +} + +fn begin_case(event: &BytesStart<'_>) -> Result { + let attrs = attributes(event)?; + let classname = attrs.get("classname").context("testcase 缺少 classname")?; + let name = attrs.get("name").context("testcase 缺少 name")?; + if classname.is_empty() || name.is_empty() { + bail!("testcase classname/name 不能为空"); + } + Ok(PendingCase { + name: format!("{}.{}", classname, name), + status: attrs.get("status").context("testcase 缺少 status")?.clone(), + result: attrs.get("result").context("testcase 缺少 result")?.clone(), + has_skipped: false, + has_failure: false, + has_error: false, + }) +} + +fn finish_case(case: PendingCase) -> Result { + let (skipped, disabled) = match (case.status.as_str(), case.result.as_str()) { + ("run", "completed") if !case.has_skipped => (false, false), + ("run", "skipped") if case.has_skipped => (true, false), + ("notrun", "suppressed") if !case.has_skipped && !case.has_failure && !case.has_error => { + (false, true) + } + _ => bail!( + "testcase {} 状态不一致: status={}, result={}, skipped_element={}", + case.name, + case.status, + case.result, + case.has_skipped + ), + }; + if skipped && (case.has_failure || case.has_error) { + bail!("testcase {} 同时包含 skip 与 failure/error", case.name); + } + Ok(GtestCase { + name: case.name, + skipped, + failed: case.has_failure, + error: case.has_error, + disabled, + }) +} + +fn insert_case( + cases: &mut Vec, + names: &mut HashSet, + case: GtestCase, +) -> Result<()> { + if !names.insert(case.name.clone()) { + bail!("gtest XML 包含重复 testcase: {}", case.name); + } + cases.push(case); + Ok(()) +} + +fn validate_aggregate(report: &GtestReport) -> Result<()> { + if report.total == 0 { + bail!("gtest XML 报告 0 个 testcase"); + } + let failures = report.cases.iter().filter(|case| case.failed).count(); + let errors = report.cases.iter().filter(|case| case.error).count(); + let disabled = report.cases.iter().filter(|case| case.disabled).count(); + let skipped = report.cases.iter().filter(|case| case.skipped).count(); + if report.total != report.cases.len() + || report.failures != failures + || report.errors != errors + || report.disabled != disabled + || report.skipped != skipped + { + bail!( + "gtest XML 汇总与 testcase 不一致: root=({},{},{},{},{}), cases=({},{},{},{},{})", + report.total, + report.failures, + report.errors, + report.disabled, + report.skipped, + report.cases.len(), + failures, + errors, + disabled, + skipped + ); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{ + fs, + time::{SystemTime, UNIX_EPOCH}, + }; + + fn parse(xml: &str) -> Result { + let path = std::env::temp_dir().join(format!( + "gvisor-runner-xml-{}-{}.xml", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + fs::write(&path, xml).unwrap(); + let result = parse_gtest_xml(&path); + let _ = fs::remove_file(path); + result + } + + #[test] + fn parses_completed_and_skipped_cases() { + let report = parse( + r#" + + + + + + + + "#, + ) + .unwrap(); + assert_eq!(report.total, 2); + assert_eq!(report.skipped, 1); + assert_eq!(report.cases[1].name, "Suite.Skip&Case"); + } + + #[test] + fn derives_skipped_count_when_root_omits_it() { + let report = parse( + r#" + + "#, + ) + .unwrap(); + assert_eq!(report.skipped, 1); + } + + #[test] + fn rejects_aggregate_mismatch() { + let error = parse(r#" + + "#).unwrap_err(); + assert!(error.to_string().contains("汇总")); + } + + #[test] + fn rejects_zero_case_report() { + assert!(parse( + r#""#, + ) + .is_err()); + } + + #[test] + fn rejects_truncated_document_with_complete_case_count() { + assert!(parse( + r#" + "#, + ) + .is_err()); + } + + #[test] + fn rejects_testcase_outside_testsuite() { + assert!(parse( + r#" + + "#, + ) + .is_err()); + } + + #[test] + fn rejects_skip_without_skip_element() { + assert!(parse(r#" + + "#).is_err()); + } + + #[test] + fn required_report_rejects_partial_or_skipped_run() { + let report = parse(r#" + + "#).unwrap(); + let none = HashSet::new(); + assert!(report.validate_required("binary", 2, &none).is_err()); + assert!(report.validate_required("binary", 1, &none).is_err()); + let allowed = HashSet::from(["Suite.Skip".to_string()]); + assert!(report.validate_required("binary", 1, &allowed).is_ok()); + } +} diff --git a/user/apps/tests/syscall/gvisor/runner/src/lib_sync.rs b/user/apps/tests/syscall/gvisor/runner/src/lib_sync.rs index ff3b6d8033..15633dce43 100644 --- a/user/apps/tests/syscall/gvisor/runner/src/lib_sync.rs +++ b/user/apps/tests/syscall/gvisor/runner/src/lib_sync.rs @@ -1,19 +1,35 @@ -use anyhow::Result; +use crate::gtest_xml::parse_gtest_xml; +use anyhow::{Context, Result}; use chrono::{DateTime, Local}; use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, fs::{self, File}, io::{BufRead, BufReader, Write}, - os::unix::process::CommandExt, + os::unix::{ + fs::PermissionsExt, + process::{CommandExt, ExitStatusExt}, + }, path::{Path, PathBuf}, - process::Command, + process::{Child, Command, ExitStatus}, sync::{ atomic::{AtomicUsize, Ordering}, Arc, }, - time::Instant, + thread, + time::{Duration, Instant}, }; +macro_rules! safe_println { + ($($arg:tt)*) => {{ + // Rust's println!/eprintln! panic when DragonOS returns a transient + // console error. Test result collection must remain fail-closed and + // continue to the next binary even when diagnostic output is lost. + let stdout = std::io::stdout(); + let mut lock = stdout.lock(); + let _ = writeln!(lock, $($arg)*); + }}; +} + /// 测试统计信息 #[derive(Debug, Default)] pub struct TestStats { @@ -60,6 +76,7 @@ pub struct Config { pub use_blocklist: bool, pub use_whitelist: bool, pub whitelist_file: PathBuf, + pub required_tests_file: PathBuf, pub tests_dir: PathBuf, pub blocklists_dir: PathBuf, pub results_dir: PathBuf, @@ -67,6 +84,7 @@ pub struct Config { pub extra_blocklist_dirs: Vec, pub test_patterns: Vec, pub output_to_stdout: bool, // 是否输出到控制台而不是文件 + pub enforce_required: bool, } impl Default for Config { @@ -79,6 +97,7 @@ impl Default for Config { use_blocklist: true, use_whitelist: true, whitelist_file: script_dir.join("whitelist.txt"), + required_tests_file: script_dir.join("required_tests.txt"), tests_dir: script_dir.join("tests"), blocklists_dir: script_dir.join("blocklists"), results_dir: script_dir.join("results"), @@ -89,6 +108,7 @@ impl Default for Config { extra_blocklist_dirs: Vec::new(), test_patterns: Vec::new(), output_to_stdout: true, + enforce_required: true, } } } @@ -96,11 +116,11 @@ impl Default for Config { /// 颜色输出辅助函数(简化版) pub fn print_colored(color: &str, prefix: &str, msg: &str) { match color { - "green" => println!("\x1b[32m[{}]\x1b[0m {}", prefix, msg), - "yellow" => println!("\x1b[33m[{}]\x1b[0m {}", prefix, msg), - "red" => eprintln!("\x1b[31m[{}]\x1b[0m {}", prefix, msg), - "blue" => println!("\x1b[34m[{}]\x1b[0m {}", prefix, msg), - _ => println!("[{}] {}", prefix, msg), + "green" => safe_println!("\x1b[32m[{}]\x1b[0m {}", prefix, msg), + "yellow" => safe_println!("\x1b[33m[{}]\x1b[0m {}", prefix, msg), + "red" => safe_println!("\x1b[31m[{}]\x1b[0m {}", prefix, msg), + "blue" => safe_println!("\x1b[34m[{}]\x1b[0m {}", prefix, msg), + _ => safe_println!("[{}] {}", prefix, msg), } } @@ -108,14 +128,27 @@ pub fn print_colored(color: &str, prefix: &str, msg: &str) { pub struct TestRunner { pub config: Config, pub stats: Arc, + required_tests: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct RequiredTest { + case_count: usize, + allowed_skips: HashSet, } impl TestRunner { - pub fn new(config: Config) -> Self { - Self { + pub fn new(config: Config) -> Result { + let required_tests = if config.enforce_required { + read_required_tests(&config.required_tests_file)? + } else { + HashMap::new() + }; + Ok(Self { config, stats: Arc::new(TestStats::default()), - } + required_tests, + }) } /// 打印信息日志 @@ -190,18 +223,6 @@ impl TestRunner { Ok(tests) } - /// 检查测试是否在白名单中 - fn is_test_whitelisted(&self, test_name: &str) -> bool { - if !self.config.use_whitelist { - return true; - } - - match self.get_whitelist_tests() { - Ok(whitelist) => whitelist.contains(test_name), - Err(_) => false, - } - } - /// 获取测试的blocklist fn get_test_blocklist(&self, test_name: &str) -> Vec { if !self.config.use_blocklist { @@ -264,17 +285,29 @@ impl TestRunner { } // 应用白名单过滤 + let all_test_names: HashSet<_> = all_tests.iter().cloned().collect(); let mut candidate_tests = Vec::new(); if self.config.use_whitelist { + let whitelist = self.get_whitelist_tests()?; for test in &all_tests { - if self.is_test_whitelisted(test) { + if whitelist.contains(test) { candidate_tests.push(test.clone()); } } - if candidate_tests.is_empty() { - self.print_warn("没有测试通过白名单过滤"); - return Ok(Vec::new()); + for stale in whitelist.difference(&all_test_names) { + if !self.required_tests.contains_key(stale) { + self.print_warn(&format!("白名单测试在发布包中不存在,已忽略: {}", stale)); + } + } + + if self.config.enforce_required { + for required in self.required_tests.keys() { + if !whitelist.contains(required) { + anyhow::bail!("required 测试未进入默认白名单: {}", required); + } + self.validate_required_binary(required)?; + } } if self.config.verbose { @@ -288,31 +321,53 @@ impl TestRunner { } // 如果没有指定模式,返回候选测试 - if self.config.test_patterns.is_empty() { - candidate_tests.sort(); - return Ok(candidate_tests); - } - - // 根据模式过滤测试 - let mut filtered_tests = HashSet::new(); - for pattern in &self.config.test_patterns { - for test in &candidate_tests { - if test == pattern { - filtered_tests.insert(test.clone()); + let mut result = if self.config.test_patterns.is_empty() { + candidate_tests + } else { + let mut filtered_tests = HashSet::new(); + for pattern in &self.config.test_patterns { + for test in &candidate_tests { + if test == pattern { + filtered_tests.insert(test.clone()); + } } } - } + filtered_tests.into_iter().collect() + }; - let mut result: Vec<_> = filtered_tests.into_iter().collect(); result.sort(); + if result.is_empty() { + anyhow::bail!("没有找到匹配的测试用例"); + } + if self.config.enforce_required { + let selected: HashSet<_> = result.iter().cloned().collect(); + for required in self.required_tests.keys() { + if !selected.contains(required) { + anyhow::bail!("默认执行列表缺少 required 测试: {}", required); + } + } + } Ok(result) } + fn validate_required_binary(&self, test_name: &str) -> Result<()> { + let path = self.config.tests_dir.join(test_name); + let metadata = fs::metadata(&path) + .with_context(|| format!("required 测试不存在: {}", path.display()))?; + if !metadata.is_file() { + anyhow::bail!("required 测试不是普通文件: {}", path.display()); + } + if metadata.permissions().mode() & 0o111 == 0 { + anyhow::bail!("required 测试不可执行: {}", path.display()); + } + Ok(()) + } + /// 运行单个测试 pub fn run_single_test(&self, test_name: &str) -> Result { - println!("[DEBUG] 开始运行测试: {}", test_name); + safe_println!("[DEBUG] 开始运行测试: {}", test_name); let test_path = self.config.tests_dir.join(test_name); - println!("[DEBUG] 测试路径: {:?}", test_path); + safe_println!("[DEBUG] 测试路径: {:?}", test_path); if !test_path.exists() || !test_path.is_file() { self.print_warn(&format!("测试不存在或不可执行: {}", test_name)); @@ -323,12 +378,18 @@ impl TestRunner { // 获取blocklist let blocked_subtests = self.get_test_blocklist(test_name); + if self.required_tests.contains_key(test_name) && !blocked_subtests.is_empty() { + anyhow::bail!("required 测试禁止 blocklist: {}", test_name); + } + + let xml_path = self.config.results_dir.join(format!("{}.xml", test_name)); + remove_stale_xml(&xml_path)?; - println!("[DEBUG] 工作目录: {:?}", self.config.tests_dir); - println!("[DEBUG] TEST_TMPDIR: {:?}", self.config.temp_dir); - println!("[DEBUG] 直接执行: {:?}", test_path); + safe_println!("[DEBUG] 工作目录: {:?}", self.config.tests_dir); + safe_println!("[DEBUG] TEST_TMPDIR: {:?}", self.config.temp_dir); + safe_println!("[DEBUG] 直接执行: {:?}", test_path); if !blocked_subtests.is_empty() { - println!("[DEBUG] gtest_filter: -{}", blocked_subtests.join(":")); + safe_println!("[DEBUG] gtest_filter: -{}", blocked_subtests.join(":")); } // 根据配置决定输出方式 @@ -367,99 +428,123 @@ impl TestRunner { // 构造并执行命令(不使用 shell,不捕获输出,不创建管道) let start_time = Instant::now(); let mut cmd = Command::new(&test_path); + cmd.arg(format!("--gtest_output=xml:{}", xml_path.display())); if !blocked_subtests.is_empty() { cmd.arg(format!("--gtest_filter=-{}", blocked_subtests.join(":"))); } - // Run each test binary in a fresh network namespace to avoid sysctl leakage. + // Run each test binary in its own process group so timeout cleanup also + // terminates descendants. Network tests additionally get a fresh + // namespace to avoid sysctl leakage. let name_lc = test_name.to_ascii_lowercase(); - if name_lc.contains("socket") || name_lc.contains("net") { - unsafe { - cmd.pre_exec(|| { + let isolate_network = name_lc.contains("socket") || name_lc.contains("net"); + unsafe { + cmd.pre_exec(move || { + if libc::setpgid(0, 0) != 0 { + return Err(std::io::Error::last_os_error()); + } + if isolate_network { let ret = libc::unshare(libc::CLONE_NEWNET); if ret != 0 { return Err(std::io::Error::last_os_error()); } - Ok(()) - }); - } + } + Ok(()) + }); } - let status = cmd - .current_dir(&self.config.tests_dir) + cmd.current_dir(&self.config.tests_dir) .env("TEST_TMPDIR", &self.config.temp_dir) .stdout(stdout) - .stderr(stderr) - .status(); + .stderr(stderr); + let status = wait_with_timeout(&mut cmd, Duration::from_secs(self.config.timeout)); // 清理临时目录 let _ = fs::remove_dir_all(&self.config.temp_dir); let _ = fs::create_dir_all(&self.config.temp_dir); let duration = start_time.elapsed(); - match status { - Ok(s) if s.success() => { - self.print_info(&format!( - "✓ {} 通过 ({:.2}s)", + let process_ok = match status { + Ok(TestProcessOutcome::Exited(ref status)) if status.success() => true, + Ok(TestProcessOutcome::Exited(ref status)) => { + self.print_error(&format!( + "✗ {} 进程失败 ({:.2}s), 退出码: {:?}, 信号: {:?}", test_name, - duration.as_secs_f64() + duration.as_secs_f64(), + status.code(), + status.signal() )); - // 只在批量测试时读取文件内容 - if !self.config.output_to_stdout { - let output_file = self - .config - .results_dir - .join(format!("{}.output", test_name)); - if let Ok(content) = fs::read_to_string(&output_file) { - let tail: String = content - .lines() - .rev() - .take(10) - .collect::>() - .into_iter() - .rev() - .map(|s| format!("{}\n", s)) - .collect(); - if !tail.is_empty() { - println!("[DEBUG] 输出尾部: \n{}", tail); - } - } - } - Ok(true) + false } - Ok(s) => { + Ok(TestProcessOutcome::TimedOut) => { self.print_error(&format!( - "✗ {} 失败 ({:.2}s), 退出码: {:?}", + "✗ {} 超时 ({:.2}s), 上限: {} 秒", test_name, duration.as_secs_f64(), - s.code() + self.config.timeout )); - // 只在批量测试时读取文件内容 - if !self.config.output_to_stdout { - let output_file = self - .config - .results_dir - .join(format!("{}.output", test_name)); - if let Ok(content) = fs::read_to_string(&output_file) { - let tail: String = content - .lines() - .rev() - .take(20) - .collect::>() - .into_iter() - .rev() - .map(|s| format!("{}\n", s)) - .collect(); - if !tail.is_empty() { - println!("[DEBUG] 错误输出尾部: \n{}", tail); - } + false + } + Err(ref error) => { + self.print_error(&format!("✗ {} 执行错误: {}", test_name, error)); + false + } + }; + + let xml_ok = match fs::symlink_metadata(&xml_path) { + Ok(metadata) if metadata.file_type().is_file() => match parse_gtest_xml(&xml_path) { + Ok(report) => { + safe_println!( + "[GTEST_XML] binary={} total={} failures={} errors={} disabled={} skipped={}", + test_name, + report.total, + report.failures, + report.errors, + report.disabled, + report.skipped + ); + match self.required_tests.get(test_name) { + Some(required) => match report.validate_required( + test_name, + required.case_count, + &required.allowed_skips, + ) { + Ok(()) => true, + Err(error) => { + self.print_error(&format!("required 结果不合格: {:#}", error)); + false + } + }, + None => true, } } - Ok(false) + Err(error) => { + self.print_error(&format!("gtest XML 无效: {:#}", error)); + false + } + }, + Ok(_) => { + self.print_error(&format!("gtest XML 不是普通文件: {}", xml_path.display())); + false } - Err(e) => { - self.print_error(&format!("✗ {} 执行错误: {}", test_name, e)); - Ok(false) + Err(error) => { + self.print_error(&format!( + "本轮未生成 gtest XML: {}: {}", + xml_path.display(), + error + )); + false } + }; + + if process_ok && xml_ok { + self.print_info(&format!( + "✓ {} 通过 ({:.2}s)", + test_name, + duration.as_secs_f64() + )); + Ok(true) + } else { + Ok(false) } } @@ -467,11 +552,6 @@ impl TestRunner { pub fn run_all_tests(&self) -> Result<()> { let test_list = self.get_test_list()?; - if test_list.is_empty() { - self.print_warn("没有找到匹配的测试用例"); - return Ok(()); - } - self.print_info(&format!("准备运行 {} 个测试用例", test_list.len())); // 初始化结果文件 @@ -497,7 +577,7 @@ impl TestRunner { } } - println!("---"); + safe_println!("---"); } Ok(()) @@ -537,7 +617,7 @@ impl TestRunner { ); file.write_all(report.as_bytes())?; - println!("{}", report); + safe_println!("{}", report); if failed > 0 { let failed_cases_file = self.config.results_dir.join("failed_cases.txt"); @@ -545,7 +625,7 @@ impl TestRunner { let failed_content = fs::read_to_string(&failed_cases_file)?; let failed_section = format!("失败的测试用例:\n{}", failed_content); file.write_all(failed_section.as_bytes())?; - println!("{}", failed_section); + safe_println!("{}", failed_section); } } @@ -593,6 +673,7 @@ impl TestRunner { } if self.config.use_whitelist { + let whitelist = self.get_whitelist_tests()?; self.print_info(&format!( "白名单模式 - 可运行的测试用例 (来自: {:?}):", self.config.whitelist_file @@ -609,7 +690,7 @@ impl TestRunner { let file_name = entry.file_name(); let file_name_str = file_name.to_string_lossy(); if file_name_str.ends_with("_test") { - if self.is_test_whitelisted(&file_name_str) { + if whitelist.contains(file_name_str.as_ref()) { log::info!(" \x1b[32m✓\x1b[0m {} (在白名单中)", file_name_str); } else { log::info!(" \x1b[33m○\x1b[0m {} (不在白名单中)", file_name_str); @@ -634,3 +715,215 @@ impl TestRunner { Ok(()) } } + +enum TestProcessOutcome { + Exited(ExitStatus), + TimedOut, +} + +fn terminate_process_group(child: &mut Child) -> Result<()> { + let pgid = -(child.id() as libc::pid_t); + if unsafe { libc::kill(pgid, libc::SIGKILL) } != 0 { + let error = std::io::Error::last_os_error(); + if error.raw_os_error() != Some(libc::ESRCH) { + let _ = child.kill(); + let _ = child.wait(); + return Err(error).context("终止 gVisor 测试进程组失败"); + } + } + let _ = child.kill(); + child.wait().context("回收 gVisor 测试进程失败")?; + Ok(()) +} + +fn wait_with_timeout(cmd: &mut Command, timeout: Duration) -> Result { + let mut child = cmd.spawn().context("启动 gVisor 测试进程失败")?; + let started = Instant::now(); + loop { + match child.try_wait() { + Ok(Some(status)) => return Ok(TestProcessOutcome::Exited(status)), + Ok(None) => {} + Err(wait_error) => { + terminate_process_group(&mut child).with_context(|| { + format!("等待 gVisor 测试进程失败后清理子进程: {wait_error}") + })?; + return Err(wait_error).context("等待 gVisor 测试进程失败"); + } + } + if started.elapsed() >= timeout { + terminate_process_group(&mut child).context("清理超时 gVisor 测试进程失败")?; + return Ok(TestProcessOutcome::TimedOut); + } + thread::sleep(Duration::from_millis(20)); + } +} + +fn read_required_tests(path: &Path) -> Result> { + let file = File::open(path) + .with_context(|| format!("required 测试清单不存在或不可读: {}", path.display()))?; + let mut required = HashMap::new(); + for (index, line) in BufReader::new(file).lines().enumerate() { + let line = line.with_context(|| format!("读取 required 清单第 {} 行失败", index + 1))?; + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let fields: Vec<_> = line.split_whitespace().collect(); + if fields.len() < 2 { + anyhow::bail!( + "required 清单第 {} 行必须是: binary case_count [allowed_skip ...]", + index + 1 + ); + } + let name = fields[0]; + if !name.ends_with("_test") + || name.contains('/') + || name.contains('\\') + || name == "." + || name == ".." + { + anyhow::bail!("required 清单第 {} 行包含非法二进制名: {}", index + 1, name); + } + let count = fields[1] + .parse::() + .with_context(|| format!("required 清单第 {} 行用例数非法", index + 1))?; + if count == 0 { + anyhow::bail!("required 清单第 {} 行用例数不能为 0", index + 1); + } + let mut allowed_skips = HashSet::new(); + for allowed in &fields[2..] { + if !allowed.contains('.') || !allowed_skips.insert((*allowed).to_string()) { + anyhow::bail!( + "required 清单第 {} 行包含非法或重复 allowed_skip: {}", + index + 1, + allowed + ); + } + } + let spec = RequiredTest { + case_count: count, + allowed_skips, + }; + if required.insert(name.to_string(), spec).is_some() { + anyhow::bail!("required 清单包含重复二进制: {}", name); + } + } + if required.is_empty() { + anyhow::bail!("required 测试清单为空: {}", path.display()); + } + Ok(required) +} + +fn remove_stale_xml(path: &Path) -> Result<()> { + match fs::remove_file(path) { + Ok(()) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(error) => { + Err(error).with_context(|| format!("无法删除旧 gtest XML: {}", path.display())) + } + } +} + +#[cfg(test)] +mod tests { + use super::{ + read_required_tests, remove_stale_xml, wait_with_timeout, Config, TestProcessOutcome, + TestRunner, + }; + use std::{ + fs, + os::unix::process::CommandExt, + process::Command, + time::Duration, + time::{SystemTime, UNIX_EPOCH}, + }; + + fn manifest( + content: &str, + ) -> anyhow::Result> { + let path = std::env::temp_dir().join(format!( + "gvisor-required-{}-{}.txt", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + fs::write(&path, content).unwrap(); + let result = read_required_tests(&path); + let _ = fs::remove_file(path); + result + } + + #[test] + fn parses_required_manifest() { + let required = + manifest("# pinned\nmount_test 87\npivot_root_test 19 PivotRootTest.OnRootFS\n") + .unwrap(); + assert_eq!(required["mount_test"].case_count, 87); + assert!(required["mount_test"].allowed_skips.is_empty()); + assert_eq!(required["pivot_root_test"].case_count, 19); + assert!(required["pivot_root_test"] + .allowed_skips + .contains("PivotRootTest.OnRootFS")); + } + + #[test] + fn rejects_duplicate_or_path_entries() { + assert!(manifest("mount_test 87\nmount_test 1\n").is_err()); + assert!(manifest("../mount_test 87\n").is_err()); + assert!(manifest("mount_test 0\n").is_err()); + assert!(manifest("mount_test 87 invalid_skip\n").is_err()); + assert!(manifest("mount_test 87 Suite.Skip Suite.Skip\n").is_err()); + } + + #[test] + fn disabled_required_enforcement_does_not_read_manifest() { + let mut config = Config::default(); + config.enforce_required = false; + config.required_tests_file = std::env::temp_dir().join(format!( + "missing-gvisor-required-{}-{}.txt", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + + let runner = TestRunner::new(config).unwrap(); + assert!(runner.required_tests.is_empty()); + } + + #[test] + fn removes_stale_xml_before_a_new_run() { + let path = std::env::temp_dir().join(format!( + "gvisor-stale-{}-{}.xml", + std::process::id(), + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + fs::write(&path, "").unwrap(); + remove_stale_xml(&path).unwrap(); + assert!(!path.exists()); + remove_stale_xml(&path).unwrap(); + } + + #[test] + fn timeout_kills_the_test_process_group() { + let mut cmd = Command::new("sh"); + cmd.args(["-c", "sleep 5"]); + unsafe { + cmd.pre_exec(|| { + if libc::setpgid(0, 0) != 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(()) + } + }); + } + let outcome = wait_with_timeout(&mut cmd, Duration::from_millis(50)).unwrap(); + assert!(matches!(outcome, TestProcessOutcome::TimedOut)); + } +} diff --git a/user/apps/tests/syscall/gvisor/runner/src/main.rs b/user/apps/tests/syscall/gvisor/runner/src/main.rs index bc820d79c6..d7f505ae3d 100644 --- a/user/apps/tests/syscall/gvisor/runner/src/main.rs +++ b/user/apps/tests/syscall/gvisor/runner/src/main.rs @@ -2,6 +2,7 @@ use anyhow::Result; use clap::{Arg, Command}; use std::path::PathBuf; +mod gtest_xml; mod lib_sync; use lib_sync::{Config, TestRunner}; @@ -12,13 +13,6 @@ fn main() -> Result<()> { let app = Command::new("gvisor-test-runner") .version("0.1.0") .about("gvisor系统调用测试运行脚本 - Rust版本") - .arg( - Arg::new("help") - .short('h') - .long("help") - .action(clap::ArgAction::Help) - .help("显示此帮助信息"), - ) .arg( Arg::new("list") .short('l') @@ -74,6 +68,12 @@ fn main() -> Result<()> { .value_name("FILE") .help("指定白名单文件路径(默认:whitelist.txt)"), ) + .arg( + Arg::new("required-tests") + .long("required-tests") + .value_name("FILE") + .help("指定 required 测试清单(默认:required_tests.txt)"), + ) .arg( Arg::new("test-patterns") .value_name("PATTERN") @@ -90,9 +90,10 @@ fn main() -> Result<()> { let matches = app.get_matches(); // 解析配置 - let mut config = Config::default(); - - config.verbose = matches.get_flag("verbose"); + let mut config = Config { + verbose: matches.get_flag("verbose"), + ..Config::default() + }; if let Some(timeout) = matches.get_one::("timeout") { config.timeout = *timeout; @@ -105,23 +106,32 @@ fn main() -> Result<()> { config.use_blocklist = !matches.get_flag("no-blocklist"); config.use_whitelist = !matches.get_flag("no-whitelist"); + let custom_whitelist = matches.get_one::("whitelist").is_some(); if let Some(whitelist_file) = matches.get_one::("whitelist") { config.whitelist_file = PathBuf::from(whitelist_file); } + if let Some(required_tests_file) = matches.get_one::("required-tests") { + config.required_tests_file = PathBuf::from(required_tests_file); + } if let Some(extra_dirs) = matches.get_many::("extra-blocklist") { - config.extra_blocklist_dirs = extra_dirs.map(|s| PathBuf::from(s)).collect(); + config.extra_blocklist_dirs = extra_dirs.map(PathBuf::from).collect(); } if let Some(patterns) = matches.get_many::("test-patterns") { config.test_patterns = patterns.cloned().collect(); } + config.enforce_required = config.use_whitelist + && !custom_whitelist + && config.test_patterns.is_empty() + && !matches.get_flag("list"); + // 设置输出方式(默认为true,--no-stdout 设为 false) config.output_to_stdout = !matches.get_flag("no-stdout"); // 创建测试运行器 - let runner = TestRunner::new(config); + let runner = TestRunner::new(config)?; // 处理特殊命令 if matches.get_flag("list") { diff --git a/user/apps/tests/syscall/gvisor/whitelist.txt b/user/apps/tests/syscall/gvisor/whitelist.txt index 306590849e..8fcff656df 100644 --- a/user/apps/tests/syscall/gvisor/whitelist.txt +++ b/user/apps/tests/syscall/gvisor/whitelist.txt @@ -14,6 +14,8 @@ pause_test # 文件系统相关测试 chroot_test +mount_test +pivot_root_test creat_test dup_test fuse_test