From c64eb42b9bc466db8dd1106002afb7db6e1bc395 Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 05:50:41 +0000 Subject: [PATCH 01/13] fix(namespace): pass mount and pivot_root conformance Align mount namespaces, propagation, pivot_root, proc mount topology, VFS permissions, and tmpfs accounting with Linux 6.6 semantics. Make current-task publication and x86 fast CPU identification safe across context switches and AP startup, and harden page-cache allocation and rollback paths. Add focused dunit and gVisor coverage for mount, pivot_root, dangling symlinks, tmpfs quota, and propagation. Make both runners fail closed on incomplete XML, unexpected skips, timeouts, and stale results while always publishing CI artifacts. Validated with kernel builds, runner unit tests and linting, slab allocator tests, QEMU boot, the full dunit suite, focused namespace tests, and gVisor mount/pivot_root tests. Signed-off-by: longjin --- .github/workflows/dunitest.yml | 8 + .github/workflows/test-x86.yml | 8 + kernel/crates/rust-slabmalloc/src/pages.rs | 10 +- kernel/src/arch/x86_64/cpu.rs | 44 +- kernel/src/arch/x86_64/process/mod.rs | 44 +- kernel/src/arch/x86_64/smp/mod.rs | 4 +- kernel/src/filesystem/page_cache.rs | 229 +++++++- kernel/src/filesystem/procfs/mount/collect.rs | 6 + kernel/src/filesystem/procfs/root.rs | 2 +- kernel/src/filesystem/tmpfs/mod.rs | 423 ++++++++++----- kernel/src/filesystem/vfs/file.rs | 46 ++ kernel/src/filesystem/vfs/mod.rs | 115 +++- kernel/src/filesystem/vfs/mount/mod.rs | 93 ++-- kernel/src/filesystem/vfs/open.rs | 54 +- kernel/src/filesystem/vfs/permission.rs | 18 +- .../src/filesystem/vfs/syscall/sys_mount.rs | 18 +- kernel/src/filesystem/vfs/utils.rs | 15 + kernel/src/mm/ucontext/address_space.rs | 10 +- kernel/src/process/idle.rs | 3 +- kernel/src/process/namespace/mnt.rs | 50 +- .../process/namespace/propagation/change.rs | 77 ++- .../process/namespace/propagation/event.rs | 290 ++++++++-- .../process/namespace/propagation/group.rs | 23 +- kernel/src/sched/mod.rs | 126 ++++- kernel/src/smp/cpu/mod.rs | 2 + user/apps/c_unitest/Makefile | 7 +- user/apps/tests/dunitest/Makefile | 1 + user/apps/tests/dunitest/default.nix | 2 + user/apps/tests/dunitest/no_skip.txt | 4 + user/apps/tests/dunitest/runner/src/main.rs | 126 ++++- user/apps/tests/dunitest/runner/src/report.rs | 44 +- user/apps/tests/dunitest/scripts/run_tests.sh | 8 +- .../suites/normal/mount_propagation.cc | 4 +- .../suites/normal/mount_reconfigure.cc | 46 ++ .../suites/normal/open_dangling_symlink.cc | 130 +++++ .../dunitest/suites/normal/test_pivot_root.cc | 168 +++--- user/apps/tests/dunitest/whitelist.txt | 1 + user/apps/tests/syscall/gvisor/Makefile | 3 +- user/apps/tests/syscall/gvisor/default.nix | 2 + .../syscall/gvisor/monitor_test_results.sh | 8 +- .../tests/syscall/gvisor/required_tests.txt | 4 + .../tests/syscall/gvisor/runner/Cargo.lock | 10 + .../tests/syscall/gvisor/runner/Cargo.toml | 1 + .../syscall/gvisor/runner/src/gtest_xml.rs | 458 ++++++++++++++++ .../syscall/gvisor/runner/src/lib_sync.rs | 499 +++++++++++++----- .../tests/syscall/gvisor/runner/src/main.rs | 34 +- user/apps/tests/syscall/gvisor/whitelist.txt | 2 + 47 files changed, 2656 insertions(+), 624 deletions(-) create mode 100644 user/apps/tests/dunitest/no_skip.txt create mode 100644 user/apps/tests/dunitest/suites/normal/open_dangling_symlink.cc create mode 100644 user/apps/tests/syscall/gvisor/required_tests.txt create mode 100644 user/apps/tests/syscall/gvisor/runner/src/gtest_xml.rs diff --git a/.github/workflows/dunitest.yml b/.github/workflows/dunitest.yml index a966e810e8..926d67e280 100644 --- a/.github/workflows/dunitest.yml +++ b/.github/workflows/dunitest.yml @@ -41,3 +41,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..8bf12c519e 100644 --- a/.github/workflows/test-x86.yml +++ b/.github/workflows/test-x86.yml @@ -42,6 +42,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/rust-slabmalloc/src/pages.rs b/kernel/crates/rust-slabmalloc/src/pages.rs index e4304b79f9..4052cb3d92 100644 --- a/kernel/crates/rust-slabmalloc/src/pages.rs +++ b/kernel/crates/rust-slabmalloc/src/pages.rs @@ -282,7 +282,15 @@ 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 { + // ObjectPage contains an enum, raw links and atomics. Treating + // arbitrary uninitialized bytes as that Rust value is undefined + // behavior even if refill() overwrites the metadata immediately + // afterwards. Initialize in place to avoid a 4 KiB stack object. + core::ptr::write_bytes(page.as_mut_ptr().cast::(), 0, Self::SIZE); + page.assume_init() + } } } diff --git a/kernel/src/arch/x86_64/cpu.rs b/kernel/src/arch/x86_64/cpu.rs index ee056e45aa..46cf27e2b4 100644 --- a/kernel/src/arch/x86_64/cpu.rs +++ b/kernel/src/arch/x86_64/cpu.rs @@ -1,10 +1,39 @@ -use core::hint::spin_loop; +use core::{ + hint::spin_loop, + sync::atomic::{AtomicBool, Ordering}, +}; use raw_cpuid::CpuId; -use x86::msr::{rdmsr, IA32_APIC_BASE, IA32_X2APIC_APICID}; +use x86::msr::{rdmsr, wrmsr, IA32_APIC_BASE, IA32_TSC_AUX, IA32_X2APIC_APICID}; use crate::{arch::smp::SMP_BOOT_DATA, smp::cpu::ProcessorId}; +static TSC_AUX_CPU_ID_READY: AtomicBool = AtomicBool::new(false); +static TSC_AUX_CPU_ID_UNSUPPORTED: AtomicBool = AtomicBool::new(false); + +/// Store the logical CPU number in the architectural per-CPU TSC_AUX MSR. +/// +/// This is called once by each CPU while the APIC-based lookup is still in +/// use. Fast reads are enabled globally only after every online CPU has +/// completed this initialization. +pub(crate) fn init_tsc_aux_cpu_id(cpu: ProcessorId) { + if CpuId::new() + .get_extended_processor_and_feature_identifiers() + .is_some_and(|features| features.has_rdtscp()) + { + unsafe { wrmsr(IA32_TSC_AUX, cpu.data() as u64) }; + } else { + TSC_AUX_CPU_ID_UNSUPPORTED.store(true, Ordering::Release); + TSC_AUX_CPU_ID_READY.store(false, Ordering::Release); + } +} + +pub(crate) fn enable_tsc_aux_cpu_id() { + if !TSC_AUX_CPU_ID_UNSUPPORTED.load(Ordering::Acquire) { + TSC_AUX_CPU_ID_READY.store(true, Ordering::Release); + } +} + /// 获取当前CPU的逻辑编号 #[inline] pub fn current_cpu_id() -> ProcessorId { @@ -12,6 +41,17 @@ pub fn current_cpu_id() -> ProcessorId { return ProcessorId::new(0); } + if TSC_AUX_CPU_ID_READY.load(Ordering::Acquire) { + let (_, cpu) = unsafe { x86::time::rdtscp() }; + return ProcessorId::new(cpu); + } + + current_cpu_id_slow() +} + +/// APIC/CPUID based lookup used while bringing up a CPU before its TSC_AUX +/// slot has been initialized. This must not consult the global fast-path flag. +pub(crate) fn current_cpu_id_slow() -> ProcessorId { let x2apic_id = current_x2apic_physical_id(); if let Some(apic_id) = x2apic_id { if let Some(cpu_id) = phys_apic_id_to_cpu_id(apic_id) { diff --git a/kernel/src/arch/x86_64/process/mod.rs b/kernel/src/arch/x86_64/process/mod.rs index 7bb15725ce..7516a07106 100644 --- a/kernel/src/arch/x86_64/process/mod.rs +++ b/kernel/src/arch/x86_64/process/mod.rs @@ -3,14 +3,13 @@ use core::{ arch::asm, intrinsics::unlikely, - mem::ManuallyDrop, sync::atomic::{compiler_fence, Ordering}, }; -use alloc::sync::{Arc, Weak}; +use alloc::sync::Arc; use kdepends::memoffset::offset_of; -use log::{error, info, warn}; +use log::{info, warn}; use system_error::SystemError; use x86::{controlregs::Cr4, segmentation::SegmentSelector}; @@ -302,25 +301,14 @@ impl ArchPCBInfo { impl ProcessControlBlock { /// 获取当前进程的pcb pub fn arch_current_pcb() -> Arc { - // 获取栈指针 - let ptr = VirtAddr::new(x86::current::registers::rsp() as usize); - - let stack_base = VirtAddr::new(ptr.data() & (!(KernelStack::ALIGN - 1))); - - // 从内核栈的最低地址处取出pcb的地址 - let p = stack_base.data() as *const *const ProcessControlBlock; - if unlikely((unsafe { *p }).is_null()) { - error!("p={:p}", p); - panic!("current_pcb is null"); - } - unsafe { - // 为了防止内核栈的pcb weak 指针被释放,这里需要将其包装一下 - let weak_wrapper: ManuallyDrop> = - ManuallyDrop::new(Weak::from_raw(*p)); - - let new_arc: Arc = weak_wrapper.upgrade().unwrap(); - return new_arc; - } + // Pin execution to this CPU before selecting its runqueue. The current + // slot uses a raw publication lock so this remains usable from context + // switch and interrupt paths without scheduler-lock recursion. + let irq_guard = unsafe { CurrentIrqArch::save_and_disable_irq() }; + let cpu = crate::smp::core::smp_get_processor_id().data() as usize; + let current = crate::sched::cpu_rq(cpu).current_local(); + drop(irq_guard); + current } } @@ -418,6 +406,7 @@ impl ProcessManager { /// - `next`:下一个进程的pcb pub unsafe fn switch_process(prev: Arc, next: Arc) { assert!(!CurrentIrqArch::is_irq_enabled()); + let cpu = crate::smp::core::smp_get_processor_id(); // 保存浮点寄存器 prev.arch_info_irqsave().save_fp_state(); @@ -437,7 +426,6 @@ impl ProcessManager { let prev_active_mm = prev_user_vm .clone() .or_else(crate::mm::tlb::tlb_state_loaded_mm); - let cpu = crate::smp::core::smp_get_processor_id(); compiler_fence(Ordering::SeqCst); // INV-1: before loading a different user mm, mark this CPU active in the @@ -478,8 +466,8 @@ impl ProcessManager { (*prev_arch).rip = switch_back as usize; // 恢复当前的 preempt count*2 - ProcessManager::current_pcb().preempt_enable(); - ProcessManager::current_pcb().preempt_enable(); + prev.preempt_enable(); + prev.preempt_enable(); // 切换tss TSSManager::current_tss().set_rsp( @@ -488,9 +476,13 @@ impl ProcessManager { ); TSSManager::invalidate_io_bitmap(); PROCESS_SWITCH_RESULT.as_mut().unwrap().get_mut().prev_pcb = Some(prev); - PROCESS_SWITCH_RESULT.as_mut().unwrap().get_mut().next_pcb = Some(next); + PROCESS_SWITCH_RESULT.as_mut().unwrap().get_mut().next_pcb = Some(next.clone()); // debug!("switch tss ok"); compiler_fence(Ordering::SeqCst); + // All switch preparation above still executes on `prev` and may use + // preemption-aware locks. Publish `next` only at the final no-lock, + // interrupts-disabled boundary immediately before changing stacks. + crate::sched::cpu_rq(cpu.data() as usize).publish_current(next); // 正式切换上下文 switch_to_inner(prev_arch, next_arch); } diff --git a/kernel/src/arch/x86_64/smp/mod.rs b/kernel/src/arch/x86_64/smp/mod.rs index e88bcafe4f..30b75f8fc6 100644 --- a/kernel/src/arch/x86_64/smp/mod.rs +++ b/kernel/src/arch/x86_64/smp/mod.rs @@ -86,7 +86,8 @@ unsafe extern "sysv64" fn smp_init_switch_stack(st: &ApStartStackInfo) -> ! { } unsafe extern "C" fn smp_ap_start_stage1() -> ! { - let id = smp_get_processor_id(); + let id = crate::arch::x86_64::cpu::current_cpu_id_slow(); + crate::arch::x86_64::cpu::init_tsc_aux_cpu_id(id); debug!("smp_ap_start_stage1: id: {}\n", id.data()); X86_64MMArch::init_current_cpu_nxe(); @@ -323,6 +324,7 @@ impl SmpCpuManager { #[allow(static_mut_refs)] pub fn arch_init(_boot_cpu: ProcessorId) { assert!(smp_get_processor_id().data() == 0); + crate::arch::x86_64::cpu::init_tsc_aux_cpu_id(ProcessorId::new(0)); // 写入APU_START_CR3,这个值会在AP处理器启动时设置到CR3寄存器 // 直接使用 table_paddr() 方法,无需获取锁 let addr = IDLE_PROCESS_ADDRESS_SPACE().table_paddr(); diff --git a/kernel/src/filesystem/page_cache.rs b/kernel/src/filesystem/page_cache.rs index cb17ffc9ec..7b42ac5848 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; 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 +1001,7 @@ pub struct InnerPageCache { dirty_preparations: usize, kind: PageCacheKind, page_cache_ref: Weak, + accounting_backend: Option>, } /// 描述一次从页缓存到目标缓冲区的拷贝 @@ -1544,7 +1600,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 +1664,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 +2996,40 @@ 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 removed = { + let mut 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 { + return Ok(false); + } + if entry.page.read().map_count() != 0 { + return Ok(false); + } + guard.remove_page(page_index) + }; + 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 +3386,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 +3402,7 @@ impl InnerPageCache { dirty_preparations: 0, kind, page_cache_ref, + accounting_backend, } } @@ -3295,6 +3416,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 +3426,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 +3468,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 +3505,15 @@ impl PageCache { kind: PageCacheKind, ) -> Arc { let id = PAGE_CACHE_ID.fetch_add(1, Ordering::SeqCst); + let accounting_backend = backend.clone(); 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 +4303,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 +4326,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 +4351,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 +4364,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 +4384,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 +4512,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 +4670,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 +4776,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 +4832,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 +4870,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 +5041,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/tmpfs/mod.rs b/kernel/src/filesystem/tmpfs/mod.rs index 9bedb19ed6..7fac42bb5b 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; @@ -51,11 +52,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 +87,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 +291,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 +314,7 @@ pub struct Tmpfs { super_block: RwSem, size_limit: RwSem>, current_size: AtomicU64, + mount_mode: RwSem, } #[derive(Debug)] @@ -344,6 +361,7 @@ pub struct TmpfsInode { metadata: Metadata, fs: Weak, special_node: Option, + inline_symlink: Option, name: DName, } @@ -374,6 +392,7 @@ impl TmpfsInode { }, fs: Weak::default(), special_node: None, + inline_symlink: None, name: Default::default(), } } @@ -409,7 +428,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,6 +500,18 @@ 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() } @@ -487,17 +525,19 @@ 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); + self.update_superblock_free_for_limit(current, 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,11 +558,7 @@ 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; - }; + fn update_superblock_free_for_limit(&self, current_bytes: u64, limit: u64) { 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); @@ -563,6 +599,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 +620,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); @@ -602,7 +640,7 @@ impl Tmpfs { ) { Ok(_) => { // 同步更新 superblock 的 free 统计,供 statfs/df 使用 - self.update_superblock_free(new_total); + self.update_superblock_free_for_limit(new_total, limit); break; } // 更新成功 Err(_) => continue, // 被其他线程修改,重试 @@ -615,17 +653,32 @@ 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 let Some(limit) = *size_limit { 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() + { + self.update_superblock_free_for_limit(new, limit); + 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 +725,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 +874,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 +906,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 +923,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 +946,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 +984,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 +1004,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); + let committed_end = offset + written; + if committed_end > inode.metadata.size as usize { + inode.metadata.size = committed_end as i64; } - if new_size > committed_size { - inode.metadata.size = new_size as i64; - } - Ok(len) + Ok(written) } fn fs(&self) -> Arc { @@ -1043,7 +1070,7 @@ impl IndexNode for LockedTmpfsInode { 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 +1079,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 +1104,154 @@ 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); + } + } + } + + let mut inode = self.0.lock(); + if end > inode.metadata.size as usize { + inode.metadata.size = end as i64; + } + 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); + } + + let now = PosixTimeSpec::now(); + let inline = target.len() + 1 <= 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: InodeMode::S_IRWXUGO, + flags: InodeFlags::empty(), + nlinks: 1, + uid: 0, + gid: 0, + 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( @@ -1138,6 +1294,7 @@ impl IndexNode for LockedTmpfsInode { }, fs: inode.fs.clone(), special_node: None, + inline_symlink: None, name: name.clone(), })); @@ -1148,7 +1305,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 +1364,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 +1373,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 +1380,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 +1406,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 +1416,6 @@ impl IndexNode for LockedTmpfsInode { inode.metadata.nlinks -= 1; tmpfs_touch_dir(&mut inode, now); - // 减少文件系统使用的大小(目录通常大小为0) - tmpfs.decrease_size(dir_size); - Ok(()) } @@ -1549,6 +1683,7 @@ impl IndexNode for LockedTmpfsInode { }, 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..86ff5c3470 100644 --- a/kernel/src/filesystem/vfs/file.rs +++ b/kernel/src/filesystem/vfs/file.rs @@ -1398,9 +1398,55 @@ impl File { self.offset .fetch_add(len, core::sync::atomic::Ordering::SeqCst); } + if len > 0 { + self.touch_atime_after_read(); + } Ok(len) } + /// Best-effort equivalent of Linux file_accessed()/touch_atime(). + fn touch_atime_after_read(&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; + } + if self + .inode + .fs() + .downcast_arc::() + .is_some_and(|mount| mount.is_readonly()) + { + return; + } + + let Ok(mut metadata) = self.inode.metadata() else { + return; + }; + let now = crate::time::PosixTimeSpec::now(); + if mount_flags.contains(MountFlags::RELATIME) { + let atime_ns = metadata.atime.total_nanos(); + let recent = now.tv_sec.saturating_sub(metadata.atime.tv_sec) < 24 * 60 * 60; + if atime_ns > metadata.mtime.total_nanos() + && atime_ns > metadata.ctime.total_nanos() + && recent + { + return; + } + } + + metadata.atime = now; + let _ = self + .inode + .set_metadata_masked(&metadata, SetMetadataMask::ATIME); + } + pub fn do_write( &self, offset: usize, diff --git a/kernel/src/filesystem/vfs/mod.rs b/kernel/src/filesystem/vfs/mod.rs index 6d6bfc456c..51763d97bd 100644 --- a/kernel/src/filesystem/vfs/mod.rs +++ b/kernel/src/filesystem/vfs/mod.rs @@ -1397,6 +1397,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 +1461,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 +1482,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()?), + 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()?), - )? - .1 - .ok_or(SystemError::ESTALE) + 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 +1534,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 +1551,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 @@ -1559,18 +1613,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 +1654,9 @@ impl dyn IndexNode { // - symlink 位于路径中间(rest_path 非空) // - 需要跟随最终 symlink(follow_final_symlink=true) // - 或者 pathname 以 '/' 结尾(trailing_slash=true) - let need_follow = !rest_path.is_empty() + let need_follow = has_more_components || follow_final_symlink - || (trailing_slash && rest_path.is_empty()); + || (trailing_slash && !has_more_components); // 兼容旧语义:symlink_follows_remaining==0 表示完全不跟随 symlink。 // 在这种模式下,如果路径解析"需要跟随"(例如 symlink 位于中间,或末尾带 '/'), @@ -1615,7 +1689,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; @@ -1644,6 +1718,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 +1738,7 @@ impl dyn IndexNode { } rest_path = String::from(rest); } else { + ownership = parent_ownership; rest_path = new_path; } @@ -1674,7 +1753,7 @@ impl dyn IndexNode { return Err(SystemError::ENOTDIR); } - return Ok((result, ownership)); + return Ok(PathWalkOutcome::Found(result, ownership)); } } @@ -2254,7 +2333,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..9cee7c1f1e 100644 --- a/kernel/src/filesystem/vfs/mount/mod.rs +++ b/kernel/src/filesystem/vfs/mount/mod.rs @@ -23,7 +23,7 @@ use crate::{ mm::{fault::PageFaultMessage, VirtRegion, VmFaultReason, VmFlags}, process::{ namespace::{ - mnt::MntNamespace, + mnt::{mount_max, MntNamespace}, propagation::{ abort_mount_propagation, commit_mount_propagation_locked, detach_mount_propagation, ensure_subtree_shared, inherit_bind_mount_propagation, @@ -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,14 +451,11 @@ 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); - } } /// @brief Mount filesystem @@ -1486,7 +1482,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 +1522,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 +2908,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,7 +3318,7 @@ impl MountFSInode { /// @return Arc pub(crate) fn overlaid_inode(&self) -> Arc { let mut current = self.self_ref.upgrade().unwrap(); - for _ in 0..1024 { + for _ in 0..mount_max() { let Some(sub_mountfs) = current.mount_fs.lookup_top(¤t) else { return current; }; @@ -3322,7 +3330,7 @@ impl MountFSInode { current = next; } - log::warn!("MountFSInode::overlaid_inode: overlay depth exceeds 1024"); + log::warn!("MountFSInode::overlaid_inode: overlay depth exceeds mount-max"); current } @@ -3350,27 +3358,30 @@ 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(); + for _ in 0..mount_max() { + 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; + 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()) + + Err(SystemError::ELOOP) } fn do_absolute_path(&self) -> Result { diff --git a/kernel/src/filesystem/vfs/open.rs b/kernel/src/filesystem/vfs/open.rs index ec832e336d..d831d855a8 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, @@ -276,7 +277,9 @@ 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); 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 +290,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 +340,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 +356,10 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result return Err(errno), }; drop(start_path); let inode = resolved.inode(); @@ -376,13 +373,6 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result Result 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 + .fs() + .downcast_arc::() + .is_some_and(|mount| mount.is_readonly()) + { + return Err(SystemError::EROFS); + } + let cred = ProcessManager::current_pcb().cred(); match inode.fs().permission_policy() { FsPermissionPolicy::Dac => cred.inode_permission(metadata, mask.bits()), 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/mm/ucontext/address_space.rs b/kernel/src/mm/ucontext/address_space.rs index b672cc8613..fe3235839b 100644 --- a/kernel/src/mm/ucontext/address_space.rs +++ b/kernel/src/mm/ucontext/address_space.rs @@ -516,13 +516,21 @@ impl AddressSpace { len, prot_flags, map_flags, - may_exec, + mut may_exec, offset, round_to_min, allocate_at_once, sysv_shm, fixed_noreplace_conflict_error_before_mmap_min, } = args; + 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 len = page_align_up(len); if len == 0 { return Err(SystemError::EINVAL); diff --git a/kernel/src/process/idle.rs b/kernel/src/process/idle.rs index 678d88c4e6..3e3a539596 100644 --- a/kernel/src/process/idle.rs +++ b/kernel/src/process/idle.rs @@ -52,7 +52,6 @@ impl ProcessManager { }; let idle_pcb = ProcessControlBlock::new_idle(i, kstack); - assert!(idle_pcb.basic().user_vm().is_none()); unsafe { idle_pcb @@ -71,7 +70,7 @@ impl ProcessManager { pi_guard.set_cpus_allowed(CpuMask::from_cpu(ProcessorId::new(i))); idle_pcb.sched_info().set_on_cpu(Some(ProcessorId::new(i))); - rq.set_current(Arc::downgrade(&idle_pcb)); + rq.initialize_current(idle_pcb.clone()); rq.set_idle(Arc::downgrade(&idle_pcb)); IDLE_CPUS.set(ProcessorId::new(i)); diff --git a/kernel/src/process/namespace/mnt.rs b/kernel/src/process/namespace/mnt.rs index 1f95e4cb5b..e94eae4238 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,7 @@ impl MntNamespace { inner: RwSem::new(InnerMntNamespace { _dead: false, root_mountfs: new_root_mntfs, - root_attached: inner.root_attached, + root_parent_mount_id: inner.root_parent_mount_id, // 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 +750,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 +914,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 +1136,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 +1145,14 @@ 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); + assert!(attached_copy.inner.read().root_parent_mount_id.is_some()); } #[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..69b46f8bdb 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>, @@ -452,21 +536,65 @@ 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 new_child_tree = try_collect_subtree(new_child)?; + let new_child_mount_count = new_child_tree.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)?); } } - 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, @@ -816,7 +944,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 +982,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 +1006,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 +1078,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 +1130,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/kernel/src/sched/mod.rs b/kernel/src/sched/mod.rs index bdc9067d6f..28ca5b88e6 100644 --- a/kernel/src/sched/mod.rs +++ b/kernel/src/sched/mod.rs @@ -13,9 +13,10 @@ pub mod prio; pub mod syscall; use core::{ + cell::UnsafeCell, intrinsics::{likely, unlikely}, panic::Location, - sync::atomic::{compiler_fence, fence, AtomicUsize, Ordering}, + sync::atomic::{compiler_fence, fence, AtomicBool, AtomicPtr, AtomicUsize, Ordering}, }; use alloc::{ @@ -465,6 +466,103 @@ pub trait SchedArch { fn initial_setup_sched_local() {} } +/// Per-CPU ownership and publication of the task currently executing. +/// +/// The owner keeps the PCB alive while it is running. Readers serialize with +/// publication through a raw lock before cloning the owner. This lock cannot +/// use the scheduler-aware spinlock because current-task publication is itself +/// part of a context switch. +#[derive(Debug)] +struct CurrentTask { + lock: AtomicBool, + owner: UnsafeCell>>, + local_ptr: AtomicPtr, +} + +// `owner` is accessed only under the raw publication lock. +unsafe impl Sync for CurrentTask {} + +impl CurrentTask { + fn new() -> Self { + Self { + lock: AtomicBool::new(false), + owner: UnsafeCell::new(None), + local_ptr: AtomicPtr::new(core::ptr::null_mut()), + } + } + + fn get(&self) -> Arc { + // This raw publication lock deliberately does not use SpinLock: its + // guard restores preemption through current_pcb(), which must not run + // after the slot has already changed to the next task. + // Disable local interrupts before taking it so an interrupt-driven + // context switch on this CPU cannot spin on a lock held by the + // interrupted reader. Remote publishers may briefly spin until the + // clone completes. + let irq_guard = unsafe { CurrentIrqArch::save_and_disable_irq() }; + self.lock_raw(); + let current = unsafe { &*self.owner.get() } + .as_ref() + .cloned() + .expect("runqueue current task is initialized"); + self.unlock_raw(); + drop(irq_guard); + current + } + + fn initialize(&mut self, pcb: Arc) { + let ptr = Arc::as_ptr(&pcb) as *mut ProcessControlBlock; + *self.owner.get_mut() = Some(pcb); + self.local_ptr.store(ptr, Ordering::Relaxed); + } + + fn set(&self, pcb: Arc) { + debug_assert!(!CurrentIrqArch::is_irq_enabled()); + self.lock_raw(); + let slot = unsafe { &mut *self.owner.get() }; + let old = slot.replace(pcb); + self.local_ptr.store( + Arc::as_ptr(slot.as_ref().unwrap()) as *mut ProcessControlBlock, + Ordering::Release, + ); + self.unlock_raw(); + // PCB destruction is not part of the publication critical section and + // may acquire unrelated locks. The pointer is already protected by the + // new slot owner at this point. + drop(old); + } + + /// Fast current-task lookup for the CPU that owns this runqueue. + /// + /// The caller disables interrupts before selecting the CPU and retains + /// that guard through the strong-count increment. Therefore this CPU + /// cannot publish a replacement (or drop the slot-owned reference) in the + /// interval between loading the pointer and acquiring its Arc reference. + fn get_local(&self) -> Arc { + debug_assert!(!CurrentIrqArch::is_irq_enabled()); + let ptr = self.local_ptr.load(Ordering::Acquire); + assert!(!ptr.is_null(), "runqueue current task is initialized"); + unsafe { + Arc::increment_strong_count(ptr); + Arc::from_raw(ptr) + } + } + + fn lock_raw(&self) { + while self + .lock + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_err() + { + core::hint::spin_loop(); + } + } + + fn unlock_raw(&self) { + self.lock.store(false, Ordering::Release); + } +} + /// ## PerCpu的运行队列,其中维护了各个调度器对应的rq #[allow(dead_code)] #[derive(Debug)] @@ -510,7 +608,7 @@ pub struct CpuRunQueue { sched_info: SchedInfo, /// 当前在运行队列上执行的进程 - current: Weak, + current: CurrentTask, idle: Weak, } @@ -543,7 +641,7 @@ impl CpuRunQueue { clock_idle: 0, cfs_tasks: LinkedList::new(), sched_info: SchedInfo::default(), - current: Weak::new(), + current: CurrentTask::new(), idle: Weak::new(), } } @@ -848,12 +946,27 @@ impl CpuRunQueue { #[inline] pub fn current(&self) -> Arc { - self.current.upgrade().unwrap() + self.current.get() } #[inline] - pub fn set_current(&mut self, pcb: Weak) { - self.current = pcb; + pub(crate) fn current_local(&self) -> Arc { + self.current.get_local() + } + + #[inline] + pub(crate) fn initialize_current(&mut self, pcb: Arc) { + self.current.initialize(pcb); + } + + /// Publish the task selected under this runqueue's lock at the final + /// architecture switch boundary. The caller must run on this queue's CPU + /// with interrupts disabled and must not perform another fallible/locking + /// operation before switching stacks. + pub(crate) unsafe fn publish_current(&self, pcb: Arc) { + assert!(!CurrentIrqArch::is_irq_enabled()); + assert_eq!(self.cpu, smp_get_processor_id()); + self.current.set(pcb); } #[inline] @@ -1247,7 +1360,6 @@ fn __schedule_inner(sched_mod: SchedMode, current: Option 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/mount_propagation.cc b/user/apps/tests/dunitest/suites/normal/mount_propagation.cc index 5f881a9f38..348a4feaf4 100644 --- a/user/apps/tests/dunitest/suites/normal/mount_propagation.cc +++ b/user/apps/tests/dunitest/suites/normal/mount_propagation.cc @@ -380,9 +380,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); } diff --git a/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc b/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc index e6cd289bc5..1d240e756e 100644 --- a/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc +++ b/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #ifndef CLONE_NEWNS @@ -150,6 +151,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 +355,21 @@ 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, 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 +383,32 @@ 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, 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..cc48872762 --- /dev/null +++ b/user/apps/tests/dunitest/suites/normal/open_dangling_symlink.cc @@ -0,0 +1,130 @@ +#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, 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..4bcd99f23d 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::{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,23 @@ 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 = read_required_tests(&config.required_tests_file)?; + Ok(Self { config, stats: Arc::new(TestStats::default()), - } + required_tests, + }) } /// 打印信息日志 @@ -190,18 +219,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 +281,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 +317,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 +374,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); + } - println!("[DEBUG] 工作目录: {:?}", self.config.tests_dir); - println!("[DEBUG] TEST_TMPDIR: {:?}", self.config.temp_dir); - println!("[DEBUG] 直接执行: {:?}", test_path); + let xml_path = self.config.results_dir.join(format!("{}.xml", test_name)); + remove_stale_xml(&xml_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 +424,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 +548,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 +573,7 @@ impl TestRunner { } } - println!("---"); + safe_println!("---"); } Ok(()) @@ -537,7 +613,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 +621,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 +669,7 @@ impl TestRunner { } if self.config.use_whitelist { + let whitelist = self.get_whitelist_tests()?; self.print_info(&format!( "白名单模式 - 可运行的测试用例 (来自: {:?}):", self.config.whitelist_file @@ -609,7 +686,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 +711,183 @@ impl TestRunner { Ok(()) } } + +enum TestProcessOutcome { + Exited(ExitStatus), + TimedOut, +} + +fn wait_with_timeout(cmd: &mut Command, timeout: Duration) -> Result { + let mut child = cmd.spawn().context("启动 gVisor 测试进程失败")?; + let started = Instant::now(); + loop { + if let Some(status) = child.try_wait().context("等待 gVisor 测试进程失败")? { + return Ok(TestProcessOutcome::Exited(status)); + } + if started.elapsed() >= timeout { + 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 测试进程失败")?; + 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, TestProcessOutcome}; + 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 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 From c6a00c9be1be677b0f8112a6dbfe48a984bf423a Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 08:08:10 +0000 Subject: [PATCH 02/13] fix(vfs): address review and CI regressions Make atime updates field-specific and synchronized in mutable filesystems, serialize FUSE and ext4 metadata updates, enforce O_NOATIME ownership, and cover directory access semantics. Compute tmpfs statfs data from atomic page accounting without taking a superblock write lock on every page operation. Preserve slab allocator soundness without clearing an entire object page. Restore the origin/master current-task implementation that predates the wait/epoll CI regressions, reject invalid open flag combinations, read complete symlink targets, and decouple existing mount topology traversal from mount-max admission limits. Add dunit coverage for metadata races, atime flags, tmpfs quota reporting, long symlinks, open errors, and lowered mount limits. Signed-off-by: longjin --- kernel/crates/rust-slabmalloc/src/pages.rs | 20 +- kernel/src/arch/x86_64/cpu.rs | 44 +-- kernel/src/arch/x86_64/process/mod.rs | 44 +-- kernel/src/arch/x86_64/smp/mod.rs | 4 +- kernel/src/driver/tty/tty_device.rs | 7 + kernel/src/filesystem/cgroup2/inode.rs | 6 + kernel/src/filesystem/devfs/full_dev.rs | 6 + kernel/src/filesystem/devfs/mod.rs | 6 + kernel/src/filesystem/devfs/null_dev.rs | 6 + kernel/src/filesystem/devfs/random_dev.rs | 6 + kernel/src/filesystem/devfs/zero_dev.rs | 6 + kernel/src/filesystem/ext4/inode.rs | 31 +++ kernel/src/filesystem/fat/fs.rs | 5 + kernel/src/filesystem/fuse/inode.rs | 5 + kernel/src/filesystem/fuse/inode/file.rs | 1 + kernel/src/filesystem/fuse/inode/vfs.rs | 41 +++ kernel/src/filesystem/overlayfs/inode.rs | 11 + kernel/src/filesystem/page_cache.rs | 10 +- kernel/src/filesystem/ramfs/mod.rs | 6 + kernel/src/filesystem/tmpfs/mod.rs | 43 +-- kernel/src/filesystem/vfs/file.rs | 35 +-- kernel/src/filesystem/vfs/mod.rs | 52 +++- kernel/src/filesystem/vfs/mount/mod.rs | 76 +++++- kernel/src/filesystem/vfs/open.rs | 13 +- kernel/src/filesystem/vfs/permission.rs | 15 ++ .../src/filesystem/vfs/syscall/sys_fcntl.rs | 6 + kernel/src/process/idle.rs | 3 +- kernel/src/sched/mod.rs | 126 +-------- kernel/src/smp/cpu/mod.rs | 2 - .../dunitest/suites/normal/mount_limit.cc | 24 ++ .../suites/normal/mount_reconfigure.cc | 251 ++++++++++++++++++ 31 files changed, 663 insertions(+), 248 deletions(-) diff --git a/kernel/crates/rust-slabmalloc/src/pages.rs b/kernel/crates/rust-slabmalloc/src/pages.rs index 4052cb3d92..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>, @@ -284,11 +285,18 @@ impl<'a> ObjectPage<'a> { pub fn new() -> Box> { let mut page = Box::>::new_uninit(); unsafe { - // ObjectPage contains an enum, raw links and atomics. Treating - // arbitrary uninitialized bytes as that Rust value is undefined - // behavior even if refill() overwrites the metadata immediately - // afterwards. Initialize in place to avoid a 4 KiB stack object. - core::ptr::write_bytes(page.as_mut_ptr().cast::(), 0, Self::SIZE); + // 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/arch/x86_64/cpu.rs b/kernel/src/arch/x86_64/cpu.rs index 46cf27e2b4..ee056e45aa 100644 --- a/kernel/src/arch/x86_64/cpu.rs +++ b/kernel/src/arch/x86_64/cpu.rs @@ -1,39 +1,10 @@ -use core::{ - hint::spin_loop, - sync::atomic::{AtomicBool, Ordering}, -}; +use core::hint::spin_loop; use raw_cpuid::CpuId; -use x86::msr::{rdmsr, wrmsr, IA32_APIC_BASE, IA32_TSC_AUX, IA32_X2APIC_APICID}; +use x86::msr::{rdmsr, IA32_APIC_BASE, IA32_X2APIC_APICID}; use crate::{arch::smp::SMP_BOOT_DATA, smp::cpu::ProcessorId}; -static TSC_AUX_CPU_ID_READY: AtomicBool = AtomicBool::new(false); -static TSC_AUX_CPU_ID_UNSUPPORTED: AtomicBool = AtomicBool::new(false); - -/// Store the logical CPU number in the architectural per-CPU TSC_AUX MSR. -/// -/// This is called once by each CPU while the APIC-based lookup is still in -/// use. Fast reads are enabled globally only after every online CPU has -/// completed this initialization. -pub(crate) fn init_tsc_aux_cpu_id(cpu: ProcessorId) { - if CpuId::new() - .get_extended_processor_and_feature_identifiers() - .is_some_and(|features| features.has_rdtscp()) - { - unsafe { wrmsr(IA32_TSC_AUX, cpu.data() as u64) }; - } else { - TSC_AUX_CPU_ID_UNSUPPORTED.store(true, Ordering::Release); - TSC_AUX_CPU_ID_READY.store(false, Ordering::Release); - } -} - -pub(crate) fn enable_tsc_aux_cpu_id() { - if !TSC_AUX_CPU_ID_UNSUPPORTED.load(Ordering::Acquire) { - TSC_AUX_CPU_ID_READY.store(true, Ordering::Release); - } -} - /// 获取当前CPU的逻辑编号 #[inline] pub fn current_cpu_id() -> ProcessorId { @@ -41,17 +12,6 @@ pub fn current_cpu_id() -> ProcessorId { return ProcessorId::new(0); } - if TSC_AUX_CPU_ID_READY.load(Ordering::Acquire) { - let (_, cpu) = unsafe { x86::time::rdtscp() }; - return ProcessorId::new(cpu); - } - - current_cpu_id_slow() -} - -/// APIC/CPUID based lookup used while bringing up a CPU before its TSC_AUX -/// slot has been initialized. This must not consult the global fast-path flag. -pub(crate) fn current_cpu_id_slow() -> ProcessorId { let x2apic_id = current_x2apic_physical_id(); if let Some(apic_id) = x2apic_id { if let Some(cpu_id) = phys_apic_id_to_cpu_id(apic_id) { diff --git a/kernel/src/arch/x86_64/process/mod.rs b/kernel/src/arch/x86_64/process/mod.rs index 7516a07106..7bb15725ce 100644 --- a/kernel/src/arch/x86_64/process/mod.rs +++ b/kernel/src/arch/x86_64/process/mod.rs @@ -3,13 +3,14 @@ use core::{ arch::asm, intrinsics::unlikely, + mem::ManuallyDrop, sync::atomic::{compiler_fence, Ordering}, }; -use alloc::sync::Arc; +use alloc::sync::{Arc, Weak}; use kdepends::memoffset::offset_of; -use log::{info, warn}; +use log::{error, info, warn}; use system_error::SystemError; use x86::{controlregs::Cr4, segmentation::SegmentSelector}; @@ -301,14 +302,25 @@ impl ArchPCBInfo { impl ProcessControlBlock { /// 获取当前进程的pcb pub fn arch_current_pcb() -> Arc { - // Pin execution to this CPU before selecting its runqueue. The current - // slot uses a raw publication lock so this remains usable from context - // switch and interrupt paths without scheduler-lock recursion. - let irq_guard = unsafe { CurrentIrqArch::save_and_disable_irq() }; - let cpu = crate::smp::core::smp_get_processor_id().data() as usize; - let current = crate::sched::cpu_rq(cpu).current_local(); - drop(irq_guard); - current + // 获取栈指针 + let ptr = VirtAddr::new(x86::current::registers::rsp() as usize); + + let stack_base = VirtAddr::new(ptr.data() & (!(KernelStack::ALIGN - 1))); + + // 从内核栈的最低地址处取出pcb的地址 + let p = stack_base.data() as *const *const ProcessControlBlock; + if unlikely((unsafe { *p }).is_null()) { + error!("p={:p}", p); + panic!("current_pcb is null"); + } + unsafe { + // 为了防止内核栈的pcb weak 指针被释放,这里需要将其包装一下 + let weak_wrapper: ManuallyDrop> = + ManuallyDrop::new(Weak::from_raw(*p)); + + let new_arc: Arc = weak_wrapper.upgrade().unwrap(); + return new_arc; + } } } @@ -406,7 +418,6 @@ impl ProcessManager { /// - `next`:下一个进程的pcb pub unsafe fn switch_process(prev: Arc, next: Arc) { assert!(!CurrentIrqArch::is_irq_enabled()); - let cpu = crate::smp::core::smp_get_processor_id(); // 保存浮点寄存器 prev.arch_info_irqsave().save_fp_state(); @@ -426,6 +437,7 @@ impl ProcessManager { let prev_active_mm = prev_user_vm .clone() .or_else(crate::mm::tlb::tlb_state_loaded_mm); + let cpu = crate::smp::core::smp_get_processor_id(); compiler_fence(Ordering::SeqCst); // INV-1: before loading a different user mm, mark this CPU active in the @@ -466,8 +478,8 @@ impl ProcessManager { (*prev_arch).rip = switch_back as usize; // 恢复当前的 preempt count*2 - prev.preempt_enable(); - prev.preempt_enable(); + ProcessManager::current_pcb().preempt_enable(); + ProcessManager::current_pcb().preempt_enable(); // 切换tss TSSManager::current_tss().set_rsp( @@ -476,13 +488,9 @@ impl ProcessManager { ); TSSManager::invalidate_io_bitmap(); PROCESS_SWITCH_RESULT.as_mut().unwrap().get_mut().prev_pcb = Some(prev); - PROCESS_SWITCH_RESULT.as_mut().unwrap().get_mut().next_pcb = Some(next.clone()); + PROCESS_SWITCH_RESULT.as_mut().unwrap().get_mut().next_pcb = Some(next); // debug!("switch tss ok"); compiler_fence(Ordering::SeqCst); - // All switch preparation above still executes on `prev` and may use - // preemption-aware locks. Publish `next` only at the final no-lock, - // interrupts-disabled boundary immediately before changing stacks. - crate::sched::cpu_rq(cpu.data() as usize).publish_current(next); // 正式切换上下文 switch_to_inner(prev_arch, next_arch); } diff --git a/kernel/src/arch/x86_64/smp/mod.rs b/kernel/src/arch/x86_64/smp/mod.rs index 30b75f8fc6..e88bcafe4f 100644 --- a/kernel/src/arch/x86_64/smp/mod.rs +++ b/kernel/src/arch/x86_64/smp/mod.rs @@ -86,8 +86,7 @@ unsafe extern "sysv64" fn smp_init_switch_stack(st: &ApStartStackInfo) -> ! { } unsafe extern "C" fn smp_ap_start_stage1() -> ! { - let id = crate::arch::x86_64::cpu::current_cpu_id_slow(); - crate::arch::x86_64::cpu::init_tsc_aux_cpu_id(id); + let id = smp_get_processor_id(); debug!("smp_ap_start_stage1: id: {}\n", id.data()); X86_64MMArch::init_current_cpu_nxe(); @@ -324,7 +323,6 @@ impl SmpCpuManager { #[allow(static_mut_refs)] pub fn arch_init(_boot_cpu: ProcessorId) { assert!(smp_get_processor_id().data() == 0); - crate::arch::x86_64::cpu::init_tsc_aux_cpu_id(ProcessorId::new(0)); // 写入APU_START_CR3,这个值会在AP处理器启动时设置到CR3寄存器 // 直接使用 table_paddr() 方法,无需获取锁 let addr = IDLE_PROCESS_ADDRESS_SPACE().table_paddr(); 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/inode.rs b/kernel/src/filesystem/ext4/inode.rs index cc6b09425d..353e83450c 100644 --- a/kernel/src/filesystem/ext4/inode.rs +++ b/kernel/src/filesystem/ext4/inode.rs @@ -768,6 +768,7 @@ 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 = @@ -808,6 +809,36 @@ impl IndexNode for LockedExt4Inode { Ok(()) } + fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let _io_guard = self.io_lock.lock(); + let mut metadata = self.metadata()?; + if !vfs::update_atime_locked(&mut metadata, now, relatime) { + return Ok(()); + } + + let _operation = self.begin_operation()?; + let (fs, inode_num) = { + let guard = self.inner.lock(); + (guard.concret_fs(), guard.inner_inode_num) + }; + let atime = now.tv_sec.max(0).min(u32::MAX as i64) as u32; + Self::retry_metadata_contention(|| { + fs.fs.setattr( + inode_num, + another_ext4::SetAttr { + mode: None, + uid: None, + gid: None, + size: None, + atime: Some(atime), + mtime: None, + ctime: None, + crtime: None, + }, + ) + }) + } + fn resize(&self, len: usize) -> Result<(), SystemError> { let _operation = self.begin_operation()?; let (fs, inode_num, page_cache) = { 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 7b42ac5848..a0bbbc6613 100644 --- a/kernel/src/filesystem/page_cache.rs +++ b/kernel/src/filesystem/page_cache.rs @@ -3505,7 +3505,15 @@ impl PageCache { kind: PageCacheKind, ) -> Arc { let id = PAGE_CACHE_ID.fetch_add(1, Ordering::SeqCst); - let accounting_backend = backend.clone(); + // 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( diff --git a/kernel/src/filesystem/ramfs/mod.rs b/kernel/src/filesystem/ramfs/mod.rs index e2d327380b..b388c8a252 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 { diff --git a/kernel/src/filesystem/tmpfs/mod.rs b/kernel/src/filesystem/tmpfs/mod.rs index 7fac42bb5b..4ffdfbac6b 100644 --- a/kernel/src/filesystem/tmpfs/mod.rs +++ b/kernel/src/filesystem/tmpfs/mod.rs @@ -513,7 +513,19 @@ impl FileSystem for Tmpfs { } 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 { @@ -531,7 +543,6 @@ impl FileSystem for Tmpfs { return Err(SystemError::EINVAL); } *limit = Some(new_limit); - self.update_superblock_free_for_limit(current, new_limit); } if let Some(mode) = parsed.mode { @@ -558,17 +569,6 @@ impl Tmpfs { bytes.div_ceil(TMPFS_BLOCK_SIZE) } - fn update_superblock_free_for_limit(&self, current_bytes: u64, limit: u64) { - 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。 @@ -638,11 +638,7 @@ impl Tmpfs { Ordering::Release, Ordering::Acquire, ) { - Ok(_) => { - // 同步更新 superblock 的 free 统计,供 statfs/df 使用 - self.update_superblock_free_for_limit(new_total, limit); - break; - } // 更新成功 + Ok(_) => break, // 更新成功 Err(_) => continue, // 被其他线程修改,重试 } } @@ -654,7 +650,7 @@ impl Tmpfs { /// 使用fetch_sub确保并发安全 fn decrease_size(&self, size: usize) { let size_limit = self.size_limit.read(); - if let Some(limit) = *size_limit { + if size_limit.is_some() { let size_to_decrease = size as u64; loop { let current = self.current_size.load(Ordering::Acquire); @@ -664,7 +660,6 @@ impl Tmpfs { .compare_exchange_weak(current, new, Ordering::Release, Ordering::Acquire) .is_ok() { - self.update_superblock_free_for_limit(new, limit); break; } } @@ -1068,6 +1063,12 @@ 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) = { @@ -1200,7 +1201,7 @@ impl IndexNode for LockedTmpfsInode { } let now = PosixTimeSpec::now(); - let inline = target.len() + 1 <= SHORT_SYMLINK_LEN; + let inline = target.len() < SHORT_SYMLINK_LEN; let result = Arc::new(LockedTmpfsInode::new(TmpfsInode { parent: parent.self_ref.clone(), self_ref: Weak::default(), diff --git a/kernel/src/filesystem/vfs/file.rs b/kernel/src/filesystem/vfs/file.rs index 86ff5c3470..baf972637f 100644 --- a/kernel/src/filesystem/vfs/file.rs +++ b/kernel/src/filesystem/vfs/file.rs @@ -1399,13 +1399,13 @@ impl File { .fetch_add(len, core::sync::atomic::Ordering::SeqCst); } if len > 0 { - self.touch_atime_after_read(); + self.touch_atime_after_access(); } Ok(len) } /// Best-effort equivalent of Linux file_accessed()/touch_atime(). - fn touch_atime_after_read(&self) { + fn touch_atime_after_access(&self) { use super::mount::{MountFS, MountFlags}; if self.flags().contains(FileFlags::O_NOATIME) { @@ -1426,25 +1426,10 @@ impl File { return; } - let Ok(mut metadata) = self.inode.metadata() else { - return; - }; let now = crate::time::PosixTimeSpec::now(); - if mount_flags.contains(MountFlags::RELATIME) { - let atime_ns = metadata.atime.total_nanos(); - let recent = now.tv_sec.saturating_sub(metadata.atime.tv_sec) < 24 * 60 * 60; - if atime_ns > metadata.mtime.total_nanos() - && atime_ns > metadata.ctime.total_nanos() - && recent - { - return; - } - } - - metadata.atime = now; let _ = self .inode - .set_metadata_masked(&metadata, SetMetadataMask::ATIME); + .update_atime(now, mount_flags.contains(MountFlags::RELATIME)); } pub fn do_write( @@ -1733,17 +1718,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 51763d97bd..da808590b3 100644 --- a/kernel/src/filesystem/vfs/mod.rs +++ b/kernel/src/filesystem/vfs/mod.rs @@ -164,6 +164,29 @@ 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 relatime { + let recent = now.tv_sec.saturating_sub(metadata.atime.tv_sec) < 24 * 60 * 60; + let atime_after_mtime = (metadata.atime.tv_sec, metadata.atime.tv_nsec) + > (metadata.mtime.tv_sec, metadata.mtime.tv_nsec); + let atime_after_ctime = (metadata.atime.tv_sec, metadata.atime.tv_nsec) + > (metadata.ctime.tv_sec, metadata.ctime.tv_nsec); + if atime_after_mtime && atime_after_ctime && recent { + return false; + } + } + metadata.atime = now; + true +} + impl From for InodeMode { fn from(val: FileType) -> Self { match val { @@ -721,6 +744,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 @@ -1654,9 +1686,7 @@ impl dyn IndexNode { // - symlink 位于路径中间(rest_path 非空) // - 需要跟随最终 symlink(follow_final_symlink=true) // - 或者 pathname 以 '/' 结尾(trailing_slash=true) - let need_follow = has_more_components - || follow_final_symlink - || (trailing_slash && !has_more_components); + let need_follow = has_more_components || follow_final_symlink || trailing_slash; // 兼容旧语义:symlink_follows_remaining==0 表示完全不跟随 symlink。 // 在这种模式下,如果路径解析"需要跟随"(例如 symlink 位于中间,或末尾带 '/'), @@ -1697,15 +1727,27 @@ 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. + let mut content = Vec::new(); + content + .try_reserve_exact(MAX_PATHLEN) + .map_err(|_| SystemError::ENOMEM)?; + content.resize(MAX_PATHLEN, 0); // 读取符号链接 // TODO:We need to clarify which interfaces require private data and which do not let len = inode.read_at( 0, - 256, + MAX_PATHLEN, &mut content, Mutex::new(FilePrivateData::Unused).lock(), )?; + if len >= MAX_PATHLEN { + return Err(SystemError::ENAMETOOLONG); + } // 将读到的数据转换为utf8字符串(先转为str,再转为String) let link_path = String::from( diff --git a/kernel/src/filesystem/vfs/mount/mod.rs b/kernel/src/filesystem/vfs/mount/mod.rs index 9cee7c1f1e..c198d58d39 100644 --- a/kernel/src/filesystem/vfs/mount/mod.rs +++ b/kernel/src/filesystem/vfs/mount/mod.rs @@ -23,7 +23,7 @@ use crate::{ mm::{fault::PageFaultMessage, VirtRegion, VmFaultReason, VmFlags}, process::{ namespace::{ - mnt::{mount_max, MntNamespace}, + mnt::MntNamespace, propagation::{ abort_mount_propagation, commit_mount_propagation_locked, detach_mount_propagation, ensure_subtree_shared, inherit_bind_mount_propagation, @@ -3318,7 +3318,8 @@ impl MountFSInode { /// @return Arc pub(crate) fn overlaid_inode(&self) -> Arc { let mut current = self.self_ref.upgrade().unwrap(); - for _ in 0..mount_max() { + let mut fast = Some(current.clone()); + loop { let Some(sub_mountfs) = current.mount_fs.lookup_top(¤t) else { return current; }; @@ -3328,10 +3329,41 @@ impl MountFSInode { 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 mount-max"); - 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> { @@ -3362,7 +3394,8 @@ impl MountFSInode { // Resolve `..` iteratively so a userspace directory walk cannot consume // one kernel stack frame per mount layer. let mut current = self.self_ref.upgrade().unwrap(); - for _ in 0..mount_max() { + 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 @@ -3371,6 +3404,27 @@ impl MountFSInode { 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; } @@ -3380,8 +3434,6 @@ impl MountFSInode { None => Ok(current), }; } - - Err(SystemError::ELOOP) } fn do_absolute_path(&self) -> Result { @@ -3755,6 +3807,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 d831d855a8..2065ac264d 100644 --- a/kernel/src/filesystem/vfs/open.rs +++ b/kernel/src/filesystem/vfs/open.rs @@ -276,10 +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(); // 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)); + 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); @@ -366,6 +371,10 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result 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/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/process/idle.rs b/kernel/src/process/idle.rs index 3e3a539596..678d88c4e6 100644 --- a/kernel/src/process/idle.rs +++ b/kernel/src/process/idle.rs @@ -52,6 +52,7 @@ impl ProcessManager { }; let idle_pcb = ProcessControlBlock::new_idle(i, kstack); + assert!(idle_pcb.basic().user_vm().is_none()); unsafe { idle_pcb @@ -70,7 +71,7 @@ impl ProcessManager { pi_guard.set_cpus_allowed(CpuMask::from_cpu(ProcessorId::new(i))); idle_pcb.sched_info().set_on_cpu(Some(ProcessorId::new(i))); - rq.initialize_current(idle_pcb.clone()); + rq.set_current(Arc::downgrade(&idle_pcb)); rq.set_idle(Arc::downgrade(&idle_pcb)); IDLE_CPUS.set(ProcessorId::new(i)); diff --git a/kernel/src/sched/mod.rs b/kernel/src/sched/mod.rs index 28ca5b88e6..bdc9067d6f 100644 --- a/kernel/src/sched/mod.rs +++ b/kernel/src/sched/mod.rs @@ -13,10 +13,9 @@ pub mod prio; pub mod syscall; use core::{ - cell::UnsafeCell, intrinsics::{likely, unlikely}, panic::Location, - sync::atomic::{compiler_fence, fence, AtomicBool, AtomicPtr, AtomicUsize, Ordering}, + sync::atomic::{compiler_fence, fence, AtomicUsize, Ordering}, }; use alloc::{ @@ -466,103 +465,6 @@ pub trait SchedArch { fn initial_setup_sched_local() {} } -/// Per-CPU ownership and publication of the task currently executing. -/// -/// The owner keeps the PCB alive while it is running. Readers serialize with -/// publication through a raw lock before cloning the owner. This lock cannot -/// use the scheduler-aware spinlock because current-task publication is itself -/// part of a context switch. -#[derive(Debug)] -struct CurrentTask { - lock: AtomicBool, - owner: UnsafeCell>>, - local_ptr: AtomicPtr, -} - -// `owner` is accessed only under the raw publication lock. -unsafe impl Sync for CurrentTask {} - -impl CurrentTask { - fn new() -> Self { - Self { - lock: AtomicBool::new(false), - owner: UnsafeCell::new(None), - local_ptr: AtomicPtr::new(core::ptr::null_mut()), - } - } - - fn get(&self) -> Arc { - // This raw publication lock deliberately does not use SpinLock: its - // guard restores preemption through current_pcb(), which must not run - // after the slot has already changed to the next task. - // Disable local interrupts before taking it so an interrupt-driven - // context switch on this CPU cannot spin on a lock held by the - // interrupted reader. Remote publishers may briefly spin until the - // clone completes. - let irq_guard = unsafe { CurrentIrqArch::save_and_disable_irq() }; - self.lock_raw(); - let current = unsafe { &*self.owner.get() } - .as_ref() - .cloned() - .expect("runqueue current task is initialized"); - self.unlock_raw(); - drop(irq_guard); - current - } - - fn initialize(&mut self, pcb: Arc) { - let ptr = Arc::as_ptr(&pcb) as *mut ProcessControlBlock; - *self.owner.get_mut() = Some(pcb); - self.local_ptr.store(ptr, Ordering::Relaxed); - } - - fn set(&self, pcb: Arc) { - debug_assert!(!CurrentIrqArch::is_irq_enabled()); - self.lock_raw(); - let slot = unsafe { &mut *self.owner.get() }; - let old = slot.replace(pcb); - self.local_ptr.store( - Arc::as_ptr(slot.as_ref().unwrap()) as *mut ProcessControlBlock, - Ordering::Release, - ); - self.unlock_raw(); - // PCB destruction is not part of the publication critical section and - // may acquire unrelated locks. The pointer is already protected by the - // new slot owner at this point. - drop(old); - } - - /// Fast current-task lookup for the CPU that owns this runqueue. - /// - /// The caller disables interrupts before selecting the CPU and retains - /// that guard through the strong-count increment. Therefore this CPU - /// cannot publish a replacement (or drop the slot-owned reference) in the - /// interval between loading the pointer and acquiring its Arc reference. - fn get_local(&self) -> Arc { - debug_assert!(!CurrentIrqArch::is_irq_enabled()); - let ptr = self.local_ptr.load(Ordering::Acquire); - assert!(!ptr.is_null(), "runqueue current task is initialized"); - unsafe { - Arc::increment_strong_count(ptr); - Arc::from_raw(ptr) - } - } - - fn lock_raw(&self) { - while self - .lock - .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) - .is_err() - { - core::hint::spin_loop(); - } - } - - fn unlock_raw(&self) { - self.lock.store(false, Ordering::Release); - } -} - /// ## PerCpu的运行队列,其中维护了各个调度器对应的rq #[allow(dead_code)] #[derive(Debug)] @@ -608,7 +510,7 @@ pub struct CpuRunQueue { sched_info: SchedInfo, /// 当前在运行队列上执行的进程 - current: CurrentTask, + current: Weak, idle: Weak, } @@ -641,7 +543,7 @@ impl CpuRunQueue { clock_idle: 0, cfs_tasks: LinkedList::new(), sched_info: SchedInfo::default(), - current: CurrentTask::new(), + current: Weak::new(), idle: Weak::new(), } } @@ -946,27 +848,12 @@ impl CpuRunQueue { #[inline] pub fn current(&self) -> Arc { - self.current.get() + self.current.upgrade().unwrap() } #[inline] - pub(crate) fn current_local(&self) -> Arc { - self.current.get_local() - } - - #[inline] - pub(crate) fn initialize_current(&mut self, pcb: Arc) { - self.current.initialize(pcb); - } - - /// Publish the task selected under this runqueue's lock at the final - /// architecture switch boundary. The caller must run on this queue's CPU - /// with interrupts disabled and must not perform another fallible/locking - /// operation before switching stacks. - pub(crate) unsafe fn publish_current(&self, pcb: Arc) { - assert!(!CurrentIrqArch::is_irq_enabled()); - assert_eq!(self.cpu, smp_get_processor_id()); - self.current.set(pcb); + pub fn set_current(&mut self, pcb: Weak) { + self.current = pcb; } #[inline] @@ -1360,6 +1247,7 @@ fn __schedule_inner(sched_mod: SchedMode, current: Option +#include +#include #include #include +#include #include #include +#include #include #include #include @@ -11,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -106,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) { @@ -409,6 +430,236 @@ TEST(MountReconfigure, StrictAtimeReadUpdatesAccessTime) { 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"; + + 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); + + 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); + } + + 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)); + 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"; From 040fd936ef8e31de3d1982d9ca86a44c2832efc6 Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 08:55:54 +0000 Subject: [PATCH 03/13] fix(vfs): skip atime updates for anonymous inodes Use the optional filesystem lookup before checking mount state during access-time updates. Anonymous kernel objects such as sockets now report that they are not backed by a filesystem, matching Linux socket read semantics and avoiding a panic in readv and splice paths.\n\nDocument the expanded try_fs contract so callers can distinguish filesystem-backed inodes from anonymous objects without relying on a panicking fs implementation.\n\nThis fixes the gVisor TCP splice timeout and UDP writev/readv failure observed in the integration workflow. Signed-off-by: longjin --- kernel/src/filesystem/vfs/file.rs | 10 +++++++--- kernel/src/filesystem/vfs/mod.rs | 8 +++++--- kernel/src/net/socket/inode.rs | 4 ++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/kernel/src/filesystem/vfs/file.rs b/kernel/src/filesystem/vfs/file.rs index baf972637f..99daf00d7a 100644 --- a/kernel/src/filesystem/vfs/file.rs +++ b/kernel/src/filesystem/vfs/file.rs @@ -1417,9 +1417,13 @@ impl File { { return; } - if self - .inode - .fs() + 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()) { diff --git a/kernel/src/filesystem/vfs/mod.rs b/kernel/src/filesystem/vfs/mod.rs index da808590b3..f17e09c251 100644 --- a/kernel/src/filesystem/vfs/mod.rs +++ b/kernel/src/filesystem/vfs/mod.rs @@ -1029,10 +1029,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()) } 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 } From 37ae9451127cd4060c19e3ad351035f2994da5e0 Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 09:10:24 +0000 Subject: [PATCH 04/13] fix(vfs): address follow-up review findings Preserve Linux open error precedence by applying O_NOATIME ownership checks after path, type, and DAC validation while keeping them ahead of direct-I/O and socket-open failures. Add regression coverage for final symlinks and directory O_DIRECT combinations.\n\nReuse one fallibly allocated PATH_MAX buffer across a symlink walk to avoid per-hop allocation and clearing.\n\nEnsure the gVisor runner terminates the full test process group and reaps its leader when try_wait fails, sharing the same cleanup path used for timeouts. Signed-off-by: longjin --- kernel/src/filesystem/vfs/mod.rs | 19 ++++++--- kernel/src/filesystem/vfs/open.rs | 32 +++++++++------ .../suites/normal/mount_reconfigure.cc | 21 ++++++++++ .../syscall/gvisor/runner/src/lib_sync.rs | 40 ++++++++++++------- 4 files changed, 79 insertions(+), 33 deletions(-) diff --git a/kernel/src/filesystem/vfs/mod.rs b/kernel/src/filesystem/vfs/mod.rs index f17e09c251..62da0f2c76 100644 --- a/kernel/src/filesystem/vfs/mod.rs +++ b/kernel/src/filesystem/vfs/mod.rs @@ -1606,6 +1606,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() { @@ -1734,17 +1737,21 @@ impl dyn IndexNode { // 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. - let mut content = Vec::new(); - content - .try_reserve_exact(MAX_PATHLEN) - .map_err(|_| SystemError::ENOMEM)?; - content.resize(MAX_PATHLEN, 0); + 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, MAX_PATHLEN, - &mut content, + content, Mutex::new(FilePrivateData::Unused).lock(), )?; if len >= MAX_PATHLEN { diff --git a/kernel/src/filesystem/vfs/open.rs b/kernel/src/filesystem/vfs/open.rs index 2065ac264d..acb8272a87 100644 --- a/kernel/src/filesystem/vfs/open.rs +++ b/kernel/src/filesystem/vfs/open.rs @@ -371,10 +371,6 @@ fn do_sys_openat2(dirfd: i32, path: &str, how: OpenHow) -> Result Result Result Result= 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); @@ -569,6 +589,7 @@ TEST(MountReconfigure, NoatimeRequiresOwnerOrCapFowner) { ASSERT_EQ(child, waitpid(child, &status, 0)) << strerror(errno); ASSERT_TRUE(WIFEXITED(status)); EXPECT_EQ(0, WEXITSTATUS(status)); + unlink(link); cleanup_mount(target); } 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 4bcd99f23d..0a65eab695 100644 --- a/user/apps/tests/syscall/gvisor/runner/src/lib_sync.rs +++ b/user/apps/tests/syscall/gvisor/runner/src/lib_sync.rs @@ -10,7 +10,7 @@ use std::{ process::{CommandExt, ExitStatusExt}, }, path::{Path, PathBuf}, - process::{Command, ExitStatus}, + process::{Child, Command, ExitStatus}, sync::{ atomic::{AtomicUsize, Ordering}, Arc, @@ -717,25 +717,37 @@ enum TestProcessOutcome { 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 { - if let Some(status) = child.try_wait().context("等待 gVisor 测试进程失败")? { - return Ok(TestProcessOutcome::Exited(status)); + 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 { - 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 测试进程失败")?; + terminate_process_group(&mut child).context("清理超时 gVisor 测试进程失败")?; return Ok(TestProcessOutcome::TimedOut); } thread::sleep(Duration::from_millis(20)); From 933f7cf79a23218174f55ae6fce911212bd1cd89 Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 10:11:52 +0000 Subject: [PATCH 05/13] fix(vfs): address latest review and integration failures Initialize new inode ownership before publication across tmpfs, ramfs, and ext4, including mknod and rename whiteouts. Preserve setgid directory inheritance while stripping unauthorized executable setgid modes. Harden transactional page-cache rollback against concurrent dirty publication, update regular-file atime on EOF reads, and avoid filesystem lookups for anonymous socket permission checks. Disable required-test manifest enforcement completely when requested and add regression coverage for ownership, SGID, EOF atime, and runner behavior. Signed-off-by: longjin --- kernel/crates/another_ext4/src/ext4/alloc.rs | 15 ++- .../crates/another_ext4/src/ext4/low_level.rs | 51 +++++++- kernel/crates/another_ext4/src/ext4/mod.rs | 2 +- kernel/crates/another_ext4/src/lib.rs | 2 +- kernel/src/filesystem/ext4/inode.rs | 69 +++++++++- kernel/src/filesystem/page_cache.rs | 67 +++++++++- kernel/src/filesystem/ramfs/mod.rs | 16 ++- kernel/src/filesystem/tmpfs/mod.rs | 27 ++-- kernel/src/filesystem/vfs/file.rs | 4 +- kernel/src/filesystem/vfs/permission.rs | 48 ++++++- .../suites/normal/mount_reconfigure.cc | 123 ++++++++++++++++++ .../syscall/gvisor/runner/src/lib_sync.rs | 28 +++- 12 files changed, 411 insertions(+), 41 deletions(-) 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..079c3ce498 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 { @@ -557,6 +563,16 @@ 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 { self.ensure_mutable()?; let _metadata_guard = self.lock_direct_metadata_mutation()?; let _namespace_guard = self.namespace_lock.lock(); @@ -567,7 +583,7 @@ 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) @@ -602,6 +618,25 @@ impl Ext4 { mode: InodeMode, 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 { self.ensure_mutable()?; let _metadata_guard = self.lock_direct_metadata_mutation()?; @@ -619,7 +654,7 @@ 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)?; @@ -1404,6 +1439,16 @@ 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 { self.ensure_mutable()?; let _metadata_guard = self.lock_direct_metadata_mutation()?; let _namespace_guard = self.namespace_lock.lock(); @@ -1415,7 +1460,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, ".") { 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/src/filesystem/ext4/inode.rs b/kernel/src/filesystem/ext4/inode.rs index 353e83450c..ceece735fb 100644 --- a/kernel/src/filesystem/ext4/inode.rs +++ b/kernel/src/filesystem/ext4/inode.rs @@ -282,19 +282,42 @@ 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; let id = if file_type == vfs::FileType::Dir { - Self::retry_metadata_contention(|| ext4.mkdir(guard.inner_inode_num, name, file_mode))? + Self::retry_metadata_contention(|| { + ext4.mkdir_with_owner( + 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( + 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); @@ -1110,7 +1133,10 @@ 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(); @@ -1123,7 +1149,7 @@ impl IndexNode for LockedExt4Inode { } // 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!( @@ -1132,17 +1158,31 @@ impl IndexNode for LockedExt4Inode { ) { // Character/block device: use mknod to store device number in i_block Self::retry_metadata_contention(|| { - ext4.mknod( + ext4.mknod_with_owner( 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( + inode_num, + filename, + file_mode, + another_ext4::InodeOwner { + uid: init.uid as u32, + gid: init.gid as u32, + }, + ) + })? }; // Wrap as VFS inode and cache @@ -1172,6 +1212,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::() @@ -1278,6 +1328,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 @@ -1293,13 +1344,17 @@ impl IndexNode for LockedExt4Inode { } let allocation = ext4_fs.begin_allocation()?; let whiteout_num = Self::retry_metadata_contention(|| { - ext4.mknod( + ext4.mknod_with_owner( 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( diff --git a/kernel/src/filesystem/page_cache.rs b/kernel/src/filesystem/page_cache.rs index a0bbbc6613..200bb78c3c 100644 --- a/kernel/src/filesystem/page_cache.rs +++ b/kernel/src/filesystem/page_cache.rs @@ -418,6 +418,27 @@ pub(crate) fn run_accounting_debug_selftest() -> Result, ) -> Result { let cache = self.upgrade()?; - let removed = { - let mut guard = cache.lock(); + 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 { + 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); } - if entry.page.read().map_count() != 0 { + (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); }; diff --git a/kernel/src/filesystem/ramfs/mod.rs b/kernel/src/filesystem/ramfs/mod.rs index b388c8a252..8d040c20b8 100644 --- a/kernel/src/filesystem/ramfs/mod.rs +++ b/kernel/src/filesystem/ramfs/mod.rs @@ -556,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 { @@ -574,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(), @@ -898,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(), @@ -915,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 4ffdfbac6b..7bee1f29f7 100644 --- a/kernel/src/filesystem/tmpfs/mod.rs +++ b/kernel/src/filesystem/tmpfs/mod.rs @@ -1199,6 +1199,11 @@ impl IndexNode for LockedTmpfsInode { if parent.children.contains_key(&name) { return Err(SystemError::EEXIST); } + 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; @@ -1218,11 +1223,11 @@ impl IndexNode for LockedTmpfsInode { ctime: now, btime: now, file_type: FileType::SymLink, - mode: InodeMode::S_IRWXUGO, + mode: init.mode, flags: InodeFlags::empty(), nlinks: 1, - uid: 0, - gid: 0, + uid: init.uid, + gid: init.gid, raw_dev: DeviceNumber::default(), }, fs: parent.fs.clone(), @@ -1268,6 +1273,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 { @@ -1286,11 +1293,11 @@ 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(), @@ -1657,6 +1664,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 { @@ -1675,10 +1684,10 @@ 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(), }, diff --git a/kernel/src/filesystem/vfs/file.rs b/kernel/src/filesystem/vfs/file.rs index 99daf00d7a..626acda291 100644 --- a/kernel/src/filesystem/vfs/file.rs +++ b/kernel/src/filesystem/vfs/file.rs @@ -1398,7 +1398,9 @@ impl File { self.offset .fetch_add(len, core::sync::atomic::Ordering::SeqCst); } - if len > 0 { + // 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) diff --git a/kernel/src/filesystem/vfs/permission.rs b/kernel/src/filesystem/vfs/permission.rs index 68e1201927..1dac1f24ff 100644 --- a/kernel/src/filesystem/vfs/permission.rs +++ b/kernel/src/filesystem/vfs/permission.rs @@ -36,6 +36,44 @@ bitflags! { } } +pub struct ChildInodeInit { + pub uid: usize, + pub gid: usize, + pub mode: InodeMode, +} + +/// Compute owner and directory-SGID inheritance before publishing a child. +pub fn child_inode_init( + parent: &Metadata, + file_type: FileType, + mut mode: InodeMode, +) -> ChildInodeInit { + 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 @@ -58,17 +96,17 @@ pub fn check_inode_permission( FileType::File | FileType::Dir | FileType::SymLink ) && inode - .fs() - .downcast_arc::() + .try_fs() + .and_then(|fs| fs.downcast_arc::()) .is_some_and(|mount| mount.is_readonly()) { return Err(SystemError::EROFS); } 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 diff --git a/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc b/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc index 86d120506c..172a1ac09f 100644 --- a/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc +++ b/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc @@ -430,6 +430,36 @@ TEST(MountReconfigure, StrictAtimeReadUpdatesAccessTime) { 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"; @@ -593,6 +623,99 @@ TEST(MountReconfigure, NoatimeRequiresOwnerOrCapFowner) { 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"; 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 0a65eab695..15633dce43 100644 --- a/user/apps/tests/syscall/gvisor/runner/src/lib_sync.rs +++ b/user/apps/tests/syscall/gvisor/runner/src/lib_sync.rs @@ -139,7 +139,11 @@ struct RequiredTest { impl TestRunner { pub fn new(config: Config) -> Result { - let required_tests = read_required_tests(&config.required_tests_file)?; + 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()), @@ -822,7 +826,10 @@ fn remove_stale_xml(path: &Path) -> Result<()> { #[cfg(test)] mod tests { - use super::{read_required_tests, remove_stale_xml, wait_with_timeout, TestProcessOutcome}; + use super::{ + read_required_tests, remove_stale_xml, wait_with_timeout, Config, TestProcessOutcome, + TestRunner, + }; use std::{ fs, os::unix::process::CommandExt, @@ -870,6 +877,23 @@ mod tests { 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!( From cab2716adb6c8403d061386fd2876d59e94ae283 Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 10:34:49 +0000 Subject: [PATCH 06/13] fix(vfs): preserve bootstrap inode creation Use the initial root credential for kernel-owned filesystem objects created before ProcessManager installs a current task. Keep setgid directory inheritance intact while avoiding an early-boot current_pcb spin during procfs initialization. Signed-off-by: longjin --- kernel/src/filesystem/vfs/permission.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/kernel/src/filesystem/vfs/permission.rs b/kernel/src/filesystem/vfs/permission.rs index 1dac1f24ff..19edff434b 100644 --- a/kernel/src/filesystem/vfs/permission.rs +++ b/kernel/src/filesystem/vfs/permission.rs @@ -48,6 +48,22 @@ pub fn child_inode_init( file_type: FileType, mut mode: InodeMode, ) -> 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 { From b58087515616a8ad28bbcb780cc8d1a269c50e45 Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 12:11:02 +0000 Subject: [PATCH 07/13] fix(vfs): preserve fallocate metadata and defer atime writes Apply Linux-compatible write-side metadata effects after successful tmpfs fallocate allocation, including timestamp updates and setid removal, while preserving rollback atomicity. Cache ext4 atime updates in memory, track them through the existing dirty inode queue, and commit them with size and mtime during periodic or explicit metadata writeback instead of synchronously writing from the read path. Add dunitest coverage for fallocate metadata effects, failure stability, setid clearing, and ext4 atime visibility and persistence. Signed-off-by: longjin --- .../crates/another_ext4/src/ext4/low_level.rs | 6 +- kernel/src/filesystem/ext4/filesystem.rs | 84 +++++++++++++------ kernel/src/filesystem/ext4/inode.rs | 69 ++++++++------- kernel/src/filesystem/tmpfs/mod.rs | 19 ++++- kernel/src/filesystem/vfs/vcore.rs | 16 +++- .../suites/normal/ext4_inode_identity.cc | 32 +++++++ .../suites/normal/fallocate_semantics.cc | 66 ++++++++++++++- 7 files changed, 227 insertions(+), 65 deletions(-) diff --git a/kernel/crates/another_ext4/src/ext4/low_level.rs b/kernel/crates/another_ext4/src/ext4/low_level.rs index 079c3ce498..f39f13ee76 100644 --- a/kernel/crates/another_ext4/src/ext4/low_level.rs +++ b/kernel/crates/another_ext4/src/ext4/low_level.rs @@ -492,6 +492,7 @@ impl Ext4 { &self, id: InodeId, size: Option, + atime: Option, mtime: Option, ) -> Result<()> { self.ensure_mutable()?; @@ -504,6 +505,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); } @@ -516,7 +520,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`. diff --git a/kernel/src/filesystem/ext4/filesystem.rs b/kernel/src/filesystem/ext4/filesystem.rs index fbd8f0d24b..fd784fafea 100644 --- a/kernel/src/filesystem/ext4/filesystem.rs +++ b/kernel/src/filesystem/ext4/filesystem.rs @@ -582,33 +582,56 @@ 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, + ) -> Result<(), SystemError> { + Self::mark_inode_dirty_with(inode, InodeDirtyState::ATIME_DIRTY, |guard| { + guard.cached_atime = Some(atime); + }) + } + + fn mark_inode_dirty_with( + inode: &Arc, + dirty: InodeDirtyState, + update_cached_metadata: F, + ) -> Result<(), SystemError> + where + 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); 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 +652,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 +674,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 +710,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 +737,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 +760,12 @@ 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_atime, cached_mtime) = { 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,6 +778,7 @@ impl Ext4FileSystem { guard.inner_inode_num, snapshot_dirty, guard.cached_file_size, + guard.cached_atime, guard.cached_mtime, ) }; @@ -784,8 +805,13 @@ impl Ext4FileSystem { } else { None }; + let atime = if snapshot_dirty.contains(InodeDirtyState::ATIME_DIRTY) { + cached_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 { @@ -804,11 +830,16 @@ impl Ext4FileSystem { { guard.dirty_state.remove(InodeDirtyState::MTIME_DIRTY); } + if snapshot_dirty.contains(InodeDirtyState::ATIME_DIRTY) + && guard.cached_atime == cached_atime + { + 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; @@ -896,6 +927,7 @@ impl Ext4FileSystem { special_node: None, cached_file_size: None, cached_mtime: None, + cached_atime: None, 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 ceece735fb..0fb03fd962 100644 --- a/kernel/src/filesystem/ext4/inode.rs +++ b/kernel/src/filesystem/ext4/inode.rs @@ -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(); } } @@ -229,6 +232,8 @@ pub struct Ext4Inode { pub(super) cached_file_size: Option, /// 缓存的 mtime。普通 buffered write 先更新内存态,fsync/O_SYNC 再刷到磁盘。 pub(super) cached_mtime: Option, + /// 缓存的 atime。读路径只更新内存态,统一由 inode writeback 刷盘。 + pub(super) cached_atime: Option, /// 脏状态标志位,对应 Linux `inode->i_state & I_DIRTY_*`。 pub(super) dirty_state: InodeDirtyState, } @@ -681,18 +686,20 @@ 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, cached_atime, cached_mtime) = { let guard = self.inner.lock(); ( guard.concret_fs(), guard.inner_inode_num, guard.vfs_inode_id, guard.cached_file_size, + guard.cached_atime, guard.cached_mtime, ) }; let attr = fs.fs.getattr(inode_num)?; let size = cached_size.unwrap_or(attr.size); + let atime = cached_atime.unwrap_or(attr.atime); let mtime = cached_mtime.unwrap_or(attr.mtime); // dev_id: filesystem device number (st_dev) @@ -714,7 +721,7 @@ 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(atime.into(), 0), btime: PosixTimeSpec::new(attr.atime.into(), 0), mtime: PosixTimeSpec::new(mtime.into(), 0), ctime: PosixTimeSpec::new(attr.ctime.into(), 0), @@ -822,10 +829,13 @@ impl IndexNode for LockedExt4Inode { { let mut guard = self.inner.lock(); guard.cached_file_size = Some(metadata.size as u64); + guard.cached_atime = Some(to_ext4_time(&metadata.atime)); guard.cached_mtime = Some(to_ext4_time(&metadata.mtime)); - guard - .dirty_state - .remove(InodeDirtyState::SIZE_DIRTY | InodeDirtyState::MTIME_DIRTY); + guard.dirty_state.remove( + InodeDirtyState::SIZE_DIRTY + | InodeDirtyState::ATIME_DIRTY + | InodeDirtyState::MTIME_DIRTY, + ); } self.release_clean_metadata_queue_owner(&fs); @@ -833,33 +843,19 @@ impl IndexNode for LockedExt4Inode { } fn update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + let _operation = self.begin_operation()?; let _io_guard = self.io_lock.lock(); let mut metadata = self.metadata()?; if !vfs::update_atime_locked(&mut metadata, now, relatime) { return Ok(()); } - let _operation = self.begin_operation()?; - let (fs, inode_num) = { + let self_arc = { let guard = self.inner.lock(); - (guard.concret_fs(), guard.inner_inode_num) + guard.self_ref.upgrade().ok_or(SystemError::ENOENT)? }; - let atime = now.tv_sec.max(0).min(u32::MAX as i64) as u32; - Self::retry_metadata_contention(|| { - fs.fs.setattr( - inode_num, - another_ext4::SetAttr { - mode: None, - uid: None, - gid: None, - size: None, - atime: Some(atime), - mtime: None, - ctime: None, - crtime: None, - }, - ) - }) + let atime = metadata.atime.tv_sec.max(0).min(u32::MAX as i64) as u32; + Ext4FileSystem::mark_inode_atime_dirty(&self_arc, atime) } fn resize(&self, len: usize) -> Result<(), SystemError> { @@ -1806,6 +1802,7 @@ impl Ext4Inode { special_node: None, cached_file_size: None, cached_mtime: None, + cached_atime: None, dirty_state: InodeDirtyState::empty(), } } @@ -1935,21 +1932,23 @@ 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_atime, cached_mtime) = { let guard = self.inner.lock(); ( guard.concret_fs(), guard.inner_inode_num, guard.dirty_state, guard.cached_file_size, + guard.cached_atime, guard.cached_mtime, ) }; 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(()); } @@ -1962,17 +1961,27 @@ impl LockedExt4Inode { } else { None }; + let atime = if !datasync && atime_dirty { + cached_atime + } else { + None + }; let mtime = if !datasync && mtime_dirty { cached_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 && atime_dirty && guard.cached_atime == cached_atime { + guard.dirty_state.remove(InodeDirtyState::ATIME_DIRTY); + } if !datasync && mtime_dirty && guard.cached_mtime == cached_mtime { guard.dirty_state.remove(InodeDirtyState::MTIME_DIRTY); } diff --git a/kernel/src/filesystem/tmpfs/mod.rs b/kernel/src/filesystem/tmpfs/mod.rs index 7bee1f29f7..edc4cc2214 100644 --- a/kernel/src/filesystem/tmpfs/mod.rs +++ b/kernel/src/filesystem/tmpfs/mod.rs @@ -20,6 +20,7 @@ use crate::{ libs::casting::DowncastArc, libs::mutex::{Mutex, MutexGuard}, mm::MemoryManagementArch, + process::ProcessManager, time::PosixTimeSpec, }; @@ -1173,10 +1174,22 @@ impl IndexNode for LockedTmpfsInode { } } + // 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(); - if end > inode.metadata.size as usize { - inode.metadata.size = end as i64; - } + 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(()) } 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/user/apps/tests/dunitest/suites/normal/ext4_inode_identity.cc b/user/apps/tests/dunitest/suites/normal/ext4_inode_identity.cc index f4566fcac4..460a37170f 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,38 @@ 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, 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) { From 03dcd4ada2ccb3d7454e2247f638e77fbdabc9b0 Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 14:54:44 +0000 Subject: [PATCH 08/13] fix(vfs): address symlink and ext4 timestamp review Enforce Linux symlink creation semantics in the common syscall path, including empty targets, trailing slashes, parent search/write permission ordering, immutable directories, and an atomic tmpfs permission recheck under the parent inode lock. Keep ext4 inode timestamps authoritative in memory and evaluate relatime without io_lock or disk getattr. Add masked setattr support and generation-based atime/mtime writeback commits so concurrent reads, writes, mmap faults, setters, resize, and writeback cannot lose dirty timestamp updates. Add dunitest coverage for symlink error and permission semantics plus ext4 relatime behavior. Validated with make kernel and the full DragonOS dunitest suite (797 tests, 0 failures). Signed-off-by: longjin --- kernel/src/filesystem/ext4/filesystem.rs | 82 ++++-- kernel/src/filesystem/ext4/inode.rs | 242 ++++++++++++++---- kernel/src/filesystem/tmpfs/mod.rs | 15 ++ kernel/src/filesystem/vfs/mod.rs | 34 ++- kernel/src/filesystem/vfs/permission.rs | 5 + .../filesystem/vfs/syscall/symlink_utils.rs | 22 +- .../suites/normal/ext4_inode_identity.cc | 28 ++ .../suites/normal/open_dangling_symlink.cc | 55 ++++ 8 files changed, 399 insertions(+), 84 deletions(-) diff --git a/kernel/src/filesystem/ext4/filesystem.rs b/kernel/src/filesystem/ext4/filesystem.rs index fd784fafea..6bd461ccae 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, @@ -407,7 +409,7 @@ impl Ext4FileSystem { dname: DName, parent: Option>, ) -> Result, SystemError> { - self.get_or_create_inode_inner(inode_num, dname, parent, false, None) + self.get_or_create_inode_inner(inode_num, dname, parent, false) } pub(super) fn publish_allocated_inode( @@ -415,10 +417,10 @@ impl Ext4FileSystem { inode_num: u32, dname: DName, parent: Option>, - file_type: another_ext4::FileType, + _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(inode_num, dname, parent, true) } fn get_or_create_inode_inner( @@ -427,7 +429,6 @@ impl Ext4FileSystem { dname: DName, parent: Option>, reuse_guard_held: bool, - known_file_type: Option, ) -> Result, SystemError> { let mut candidate = None; let mut admission_guard = None; @@ -468,7 +469,6 @@ impl Ext4FileSystem { Arc::downgrade(self), dname.clone(), parent.clone(), - known_file_type, )?); continue; } @@ -588,10 +588,27 @@ impl Ext4FileSystem { pub(super) fn mark_inode_atime_dirty( inode: &Arc, atime: u32, + relatime: bool, ) -> Result<(), SystemError> { - Self::mark_inode_dirty_with(inode, InodeDirtyState::ATIME_DIRTY, |guard| { - guard.cached_atime = Some(atime); - }) + 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( @@ -601,6 +618,19 @@ impl Ext4FileSystem { ) -> 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. @@ -609,6 +639,11 @@ impl Ext4FileSystem { inode.retain(InodeRetentionKind::AsyncWork)?; let queue_fs = { let mut guard = inode.inner.lock(); + if !should_update(&guard) { + drop(guard); + inode.release(InodeRetentionKind::AsyncWork); + return Ok(()); + } let should_queue = !guard .dirty_state .intersects(InodeDirtyState::QUEUED | InodeDirtyState::WRITEBACK); @@ -760,7 +795,15 @@ impl Ext4FileSystem { let result = { let _operation = operation; let _io_guard = inode.io_lock.lock(); - let (fs, inode_num, snapshot_dirty, cached_size, cached_atime, 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 @@ -778,8 +821,9 @@ impl Ext4FileSystem { guard.inner_inode_num, snapshot_dirty, guard.cached_file_size, - guard.cached_atime, - guard.cached_mtime, + guard.cached_times, + guard.cached_atime_version, + guard.cached_mtime_version, ) }; @@ -801,12 +845,12 @@ 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) { - cached_atime + Some(cached_times.atime) } else { None }; @@ -826,12 +870,12 @@ 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 == cached_atime + && guard.cached_atime_version == cached_atime_version { guard.dirty_state.remove(InodeDirtyState::ATIME_DIRTY); } @@ -913,6 +957,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 { @@ -926,8 +971,9 @@ impl Ext4FileSystem { self_ref: self_ref.clone(), special_node: None, cached_file_size: None, - cached_mtime: None, - cached_atime: 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 0fb03fd962..385d76f32c 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, @@ -207,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, @@ -230,10 +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, - /// 缓存的 atime。读路径只更新内存态,统一由 inode writeback 刷盘。 - pub(super) cached_atime: 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, } @@ -494,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( @@ -686,21 +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_atime, 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_atime, - 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 atime = cached_atime.unwrap_or(attr.atime); - let mtime = cached_mtime.unwrap_or(attr.mtime); // dev_id: filesystem device number (st_dev) let dev_id = fs.raw_dev.data() as usize; @@ -721,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(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(), @@ -804,9 +828,14 @@ impl IndexNode for LockedExt4Inode { 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(|| { @@ -829,33 +858,123 @@ impl IndexNode for LockedExt4Inode { { let mut guard = self.inner.lock(); guard.cached_file_size = Some(metadata.size as u64); - guard.cached_atime = Some(to_ext4_time(&metadata.atime)); - guard.cached_mtime = Some(to_ext4_time(&metadata.mtime)); - guard.dirty_state.remove( - InodeDirtyState::SIZE_DIRTY - | InodeDirtyState::ATIME_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 update_atime(&self, now: PosixTimeSpec, relatime: bool) -> Result<(), SystemError> { + 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 mut metadata = self.metadata()?; - if !vfs::update_atime_locked(&mut metadata, now, relatime) { - return Ok(()); + 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)? }; - let atime = metadata.atime.tv_sec.max(0).min(u32::MAX as i64) as u32; - Ext4FileSystem::mark_inode_atime_dirty(&self_arc, atime) + Ext4FileSystem::mark_inode_atime_dirty(&self_arc, atime, relatime) } fn resize(&self, len: usize) -> Result<(), SystemError> { @@ -891,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(()) @@ -1714,11 +1831,18 @@ 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)?; 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(()), @@ -1744,16 +1868,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); @@ -1784,11 +1902,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, @@ -1801,8 +1920,9 @@ impl Ext4Inode { self_ref: Weak::new(), // 将在LockedExt4Inode::new()中设置 special_node: None, cached_file_size: None, - cached_mtime: None, - cached_atime: None, + cached_times: times, + cached_atime_version: 0, + cached_mtime_version: 0, dirty_state: InodeDirtyState::empty(), } } @@ -1932,15 +2052,24 @@ 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_atime, 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_atime, - guard.cached_mtime, + guard.cached_times, + guard.cached_atime_version, + guard.cached_mtime_version, ) }; @@ -1962,12 +2091,12 @@ impl LockedExt4Inode { None }; let atime = if !datasync && atime_dirty { - cached_atime + Some(cached_times.atime) } else { None }; let mtime = if !datasync && mtime_dirty { - cached_mtime + Some(cached_times.mtime) } else { None }; @@ -1979,10 +2108,10 @@ impl LockedExt4Inode { if size_dirty && guard.cached_file_size == cached_size { guard.dirty_state.remove(InodeDirtyState::SIZE_DIRTY); } - if !datasync && atime_dirty && guard.cached_atime == cached_atime { + 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 == cached_mtime { + if !datasync && mtime_dirty && guard.cached_mtime_version == cached_mtime_version { guard.dirty_state.remove(InodeDirtyState::MTIME_DIRTY); } drop(guard); @@ -2039,7 +2168,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/tmpfs/mod.rs b/kernel/src/filesystem/tmpfs/mod.rs index edc4cc2214..9d93d91d28 100644 --- a/kernel/src/filesystem/tmpfs/mod.rs +++ b/kernel/src/filesystem/tmpfs/mod.rs @@ -1212,6 +1212,21 @@ impl IndexNode for LockedTmpfsInode { 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, diff --git a/kernel/src/filesystem/vfs/mod.rs b/kernel/src/filesystem/vfs/mod.rs index 62da0f2c76..da1ec9214e 100644 --- a/kernel/src/filesystem/vfs/mod.rs +++ b/kernel/src/filesystem/vfs/mod.rs @@ -173,18 +173,38 @@ pub fn update_atime_locked(metadata: &mut Metadata, now: PosixTimeSpec, relatime 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(metadata.atime.tv_sec) < 24 * 60 * 60; - let atime_after_mtime = (metadata.atime.tv_sec, metadata.atime.tv_nsec) - > (metadata.mtime.tv_sec, metadata.mtime.tv_nsec); - let atime_after_ctime = (metadata.atime.tv_sec, metadata.atime.tv_nsec) - > (metadata.ctime.tv_sec, metadata.ctime.tv_nsec); + 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; } } - metadata.atime = now; - true + atime != now } impl From for InodeMode { diff --git a/kernel/src/filesystem/vfs/permission.rs b/kernel/src/filesystem/vfs/permission.rs index 19edff434b..bd92177b0f 100644 --- a/kernel/src/filesystem/vfs/permission.rs +++ b/kernel/src/filesystem/vfs/permission.rs @@ -118,6 +118,11 @@ pub fn check_inode_permission( { 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.try_fs().map(|fs| fs.permission_policy()) { 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/user/apps/tests/dunitest/suites/normal/ext4_inode_identity.cc b/user/apps/tests/dunitest/suites/normal/ext4_inode_identity.cc index 460a37170f..e3f0ec5962 100644 --- a/user/apps/tests/dunitest/suites/normal/ext4_inode_identity.cc +++ b/user/apps/tests/dunitest/suites/normal/ext4_inode_identity.cc @@ -387,6 +387,34 @@ TEST(Ext4InodeIdentity, ReadAtimeIsImmediatelyVisibleAndPersistsAfterFsync) { 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/open_dangling_symlink.cc b/user/apps/tests/dunitest/suites/normal/open_dangling_symlink.cc index cc48872762..3afc1b1169 100644 --- a/user/apps/tests/dunitest/suites/normal/open_dangling_symlink.cc +++ b/user/apps/tests/dunitest/suites/normal/open_dangling_symlink.cc @@ -6,6 +6,7 @@ #include #include #include +#include #include namespace { @@ -51,6 +52,60 @@ TEST_F(OpenDanglingSymlinkTest, CreatesRelativeTargetAfterFollowingFinalSymlink) 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); From 53d58060ed94c4e7650abf560054b81e88e1bbf3 Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 15:09:32 +0000 Subject: [PATCH 09/13] ci(test): align job and guest timeout budgets Raise the syscall workflow timeout to cover its 50-minute guest monitor after the build and disk preparation phases, while preserving time for cleanup and diagnostic uploads. Apply the same outer-timeout invariant to dunitest so its 30-minute monitor can terminate QEMU and publish the serial artifact before the Actions job deadline. Signed-off-by: longjin --- .github/workflows/dunitest.yml | 3 ++- .github/workflows/test-x86.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dunitest.yml b/.github/workflows/dunitest.yml index 926d67e280..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 diff --git a/.github/workflows/test-x86.yml b/.github/workflows/test-x86.yml index 8bf12c519e..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 From 89070a6c7b989a7ad49c264ea93e6c380c6cd335 Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 15:34:16 +0000 Subject: [PATCH 10/13] fix(ext4): publish created inode without reread Return authoritative attributes from the in-memory inode used by create, mkdir, and mknod transactions, while preserving the existing inode-number APIs. Initialize canonical VFS inodes from those attributes for regular creation, special nodes, and rename whiteouts. This removes the post-link getattr failure window that could report EIO after the directory entry was already committed. Keep lookup-time getattr behavior unchanged and add coverage for complete in-memory FileAttr conversion. Validated with make kernel, all 130 another_ext4 tests, and the full DragonOS dunitest suite (797 tests, 0 failures). Signed-off-by: longjin --- .../crates/another_ext4/src/ext4/low_level.rs | 94 +++++++++++++++++-- kernel/src/filesystem/ext4/filesystem.rs | 30 ++++-- kernel/src/filesystem/ext4/inode.rs | 61 ++++++------ 3 files changed, 136 insertions(+), 49 deletions(-) diff --git a/kernel/crates/another_ext4/src/ext4/low_level.rs b/kernel/crates/another_ext4/src/ext4/low_level.rs index f39f13ee76..d0611ed810 100644 --- a/kernel/crates/another_ext4/src/ext4/low_level.rs +++ b/kernel/crates/another_ext4/src/ext4/low_level.rs @@ -276,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() @@ -283,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(), @@ -297,7 +301,7 @@ impl Ext4 { uid: inode.inode.uid(), gid: inode.inode.gid(), rdev, - }) + } } /// Set file attributes. @@ -577,6 +581,20 @@ impl Ext4 { 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(); @@ -589,8 +607,7 @@ impl Ext4 { // Create child inode and link it to parent directory 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). @@ -642,6 +659,22 @@ impl Ext4 { 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(); @@ -664,7 +697,7 @@ impl Ext4 { 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()` @@ -1453,6 +1486,20 @@ impl Ext4 { 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(); @@ -1476,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. @@ -1754,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/src/filesystem/ext4/filesystem.rs b/kernel/src/filesystem/ext4/filesystem.rs index 6bd461ccae..fc05997286 100644 --- a/kernel/src/filesystem/ext4/filesystem.rs +++ b/kernel/src/filesystem/ext4/filesystem.rs @@ -409,18 +409,17 @@ impl Ext4FileSystem { dname: DName, parent: Option>, ) -> Result, SystemError> { - self.get_or_create_inode_inner(inode_num, dname, parent, false) + self.get_or_create_inode_inner(inode_num, dname, parent, false, None) } 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) + self.get_or_create_inode_inner(attr.ino, dname, parent, true, Some(attr)) } fn get_or_create_inode_inner( @@ -429,6 +428,7 @@ impl Ext4FileSystem { dname: DName, parent: Option>, reuse_guard_held: bool, + allocated_attr: Option, ) -> Result, SystemError> { let mut candidate = None; let mut admission_guard = None; @@ -464,12 +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(), - )?); + 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"); diff --git a/kernel/src/filesystem/ext4/inode.rs b/kernel/src/filesystem/ext4/inode.rs index 385d76f32c..a0f3a62ca9 100644 --- a/kernel/src/filesystem/ext4/inode.rs +++ b/kernel/src/filesystem/ext4/inode.rs @@ -321,10 +321,13 @@ impl IndexNode for LockedExt4Inode { 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 { + let attr = if file_type == vfs::FileType::Dir { Self::retry_metadata_contention(|| { - ext4.mkdir_with_owner( + ext4.mkdir_with_owner_and_attr( guard.inner_inode_num, name, file_mode, @@ -336,7 +339,7 @@ impl IndexNode for LockedExt4Inode { })? } else { Self::retry_metadata_contention(|| { - ext4.create_with_owner( + ext4.create_with_owner_and_attr( guard.inner_inode_num, name, file_mode, @@ -349,13 +352,10 @@ impl IndexNode for LockedExt4Inode { }; 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 缓存 @@ -1256,6 +1256,9 @@ impl IndexNode for LockedExt4Inode { 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); @@ -1265,13 +1268,13 @@ impl IndexNode for LockedExt4Inode { 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_with_owner( + ext4.mknod_with_owner_and_attr( inode_num, filename, file_mode, @@ -1286,7 +1289,7 @@ impl IndexNode for LockedExt4Inode { } else { // FIFO, Socket, etc.: use regular create (no device number needed) Self::retry_metadata_contention(|| { - ext4.create_with_owner( + ext4.create_with_owner_and_attr( inode_num, filename, file_mode, @@ -1300,12 +1303,10 @@ impl IndexNode for LockedExt4Inode { // 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()); @@ -1456,8 +1457,8 @@ impl IndexNode for LockedExt4Inode { continue; } let allocation = ext4_fs.begin_allocation()?; - let whiteout_num = Self::retry_metadata_contention(|| { - ext4.mknod_with_owner( + let whiteout_attr = Self::retry_metadata_contention(|| { + ext4.mknod_with_owner_and_attr( src_inode_num, &candidate, another_ext4::InodeMode::CHARDEV @@ -1471,10 +1472,9 @@ impl IndexNode for LockedExt4Inode { ) })?; 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), @@ -1691,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() @@ -1834,6 +1820,17 @@ impl LockedExt4Inode { ) -> 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( @@ -1841,7 +1838,7 @@ impl LockedExt4Inode { fs_ptr.clone(), dname, parent, - Ext4InodeTimes::from(&attr), + Ext4InodeTimes::from(attr), )), io_lock: Mutex::new(()), size_lock: RwSem::new(()), From c8fe6f0f94f8e16732aff010416057ddfacadfa8 Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 16:21:15 +0000 Subject: [PATCH 11/13] fix(namespace): stabilize propagation conformance checks Replace the scheduler-dependent recursive propagation observer with explicit shared/private phase acknowledgements and a midpoint stress snapshot handshake. Preserve the two unsynchronized 512-transition halves, namespace copying, and concurrent topology mutation while reporting the exact failing child status. Allocate a fresh conceptual hidden-parent mount ID when copying an attached namespace so mountinfo never exposes the source namespace's parent identity. Keep ordinary overlay traversal single-pass for the common shallow path and defer cycle detection to pathological depths without imposing a semantic limit. Require the recursive-bind topology and mount-limit rollback suites to remain skip-free alongside the existing namespace conformance binaries. Validated with make kernel, 12 dunit runner tests, the full DragonOS dunit suite, and 100-run focused stress loops on Linux and in the DragonOS guest. Signed-off-by: longjin --- kernel/src/filesystem/vfs/mount/mod.rs | 24 ++++++ kernel/src/process/namespace/mnt.rs | 12 ++- user/apps/tests/dunitest/no_skip.txt | 2 + .../suites/normal/mount_propagation.cc | 73 +++++++++++++++---- 4 files changed, 93 insertions(+), 18 deletions(-) diff --git a/kernel/src/filesystem/vfs/mount/mod.rs b/kernel/src/filesystem/vfs/mount/mod.rs index c198d58d39..7cc21bdb92 100644 --- a/kernel/src/filesystem/vfs/mount/mod.rs +++ b/kernel/src/filesystem/vfs/mount/mod.rs @@ -456,6 +456,12 @@ impl MountId { .expect("mount ID space exhausted"); MountId(id) } + + /// 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() + } } /// @brief Mount filesystem @@ -3318,6 +3324,24 @@ 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 { diff --git a/kernel/src/process/namespace/mnt.rs b/kernel/src/process/namespace/mnt.rs index e94eae4238..ee073d0ee9 100644 --- a/kernel/src/process/namespace/mnt.rs +++ b/kernel/src/process/namespace/mnt.rs @@ -716,7 +716,12 @@ impl MntNamespace { inner: RwSem::new(InnerMntNamespace { _dead: false, root_mountfs: new_root_mntfs, - root_parent_mount_id: inner.root_parent_mount_id, + // 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. @@ -1152,7 +1157,10 @@ mod tests { let attached_copy = attached .copy_mnt_ns(&CloneFlags::CLONE_NEWNS, INIT_USER_NAMESPACE.clone()) .unwrap(); - assert!(attached_copy.inner.read().root_parent_mount_id.is_some()); + 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/user/apps/tests/dunitest/no_skip.txt b/user/apps/tests/dunitest/no_skip.txt index 349f79ed3d..b9b2ebedac 100644 --- a/user/apps/tests/dunitest/no_skip.txt +++ b/user/apps/tests/dunitest/no_skip.txt @@ -1,4 +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/suites/normal/mount_propagation.cc b/user/apps/tests/dunitest/suites/normal/mount_propagation.cc index 348a4feaf4..424a681545 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) { @@ -1631,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) { @@ -1643,6 +1666,12 @@ TEST_F(MountPropagationTest, RecursiveChangesAreAtomicAgainstSnapshotsAndNamespa if ((i & 15) == 15) { sched_yield(); } + // Do not let the complete stress phase outrun the observer: one + // uniform snapshot must land between the two unsynchronised halves. + if (i == 511 && + (!read_exact(ready_pipe[0], &token, 1) || token != 'X')) { + _exit(3); + } } char done[2] = {}; if (!read_exact(worker_done_pipe[0], done, sizeof(done)) || @@ -1674,17 +1703,29 @@ 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); + } + } + 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); + } + bool reported_stress_sample = false; for (;;) { const ssize_t status = read(activity_pipe[0], &token, 1); if (status == 1 && token == 'D') { @@ -1697,12 +1738,12 @@ 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); + if (!reported_stress_sample) { + if (!write_exact(ready_pipe[1], "X", 1)) { + _exit(10); + } + reported_stress_sample = true; + } } _exit(0); } From 36300bd3bac92842d4f9a5b8bca79067556ea5aa Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 16:51:06 +0000 Subject: [PATCH 12/13] fix(namespace): address propagation review findings Signed-off-by: longjin --- .../process/namespace/propagation/event.rs | 41 ++++++++++++++++--- .../suites/normal/mount_propagation.cc | 29 +++++++------ 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/kernel/src/process/namespace/propagation/event.rs b/kernel/src/process/namespace/propagation/event.rs index 69b46f8bdb..9305325b7f 100644 --- a/kernel/src/process/namespace/propagation/event.rs +++ b/kernel/src/process/namespace/propagation/event.rs @@ -514,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( @@ -522,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())?; @@ -536,14 +537,26 @@ 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 new_child_tree = try_collect_subtree(new_child)?; - let new_child_mount_count = new_child_tree.len(); + 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(new_child_tree)?); + count_reservations.push( + namespace.reserve_mounts( + new_child_tree + .take() + .expect("new child tree is reserved at most once"), + )?, + ); } } let targets = if source_prop.is_shared() { @@ -691,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)); } @@ -858,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(); @@ -877,6 +905,7 @@ pub(crate) fn prepare_moved_tree_propagation_locked( moved_root_mountpoint, moved_root, false, + Some(subtree), ) { Ok(propagation) => propagation, Err(error) => { diff --git a/user/apps/tests/dunitest/suites/normal/mount_propagation.cc b/user/apps/tests/dunitest/suites/normal/mount_propagation.cc index 424a681545..7773b0fef3 100644 --- a/user/apps/tests/dunitest/suites/normal/mount_propagation.cc +++ b/user/apps/tests/dunitest/suites/normal/mount_propagation.cc @@ -1666,11 +1666,14 @@ TEST_F(MountPropagationTest, RecursiveChangesAreAtomicAgainstSnapshotsAndNamespa if ((i & 15) == 15) { sched_yield(); } - // Do not let the complete stress phase outrun the observer: one - // uniform snapshot must land between the two unsynchronised halves. - if (i == 511 && - (!read_exact(ready_pipe[0], &token, 1) || token != 'X')) { - _exit(3); + // 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] = {}; @@ -1721,11 +1724,19 @@ TEST_F(MountPropagationTest, RecursiveChangesAreAtomicAgainstSnapshotsAndNamespa _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); } - bool reported_stress_sample = false; for (;;) { const ssize_t status = read(activity_pipe[0], &token, 1); if (status == 1 && token == 'D') { @@ -1738,12 +1749,6 @@ TEST_F(MountPropagationTest, RecursiveChangesAreAtomicAgainstSnapshotsAndNamespa if (!read_propagation_snapshot(paths, 3, tags) || !snapshot_is_uniform(tags, 3)) { _exit(10); } - if (!reported_stress_sample) { - if (!write_exact(ready_pipe[1], "X", 1)) { - _exit(10); - } - reported_stress_sample = true; - } } _exit(0); } From 7bee8bcd1e0e003f623c38caee9d8f969692bb5e Mon Sep 17 00:00:00 2001 From: longjin Date: Fri, 17 Jul 2026 17:12:27 +0000 Subject: [PATCH 13/13] fix(mm): preserve zero-length mmap error priority Signed-off-by: longjin --- kernel/src/mm/ucontext/address_space.rs | 8 ++++---- .../tests/dunitest/suites/normal/mount_reconfigure.cc | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/kernel/src/mm/ucontext/address_space.rs b/kernel/src/mm/ucontext/address_space.rs index fe3235839b..8c5df309f2 100644 --- a/kernel/src/mm/ucontext/address_space.rs +++ b/kernel/src/mm/ucontext/address_space.rs @@ -523,6 +523,10 @@ impl AddressSpace { sysv_shm, fixed_noreplace_conflict_error_before_mmap_min, } = args; + let len = page_align_up(len); + if len == 0 { + return Err(SystemError::EINVAL); + } let noexec = file .inode() .mount_flags() @@ -531,10 +535,6 @@ impl AddressSpace { return Err(SystemError::EPERM); } may_exec &= !noexec; - let len = page_align_up(len); - if len == 0 { - return Err(SystemError::EINVAL); - } let _force_lazy_on_page_fault_arch = allocate_at_once && MMArch::PAGE_FAULT_ENABLED; diff --git a/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc b/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc index 172a1ac09f..e5b8c4a3ce 100644 --- a/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc +++ b/user/apps/tests/dunitest/suites/normal/mount_reconfigure.cc @@ -379,7 +379,12 @@ TEST(MountReconfigure, NoexecRejectsExec) { int fd = open(target_file, O_RDONLY); ASSERT_GE(fd, 0) << strerror(errno); errno = 0; - void *mapping = mmap(NULL, 4096, PROT_READ | PROT_EXEC, MAP_PRIVATE, fd, 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);