From 44b822cfea10ab7f28ee6f386e7a08e1ac1550af Mon Sep 17 00:00:00 2001 From: yuming Date: Wed, 12 Aug 2026 22:01:48 +0800 Subject: [PATCH 01/34] refactor(ipc): extract generic SysV IPC permission module --- kernel/src/ipc/ipc_perm.rs | 269 +++++++++++++++++++++++++++++++++++++ kernel/src/ipc/mod.rs | 1 + kernel/src/ipc/shm.rs | 181 +++++-------------------- 3 files changed, 307 insertions(+), 144 deletions(-) create mode 100644 kernel/src/ipc/ipc_perm.rs diff --git a/kernel/src/ipc/ipc_perm.rs b/kernel/src/ipc/ipc_perm.rs new file mode 100644 index 0000000000..3296cde094 --- /dev/null +++ b/kernel/src/ipc/ipc_perm.rs @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// Generic SysV IPC permission object and permission checks shared by shm and sem. + +use alloc::sync::Arc; + +use num::ToPrimitive; +use system_error::SystemError; + +use crate::process::{ + cred::{ns_capable, CAPFlags, Cred, Kgid, Kuid}, + namespace::user_namespace::{map_id_down, map_id_up, UserNamespace}, + ProcessManager, +}; + +const DEFAULT_OVERFLOW_ID: u32 = 65534; + +/// Permission bits shared by all SysV IPC objects, matching Linux `ipc_perm.mode`. +pub const PERM_MASK: u32 = 0o777; + +/// Generic SysV IPC permission object, the counterpart of Linux `struct kern_ipc_perm`. +#[derive(Debug)] +pub struct IpcPerm { + /// IPC object id (encoded raw id, see `IpcIdAllocator`) + pub id: usize, + /// User-specified key + pub key: usize, + /// Owner user id (kernel-global uid) + pub uid: Kuid, + /// Owner group id (kernel-global gid) + pub gid: Kgid, + /// Creator user id (kernel-global uid) + pub cuid: Kuid, + /// Creator group id (kernel-global gid) + pub cgid: Kgid, + /// Permission mode bits (low 9 bits) + pub mode: u32, + /// Sequence number distinguishing stale user ids after index reuse + pub seq: usize, +} + +/// View of an IPC object's permission fields, implemented by concrete objects +/// (shm's `KernIpcPerm`, sem's `IpcPerm`). +pub trait IpcPermView { + fn uid(&self) -> Kuid; + fn gid(&self) -> Kgid; + fn cuid(&self) -> Kuid; + fn cgid(&self) -> Kgid; + fn mode(&self) -> u32; + + /// IPC object key in userspace form; default 0 for objects without a key. + fn key(&self) -> usize { + 0 + } + + fn seq(&self) -> usize { + 0 + } + + fn to_posix(&self, user_ns: &Arc) -> Result { + Ok(PosixIpcPerm { + key: self.key() as u32 as i32, + uid: kuid_to_user(user_ns, self.uid()), + gid: kgid_to_user(user_ns, self.gid()), + cuid: kuid_to_user(user_ns, self.cuid()), + cgid: kgid_to_user(user_ns, self.cgid()), + mode: self.mode(), + seq: self.seq().to_i32().ok_or(SystemError::EOVERFLOW)?, + _pad1: 0, + _unused1: 0, + _unused2: 0, + }) + } +} + +impl IpcPermView for IpcPerm { + fn uid(&self) -> Kuid { + self.uid + } + + fn gid(&self) -> Kgid { + self.gid + } + + fn cuid(&self) -> Kuid { + self.cuid + } + + fn cgid(&self) -> Kgid { + self.cgid + } + + fn mode(&self) -> u32 { + self.mode + } + + fn key(&self) -> usize { + self.key + } + + fn seq(&self) -> usize { + self.seq + } +} + +impl IpcPerm { + pub fn new_with_cred(id: usize, key: usize, cred: Arc, mode: u32, seq: usize) -> Self { + IpcPerm { + id, + key, + uid: cred.euid, + gid: cred.egid, + cuid: cred.euid, + cgid: cred.egid, + mode: mode & PERM_MASK, + seq, + } + } + + pub fn copy_from_posix( + &mut self, + uid: u32, + gid: u32, + mode: u32, + user_ns: &Arc, + ) -> Result<(), SystemError> { + self.uid = make_kuid(user_ns, uid)?; + self.gid = make_kgid(user_ns, gid)?; + self.mode = mode & PERM_MASK; + Ok(()) + } +} + +/// Generic version of the shm/sem permission check, matching Linux `ipcperms()`. +pub fn ipc_permission( + perm: &P, + requested: u32, + target_user_ns: &Arc, +) -> Result<(), SystemError> { + let requested = ((requested >> 6) | (requested >> 3) | requested) & 0o7; + if requested == 0 { + return Ok(()); + } + + let cred = ProcessManager::current_pcb().cred(); + let mut granted = perm.mode(); + if cred.euid == perm.cuid() || cred.euid == perm.uid() { + granted >>= 6; + } else if cred_in_group(&cred, perm.cgid()) || cred_in_group(&cred, perm.gid()) { + granted >>= 3; + } + + if (requested & !(granted & 0o7)) != 0 && !ns_capable(target_user_ns, CAPFlags::CAP_IPC_OWNER) + { + return Err(SystemError::EACCES); + } + + Ok(()) +} + +/// Permission check for control operations (IPC_SET/IPC_RMID), matching Linux +/// `ipcctl_pre_down`'s caller check. +pub fn check_control_permission( + perm: &P, + target_user_ns: &Arc, +) -> Result<(), SystemError> { + let cred = ProcessManager::current_pcb().cred(); + if cred.euid == perm.cuid() + || cred.euid == perm.uid() + || ns_capable(target_user_ns, CAPFlags::CAP_SYS_ADMIN) + { + Ok(()) + } else { + Err(SystemError::EPERM) + } +} + +/// Permission check for SHM_LOCK/SHM_UNLOCK, matching Linux `security_shm_shmctl`. +pub fn check_lock_permission( + perm: &P, + target_user_ns: &Arc, +) -> Result<(), SystemError> { + let cred = ProcessManager::current_pcb().cred(); + if cred.euid == perm.cuid() + || cred.euid == perm.uid() + || ns_capable(target_user_ns, CAPFlags::CAP_IPC_LOCK) + { + Ok(()) + } else { + Err(SystemError::EPERM) + } +} + +fn cred_in_group(cred: &Cred, gid: Kgid) -> bool { + cred.fsgid == gid + || cred.groups.contains(&gid) + || cred + .group_info + .as_ref() + .map(|group_info| group_info.gids.contains(&gid)) + .unwrap_or(false) +} + +pub fn make_kuid(user_ns: &Arc, uid: u32) -> Result { + let inner = user_ns.inner.lock(); + map_id_down(&inner.uid_map, uid) + .map(|uid| Kuid::new(uid as usize)) + .ok_or(SystemError::EINVAL) +} + +pub fn make_kgid(user_ns: &Arc, gid: u32) -> Result { + let inner = user_ns.inner.lock(); + map_id_down(&inner.gid_map, gid) + .map(|gid| Kgid::new(gid as usize)) + .ok_or(SystemError::EINVAL) +} + +pub fn kuid_to_user(user_ns: &Arc, kuid: Kuid) -> u32 { + let Ok(uid) = u32::try_from(kuid.data()) else { + return DEFAULT_OVERFLOW_ID; + }; + let inner = user_ns.inner.lock(); + map_id_up(&inner.uid_map, uid).unwrap_or(DEFAULT_OVERFLOW_ID) +} + +pub fn kgid_to_user(user_ns: &Arc, kgid: Kgid) -> u32 { + let Ok(gid) = u32::try_from(kgid.data()) else { + return DEFAULT_OVERFLOW_ID; + }; + let inner = user_ns.inner.lock(); + map_id_up(&inner.gid_map, gid).unwrap_or(DEFAULT_OVERFLOW_ID) +} + +/// IPC permission object in the userspace ABI, matching Linux `struct ipc_perm` (48 bytes on x86_64). +#[repr(C)] +#[derive(Debug, Clone, Copy, Default)] +pub struct PosixIpcPerm { + /// IPC object key + key: i32, + /// Current user id + uid: u32, + /// Current user group id + gid: u32, + /// Creator user id + cuid: u32, + /// Creator group id + cgid: u32, + /// Permission mode + mode: u32, + /// Sequence number + seq: i32, + _pad1: i32, + _unused1: usize, + _unused2: usize, +} + +impl PosixIpcPerm { + pub fn uid(&self) -> u32 { + self.uid + } + + pub fn gid(&self) -> u32 { + self.gid + } + + pub fn mode(&self) -> u32 { + self.mode + } +} diff --git a/kernel/src/ipc/mod.rs b/kernel/src/ipc/mod.rs index ea636f6178..69b5202b29 100644 --- a/kernel/src/ipc/mod.rs +++ b/kernel/src/ipc/mod.rs @@ -1,5 +1,6 @@ pub mod generic_signal; pub mod id; +pub mod ipc_perm; pub mod kill; pub mod pipe; pub mod shm; diff --git a/kernel/src/ipc/shm.rs b/kernel/src/ipc/shm.rs index 430292171a..9a280a3594 100644 --- a/kernel/src/ipc/shm.rs +++ b/kernel/src/ipc/shm.rs @@ -8,14 +8,17 @@ use crate::{ InodeId, }, }, - ipc::id::IpcIdAllocator, + ipc::{ + id::IpcIdAllocator, + ipc_perm::{self, IpcPermView, PosixIpcPerm}, + }, libs::mutex::Mutex, mm::MemoryManagementArch, process::{ cred::{capable, ns_capable, CAPFlags, Cred, Kgid, Kuid}, namespace::{ ipc_namespace::IpcNamespace, - user_namespace::{map_id_down, map_id_up, UserNamespace}, + user_namespace::UserNamespace, }, resource::RLimitID, ProcessManager, RawPid, @@ -34,7 +37,6 @@ use system_error::SystemError; /// 用于创建新的私有IPC对象 pub const IPC_PRIVATE: ShmKey = ShmKey::new(0); -const DEFAULT_OVERFLOW_ID: u32 = 65534; int_like!(ShmId, usize); int_like!(ShmKey, usize); @@ -559,75 +561,6 @@ impl ShmManager { None } - fn cred_in_group(cred: &Cred, gid: Kgid) -> bool { - cred.fsgid == gid - || cred.groups.contains(&gid) - || cred - .group_info - .as_ref() - .map(|group_info| group_info.gids.contains(&gid)) - .unwrap_or(false) - } - - fn ipc_permission( - kern_ipc_perm: &KernIpcPerm, - requested: u32, - target_user_ns: &Arc, - ) -> Result<(), SystemError> { - let requested = ((requested >> 6) | (requested >> 3) | requested) & 0o7; - if requested == 0 { - return Ok(()); - } - - let cred = ProcessManager::current_pcb().cred(); - let mut granted = kern_ipc_perm.mode.bits(); - if cred.euid == kern_ipc_perm.cuid || cred.euid == kern_ipc_perm.uid { - granted >>= 6; - } else if Self::cred_in_group(&cred, kern_ipc_perm.cgid) - || Self::cred_in_group(&cred, kern_ipc_perm.gid) - { - granted >>= 3; - } - - if (requested & !(granted & 0o7)) != 0 - && !ns_capable(target_user_ns, CAPFlags::CAP_IPC_OWNER) - { - return Err(SystemError::EACCES); - } - - Ok(()) - } - - fn check_control_permission( - kern_ipc_perm: &KernIpcPerm, - target_user_ns: &Arc, - ) -> Result<(), SystemError> { - let cred = ProcessManager::current_pcb().cred(); - if cred.euid == kern_ipc_perm.cuid - || cred.euid == kern_ipc_perm.uid - || ns_capable(target_user_ns, CAPFlags::CAP_SYS_ADMIN) - { - Ok(()) - } else { - Err(SystemError::EPERM) - } - } - - fn check_lock_permission( - kern_ipc_perm: &KernIpcPerm, - target_user_ns: &Arc, - ) -> Result<(), SystemError> { - let cred = ProcessManager::current_pcb().cred(); - if cred.euid == kern_ipc_perm.cuid - || cred.euid == kern_ipc_perm.uid - || ns_capable(target_user_ns, CAPFlags::CAP_IPC_LOCK) - { - Ok(()) - } else { - Err(SystemError::EPERM) - } - } - pub(crate) fn charge_memlock_for_shm(size: usize) -> Result { let pcb = ProcessManager::current_pcb(); let rlimit = pcb.get_rlimit(RLimitID::Memlock).rlim_cur; @@ -662,7 +595,7 @@ impl ShmManager { let kernel_shm = self.get_by_shmid_checked(id)?; let requested = shmflg.bits() & ShmFlags::PERM_MASK.bits(); let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - Self::ipc_permission(&kernel_shm.kern_ipc_perm, requested, &target_user_ns) + ipc_perm::ipc_permission(&kernel_shm.kern_ipc_perm, requested, &target_user_ns) } fn maybe_take_destroy_candidate_locked(&mut self, id: ShmId) -> Option { @@ -697,7 +630,7 @@ impl ShmManager { if executable { requested |= Self::IPC_EXEC; } - Self::ipc_permission(&kernel_shm.kern_ipc_perm, requested, &ipcns.user_ns)?; + ipc_perm::ipc_permission(&kernel_shm.kern_ipc_perm, requested, &ipcns.user_ns)?; let backing = kernel_shm.backing.clone(); let backing_inode_id = kernel_shm.backing_inode_id; let size = kernel_shm.size(); @@ -809,7 +742,7 @@ impl ShmManager { }; if cmd != ShmCtlCmd::ShmtStatAny { let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - Self::ipc_permission(&kernel_shm.kern_ipc_perm, Self::IPC_READ, &target_user_ns)?; + ipc_perm::ipc_permission(&kernel_shm.kern_ipc_perm, Self::IPC_READ, &target_user_ns)?; } let kern_ipc_perm = &kernel_shm.kern_ipc_perm; let current_user_ns = ProcessManager::current_user_ns(); @@ -854,7 +787,7 @@ impl ShmManager { pub fn ipc_set(&mut self, id: ShmId, shm_id_ds: PosixShmIdDs) -> Result { let kernel_shm = self.get_by_shmid_checked_mut(id)?; let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - Self::check_control_permission(&kernel_shm.kern_ipc_perm, &target_user_ns)?; + ipc_perm::check_control_permission(&kernel_shm.kern_ipc_perm, &target_user_ns)?; let current_user_ns = ProcessManager::current_user_ns(); kernel_shm.copy_from(shm_id_ds, ¤t_user_ns)?; @@ -866,7 +799,7 @@ impl ShmManager { let key = { let kernel_shm = self.get_by_shmid_checked_mut(id)?; let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - Self::check_control_permission(&kernel_shm.kern_ipc_perm, &target_user_ns)?; + ipc_perm::check_control_permission(&kernel_shm.kern_ipc_perm, &target_user_ns)?; // Linux do_shm_rmid() marks an attached segment as SHM_DEST and // hides its key, but does not refresh shm_ctim. IPC_SET remains // the metadata-changing operation that updates shm_ctim. @@ -882,7 +815,7 @@ impl ShmManager { pub(crate) fn shm_lock_begin(&mut self, id: ShmId) -> Result { let kernel_shm = self.get_by_shmid_checked_mut(id)?; let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - Self::check_lock_permission(&kernel_shm.kern_ipc_perm, &target_user_ns)?; + ipc_perm::check_lock_permission(&kernel_shm.kern_ipc_perm, &target_user_ns)?; let has_target_ns_cap = ns_capable(&target_user_ns, CAPFlags::CAP_IPC_LOCK); if ProcessManager::current_pcb() .get_rlimit(RLimitID::Memlock) @@ -914,7 +847,7 @@ impl ShmManager { } }; let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - if let Err(err) = Self::check_lock_permission(&kernel_shm.kern_ipc_perm, &target_user_ns) { + if let Err(err) = ipc_perm::check_lock_permission(&kernel_shm.kern_ipc_perm, &target_user_ns) { token.release(); return Err(err); } @@ -932,7 +865,7 @@ impl ShmManager { pub fn shm_unlock(&mut self, id: ShmId) -> Result, bool)>, SystemError> { let kernel_shm = self.get_by_shmid_checked_mut(id)?; let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - Self::check_lock_permission(&kernel_shm.kern_ipc_perm, &target_user_ns)?; + ipc_perm::check_lock_permission(&kernel_shm.kern_ipc_perm, &target_user_ns)?; if !kernel_shm.mode().contains(ShmFlags::SHM_LOCKED) { return Ok(None); } @@ -1095,8 +1028,8 @@ impl KernelShm { shm_id_ds: PosixShmIdDs, user_ns: &Arc, ) -> Result<(), SystemError> { - let uid = KernIpcPerm::make_kuid(user_ns, shm_id_ds.uid())?; - let gid = KernIpcPerm::make_kgid(user_ns, shm_id_ds.gid())?; + let uid = ipc_perm::make_kuid(user_ns, shm_id_ds.uid())?; + let gid = ipc_perm::make_kgid(user_ns, shm_id_ds.gid())?; let perm_bits = ShmFlags::from_bits_truncate(shm_id_ds.mode()) & ShmFlags::PERM_MASK; self.kern_ipc_perm.uid = uid; self.kern_ipc_perm.gid = gid; @@ -1250,52 +1183,35 @@ impl KernIpcPerm { seq, } } +} - fn make_kuid(user_ns: &Arc, uid: u32) -> Result { - let inner = user_ns.inner.lock(); - map_id_down(&inner.uid_map, uid) - .map(|uid| Kuid::new(uid as usize)) - .ok_or(SystemError::EINVAL) +impl IpcPermView for KernIpcPerm { + fn uid(&self) -> Kuid { + self.uid } - fn make_kgid(user_ns: &Arc, gid: u32) -> Result { - let inner = user_ns.inner.lock(); - map_id_down(&inner.gid_map, gid) - .map(|gid| Kgid::new(gid as usize)) - .ok_or(SystemError::EINVAL) + fn gid(&self) -> Kgid { + self.gid } - fn kuid_to_user(user_ns: &Arc, kuid: Kuid) -> u32 { - let Ok(uid) = u32::try_from(kuid.data()) else { - return DEFAULT_OVERFLOW_ID; - }; - let inner = user_ns.inner.lock(); - map_id_up(&inner.uid_map, uid).unwrap_or(DEFAULT_OVERFLOW_ID) + fn cuid(&self) -> Kuid { + self.cuid } - fn kgid_to_user(user_ns: &Arc, kgid: Kgid) -> u32 { - let Ok(gid) = u32::try_from(kgid.data()) else { - return DEFAULT_OVERFLOW_ID; - }; - let inner = user_ns.inner.lock(); - map_id_up(&inner.gid_map, gid).unwrap_or(DEFAULT_OVERFLOW_ID) + fn cgid(&self) -> Kgid { + self.cgid } - fn to_posix(&self, user_ns: &Arc) -> Result { - let key = self.key.data() as u32 as i32; + fn mode(&self) -> u32 { + self.mode.bits() + } - Ok(PosixIpcPerm { - key, - uid: Self::kuid_to_user(user_ns, self.uid), - gid: Self::kgid_to_user(user_ns, self.gid), - cuid: Self::kuid_to_user(user_ns, self.cuid), - cgid: Self::kgid_to_user(user_ns, self.cgid), - mode: self.mode.bits(), - seq: self.seq.to_i32().ok_or(SystemError::EOVERFLOW)?, - _pad1: 0, - _unused1: 0, - _unused2: 0, - }) + fn key(&self) -> usize { + self.key.data() + } + + fn seq(&self) -> usize { + self.seq } } @@ -1413,37 +1329,14 @@ pub struct PosixShmIdDs { impl PosixShmIdDs { pub fn uid(&self) -> u32 { - self.shm_perm.uid + self.shm_perm.uid() } pub fn gid(&self) -> u32 { - self.shm_perm.gid + self.shm_perm.gid() } pub fn mode(&self) -> u32 { - self.shm_perm.mode + self.shm_perm.mode() } } - -/// 共享内存段权限,符合POSIX标准 -#[repr(C)] -#[derive(Debug, Clone, Copy, Default)] -pub struct PosixIpcPerm { - /// IPC对象键值 - key: i32, - /// 当前用户id - uid: u32, - /// 当前用户组id - gid: u32, - /// 创建者用户id - cuid: u32, - /// 创建者组id - cgid: u32, - /// 权限 - mode: u32, - /// 序列号 - seq: i32, - _pad1: i32, - _unused1: usize, - _unused2: usize, -} From 22dcdd8f1489cca567d0c4d12aedb47fe445709a Mon Sep 17 00:00:00 2001 From: yuming Date: Wed, 12 Aug 2026 22:02:20 +0800 Subject: [PATCH 02/34] feat(ipc): implement System V semaphore syscalls --- kernel/src/ipc/mod.rs | 1 + kernel/src/ipc/sem.rs | 988 ++++++++++++++++++ kernel/src/ipc/syscall/mod.rs | 4 + kernel/src/ipc/syscall/sys_semctl.rs | 186 ++++ kernel/src/ipc/syscall/sys_semget.rs | 50 + kernel/src/ipc/syscall/sys_semop.rs | 74 ++ kernel/src/ipc/syscall/sys_semtimedop.rs | 134 +++ kernel/src/process/namespace/ipc_namespace.rs | 7 +- 8 files changed, 1443 insertions(+), 1 deletion(-) create mode 100644 kernel/src/ipc/sem.rs create mode 100644 kernel/src/ipc/syscall/sys_semctl.rs create mode 100644 kernel/src/ipc/syscall/sys_semget.rs create mode 100644 kernel/src/ipc/syscall/sys_semop.rs create mode 100644 kernel/src/ipc/syscall/sys_semtimedop.rs diff --git a/kernel/src/ipc/mod.rs b/kernel/src/ipc/mod.rs index 69b5202b29..5923f2381a 100644 --- a/kernel/src/ipc/mod.rs +++ b/kernel/src/ipc/mod.rs @@ -3,6 +3,7 @@ pub mod id; pub mod ipc_perm; pub mod kill; pub mod pipe; +pub mod sem; pub mod shm; pub mod sighand; pub mod signal; diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs new file mode 100644 index 0000000000..6c8eac4c0e --- /dev/null +++ b/kernel/src/ipc/sem.rs @@ -0,0 +1,988 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// +// System V semaphore subsystem, tracking Linux 6.6 `ipc/sem.c` observable +// behavior. SEM_UNDO is intentionally unsupported and rejected with ENOSYS. + +use alloc::{collections::VecDeque, sync::Arc, vec::Vec}; +use core::fmt; + +use hashbrown::HashMap; +use system_error::SystemError; + +use crate::{ + ipc::{ + id::IpcIdAllocator, + ipc_perm::{self, IpcPerm, IpcPermView, PosixIpcPerm}, + }, + libs::{ + spinlock::SpinLock, + wait_queue::{TimeoutWaker, Waiter, Waker}, + }, + process::{ + namespace::ipc_namespace::IpcNamespace, + pid::{Pid, PidType}, + ProcessManager, + }, + time::{ + timer::{clock, next_n_us_timer_jiffies, Timer}, + Duration, PosixTimeSpec, + }, +}; + +/// 用于创建新的私有信号量集合 +pub const IPC_PRIVATE: SemKey = SemKey::new(0); + +int_like!(SemId, usize); +int_like!(SemKey, usize); + +#[derive(Debug, Clone, Copy)] +pub struct SemSetAllToken { + id: SemId, + nsems: usize, +} + +impl SemSetAllToken { + fn new(id: SemId, nsems: usize) -> Self { + Self { id, nsems } + } + + pub fn nsems(&self) -> usize { + self.nsems + } +} + +// Linux include/uapi/linux/sem.h 的限制常量 +pub const SEMMNI: usize = 32000; +pub const SEMMSL: usize = 32000; +pub const SEMMNS: usize = SEMMNI * SEMMSL; +pub const SEMOPM: usize = 500; +pub const SEMVMX: i32 = 32767; + +bitflags! { + pub struct SemFlags: u32 { + const PERM_MASK = 0o777; + const IPC_CREAT = 0o1000; + const IPC_EXCL = 0o2000; + const IPC_NOWAIT = 0x800; + const SEM_UNDO = 0x1000; + } +} + +/// 管理信号量集合信息的操作码(Linux x86_64 UAPI include/uapi/linux/sem.h) +#[derive(Eq, Clone, Copy)] +pub enum SemCtlCmd { + /// 删除信号量集合 + IpcRmid = 0, + /// 设置权限 + IpcSet = 1, + /// 获取 SemIdDs + IpcStat = 2, + /// 查看 SemInfo + IpcInfo = 3, + /// 获取最后操作指定信号量的进程pid + GetPid = 11, + /// 获取指定信号量的值 + GetVal = 12, + /// 获取集合内所有信号量的值 + GetAll = 13, + /// 获取等待指定信号量值增加的进程数 + GetNcnt = 14, + /// 获取等待指定信号量值为0的进程数 + GetZcnt = 15, + /// 设置指定信号量的值 + SetVal = 16, + /// 设置集合内所有信号量的值 + SetAll = 17, + /// 按索引获取 SemIdDs + SemStat = 18, + /// 查看 SemInfo + SemInfo = 19, + /// 按索引获取 SemIdDs(无权限检查) + SemStatAny = 20, + + Default, +} + +impl From for SemCtlCmd { + fn from(cmd: usize) -> SemCtlCmd { + match cmd { + 0 => Self::IpcRmid, + 1 => Self::IpcSet, + 2 => Self::IpcStat, + 3 => Self::IpcInfo, + 11 => Self::GetPid, + 12 => Self::GetVal, + 13 => Self::GetAll, + 14 => Self::GetNcnt, + 15 => Self::GetZcnt, + 16 => Self::SetVal, + 17 => Self::SetAll, + 18 => Self::SemStat, + 19 => Self::SemInfo, + 20 => Self::SemStatAny, + _ => Self::Default, + } + } +} + +impl fmt::Display for SemCtlCmd { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SemCtlCmd::IpcRmid => write!(f, "IPC_RMID"), + SemCtlCmd::IpcSet => write!(f, "IPC_SET"), + SemCtlCmd::IpcStat => write!(f, "IPC_STAT"), + SemCtlCmd::IpcInfo => write!(f, "IPC_INFO"), + SemCtlCmd::GetPid => write!(f, "GETPID"), + SemCtlCmd::GetVal => write!(f, "GETVAL"), + SemCtlCmd::GetAll => write!(f, "GETALL"), + SemCtlCmd::GetNcnt => write!(f, "GETNCNT"), + SemCtlCmd::GetZcnt => write!(f, "GETZCNT"), + SemCtlCmd::SetVal => write!(f, "SETVAL"), + SemCtlCmd::SetAll => write!(f, "SETALL"), + SemCtlCmd::SemStat => write!(f, "SEM_STAT"), + SemCtlCmd::SemInfo => write!(f, "SEM_INFO"), + SemCtlCmd::SemStatAny => write!(f, "SEM_STAT_ANY"), + SemCtlCmd::Default => write!(f, "DEFAULT (Invalid Cmd)"), + } + } +} + +impl PartialEq for SemCtlCmd { + fn eq(&self, other: &SemCtlCmd) -> bool { + *self as usize == *other as usize + } +} + +/// 单个信号量(Linux struct sem 的字段) +#[derive(Debug, Clone)] +pub struct KernelSem { + /// semval + val: i32, + /// sempid:最后操作本信号量的进程 + pid: Option>, +} + +/// 用户态 sembuf(Linux struct sembuf,6 字节) +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct PosixSemBuf { + pub sem_num: u16, + pub sem_op: i16, + pub sem_flg: i16, +} + +/// 信号量集合信息,符合 Linux x86_64 struct semid64_ds(104 字节,带 +/// 32 位时间戳高半区字段) +#[repr(C)] +#[derive(Debug, Clone, Copy, Default)] +pub struct PosixSemIdDs { + /// 权限信息 + pub sem_perm: PosixIpcPerm, + /// 最后一次 semop 的时间(64 位;高 32 位落入 __sem_otime_high) + pub sem_otime: i64, + _sem_otime_high: i64, + /// 最后一次更改信息的时间 + pub sem_ctime: i64, + _sem_ctime_high: i64, + /// 集合内信号量数量 + pub sem_nsems: usize, + _unused1: usize, + _unused2: usize, +} + +/// 信号量系统信息,符合 Linux struct seminfo(40 字节) +#[repr(C)] +#[derive(Debug, Clone, Copy, Default)] +pub struct PosixSemInfo { + pub semmap: i32, + pub semmni: i32, + pub semmns: i32, + pub semmnu: i32, + pub semmsl: i32, + pub semopm: i32, + pub semume: i32, + pub semusz: i32, + pub semvmx: i32, + pub semaem: i32, +} + +impl PosixSemInfo { + fn new(cmd: SemCtlCmd, set_count: usize, total_sems: usize) -> Self { + let (semusz, semaem) = if cmd == SemCtlCmd::SemInfo { + (set_count as i32, total_sems as i32) + } else { + (20, SEMVMX) + }; + PosixSemInfo { + semmap: SEMMNS as i32, + semmni: SEMMNI as i32, + semmns: SEMMNS as i32, + semmnu: SEMMNS as i32, + semmsl: SEMMSL as i32, + semopm: SEMOPM as i32, + semume: SEMOPM as i32, + semusz, + semvmx: SEMVMX, + semaem, + } + } +} + +/// 等待者阻塞的原因,决定唤醒时机与 GETNCNT/GETZCNT 统计 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SemWaitType { + /// sem_op < 0:等待 semval 增加(GETNCNT) + Increase, + /// sem_op == 0:等待 semval 变为 0(GETZCNT) + Zero, +} + +/// The precise operation currently blocking an operation group. +#[derive(Debug, Clone, Copy)] +struct SemBlockedOp { + semnum: usize, + wait_type: SemWaitType, + nowait: bool, +} + +#[derive(Debug)] +enum SemQueueStatus { + Queued(SemBlockedOp), + Completed(Result), +} + +/// An Arc-owned queue entry shared by the set and the blocked caller. +#[derive(Debug)] +struct SemQueueEntry { + sops: Vec, + pid: Option>, + waker: Arc, + status: SpinLock, +} + +impl SemQueueEntry { + fn new( + sops: &[PosixSemBuf], + pid: Option>, + waker: Arc, + blocker: SemBlockedOp, + ) -> Self { + Self { + sops: sops.to_vec(), + pid, + waker, + status: SpinLock::new(SemQueueStatus::Queued(blocker)), + } + } + + fn completed_result(&self) -> Option> { + match &*self.status.lock() { + SemQueueStatus::Queued(_) => None, + SemQueueStatus::Completed(result) => Some(result.clone()), + } + } + + fn update_blocker(&self, blocker: SemBlockedOp) { + let mut status = self.status.lock(); + if matches!(&*status, SemQueueStatus::Queued(_)) { + *status = SemQueueStatus::Queued(blocker); + } + } + + fn complete(&self, result: Result) -> bool { + let mut status = self.status.lock(); + if matches!(&*status, SemQueueStatus::Completed(_)) { + return false; + } + *status = SemQueueStatus::Completed(result); + true + } + + fn is_waiting_on(&self, semnum: usize, wait_type: SemWaitType) -> bool { + matches!( + &*self.status.lock(), + SemQueueStatus::Queued(blocker) + if blocker.semnum == semnum && blocker.wait_type == wait_type + ) + } +} + +/// 信号量集合 +#[derive(Debug)] +pub struct KernelSemSet { + /// 权限信息 + pub kern_ipc_perm: IpcPerm, + /// 集合内信号量 + pub sems: Vec, + /// 最后一次 semop 的时间 + pub sem_otime: i64, + /// 最后一次更改信息的时间 + pub sem_ctime: i64, + /// 等待者队列 + waiters: VecDeque>, +} + +impl KernelSemSet { + pub fn new(kern_ipc_perm: IpcPerm, nsems: usize) -> Self { + KernelSemSet { + kern_ipc_perm, + sems: core::iter::repeat(KernelSem { val: 0, pid: None }) + .take(nsems) + .collect(), + sem_otime: 0, + sem_ctime: PosixTimeSpec::now().tv_sec, + waiters: VecDeque::new(), + } + } + + fn enqueue_waiter(&mut self, waiter: Arc) { + self.waiters.push_back(waiter); + } + + fn remove_waiter(&mut self, target: &Arc) { + self.waiters.retain(|entry| !Arc::ptr_eq(entry, target)); + } + + /// Complete and wake all entries during IPC_RMID under the manager lock. + fn complete_all_removed(&mut self) { + for entry in self.waiters.drain(..) { + entry.complete(Err(SystemError::EIDRM)); + entry.waker.wake(); + } + } + + fn ncnt(&self, semnum: usize) -> usize { + self.waiters + .iter() + .filter(|entry| entry.is_waiting_on(semnum, SemWaitType::Increase)) + .count() + } + + fn zcnt(&self, semnum: usize) -> usize { + self.waiters + .iter() + .filter(|entry| entry.is_waiting_on(semnum, SemWaitType::Zero)) + .count() + } +} + +#[derive(Debug)] +struct SemopSimulation { + values: HashMap, +} + +/// semop 尝试执行的结果 +#[derive(Debug)] +enum SemopOutcome { + Ready(SemopSimulation), + Blocked(SemBlockedOp), +} + +/// 信号量管理器 +#[derive(Debug)] +pub struct SemManager { + /// SemId 分配器 + id_allocator: IpcIdAllocator, + /// 低位 IPC idx 映射信号量集合表 + id2sem: HashMap, + /// SemKey 映射 SemId 表 + key2id: HashMap, + /// namespace 内信号量总数(Linux semmns 会计) + total_sems: usize, +} + +impl Default for SemManager { + fn default() -> Self { + Self::new() + } +} + +impl SemManager { + const IPC_READ: u32 = 0o4; + const IPC_WRITE: u32 = 0o2; + + pub fn new() -> Self { + SemManager { + id_allocator: IpcIdAllocator::new(SEMMNI).unwrap(), + id2sem: HashMap::new(), + key2id: HashMap::new(), + total_sems: 0, + } + } + + fn get_by_semid_checked(&self, id: SemId) -> Result<&KernelSemSet, SystemError> { + let decoded = IpcIdAllocator::decode(id.data())?; + let set = self.id2sem.get(&decoded.idx).ok_or(SystemError::EINVAL)?; + if set.kern_ipc_perm.id != id.data() || set.kern_ipc_perm.seq != decoded.seq { + return Err(SystemError::EINVAL); + } + Ok(set) + } + + fn get_by_semid_checked_mut(&mut self, id: SemId) -> Result<&mut KernelSemSet, SystemError> { + let decoded = IpcIdAllocator::decode(id.data())?; + let set = self + .id2sem + .get_mut(&decoded.idx) + .ok_or(SystemError::EINVAL)?; + if set.kern_ipc_perm.id != id.data() || set.kern_ipc_perm.seq != decoded.seq { + return Err(SystemError::EINVAL); + } + Ok(set) + } + + fn get_by_index(&self, id: usize) -> Result<&KernelSemSet, SystemError> { + let idx = id & IpcIdAllocator::IPC_ID_IDX_MASK; + self.id2sem.get(&idx).ok_or(SystemError::EINVAL) + } + + fn current_max_index(&self) -> usize { + self.id2sem.keys().copied().max().unwrap_or(0) + } + + /// # semget:创建或查找信号量集合 + pub fn semget( + &mut self, + key: SemKey, + nsems: usize, + semflg: SemFlags, + ) -> Result { + if nsems > SEMMSL { + return Err(SystemError::EINVAL); + } + + if key == IPC_PRIVATE { + return self.create(key, nsems, semflg); + } + + if let Some(&id) = self.key2id.get(&key) { + if semflg.contains(SemFlags::IPC_CREAT | SemFlags::IPC_EXCL) { + return Err(SystemError::EEXIST); + } + let set = self.get_by_semid_checked(id)?; + if nsems > set.sems.len() { + return Err(SystemError::EINVAL); + } + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission( + &set.kern_ipc_perm, + semflg.bits() & SemFlags::PERM_MASK.bits(), + &target_user_ns, + )?; + return Ok(id.data()); + } + + if !semflg.contains(SemFlags::IPC_CREAT) { + return Err(SystemError::ENOENT); + } + self.create(key, nsems, semflg) + } + + fn create( + &mut self, + key: SemKey, + nsems: usize, + semflg: SemFlags, + ) -> Result { + if nsems == 0 { + return Err(SystemError::EINVAL); + } + if self.id2sem.len() >= SEMMNI { + return Err(SystemError::ENOSPC); + } + let total_after = self + .total_sems + .checked_add(nsems) + .ok_or(SystemError::ENOSPC)?; + if total_after > SEMMNS { + return Err(SystemError::ENOSPC); + } + + let ipc_id = self.id_allocator.alloc()?; + let sem_id = SemId::new(ipc_id.raw); + let current_cred = ProcessManager::current_pcb().cred(); + let kern_ipc_perm = IpcPerm::new_with_cred( + sem_id.data(), + key.data(), + current_cred, + semflg.bits() & SemFlags::PERM_MASK.bits(), + ipc_id.seq, + ); + let set = KernelSemSet::new(kern_ipc_perm, nsems); + + if key != IPC_PRIVATE { + self.key2id.insert(key, sem_id); + } + self.id2sem.insert(ipc_id.idx, set); + self.total_sems = total_after; + + Ok(sem_id.data()) + } + + /// Simulate sops in order without changing the real semaphore values. + fn simulate_semop( + set: &KernelSemSet, + sops: &[PosixSemBuf], + ) -> Result { + let mut values = HashMap::with_capacity(sops.len()); + + for op in sops { + let idx = op.sem_num as usize; + if idx >= set.sems.len() { + return Err(SystemError::EFBIG); + } + + let value = values.entry(idx).or_insert(set.sems[idx].val); + let current = *value; + if op.sem_op == 0 { + if current != 0 { + return Ok(SemopOutcome::Blocked(SemBlockedOp { + semnum: idx, + wait_type: SemWaitType::Zero, + nowait: (op.sem_flg as u32) & SemFlags::IPC_NOWAIT.bits() != 0, + })); + } + continue; + } + + let result = current as i64 + op.sem_op as i64; + if result > SEMVMX as i64 { + return Err(SystemError::ERANGE); + } + if result < 0 { + return Ok(SemopOutcome::Blocked(SemBlockedOp { + semnum: idx, + wait_type: SemWaitType::Increase, + nowait: (op.sem_flg as u32) & SemFlags::IPC_NOWAIT.bits() != 0, + })); + } + *value = result as i32; + } + + Ok(SemopOutcome::Ready(SemopSimulation { values })) + } + + /// Commit a successful simulation while the manager lock is held. + fn commit_semop( + set: &mut KernelSemSet, + simulation: SemopSimulation, + pid: Option>, + ) -> bool { + let mut values_changed = false; + for (idx, value) in simulation.values { + let sem = &mut set.sems[idx]; + values_changed |= sem.val != value; + sem.val = value; + sem.pid = pid.clone(); + } + set.sem_otime = PosixTimeSpec::now().tv_sec; + values_changed + } + + /// Complete every executable queue entry without head-of-line blocking. + fn update_queue(set: &mut KernelSemSet) { + loop { + let mut values_changed = false; + let mut index = 0; + + while index < set.waiters.len() { + let entry = set.waiters[index].clone(); + match Self::simulate_semop(set, &entry.sops) { + Ok(SemopOutcome::Ready(simulation)) => { + let changed = Self::commit_semop(set, simulation, entry.pid.clone()); + values_changed |= changed; + set.waiters.remove(index); + entry.complete(Ok(0)); + entry.waker.wake(); + if changed { + break; + } + } + Ok(SemopOutcome::Blocked(blocker)) if blocker.nowait => { + set.waiters.remove(index); + entry.complete(Err(SystemError::EAGAIN_OR_EWOULDBLOCK)); + entry.waker.wake(); + } + Ok(SemopOutcome::Blocked(blocker)) => { + entry.update_blocker(blocker); + index += 1; + } + Err(error) => { + set.waiters.remove(index); + entry.complete(Err(error)); + entry.waker.wake(); + } + } + } + + if !values_changed { + return; + } + } + } + + fn cancel_queued_entry( + &mut self, + semid: SemId, + entry: &Arc, + error: SystemError, + ) -> Result { + if let Some(result) = entry.completed_result() { + return result; + } + + let set = match self.get_by_semid_checked_mut(semid) { + Ok(set) => set, + Err(_) => { + entry.complete(Err(SystemError::EIDRM)); + return Err(SystemError::EIDRM); + } + }; + set.remove_waiter(entry); + entry.complete(Err(error.clone())); + Err(error) + } + + /// # semtimedop:原子执行 sops,必要时阻塞 + /// + /// 锁由本函数内部管理(等待期间需要释放锁),调用方不得预先持有 + /// `ipcns.sem` 锁。 + /// + /// - timeout == None:无限等待(等价于 semop) + /// - timeout == Some(Duration::ZERO):不阻塞 + /// - 其余:阻塞至超时,超时返回 EAGAIN + pub fn semtimedop( + ipcns: &Arc, + semid: SemId, + sops: &[PosixSemBuf], + timeout: Option, + ) -> Result { + if sops.is_empty() { + return Err(SystemError::EINVAL); + } + if sops.len() > SEMOPM { + return Err(SystemError::E2BIG); + } + if sops + .iter() + .any(|op| (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() != 0) + { + return Err(SystemError::ENOSYS); + } + let non_blocking = timeout == Some(Duration::ZERO); + // 仅等待/等零(全部 sem_op == 0)时检查读权限,否则检查写权限(Linux 语义) + let alter = sops.iter().any(|op| op.sem_op != 0); + + let target_user_ns = ipcns.user_ns.clone(); + let deadline_ticks = timeout.map(|d| next_n_us_timer_jiffies(d.total_micros())); + + let (waiter, waker) = Waiter::new_pair(); + let timer = + deadline_ticks.map(|deadline| Timer::new(TimeoutWaker::new(waker.clone()), deadline)); + let pid = ProcessManager::current_pcb().task_pid_ptr(PidType::TGID); + + let entry = { + let mut guard = ipcns.sem.lock(); + let set = guard.get_by_semid_checked_mut(semid)?; + // 与 Linux 一致:先检查 semnum 越界(EFBIG),再检查权限(EACCES) + if sops.iter().any(|op| op.sem_num as usize >= set.sems.len()) { + return Err(SystemError::EFBIG); + } + ipc_perm::ipc_permission( + &set.kern_ipc_perm, + if alter { + Self::IPC_WRITE + } else { + Self::IPC_READ + }, + &target_user_ns, + )?; + + match Self::simulate_semop(set, sops)? { + SemopOutcome::Ready(simulation) => { + Self::commit_semop(set, simulation, pid); + Self::update_queue(set); + return Ok(0); + } + SemopOutcome::Blocked(blocker) => { + if blocker.nowait || non_blocking { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + if deadline_ticks.is_some_and(|deadline| clock() >= deadline) { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + let entry = Arc::new(SemQueueEntry::new(sops, pid, waker.clone(), blocker)); + set.enqueue_waiter(entry.clone()); + entry + } + } + }; + + if let Some(timer) = timer.as_ref() { + timer.activate(); + } + let _wait_result = waiter.wait(true); + let completed = entry.completed_result(); + let was_timeout = timer.as_ref().is_some_and(|timer| timer.timeout()); + if !was_timeout { + if let Some(timer) = timer.as_ref() { + timer.cancel(); + } + } + if let Some(result) = completed { + return result; + } + + let error = if was_timeout { + SystemError::EAGAIN_OR_EWOULDBLOCK + } else { + SystemError::EINTR + }; + let mut guard = ipcns.sem.lock(); + guard.cancel_queued_entry(semid, &entry, error) + } + + /// # IPC_RMID:删除信号量集合并唤醒所有等待者(返回 EIDRM) + pub fn ipc_rmid(&mut self, id: SemId) -> Result<(), SystemError> { + let decoded = IpcIdAllocator::decode(id.data())?; + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + let key = { + let set = self.get_by_semid_checked(id)?; + ipc_perm::check_control_permission(&set.kern_ipc_perm, &target_user_ns)?; + set.kern_ipc_perm.key + }; + let mut set = self + .id2sem + .remove(&decoded.idx) + .ok_or(SystemError::EINVAL)?; + self.key2id.remove(&SemKey::new(key)); + self.id_allocator.free_idx(decoded.idx); + self.total_sems = self.total_sems.saturating_sub(set.sems.len()); + set.complete_all_removed(); + Ok(()) + } + + /// # IPC_SET:更新权限(uid/gid/mode)并刷新 sem_ctime + pub fn ipc_set(&mut self, id: SemId, semid_ds: PosixSemIdDs) -> Result<(), SystemError> { + let set = self.get_by_semid_checked_mut(id)?; + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::check_control_permission(&set.kern_ipc_perm, &target_user_ns)?; + let current_user_ns = ProcessManager::current_user_ns(); + set.kern_ipc_perm.copy_from_posix( + semid_ds.sem_perm.uid(), + semid_ds.sem_perm.gid(), + semid_ds.sem_perm.mode(), + ¤t_user_ns, + )?; + set.sem_ctime = PosixTimeSpec::now().tv_sec; + Ok(()) + } + + /// IPC_STAT/SEM_STAT/SEM_STAT_ANY:输出 semid_ds + pub fn sem_stat_data( + &self, + id_or_index: SemId, + cmd: SemCtlCmd, + ) -> Result<(usize, PosixSemIdDs), SystemError> { + let set = match cmd { + SemCtlCmd::IpcStat => self.get_by_semid_checked(id_or_index)?, + SemCtlCmd::SemStat | SemCtlCmd::SemStatAny => self.get_by_index(id_or_index.data())?, + _ => return Err(SystemError::EINVAL), + }; + if cmd != SemCtlCmd::SemStatAny { + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_READ, &target_user_ns)?; + } + let current_user_ns = ProcessManager::current_user_ns(); + let sem_perm = set.kern_ipc_perm.to_posix(¤t_user_ns)?; + let semid_ds = PosixSemIdDs { + sem_perm, + sem_otime: set.sem_otime, + _sem_otime_high: 0, + sem_ctime: set.sem_ctime, + _sem_ctime_high: 0, + sem_nsems: set.sems.len(), + _unused1: 0, + _unused2: 0, + }; + let ret = if cmd == SemCtlCmd::IpcStat { + 0 + } else { + set.kern_ipc_perm.id + }; + Ok((ret, semid_ds)) + } + + /// IPC_INFO/SEM_INFO:输出系统信息 + pub fn sem_info_data(&self, cmd: SemCtlCmd) -> (usize, PosixSemInfo) { + ( + self.current_max_index(), + PosixSemInfo::new(cmd, self.id2sem.len(), self.total_sems), + ) + } + + /// GETVAL/GETPID/GETNCNT/GETZCNT:单信号量查询 + pub fn sem_get_value( + &self, + id: SemId, + semnum: usize, + cmd: SemCtlCmd, + ) -> Result { + let set = self.get_by_semid_checked(id)?; + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_READ, &target_user_ns)?; + if semnum >= set.sems.len() { + return Err(SystemError::EINVAL); + } + match cmd { + SemCtlCmd::GetVal => Ok(set.sems[semnum].val as usize), + SemCtlCmd::GetPid => Ok(set.sems[semnum] + .pid + .as_ref() + .map(|pid| pid.pid_vnr().data()) + .unwrap_or(0)), + SemCtlCmd::GetNcnt => Ok(set.ncnt(semnum)), + SemCtlCmd::GetZcnt => Ok(set.zcnt(semnum)), + _ => Err(SystemError::EINVAL), + } + } + + /// # SETVAL:设置单个信号量的值 + pub fn setval(&mut self, id: SemId, semnum: usize, val: i32) -> Result<(), SystemError> { + // 与 Linux 一致:先校验值(ERANGE),再 semnum(EINVAL),再权限(EACCES) + if !(0..=SEMVMX).contains(&val) { + return Err(SystemError::ERANGE); + } + let set = self.get_by_semid_checked_mut(id)?; + if semnum >= set.sems.len() { + return Err(SystemError::EINVAL); + } + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_WRITE, &target_user_ns)?; + + let sem = &mut set.sems[semnum]; + sem.val = val; + sem.pid = ProcessManager::current_pcb().task_pid_ptr(PidType::TGID); + set.sem_ctime = PosixTimeSpec::now().tv_sec; + Self::update_queue(set); + Ok(()) + } + + /// # SETALL:设置集合内所有信号量的值(校验失败时不改变任何值) + pub fn setall(&mut self, token: SemSetAllToken, vals: &[u16]) -> Result<(), SystemError> { + let set = self + .get_by_semid_checked_mut(token.id) + .map_err(|_| SystemError::EIDRM)?; + if vals.len() != token.nsems || vals.len() != set.sems.len() { + return Err(SystemError::EINVAL); + } + if vals.iter().any(|&v| v as i32 > SEMVMX) { + return Err(SystemError::ERANGE); + } + + let pid = ProcessManager::current_pcb().task_pid_ptr(PidType::TGID); + for (i, &v) in vals.iter().enumerate() { + let sem = &mut set.sems[i]; + sem.val = v as i32; + sem.pid = pid.clone(); + } + set.sem_ctime = PosixTimeSpec::now().tv_sec; + Self::update_queue(set); + Ok(()) + } + + /// # GETALL:获取集合内所有信号量的值 + pub fn getall(&self, id: SemId) -> Result, SystemError> { + let set = self.get_by_semid_checked(id)?; + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_READ, &target_user_ns)?; + Ok(set.sems.iter().map(|s| s.val as u16).collect()) + } + + /// Validate SETALL before the caller accesses the userspace array. + pub fn prepare_setall(&self, id: SemId) -> Result { + let set = self.get_by_semid_checked(id)?; + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_WRITE, &target_user_ns)?; + Ok(SemSetAllToken::new(id, set.sems.len())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::process::cred::{Kgid, Kuid}; + + fn test_perm(id: SemId, key: SemKey, seq: usize) -> IpcPerm { + IpcPerm { + id: id.data(), + key: key.data(), + uid: Kuid::new(0), + gid: Kgid::new(0), + cuid: Kuid::new(0), + cgid: Kgid::new(0), + mode: 0o600, + seq, + } + } + + fn insert_test_set(manager: &mut SemManager, key: SemKey, vals: &[i32]) -> SemId { + let ipc_id = manager.id_allocator.alloc().unwrap(); + let id = SemId::new(ipc_id.raw); + let mut set = KernelSemSet::new(test_perm(id, key, ipc_id.seq), vals.len()); + for (sem, val) in set.sems.iter_mut().zip(vals.iter().copied()) { + sem.val = val; + } + manager.key2id.insert(key, id); + manager.id2sem.insert(ipc_id.idx, set); + manager.total_sems += vals.len(); + id + } + + fn remove_test_set(manager: &mut SemManager, id: SemId) { + let decoded = IpcIdAllocator::decode(id.data()).unwrap(); + let set = manager.id2sem.remove(&decoded.idx).unwrap(); + manager.key2id.remove(&SemKey::new(set.kern_ipc_perm.key)); + manager.id_allocator.free_idx(decoded.idx); + manager.total_sems = manager.total_sems.saturating_sub(set.sems.len()); + } + + fn sem_values(manager: &SemManager, id: SemId) -> Vec { + manager + .get_by_semid_checked(id) + .unwrap() + .sems + .iter() + .map(|sem| sem.val) + .collect() + } + + #[test] + fn prepared_setall_token_returns_eidrm_after_rmid() { + let mut manager = SemManager::new(); + let id = insert_test_set(&mut manager, SemKey::new(11), &[1, 2]); + let token = SemSetAllToken::new(id, 2); + + remove_test_set(&mut manager, id); + + assert_eq!(manager.setall(token, &[7, 8]), Err(SystemError::EIDRM)); + } + + #[test] + fn stale_prepared_setall_token_does_not_modify_reused_index() { + let mut manager = SemManager::new(); + let old_id = insert_test_set(&mut manager, SemKey::new(21), &[1, 2]); + let token = SemSetAllToken::new(old_id, 2); + + remove_test_set(&mut manager, old_id); + let new_id = insert_test_set(&mut manager, SemKey::new(22), &[3, 4]); + assert_ne!(old_id, new_id); + assert_eq!( + old_id.data() & IpcIdAllocator::IPC_ID_IDX_MASK, + new_id.data() & IpcIdAllocator::IPC_ID_IDX_MASK + ); + + assert_eq!(manager.setall(token, &[7, 8]), Err(SystemError::EIDRM)); + assert_eq!(sem_values(&manager, new_id), vec![3, 4]); + } +} diff --git a/kernel/src/ipc/syscall/mod.rs b/kernel/src/ipc/syscall/mod.rs index 068cb427f2..f8f3eee195 100644 --- a/kernel/src/ipc/syscall/mod.rs +++ b/kernel/src/ipc/syscall/mod.rs @@ -9,6 +9,10 @@ mod sys_rt_sigqueueinfo; mod sys_rt_sigsuspend; pub mod sys_rt_sigtimedwait; mod sys_rt_tgsigqueueinfo; +mod sys_semctl; +mod sys_semget; +mod sys_semop; +mod sys_semtimedop; mod sys_shmat; mod sys_shmctl; mod sys_shmdt; diff --git a/kernel/src/ipc/syscall/sys_semctl.rs b/kernel/src/ipc/syscall/sys_semctl.rs new file mode 100644 index 0000000000..cdc0054c2d --- /dev/null +++ b/kernel/src/ipc/syscall/sys_semctl.rs @@ -0,0 +1,186 @@ +use crate::alloc::vec::Vec; +use crate::arch::interrupt::TrapFrame; +use crate::syscall::table::FormattedSyscallParam; +use crate::{ + arch::syscall::nr::SYS_SEMCTL, + ipc::sem::{PosixSemIdDs, PosixSemInfo, SemCtlCmd, SemId}, + process::ProcessManager, + syscall::table::Syscall, + syscall::user_access::{UserBufferReader, UserBufferWriter}, +}; +use syscall_table_macros::declare_syscall; +use system_error::SystemError; + +pub struct SysSemctlHandle; + +/// # SYS_SEMCTL 系统调用:控制信号量集合 +/// +/// ## 参数 +/// +/// - `semid`: 信号量集合 id +/// - `semnum`: 信号量索引(部分命令使用) +/// - `cmd`: 操作码 +/// - `arg`: union semun 的地址(SETVAL 时直接为值) +/// +/// ## 返回值 +/// +/// 成功:命令相关返回值(GETVAL 等返回查询值,其余返回 0) +/// 失败:错误码 +pub(super) fn do_kernel_semctl( + semid: SemId, + semnum: usize, + cmd: SemCtlCmd, + arg: usize, + from_user: bool, +) -> Result { + let ipcns = ProcessManager::current_ipcns(); + + match cmd { + // 查看信号量系统信息 + SemCtlCmd::IpcInfo | SemCtlCmd::SemInfo => { + let (ret, sem_info) = { + let guard = ipcns.sem.lock(); + guard.sem_info_data(cmd) + }; + let mut user_buffer_writer = UserBufferWriter::new( + arg as *mut u8, + core::mem::size_of::(), + from_user, + )?; + user_buffer_writer.copy_one_to_user(&sem_info, 0)?; + Ok(ret) + } + // 查看 id 对应的信号量集合信息 + SemCtlCmd::IpcStat | SemCtlCmd::SemStat | SemCtlCmd::SemStatAny => { + let (ret, sem_id_ds) = { + let guard = ipcns.sem.lock(); + guard.sem_stat_data(semid, cmd)? + }; + let mut user_buffer_writer = UserBufferWriter::new( + arg as *mut u8, + core::mem::size_of::(), + from_user, + )?; + user_buffer_writer.copy_one_to_user(&sem_id_ds, 0)?; + Ok(ret) + } + // 设置权限 + SemCtlCmd::IpcSet => { + let mut sem_id_ds = PosixSemIdDs::default(); + let user_buffer_reader = UserBufferReader::new( + arg as *const u8, + core::mem::size_of::(), + from_user, + )?; + user_buffer_reader.copy_one_from_user(&mut sem_id_ds, 0)?; + let mut guard = ipcns.sem.lock(); + guard.ipc_set(semid, sem_id_ds)?; + Ok(0) + } + // 删除信号量集合 + SemCtlCmd::IpcRmid => { + let mut guard = ipcns.sem.lock(); + guard.ipc_rmid(semid)?; + Ok(0) + } + // 单信号量查询 + SemCtlCmd::GetVal | SemCtlCmd::GetPid | SemCtlCmd::GetNcnt | SemCtlCmd::GetZcnt => { + let guard = ipcns.sem.lock(); + guard.sem_get_value(semid, semnum, cmd) + } + // 设置单个信号量的值(arg 直接为数值,非指针) + SemCtlCmd::SetVal => { + let val = arg as u32 as i32; + let mut guard = ipcns.sem.lock(); + guard.setval(semid, semnum, val)?; + Ok(0) + } + // 获取集合内所有信号量的值 + SemCtlCmd::GetAll => { + let vals = { + let guard = ipcns.sem.lock(); + guard.getall(semid)? + }; + let mut user_buffer_writer = UserBufferWriter::new( + arg as *mut u8, + vals.len() * core::mem::size_of::(), + from_user, + )?; + for (i, v) in vals.iter().enumerate() { + user_buffer_writer.copy_one_to_user(v, i * core::mem::size_of::())?; + } + Ok(0) + } + // 设置集合内所有信号量的值 + SemCtlCmd::SetAll => { + let token = { + let guard = ipcns.sem.lock(); + guard.prepare_setall(semid)? + }; + let mut vals: Vec = vec![0; token.nsems()]; + let user_buffer_reader = UserBufferReader::new( + arg as *const u8, + token.nsems() * core::mem::size_of::(), + from_user, + )?; + for (i, v) in vals.iter_mut().enumerate() { + user_buffer_reader.copy_one_from_user(v, i * core::mem::size_of::())?; + } + let mut guard = ipcns.sem.lock(); + guard.setall(token, &vals)?; + Ok(0) + } + // 无效操作码 + SemCtlCmd::Default => Err(SystemError::EINVAL), + } +} + +impl SysSemctlHandle { + #[inline(always)] + fn semid(args: &[usize]) -> SemId { + SemId::new(args[0]) + } + + #[inline(always)] + fn semnum(args: &[usize]) -> usize { + args[1] + } + + #[inline(always)] + fn cmd(args: &[usize]) -> SemCtlCmd { + SemCtlCmd::from(args[2]) + } + + #[inline(always)] + fn arg(args: &[usize]) -> usize { + args[3] + } +} + +impl Syscall for SysSemctlHandle { + fn num_args(&self) -> usize { + 4 + } + + fn handle(&self, args: &[usize], frame: &mut TrapFrame) -> Result { + if args[0] > i32::MAX as usize || args[2] > i32::MAX as usize { + return Err(SystemError::EINVAL); + } + let semid = Self::semid(args); + let semnum = Self::semnum(args); + let cmd = Self::cmd(args); + let arg = Self::arg(args); + do_kernel_semctl(semid, semnum, cmd, arg, frame.is_from_user()) + } + + fn entry_format(&self, args: &[usize]) -> Vec { + vec![ + FormattedSyscallParam::new("semid", format!("{}", Self::semid(args).data())), + FormattedSyscallParam::new("semnum", format!("{}", Self::semnum(args))), + FormattedSyscallParam::new("cmd", format!("{}", Self::cmd(args))), + FormattedSyscallParam::new("arg", format!("{:#x}", Self::arg(args))), + ] + } +} + +declare_syscall!(SYS_SEMCTL, SysSemctlHandle); diff --git a/kernel/src/ipc/syscall/sys_semget.rs b/kernel/src/ipc/syscall/sys_semget.rs new file mode 100644 index 0000000000..4c8e410937 --- /dev/null +++ b/kernel/src/ipc/syscall/sys_semget.rs @@ -0,0 +1,50 @@ +use crate::alloc::vec::Vec; +use crate::arch::interrupt::TrapFrame; +use crate::syscall::table::FormattedSyscallParam; +use crate::{ + arch::syscall::nr::SYS_SEMGET, + ipc::sem::{SemFlags, SemKey}, + process::ProcessManager, + syscall::table::Syscall, +}; +use syscall_table_macros::declare_syscall; +use system_error::SystemError; + +pub struct SysSemgetHandle; + +/// # SYS_SEMGET 系统调用:创建或获取信号量集合 +/// +/// ## 参数 +/// +/// - `key`: 信号量集合键值 +/// - `nsems`: 集合内信号量数量 +/// - `semflg`: 标志位(IPC_CREAT/IPC_EXCL/权限位) +/// +/// ## 返回值 +/// +/// 成功:信号量集合 id +/// 失败:错误码 +impl Syscall for SysSemgetHandle { + fn num_args(&self) -> usize { + 3 + } + + fn handle(&self, args: &[usize], _frame: &mut TrapFrame) -> Result { + let key = SemKey::new(args[0] as u32 as usize); + let nsems = args[1]; + let semflg = SemFlags::from_bits_truncate(args[2] as u32); + let ipcns = ProcessManager::current_ipcns(); + let mut guard = ipcns.sem.lock(); + guard.semget(key, nsems, semflg) + } + + fn entry_format(&self, args: &[usize]) -> Vec { + vec![ + FormattedSyscallParam::new("key", format!("{}", args[0])), + FormattedSyscallParam::new("nsems", format!("{}", args[1])), + FormattedSyscallParam::new("semflg", format!("{:#x}", args[2])), + ] + } +} + +declare_syscall!(SYS_SEMGET, SysSemgetHandle); diff --git a/kernel/src/ipc/syscall/sys_semop.rs b/kernel/src/ipc/syscall/sys_semop.rs new file mode 100644 index 0000000000..2a2964c76a --- /dev/null +++ b/kernel/src/ipc/syscall/sys_semop.rs @@ -0,0 +1,74 @@ +use crate::alloc::vec::Vec; +use crate::arch::interrupt::TrapFrame; +use crate::syscall::table::FormattedSyscallParam; +use crate::{ + arch::syscall::nr::SYS_SEMOP, + ipc::sem::{PosixSemBuf, SemId}, + syscall::table::Syscall, + syscall::user_access::UserBufferReader, +}; +use syscall_table_macros::declare_syscall; +use system_error::SystemError; + +use super::sys_semtimedop::do_kernel_semtimedop; + +pub struct SysSemopHandle; + +/// # SYS_SEMOP 系统调用:原子执行一组信号量操作(无限等待) +/// +/// 与 SYS_SEMTIMEDOP 共享实现,timeout 为 NULL。 +/// +/// ## 参数 +/// +/// - `semid`: 信号量集合 id +/// - `sops`: 用户态 sembuf 数组指针 +/// - `nsops`: 操作数 +/// +/// ## 返回值 +/// +/// 成功:0 +/// 失败:错误码 +impl Syscall for SysSemopHandle { + fn num_args(&self) -> usize { + 3 + } + + fn handle(&self, args: &[usize], frame: &mut TrapFrame) -> Result { + let semid = SemId::new(args[0]); + let nsops = args[2]; + let from_user = frame.is_from_user(); + + if nsops == 0 { + return Err(SystemError::EINVAL); + } + if nsops > crate::ipc::sem::SEMOPM { + return Err(SystemError::E2BIG); + } + + let mut sops: Vec = vec![PosixSemBuf { + sem_num: 0, + sem_op: 0, + sem_flg: 0, + }; nsops]; + let sops_reader = UserBufferReader::new( + args[1] as *const u8, + core::mem::size_of::() * nsops, + from_user, + )?; + for (i, op) in sops.iter_mut().enumerate() { + sops_reader.copy_one_from_user(op, i * core::mem::size_of::())?; + } + + do_kernel_semtimedop(semid, &sops, None) + } + + fn entry_format(&self, args: &[usize]) -> Vec { + vec![ + FormattedSyscallParam::new("semid", format!("{}", args[0])), + FormattedSyscallParam::new("sops", format!("{:#x}", args[1])), + FormattedSyscallParam::new("nsops", format!("{}", args[2])), + ] + } +} + +declare_syscall!(SYS_SEMOP, SysSemopHandle); diff --git a/kernel/src/ipc/syscall/sys_semtimedop.rs b/kernel/src/ipc/syscall/sys_semtimedop.rs new file mode 100644 index 0000000000..f5e20b20fa --- /dev/null +++ b/kernel/src/ipc/syscall/sys_semtimedop.rs @@ -0,0 +1,134 @@ +use crate::alloc::vec::Vec; +use crate::arch::interrupt::TrapFrame; +use crate::ipc::sem::SemManager; +use crate::syscall::table::FormattedSyscallParam; +use crate::{ + arch::syscall::nr::SYS_SEMTIMEDOP, + ipc::sem::{PosixSemBuf, SemId}, + process::ProcessManager, + syscall::table::Syscall, + syscall::user_access::UserBufferReader, + time::{Duration, PosixTimeSpec}, +}; +use syscall_table_macros::declare_syscall; +use system_error::SystemError; + +pub struct SysSemtimedopHandle; + +/// # SYS_SEMTIMEDOP 系统调用:原子执行一组信号量操作,可指定超时 +/// +/// ## 参数 +/// +/// - `semid`: 信号量集合 id +/// - `sops`: 用户态 sembuf 数组指针 +/// - `nsops`: 操作数 +/// - `timeout`: 指向 struct timespec 的指针,为 NULL 时无限等待 +/// +/// ## 返回值 +/// +/// 成功:0 +/// 失败:错误码(EAGAIN 超时、EINTR 被信号中断等) +pub(super) fn do_kernel_semtimedop( + semid: SemId, + sops: &[PosixSemBuf], + timeout: Option, +) -> Result { + let timeout = match timeout { + None => None, + Some(ts) => { + if !ts.is_valid_timeout() { + return Err(SystemError::EINVAL); + } + if ts.tv_sec == 0 && ts.tv_nsec == 0 { + Some(Duration::ZERO) + } else { + let micros = ts.to_ktime_ns().div_ceil(1000); + Some(Duration::from_micros(micros)) + } + } + }; + + let ipcns = ProcessManager::current_ipcns(); + SemManager::semtimedop(&ipcns, semid, sops, timeout) +} + +impl SysSemtimedopHandle { + #[inline(always)] + fn semid(args: &[usize]) -> SemId { + SemId::new(args[0]) + } + + #[inline(always)] + fn sops(args: &[usize]) -> *const u8 { + args[1] as *const u8 + } + + #[inline(always)] + fn nsops(args: &[usize]) -> usize { + args[2] + } + + #[inline(always)] + fn timeout(args: &[usize]) -> *const u8 { + args[3] as *const u8 + } +} + +impl Syscall for SysSemtimedopHandle { + fn num_args(&self) -> usize { + 4 + } + + fn handle(&self, args: &[usize], frame: &mut TrapFrame) -> Result { + let semid = Self::semid(args); + let nsops = Self::nsops(args); + let sops_ptr = Self::sops(args); + let from_user = frame.is_from_user(); + + // 与 Linux 一致:先于任何分配校验 nsops 范围 + if nsops == 0 { + return Err(SystemError::EINVAL); + } + if nsops > crate::ipc::sem::SEMOPM { + return Err(SystemError::E2BIG); + } + + let mut sops: Vec = vec![PosixSemBuf { + sem_num: 0, + sem_op: 0, + sem_flg: 0, + }; nsops]; + let sops_reader = UserBufferReader::new( + sops_ptr, + core::mem::size_of::() * nsops, + from_user, + )?; + for (i, op) in sops.iter_mut().enumerate() { + sops_reader.copy_one_from_user(op, i * core::mem::size_of::())?; + } + + let timeout_ptr = Self::timeout(args); + let timeout = if timeout_ptr.is_null() { + None + } else { + let mut ts = PosixTimeSpec::new(0, 0); + let ts_reader = + UserBufferReader::new(timeout_ptr, core::mem::size_of::(), from_user)?; + ts_reader.copy_one_from_user(&mut ts, 0)?; + Some(ts) + }; + + do_kernel_semtimedop(semid, &sops, timeout) + } + + fn entry_format(&self, args: &[usize]) -> Vec { + vec![ + FormattedSyscallParam::new("semid", format!("{}", Self::semid(args).data())), + FormattedSyscallParam::new("sops", format!("{:#x}", Self::sops(args) as usize)), + FormattedSyscallParam::new("nsops", format!("{}", Self::nsops(args))), + FormattedSyscallParam::new("timeout", format!("{:#x}", Self::timeout(args) as usize)), + ] + } +} + +declare_syscall!(SYS_SEMTIMEDOP, SysSemtimedopHandle); diff --git a/kernel/src/process/namespace/ipc_namespace.rs b/kernel/src/process/namespace/ipc_namespace.rs index 9bb126ecd4..36378436b0 100644 --- a/kernel/src/process/namespace/ipc_namespace.rs +++ b/kernel/src/process/namespace/ipc_namespace.rs @@ -1,5 +1,6 @@ use alloc::sync::{Arc, Weak}; +use crate::ipc::sem::SemManager; use crate::ipc::shm::ShmManager; use crate::libs::spinlock::SpinLock; use crate::process::namespace::{ @@ -21,6 +22,8 @@ pub struct IpcNamespace { /// SysV SHM 管理器(阶段一:仅支持 per-ns shm) pub shm: SpinLock, + /// SysV 信号量管理器 + pub sem: SpinLock, } impl NamespaceOps for IpcNamespace { @@ -36,6 +39,7 @@ impl IpcNamespace { self_ref: weak_self.clone(), user_ns: crate::process::namespace::user_namespace::INIT_USER_NAMESPACE.clone(), shm: SpinLock::new(ShmManager::new()), + sem: SpinLock::new(SemManager::new()), }) } @@ -49,12 +53,13 @@ impl IpcNamespace { if !clone_flags.contains(CloneFlags::CLONE_NEWIPC) { return self.self_ref.upgrade().unwrap(); } - // 创建新的 IPC 命名空间,SHM 空间独立 + // 创建新的 IPC 命名空间,SHM/SEM 空间独立 Arc::new_cyclic(|weak_self| IpcNamespace { ns_common: NsCommon::new(self.ns_common.level + 1, NamespaceType::Ipc), self_ref: weak_self.clone(), user_ns, shm: SpinLock::new(ShmManager::new()), + sem: SpinLock::new(SemManager::new()), }) } } From 7dabc9e0e5823bde12a3c7bfe10ccb6afb4268f2 Mon Sep 17 00:00:00 2001 From: yuming Date: Wed, 12 Aug 2026 22:02:39 +0800 Subject: [PATCH 03/34] test(ipc): add SysV semaphore dunitest suite --- .../suites/normal/sysv_sem_semantics.cc | 991 ++++++++++++++++++ user/apps/tests/dunitest/whitelist.txt | 1 + 2 files changed, 992 insertions(+) create mode 100644 user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc new file mode 100644 index 0000000000..34675964b8 --- /dev/null +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -0,0 +1,991 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef SYS_semget +#define SYS_semget 64 +#endif +#ifndef SYS_semop +#define SYS_semop 65 +#endif +#ifndef SYS_semctl +#define SYS_semctl 66 +#endif +#ifndef SYS_semtimedop +#define SYS_semtimedop 220 +#endif + +#ifndef SEM_STAT +#define SEM_STAT 18 +#endif +#ifndef SEM_STAT_ANY +#define SEM_STAT_ANY 20 +#endif + +// ============ helpers ============ + +int SemGet(key_t key, int nsems, int flags) { + return static_cast(syscall(SYS_semget, key, nsems, flags)); +} + +int SemCtl(int semid, int semnum, int cmd, unsigned long arg) { + return static_cast(syscall(SYS_semctl, semid, semnum, cmd, arg)); +} + +int SemOp(int semid, struct sembuf* sops, size_t nsops) { + return static_cast(syscall(SYS_semop, semid, sops, nsops)); +} + +int SemTimedOp(int semid, struct sembuf* sops, size_t nsops, const struct timespec* timeout) { + return static_cast(syscall(SYS_semtimedop, semid, sops, nsops, timeout)); +} + +key_t UniqueKey() { + static int seq = 0; + return static_cast(0x53000000 ^ (getpid() << 8) ^ (++seq)); +} + +class SemSet { + public: + SemSet(key_t key, int nsems, int flags) : id_(SemGet(key, nsems, flags)) {} + + SemSet(int nsems, int flags) : id_(SemGet(IPC_PRIVATE, nsems, flags)) {} + + // 接管已存在的 id + SemSet(int existing_id, bool owns) : id_(existing_id), owns_(owns) {} + + ~SemSet() { + if (id_ >= 0 && owns_) { + SemCtl(id_, 0, IPC_RMID, 0); + } + } + + SemSet(const SemSet&) = delete; + SemSet& operator=(const SemSet&) = delete; + + bool valid() const { + return id_ >= 0; + } + + int id() const { + return id_; + } + + int release() { + owns_ = false; + return id_; + } + + private: + int id_ = -1; + bool owns_ = true; +}; + +class ChildGuard { + public: + explicit ChildGuard(pid_t pid) : pid_(pid) {} + + ~ChildGuard() { + if (pid_ <= 0) { + return; + } + kill(pid_, SIGKILL); + while (waitpid(pid_, nullptr, 0) < 0 && errno == EINTR) { + } + } + + ChildGuard(const ChildGuard&) = delete; + ChildGuard& operator=(const ChildGuard&) = delete; + + pid_t pid() const { + return pid_; + } + + void MarkReaped() { + pid_ = -1; + } + + private: + pid_t pid_; +}; + +class ScopedAffinity { + public: + bool PinToFirstCpu() { + CPU_ZERO(&saved_); + if (sched_getaffinity(0, sizeof(saved_), &saved_) != 0) { + return false; + } + + cpu_set_t target; + CPU_ZERO(&target); + for (int cpu = 0; cpu < CPU_SETSIZE; ++cpu) { + if (CPU_ISSET(cpu, &saved_)) { + CPU_SET(cpu, &target); + active_ = sched_setaffinity(0, sizeof(target), &target) == 0; + return active_; + } + } + return false; + } + + ~ScopedAffinity() { + if (active_ && sched_setaffinity(0, sizeof(saved_), &saved_) != 0) { + ADD_FAILURE() << "failed to restore affinity: errno=" << errno << " (" + << strerror(errno) << ")"; + } + } + + ScopedAffinity(const ScopedAffinity&) = delete; + ScopedAffinity& operator=(const ScopedAffinity&) = delete; + ScopedAffinity() = default; + + private: + cpu_set_t saved_ = {}; + bool active_ = false; +}; + +class FdGuard { + public: + explicit FdGuard(int fd) : fd_(fd) {} + + ~FdGuard() { + Close(); + } + + FdGuard(const FdGuard&) = delete; + FdGuard& operator=(const FdGuard&) = delete; + + int get() const { + return fd_; + } + + void Close() { + if (fd_ >= 0) { + close(fd_); + fd_ = -1; + } + } + + private: + int fd_; +}; + +void WaitChildOk(pid_t child) { + int status = 0; + ASSERT_EQ(child, waitpid(child, &status, 0)) + << "waitpid failed: errno=" << errno << " (" << strerror(errno) << ")"; + ASSERT_TRUE(WIFEXITED(status)) << "child did not exit normally, status=" << status; + EXPECT_EQ(0, WEXITSTATUS(status)) << "child failed, status=" << status; +} + +void WaitChildOk(ChildGuard* child) { + const pid_t pid = child->pid(); + int status = 0; + pid_t waited; + do { + waited = waitpid(pid, &status, 0); + } while (waited < 0 && errno == EINTR); + if (waited == pid) { + child->MarkReaped(); + } + ASSERT_EQ(pid, waited) + << "waitpid failed: errno=" << errno << " (" << strerror(errno) << ")"; + ASSERT_TRUE(WIFEXITED(status)) << "child did not exit normally, status=" << status; + EXPECT_EQ(0, WEXITSTATUS(status)) << "child failed, status=" << status; +} + +// 等待指定信号量的等待者数量达到预期(用于确认子进程已阻塞) +bool WaitForWaiters(int semid, int semnum, int expected, int timeout_ms = 5000) { + const int deadline = timeout_ms * 1000; + for (int elapsed = 0; elapsed < deadline; elapsed += 10000) { + int ncnt = SemCtl(semid, semnum, GETNCNT, 0); + int zcnt = SemCtl(semid, semnum, GETZCNT, 0); + if (ncnt >= 0 && ncnt + zcnt >= expected) { + return true; + } + usleep(10000); + } + return false; +} + +// ============ creation & lookup ============ + +TEST(SysVSem, CreatePrivateSet) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()) << "semget(IPC_PRIVATE) failed: errno=" << errno; + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, CreateMultipleSems) { + SemSet sem(4, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + for (int i = 0; i < 4; ++i) { + EXPECT_EQ(0, SemCtl(sem.id(), i, GETVAL, 0)) << "sem " << i; + } + EXPECT_EQ(-1, SemCtl(sem.id(), 4, GETVAL, 0)); + EXPECT_EQ(EINVAL, errno) << "GETVAL with out-of-range semnum"; +} + +TEST(SysVSem, KeyedCreateAndLookup) { + const key_t key = UniqueKey(); + const int id1 = SemGet(key, 1, IPC_CREAT | IPC_EXCL | 0600); + ASSERT_GE(id1, 0) << "first semget failed: errno=" << errno; + SemSet guard(id1, true /* owns */); + + const int id2 = SemGet(key, 1, IPC_CREAT | 0600); + ASSERT_GE(id2, 0) << "lookup failed: errno=" << errno; + EXPECT_EQ(id1, id2) << "same key must map to same id"; +} + +TEST(SysVSem, ExistingKeyAllowsZeroNsems) { + const key_t key = UniqueKey(); + SemSet sem(key, 2, IPC_CREAT | IPC_EXCL | 0600); + ASSERT_TRUE(sem.valid()) << "creation failed: errno=" << errno; + + EXPECT_EQ(sem.id(), SemGet(key, 0, 0)) << "existing lookup must allow nsems == 0"; +} + +TEST(SysVSem, NegativeKeyPreservesLow32Bits) { + const key_t key = -((UniqueKey() & 0x3fffffff) | 1); + SemSet sem(key, 1, IPC_CREAT | IPC_EXCL | 0600); + ASSERT_TRUE(sem.valid()) << "negative key creation failed: errno=" << errno; + + EXPECT_EQ(sem.id(), SemGet(key, 0, 0)) << "negative key lookup must find the same set"; +} + +TEST(SysVSem, KeyedCreateExclConflict) { + const key_t key = UniqueKey(); + const int id = SemGet(key, 1, IPC_CREAT | IPC_EXCL | 0600); + ASSERT_GE(id, 0); + SemSet guard(id, true); + + errno = 0; + EXPECT_EQ(-1, SemGet(key, 1, IPC_CREAT | IPC_EXCL | 0600)); + EXPECT_EQ(EEXIST, errno); +} + +TEST(SysVSem, LookupNoCreat) { + errno = 0; + EXPECT_EQ(-1, SemGet(UniqueKey(), 1, 0600)); + EXPECT_EQ(ENOENT, errno); +} + +TEST(SysVSem, CreateLargerNsemsFails) { + const key_t key = UniqueKey(); + const int id = SemGet(key, 2, IPC_CREAT | IPC_EXCL | 0600); + ASSERT_GE(id, 0); + SemSet guard(id, true); + + errno = 0; + EXPECT_EQ(-1, SemGet(key, 3, IPC_CREAT | 0600)); + EXPECT_EQ(EINVAL, errno) << "requesting more sems than existing set"; + + errno = 0; + EXPECT_EQ(-1, SemGet(key, 32001, 0)); + EXPECT_EQ(EINVAL, errno) << "nsems > SEMMSL must fail even for an existing set"; +} + +TEST(SysVSem, InvalidNsems) { + errno = 0; + EXPECT_EQ(-1, SemGet(IPC_PRIVATE, 0, IPC_CREAT | 0600)); + EXPECT_EQ(EINVAL, errno) << "nsems == 0"; + + errno = 0; + EXPECT_EQ(-1, SemGet(IPC_PRIVATE, 32001, IPC_CREAT | 0600)); + EXPECT_EQ(EINVAL, errno) << "nsems > SEMMSL"; +} + +// ============ semctl ============ + +TEST(SysVSem, IpcStatFields) { + SemSet sem(2, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct semid_ds ds; + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_STAT, reinterpret_cast(&ds))) + << "IPC_STAT failed: errno=" << errno; + EXPECT_EQ(2u, ds.sem_nsems); + EXPECT_EQ(0, ds.sem_otime) << "otime must be 0 before any semop"; + EXPECT_GT(ds.sem_ctime, 0) << "ctime set at creation"; + EXPECT_EQ(0600, static_cast(ds.sem_perm.mode & 0777)); +} + +TEST(SysVSem, SemStatAndSemStatAny) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + const int index = sem.id() & 0x7fff; + struct semid_ds ds; + int ret = SemCtl(index, INT_MAX, SEM_STAT, reinterpret_cast(&ds)); + ASSERT_GE(ret, 0) << "SEM_STAT failed: errno=" << errno; + EXPECT_EQ(sem.id(), ret) << "SEM_STAT must return the full semid"; + EXPECT_EQ(1u, ds.sem_nsems); + + ret = SemCtl(index, INT_MAX, SEM_STAT_ANY, reinterpret_cast(&ds)); + ASSERT_GE(ret, 0) << "SEM_STAT_ANY failed: errno=" << errno; + EXPECT_EQ(sem.id(), ret) << "SEM_STAT_ANY must return the full semid"; + EXPECT_EQ(1u, ds.sem_nsems); +} + +TEST(SysVSem, IpcSetMode) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct semid_ds ds; + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_STAT, reinterpret_cast(&ds))); + ds.sem_perm.mode = 0644; + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_SET, reinterpret_cast(&ds))) + << "IPC_SET failed: errno=" << errno; + + memset(&ds, 0, sizeof(ds)); + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_STAT, reinterpret_cast(&ds))); + EXPECT_EQ(0644, static_cast(ds.sem_perm.mode & 0777)); +} + +TEST(SysVSem, OwnerCanControlModeZeroSet) { + const pid_t child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + if (setgid(1000) != 0 || setuid(1000) != 0) { + _exit(20); + } + + SemSet set_target(1, IPC_CREAT | 0000); + if (!set_target.valid()) { + _exit(21); + } + struct semid_ds ds = {}; + ds.sem_perm.uid = geteuid(); + ds.sem_perm.gid = getegid(); + ds.sem_perm.mode = 0400; + if (SemCtl(set_target.id(), 0, IPC_SET, reinterpret_cast(&ds)) != 0) { + _exit(22); + } + + SemSet remove_target(1, IPC_CREAT | 0000); + if (!remove_target.valid()) { + _exit(23); + } + if (SemCtl(remove_target.id(), 0, IPC_RMID, 0) != 0) { + _exit(24); + } + remove_target.release(); + _exit(0); + } + WaitChildOk(child); +} + +TEST(SysVSem, IpcInfoAndSemInfo) { + struct seminfo info; + memset(&info, 0, sizeof(info)); + int max_id = SemCtl(0, 0, IPC_INFO, reinterpret_cast(&info)); + EXPECT_GE(max_id, 0); + EXPECT_EQ(32000 * 32000, info.semmap); + EXPECT_EQ(500, info.semopm); + EXPECT_EQ(500, info.semume); + EXPECT_EQ(20, info.semusz); + EXPECT_EQ(32767, info.semvmx); + EXPECT_EQ(32767, info.semaem); + + struct seminfo before = {}; + ASSERT_GE(SemCtl(0, 0, SEM_INFO, reinterpret_cast(&before)), 0); + + SemSet first(2, IPC_CREAT | 0600); + SemSet second(3, IPC_CREAT | 0600); + ASSERT_TRUE(first.valid()); + ASSERT_TRUE(second.valid()); + + memset(&info, 0, sizeof(info)); + max_id = SemCtl(0, 0, SEM_INFO, reinterpret_cast(&info)); + EXPECT_GE(max_id, 0); + EXPECT_EQ(before.semusz + 2, info.semusz) << "SEM_INFO semusz is the set count"; + EXPECT_EQ(before.semaem + 5, info.semaem) << "SEM_INFO semaem is the semaphore count"; +} + +TEST(SysVSem, SetValGetVal) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 5)) << "SETVAL failed: errno=" << errno; + EXPECT_EQ(5, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, SetValRangeErrors) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + errno = 0; + EXPECT_EQ(-1, SemCtl(sem.id(), 0, SETVAL, 32768)); + EXPECT_EQ(ERANGE, errno) << "SETVAL > SEMVMX"; + + errno = 0; + EXPECT_EQ(-1, SemCtl(sem.id(), 0, SETVAL, -1)); + EXPECT_EQ(ERANGE, errno) << "SETVAL < 0"; +} + +TEST(SysVSem, SetAllGetAll) { + SemSet sem(3, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + unsigned short vals[3] = {1, 2, 3}; + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETALL, reinterpret_cast(vals))) + << "SETALL failed: errno=" << errno; + + unsigned short out[3] = {0, 0, 0}; + ASSERT_EQ(0, SemCtl(sem.id(), 0, GETALL, reinterpret_cast(out))); + EXPECT_EQ(1u, out[0]); + EXPECT_EQ(2u, out[1]); + EXPECT_EQ(3u, out[2]); +} + +TEST(SysVSem, SetAllAtomicRangeError) { + SemSet sem(3, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + unsigned short ok[3] = {1, 2, 3}; + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETALL, reinterpret_cast(ok))); + + unsigned short bad[3] = {1, 40000, 3}; + errno = 0; + EXPECT_EQ(-1, SemCtl(sem.id(), 0, SETALL, reinterpret_cast(bad))); + EXPECT_EQ(ERANGE, errno); + + // 校验失败不得改变任何值 + unsigned short out[3] = {0, 0, 0}; + ASSERT_EQ(0, SemCtl(sem.id(), 0, GETALL, reinterpret_cast(out))); + EXPECT_EQ(1u, out[0]); + EXPECT_EQ(2u, out[1]); + EXPECT_EQ(3u, out[2]); +} + +TEST(SysVSem, SetAllValidatesSetAndPermissionBeforeUserArray) { + SemSet sem(1, IPC_CREAT | 0000); + ASSERT_TRUE(sem.valid()); + + const pid_t child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + if (setgid(1000) != 0 || setuid(1000) != 0) { + _exit(20); + } + errno = 0; + if (SemCtl(sem.id(), 0, SETALL, 1) != -1 || errno != EACCES) { + _exit(21); + } + _exit(0); + } + WaitChildOk(child); + + errno = 0; + EXPECT_EQ(-1, SemCtl(0x3FFFFFFF, 0, SETALL, 1)); + EXPECT_EQ(EINVAL, errno) << "object existence must be checked before the user pointer"; +} + +TEST(SysVSem, GetPidTracksLastSemop) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct sembuf op = {0, 1, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &op, 1)) << "semop failed: errno=" << errno; + EXPECT_EQ(getpid(), SemCtl(sem.id(), 0, GETPID, 0)) << "GETPID must be current pid"; + + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 3)); + EXPECT_EQ(getpid(), SemCtl(sem.id(), 0, GETPID, 0)) << "SETVAL also updates sempid"; +} + +// ============ semop semantics ============ + +TEST(SysVSem, IncrementDecrement) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct sembuf inc = {0, 2, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &inc, 1)); + EXPECT_EQ(2, SemCtl(sem.id(), 0, GETVAL, 0)); + + struct sembuf dec = {0, -1, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &dec, 1)); + EXPECT_EQ(1, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, IncrementOverflowIsErange) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 32766)); + + // semval + op > SEMVMX:立即 ERANGE,不阻塞、不改变任何值 + struct sembuf inc = {0, 2, 0}; + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), &inc, 1)); + EXPECT_EQ(ERANGE, errno); + EXPECT_EQ(32766, SemCtl(sem.id(), 0, GETVAL, 0)) << "no change on ERANGE"; +} + +TEST(SysVSem, WaitForZero) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + // val == 0:立即成功 + struct sembuf op = {0, 0, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &op, 1)); + + // val != 0 + IPC_NOWAIT:EAGAIN + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 3)); + struct sembuf op_nowait = {0, 0, IPC_NOWAIT}; + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), &op_nowait, 1)); + EXPECT_EQ(EAGAIN, errno); +} + +TEST(SysVSem, DecrementBelowZeroNowait) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct sembuf op = {0, -1, IPC_NOWAIT}; + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), &op, 1)); + EXPECT_EQ(EAGAIN, errno); +} + +TEST(SysVSem, AtomicMultiOpRollback) { + SemSet sem(2, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 1)); + + // op1 成功(1-1=0),op2 失败(0-1<0,IPC_NOWAIT)→ 整体 EAGAIN,op1 回滚 + struct sembuf ops[2] = {{0, -1, 0}, {1, -1, IPC_NOWAIT}}; + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), ops, 2)); + EXPECT_EQ(EAGAIN, errno); + + EXPECT_EQ(1, SemCtl(sem.id(), 0, GETVAL, 0)) << "op1 must be rolled back"; +} + +TEST(SysVSem, AtomicMultiOpAllSucceed) { + SemSet sem(2, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 2)); + ASSERT_EQ(0, SemCtl(sem.id(), 1, SETVAL, 5)); + + struct sembuf ops[2] = {{0, -1, 0}, {1, 3, 0}}; + ASSERT_EQ(0, SemOp(sem.id(), ops, 2)); + EXPECT_EQ(1, SemCtl(sem.id(), 0, GETVAL, 0)); + EXPECT_EQ(8, SemCtl(sem.id(), 1, GETVAL, 0)); +} + +TEST(SysVSem, RepeatedSemOperationsObservePrecedingOperations) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct sembuf ops[2] = {{0, 1, 0}, {0, -1, 0}}; + struct timespec timeout = {0, 100 * 1000 * 1000}; + ASSERT_EQ(0, SemTimedOp(sem.id(), ops, 2, &timeout)) + << "the decrement must observe the preceding increment"; + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, RepeatedSemNowaitFailureRollsBackWholeGroup) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 1)); + + struct sembuf ops[2] = {{0, -1, 0}, {0, -1, IPC_NOWAIT}}; + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), ops, 2)); + EXPECT_EQ(EAGAIN, errno); + EXPECT_EQ(1, SemCtl(sem.id(), 0, GETVAL, 0)) << "failed groups must not commit a prefix"; +} + +TEST(SysVSem, CumulativeOverflowRollsBackWholeGroup) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 32766)); + + struct sembuf ops[2] = {{0, 1, 0}, {0, 1, 0}}; + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), ops, 2)); + EXPECT_EQ(ERANGE, errno); + EXPECT_EQ(32766, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, InvalidSemnum) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct sembuf op = {1, 1, 0}; + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), &op, 1)); + EXPECT_EQ(EFBIG, errno) << "out-of-range semnum"; +} + +TEST(SysVSem, InvalidSemid) { + struct sembuf op = {0, 1, 0}; + errno = 0; + EXPECT_EQ(-1, SemOp(0x3FFFFFFF, &op, 1)); + EXPECT_EQ(EINVAL, errno); + + errno = 0; + EXPECT_EQ(-1, SemCtl(0x3FFFFFFF, 0, GETVAL, 0)); + EXPECT_EQ(EINVAL, errno); +} + +TEST(SysVSem, SemUndoRejected) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct sembuf op = {0, 1, SEM_UNDO}; + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), &op, 1)); + EXPECT_EQ(ENOSYS, errno) << "SEM_UNDO unsupported"; +} + +// ============ blocking & wakeup ============ + +TEST(SysVSem, BlockingWakeup) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + const pid_t child = fork(); + ASSERT_GE(child, 0) << "fork failed: errno=" << errno; + + if (child == 0) { + // 子进程阻塞在 -1(val==0) + struct sembuf dec = {0, -1, 0}; + if (SemOp(sem.id(), &dec, 1) != 0) { + _exit(10); + } + _exit(0); + } + + ASSERT_TRUE(WaitForWaiters(sem.id(), 0, 1)) << "child did not block"; + + struct sembuf inc = {0, 1, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &inc, 1)) << "parent inc failed: errno=" << errno; + WaitChildOk(child); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)) << "sem consumed by child"; +} + +TEST(SysVSem, BlockingWakeupZero) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 1)); + + const pid_t child = fork(); + ASSERT_GE(child, 0); + + if (child == 0) { + // 子进程等待 val 变 0 + struct sembuf wait = {0, 0, 0}; + if (SemOp(sem.id(), &wait, 1) != 0) { + _exit(10); + } + _exit(0); + } + + ASSERT_TRUE(WaitForWaiters(sem.id(), 0, 1)) << "child did not block"; + EXPECT_EQ(1, SemCtl(sem.id(), 0, GETZCNT, 0)) << "GETZCNT must count zero-waiters"; + + struct sembuf dec = {0, -1, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &dec, 1)); + WaitChildOk(child); +} + +TEST(SysVSem, GetNcntCountsBlocked) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + const pid_t child = fork(); + ASSERT_GE(child, 0); + + if (child == 0) { + struct sembuf dec = {0, -1, 0}; + if (SemOp(sem.id(), &dec, 1) != 0) { + _exit(10); + } + _exit(0); + } + + ASSERT_TRUE(WaitForWaiters(sem.id(), 0, 1)) << "child did not block"; + EXPECT_EQ(1, SemCtl(sem.id(), 0, GETNCNT, 0)) << "GETNCNT must count blocked decrements"; + + struct sembuf inc = {0, 1, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &inc, 1)); + WaitChildOk(child); +} + +TEST(SysVSem, OneWakeAllMultiWaiters) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + constexpr int kWaiters = 4; + pid_t children[kWaiters]; + for (int i = 0; i < kWaiters; ++i) { + children[i] = fork(); + ASSERT_GE(children[i], 0); + if (children[i] == 0) { + struct sembuf dec = {0, -1, 0}; + if (SemOp(sem.id(), &dec, 1) != 0) { + _exit(10); + } + _exit(0); + } + } + + ASSERT_TRUE(WaitForWaiters(sem.id(), 0, kWaiters)) << "children did not block"; + EXPECT_EQ(kWaiters, SemCtl(sem.id(), 0, GETNCNT, 0)); + + // 一次 +kWaiters 唤醒全部等待者 + struct sembuf inc = {0, kWaiters, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &inc, 1)) << "parent inc failed: errno=" << errno; + for (int i = 0; i < kWaiters; ++i) { + WaitChildOk(children[i]); + } + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, SemtimedopTimeout) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct sembuf dec = {0, -1, 0}; + struct timespec timeout = {0, 100 * 1000 * 1000}; // 100ms + errno = 0; + EXPECT_EQ(-1, SemTimedOp(sem.id(), &dec, 1, &timeout)); + EXPECT_EQ(EAGAIN, errno) << "timeout must return EAGAIN"; + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETNCNT, 0)) << "timeout must remove its queue entry"; + + struct sembuf inc = {0, 1, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &inc, 1)); + EXPECT_EQ(1, SemCtl(sem.id(), 0, GETVAL, 0)) + << "a timed-out operation must not consume a future token"; +} + +TEST(SysVSem, SemtimedopZeroTimeoutNoBlock) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct sembuf dec = {0, -1, 0}; + struct timespec timeout = {0, 0}; + errno = 0; + EXPECT_EQ(-1, SemTimedOp(sem.id(), &dec, 1, &timeout)); + EXPECT_EQ(EAGAIN, errno); +} + +TEST(SysVSem, SemtimedopSucceedsWithinTimeout) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct sembuf dec = {0, -1, 0}; + struct timespec timeout = {2, 0}; // 2s + const pid_t child = fork(); + ASSERT_GE(child, 0); + + if (child == 0) { + if (SemTimedOp(sem.id(), &dec, 1, &timeout) != 0) { + _exit(10); + } + _exit(0); + } + + ASSERT_TRUE(WaitForWaiters(sem.id(), 0, 1)) << "child did not block"; + struct sembuf inc = {0, 1, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &inc, 1)); + WaitChildOk(child); +} + +TEST(SysVSem, SignalInterruptsBlockedSemop) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + const pid_t child_pid = fork(); + ASSERT_GE(child_pid, 0); + ChildGuard child(child_pid); + + if (child_pid == 0) { + struct sigaction sa = {}; + sa.sa_handler = [](int) {}; + sigemptyset(&sa.sa_mask); + if (sigaction(SIGUSR1, &sa, nullptr) != 0) { + _exit(13); + } + + struct sembuf dec = {0, -1, 0}; + int ret = SemOp(sem.id(), &dec, 1); + // 阻塞被打断:必须返回 EINTR + if (ret == 0) { + _exit(11); + } + if (errno != EINTR) { + _exit(12); + } + _exit(0); + } + + ASSERT_TRUE(WaitForWaiters(sem.id(), 0, 1)) << "child did not block"; + ASSERT_EQ(0, kill(child.pid(), SIGUSR1)) << "kill failed: errno=" << errno; + WaitChildOk(&child); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETNCNT, 0)) << "EINTR must remove its queue entry"; + + struct sembuf inc = {0, 1, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &inc, 1)); + EXPECT_EQ(1, SemCtl(sem.id(), 0, GETVAL, 0)) + << "an interrupted operation must not consume a future token"; +} + +TEST(SysVSem, WakingSemopCompletesEligibleQueuedOperationsBeforeReturn) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 1)); + + ScopedAffinity affinity; + ASSERT_TRUE(affinity.PinToFirstCpu()) << "failed to pin test to one CPU: errno=" << errno; + + const pid_t first_pid = fork(); + ASSERT_GE(first_pid, 0); + ChildGuard first(first_pid); + if (first_pid == 0) { + struct sembuf wait_zero = {0, 0, 0}; + _exit(SemOp(sem.id(), &wait_zero, 1) == 0 ? 0 : 11); + } + ASSERT_TRUE(WaitForWaiters(sem.id(), 0, 1)); + ASSERT_EQ(1, SemCtl(sem.id(), 0, GETZCNT, 0)); + + int report_pipe[2]; + ASSERT_EQ(0, pipe(report_pipe)); + FdGuard report_read(report_pipe[0]); + FdGuard report_write(report_pipe[1]); + const pid_t child_pid = fork(); + ASSERT_GE(child_pid, 0); + ChildGuard child(child_pid); + + struct QueueCompletionReport { + int semop_result; + int semop_errno; + int value_after_semop; + int ncnt_after_semop; + int zcnt_after_semop; + }; + + if (child_pid == 0) { + report_read.Close(); + QueueCompletionReport report = {-1, 0, -1, -1, -1}; + if (!WaitForWaiters(sem.id(), 0, 2)) { + report.semop_errno = ETIMEDOUT; + SemCtl(sem.id(), 0, SETVAL, 2); + } else { + struct sembuf inc = {0, 1, 0}; + report.semop_result = SemOp(sem.id(), &inc, 1); + report.semop_errno = errno; + report.value_after_semop = SemCtl(sem.id(), 0, GETVAL, 0); + report.ncnt_after_semop = SemCtl(sem.id(), 0, GETNCNT, 0); + report.zcnt_after_semop = SemCtl(sem.id(), 0, GETZCNT, 0); + } + const ssize_t written = write(report_write.get(), &report, sizeof(report)); + report_write.Close(); + _exit(written == static_cast(sizeof(report)) ? 0 : 10); + } + + report_write.Close(); + struct sembuf dec_two = {0, -2, 0}; + struct timespec queue_timeout = {7, 0}; + ASSERT_EQ(0, SemTimedOp(sem.id(), &dec_two, 1, &queue_timeout)); + + QueueCompletionReport report = {}; + ssize_t received; + do { + received = read(report_read.get(), &report, sizeof(report)); + } while (received < 0 && errno == EINTR); + report_read.Close(); + ASSERT_EQ(static_cast(sizeof(report)), received); + EXPECT_EQ(0, report.semop_result) << "waking semop failed: errno=" << report.semop_errno; + EXPECT_EQ(0, report.value_after_semop) + << "the eligible non-head decrement must commit before the waking semop returns"; + EXPECT_EQ(0, report.ncnt_after_semop); + EXPECT_EQ(0, report.zcnt_after_semop) + << "the zero waiter enabled by the decrement must complete in the same queue scan"; + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 0)); + WaitChildOk(&child); + WaitChildOk(&first); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, RemoveWhileWaiting) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + const pid_t child = fork(); + ASSERT_GE(child, 0); + + if (child == 0) { + struct sembuf dec = {0, -1, 0}; + int ret = SemOp(sem.id(), &dec, 1); + if (ret == 0) { + _exit(11); + } + if (errno != EIDRM) { + _exit(12); + } + _exit(0); + } + + ASSERT_TRUE(WaitForWaiters(sem.id(), 0, 1)) << "child did not block"; + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_RMID, 0)) << "IPC_RMID failed: errno=" << errno; + sem.release(); + WaitChildOk(child); +} + +// ============ concurrency ============ + +TEST(SysVSem, ConcurrentWorkers) { + constexpr int kWorkers = 8; + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, kWorkers)); + + pid_t children[kWorkers]; + for (int i = 0; i < kWorkers; ++i) { + children[i] = fork(); + ASSERT_GE(children[i], 0); + if (children[i] == 0) { + // 每对 (-1, +1):-1 可能阻塞,+1 释放,并发竞争测原子性与唤醒 + struct sembuf dec = {0, -1, 0}; + struct sembuf inc = {0, 1, 0}; + for (int j = 0; j < 100; ++j) { + if (SemOp(sem.id(), &dec, 1) != 0) { + _exit(10); + } + if (SemOp(sem.id(), &inc, 1) != 0) { + _exit(11); + } + } + _exit(0); + } + } + + for (int i = 0; i < kWorkers; ++i) { + WaitChildOk(children[i]); + } + EXPECT_EQ(kWorkers, SemCtl(sem.id(), 0, GETVAL, 0)) << "net zero for each worker"; +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/user/apps/tests/dunitest/whitelist.txt b/user/apps/tests/dunitest/whitelist.txt index e80cea20fc..3e8eb27bb1 100644 --- a/user/apps/tests/dunitest/whitelist.txt +++ b/user/apps/tests/dunitest/whitelist.txt @@ -34,6 +34,7 @@ normal/proc_fd_devfs_readlink normal/mlock_semantics normal/mmap_truncate_cow normal/sysv_shm_semantics +normal/sysv_sem_semantics normal/sched_affinity normal/sched_policy_semantics normal/sync_file_range From a807b3ceb7ffd550c3634bf06347979e00ec75e3 Mon Sep 17 00:00:00 2001 From: yuming Date: Mon, 17 Aug 2026 17:43:52 +0800 Subject: [PATCH 04/34] style(ipc): satisfy kernel formatting checks --- kernel/src/ipc/ipc_perm.rs | 3 +-- kernel/src/ipc/sem.rs | 4 +--- kernel/src/ipc/shm.rs | 9 ++++----- kernel/src/ipc/syscall/sys_semop.rs | 13 ++++++++----- kernel/src/ipc/syscall/sys_semtimedop.rs | 20 +++++++++++++------- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/kernel/src/ipc/ipc_perm.rs b/kernel/src/ipc/ipc_perm.rs index 3296cde094..99409dc1f1 100644 --- a/kernel/src/ipc/ipc_perm.rs +++ b/kernel/src/ipc/ipc_perm.rs @@ -150,8 +150,7 @@ pub fn ipc_permission( granted >>= 3; } - if (requested & !(granted & 0o7)) != 0 && !ns_capable(target_user_ns, CAPFlags::CAP_IPC_OWNER) - { + if (requested & !(granted & 0o7)) != 0 && !ns_capable(target_user_ns, CAPFlags::CAP_IPC_OWNER) { return Err(SystemError::EACCES); } diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index 6c8eac4c0e..d6e566e933 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -326,9 +326,7 @@ impl KernelSemSet { pub fn new(kern_ipc_perm: IpcPerm, nsems: usize) -> Self { KernelSemSet { kern_ipc_perm, - sems: core::iter::repeat(KernelSem { val: 0, pid: None }) - .take(nsems) - .collect(), + sems: core::iter::repeat_n(KernelSem { val: 0, pid: None }, nsems).collect(), sem_otime: 0, sem_ctime: PosixTimeSpec::now().tv_sec, waiters: VecDeque::new(), diff --git a/kernel/src/ipc/shm.rs b/kernel/src/ipc/shm.rs index 9a280a3594..053806f17c 100644 --- a/kernel/src/ipc/shm.rs +++ b/kernel/src/ipc/shm.rs @@ -16,10 +16,7 @@ use crate::{ mm::MemoryManagementArch, process::{ cred::{capable, ns_capable, CAPFlags, Cred, Kgid, Kuid}, - namespace::{ - ipc_namespace::IpcNamespace, - user_namespace::UserNamespace, - }, + namespace::{ipc_namespace::IpcNamespace, user_namespace::UserNamespace}, resource::RLimitID, ProcessManager, RawPid, }, @@ -847,7 +844,9 @@ impl ShmManager { } }; let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - if let Err(err) = ipc_perm::check_lock_permission(&kernel_shm.kern_ipc_perm, &target_user_ns) { + if let Err(err) = + ipc_perm::check_lock_permission(&kernel_shm.kern_ipc_perm, &target_user_ns) + { token.release(); return Err(err); } diff --git a/kernel/src/ipc/syscall/sys_semop.rs b/kernel/src/ipc/syscall/sys_semop.rs index 2a2964c76a..7ee0adecfa 100644 --- a/kernel/src/ipc/syscall/sys_semop.rs +++ b/kernel/src/ipc/syscall/sys_semop.rs @@ -45,11 +45,14 @@ impl Syscall for SysSemopHandle { return Err(SystemError::E2BIG); } - let mut sops: Vec = vec![PosixSemBuf { - sem_num: 0, - sem_op: 0, - sem_flg: 0, - }; nsops]; + let mut sops: Vec = vec![ + PosixSemBuf { + sem_num: 0, + sem_op: 0, + sem_flg: 0, + }; + nsops + ]; let sops_reader = UserBufferReader::new( args[1] as *const u8, core::mem::size_of::() * nsops, diff --git a/kernel/src/ipc/syscall/sys_semtimedop.rs b/kernel/src/ipc/syscall/sys_semtimedop.rs index f5e20b20fa..fb2d4c896d 100644 --- a/kernel/src/ipc/syscall/sys_semtimedop.rs +++ b/kernel/src/ipc/syscall/sys_semtimedop.rs @@ -93,11 +93,14 @@ impl Syscall for SysSemtimedopHandle { return Err(SystemError::E2BIG); } - let mut sops: Vec = vec![PosixSemBuf { - sem_num: 0, - sem_op: 0, - sem_flg: 0, - }; nsops]; + let mut sops: Vec = vec![ + PosixSemBuf { + sem_num: 0, + sem_op: 0, + sem_flg: 0, + }; + nsops + ]; let sops_reader = UserBufferReader::new( sops_ptr, core::mem::size_of::() * nsops, @@ -112,8 +115,11 @@ impl Syscall for SysSemtimedopHandle { None } else { let mut ts = PosixTimeSpec::new(0, 0); - let ts_reader = - UserBufferReader::new(timeout_ptr, core::mem::size_of::(), from_user)?; + let ts_reader = UserBufferReader::new( + timeout_ptr, + core::mem::size_of::(), + from_user, + )?; ts_reader.copy_one_from_user(&mut ts, 0)?; Some(ts) }; From 49075ea09b960176a1d24f78d3206cc71920382a Mon Sep 17 00:00:00 2001 From: yuming Date: Sun, 23 Aug 2026 21:19:51 +0800 Subject: [PATCH 05/34] style(ipc): translate SysV semaphore comments to English --- kernel/src/ipc/sem.rs | 122 +++++++++--------- kernel/src/ipc/syscall/sys_semctl.rs | 37 +++--- kernel/src/ipc/syscall/sys_semget.rs | 16 +-- kernel/src/ipc/syscall/sys_semop.rs | 18 +-- kernel/src/ipc/syscall/sys_semtimedop.rs | 21 +-- kernel/src/process/namespace/ipc_namespace.rs | 14 +- .../suites/normal/sysv_sem_semantics.cc | 25 ++-- 7 files changed, 130 insertions(+), 123 deletions(-) diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index d6e566e933..88875f1ed5 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -29,7 +29,7 @@ use crate::{ }, }; -/// 用于创建新的私有信号量集合 +/// Used to create a new private semaphore set pub const IPC_PRIVATE: SemKey = SemKey::new(0); int_like!(SemId, usize); @@ -51,7 +51,7 @@ impl SemSetAllToken { } } -// Linux include/uapi/linux/sem.h 的限制常量 +// Limit constants from Linux include/uapi/linux/sem.h pub const SEMMNI: usize = 32000; pub const SEMMSL: usize = 32000; pub const SEMMNS: usize = SEMMNI * SEMMSL; @@ -68,36 +68,36 @@ bitflags! { } } -/// 管理信号量集合信息的操作码(Linux x86_64 UAPI include/uapi/linux/sem.h) +/// Semaphore-set control commands (Linux x86_64 UAPI include/uapi/linux/sem.h) #[derive(Eq, Clone, Copy)] pub enum SemCtlCmd { - /// 删除信号量集合 + /// Remove the semaphore set IpcRmid = 0, - /// 设置权限 + /// Set permissions IpcSet = 1, - /// 获取 SemIdDs + /// Retrieve `SemIdDs` IpcStat = 2, - /// 查看 SemInfo + /// Retrieve `SemInfo` IpcInfo = 3, - /// 获取最后操作指定信号量的进程pid + /// Get the PID of the last process to operate on the specified semaphore GetPid = 11, - /// 获取指定信号量的值 + /// Get the specified semaphore value GetVal = 12, - /// 获取集合内所有信号量的值 + /// Get values of all semaphores in the set GetAll = 13, - /// 获取等待指定信号量值增加的进程数 + /// Get the number of processes waiting for the specified semaphore to increase GetNcnt = 14, - /// 获取等待指定信号量值为0的进程数 + /// Get the number of processes waiting for the specified semaphore to reach zero GetZcnt = 15, - /// 设置指定信号量的值 + /// Set the specified semaphore value SetVal = 16, - /// 设置集合内所有信号量的值 + /// Set values of all semaphores in the set SetAll = 17, - /// 按索引获取 SemIdDs + /// Retrieve `SemIdDs` by index SemStat = 18, - /// 查看 SemInfo + /// Retrieve `SemInfo` SemInfo = 19, - /// 按索引获取 SemIdDs(无权限检查) + /// Retrieve `SemIdDs` by index without permission checks SemStatAny = 20, Default, @@ -153,16 +153,16 @@ impl PartialEq for SemCtlCmd { } } -/// 单个信号量(Linux struct sem 的字段) +/// A single semaphore (fields of Linux `struct sem`) #[derive(Debug, Clone)] pub struct KernelSem { /// semval val: i32, - /// sempid:最后操作本信号量的进程 + /// sempid: process that last operated on this semaphore pid: Option>, } -/// 用户态 sembuf(Linux struct sembuf,6 字节) +/// Userspace `sembuf` (Linux `struct sembuf`, 6 bytes) #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct PosixSemBuf { @@ -171,26 +171,26 @@ pub struct PosixSemBuf { pub sem_flg: i16, } -/// 信号量集合信息,符合 Linux x86_64 struct semid64_ds(104 字节,带 -/// 32 位时间戳高半区字段) +/// Semaphore-set information matching Linux x86_64 `struct semid64_ds` (104 bytes, +/// including the high halves of 32-bit timestamp fields) #[repr(C)] #[derive(Debug, Clone, Copy, Default)] pub struct PosixSemIdDs { - /// 权限信息 + /// Permission information pub sem_perm: PosixIpcPerm, - /// 最后一次 semop 的时间(64 位;高 32 位落入 __sem_otime_high) + /// Time of the last `semop` (64-bit; upper 32 bits are in `__sem_otime_high`) pub sem_otime: i64, _sem_otime_high: i64, - /// 最后一次更改信息的时间 + /// Time of the last metadata change pub sem_ctime: i64, _sem_ctime_high: i64, - /// 集合内信号量数量 + /// Number of semaphores in the set pub sem_nsems: usize, _unused1: usize, _unused2: usize, } -/// 信号量系统信息,符合 Linux struct seminfo(40 字节) +/// Semaphore system information matching Linux `struct seminfo` (40 bytes) #[repr(C)] #[derive(Debug, Clone, Copy, Default)] pub struct PosixSemInfo { @@ -228,12 +228,12 @@ impl PosixSemInfo { } } -/// 等待者阻塞的原因,决定唤醒时机与 GETNCNT/GETZCNT 统计 +/// Why a waiter is blocked, determining wakeup timing and GETNCNT/GETZCNT accounting #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum SemWaitType { - /// sem_op < 0:等待 semval 增加(GETNCNT) + /// `sem_op < 0`: wait for semval to increase (GETNCNT) Increase, - /// sem_op == 0:等待 semval 变为 0(GETZCNT) + /// `sem_op == 0`: wait for semval to reach zero (GETZCNT) Zero, } @@ -307,18 +307,18 @@ impl SemQueueEntry { } } -/// 信号量集合 +/// Semaphore set #[derive(Debug)] pub struct KernelSemSet { - /// 权限信息 + /// Permission information pub kern_ipc_perm: IpcPerm, - /// 集合内信号量 + /// Semaphores in the set pub sems: Vec, - /// 最后一次 semop 的时间 + /// Time of the last `semop` pub sem_otime: i64, - /// 最后一次更改信息的时间 + /// Time of the last metadata change pub sem_ctime: i64, - /// 等待者队列 + /// Waiter queue waiters: VecDeque>, } @@ -369,23 +369,23 @@ struct SemopSimulation { values: HashMap, } -/// semop 尝试执行的结果 +/// Result of an attempted `semop` execution #[derive(Debug)] enum SemopOutcome { Ready(SemopSimulation), Blocked(SemBlockedOp), } -/// 信号量管理器 +/// Semaphore manager #[derive(Debug)] pub struct SemManager { - /// SemId 分配器 + /// SemId allocator id_allocator: IpcIdAllocator, - /// 低位 IPC idx 映射信号量集合表 + /// Semaphore set table keyed by low IPC index id2sem: HashMap, - /// SemKey 映射 SemId 表 + /// SemId table keyed by SemKey key2id: HashMap, - /// namespace 内信号量总数(Linux semmns 会计) + /// Total semaphores in the namespace (Linux semmns accounting) total_sems: usize, } @@ -438,7 +438,7 @@ impl SemManager { self.id2sem.keys().copied().max().unwrap_or(0) } - /// # semget:创建或查找信号量集合 + /// # semget: create or look up a semaphore set pub fn semget( &mut self, key: SemKey, @@ -641,14 +641,14 @@ impl SemManager { Err(error) } - /// # semtimedop:原子执行 sops,必要时阻塞 + /// # semtimedop: execute `sops` atomically, blocking if necessary /// - /// 锁由本函数内部管理(等待期间需要释放锁),调用方不得预先持有 - /// `ipcns.sem` 锁。 + /// This function manages the lock internally (it must release it while waiting); + /// callers must not hold the `ipcns.sem` lock in advance. /// - /// - timeout == None:无限等待(等价于 semop) - /// - timeout == Some(Duration::ZERO):不阻塞 - /// - 其余:阻塞至超时,超时返回 EAGAIN + /// - `timeout == None`: wait indefinitely (equivalent to `semop`) + /// - `timeout == Some(Duration::ZERO)`: do not block + /// - Otherwise: block until timeout and return EAGAIN pub fn semtimedop( ipcns: &Arc, semid: SemId, @@ -668,7 +668,8 @@ impl SemManager { return Err(SystemError::ENOSYS); } let non_blocking = timeout == Some(Duration::ZERO); - // 仅等待/等零(全部 sem_op == 0)时检查读权限,否则检查写权限(Linux 语义) + // Check read permission only for all-zero waits; otherwise check write permission + // to match Linux semantics. let alter = sops.iter().any(|op| op.sem_op != 0); let target_user_ns = ipcns.user_ns.clone(); @@ -682,7 +683,7 @@ impl SemManager { let entry = { let mut guard = ipcns.sem.lock(); let set = guard.get_by_semid_checked_mut(semid)?; - // 与 Linux 一致:先检查 semnum 越界(EFBIG),再检查权限(EACCES) + // Match Linux: check semnum bounds (EFBIG) before permissions (EACCES). if sops.iter().any(|op| op.sem_num as usize >= set.sems.len()) { return Err(SystemError::EFBIG); } @@ -740,7 +741,7 @@ impl SemManager { guard.cancel_queued_entry(semid, &entry, error) } - /// # IPC_RMID:删除信号量集合并唤醒所有等待者(返回 EIDRM) + /// # IPC_RMID: remove the semaphore set and wake all waiters with EIDRM pub fn ipc_rmid(&mut self, id: SemId) -> Result<(), SystemError> { let decoded = IpcIdAllocator::decode(id.data())?; let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); @@ -760,7 +761,7 @@ impl SemManager { Ok(()) } - /// # IPC_SET:更新权限(uid/gid/mode)并刷新 sem_ctime + /// # IPC_SET: update permissions (uid/gid/mode) and refresh `sem_ctime` pub fn ipc_set(&mut self, id: SemId, semid_ds: PosixSemIdDs) -> Result<(), SystemError> { let set = self.get_by_semid_checked_mut(id)?; let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); @@ -776,7 +777,7 @@ impl SemManager { Ok(()) } - /// IPC_STAT/SEM_STAT/SEM_STAT_ANY:输出 semid_ds + /// IPC_STAT/SEM_STAT/SEM_STAT_ANY: return `semid_ds` pub fn sem_stat_data( &self, id_or_index: SemId, @@ -811,7 +812,7 @@ impl SemManager { Ok((ret, semid_ds)) } - /// IPC_INFO/SEM_INFO:输出系统信息 + /// IPC_INFO/SEM_INFO: return system information pub fn sem_info_data(&self, cmd: SemCtlCmd) -> (usize, PosixSemInfo) { ( self.current_max_index(), @@ -819,7 +820,7 @@ impl SemManager { ) } - /// GETVAL/GETPID/GETNCNT/GETZCNT:单信号量查询 + /// GETVAL/GETPID/GETNCNT/GETZCNT: query a single semaphore pub fn sem_get_value( &self, id: SemId, @@ -845,9 +846,10 @@ impl SemManager { } } - /// # SETVAL:设置单个信号量的值 + /// # SETVAL: set a single semaphore value pub fn setval(&mut self, id: SemId, semnum: usize, val: i32) -> Result<(), SystemError> { - // 与 Linux 一致:先校验值(ERANGE),再 semnum(EINVAL),再权限(EACCES) + // Match Linux: validate the value (ERANGE), then semnum (EINVAL), then permissions + // (EACCES). if !(0..=SEMVMX).contains(&val) { return Err(SystemError::ERANGE); } @@ -866,7 +868,7 @@ impl SemManager { Ok(()) } - /// # SETALL:设置集合内所有信号量的值(校验失败时不改变任何值) + /// # SETALL: set values of all semaphores in the set without changes on validation failure pub fn setall(&mut self, token: SemSetAllToken, vals: &[u16]) -> Result<(), SystemError> { let set = self .get_by_semid_checked_mut(token.id) @@ -889,7 +891,7 @@ impl SemManager { Ok(()) } - /// # GETALL:获取集合内所有信号量的值 + /// # GETALL: get values of all semaphores in the set pub fn getall(&self, id: SemId) -> Result, SystemError> { let set = self.get_by_semid_checked(id)?; let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); diff --git a/kernel/src/ipc/syscall/sys_semctl.rs b/kernel/src/ipc/syscall/sys_semctl.rs index cdc0054c2d..12a07b83aa 100644 --- a/kernel/src/ipc/syscall/sys_semctl.rs +++ b/kernel/src/ipc/syscall/sys_semctl.rs @@ -13,19 +13,20 @@ use system_error::SystemError; pub struct SysSemctlHandle; -/// # SYS_SEMCTL 系统调用:控制信号量集合 +/// # SYS_SEMCTL syscall: control a semaphore set /// -/// ## 参数 +/// ## Parameters /// -/// - `semid`: 信号量集合 id -/// - `semnum`: 信号量索引(部分命令使用) -/// - `cmd`: 操作码 -/// - `arg`: union semun 的地址(SETVAL 时直接为值) +/// - `semid`: semaphore set ID +/// - `semnum`: semaphore index (used by some commands) +/// - `cmd`: command +/// - `arg`: address of `union semun` (the value itself for SETVAL) /// -/// ## 返回值 +/// ## Return value /// -/// 成功:命令相关返回值(GETVAL 等返回查询值,其余返回 0) -/// 失败:错误码 +/// On success: command-specific result (query commands such as GETVAL return a value; all +/// others return 0). +/// On failure: error code pub(super) fn do_kernel_semctl( semid: SemId, semnum: usize, @@ -36,7 +37,7 @@ pub(super) fn do_kernel_semctl( let ipcns = ProcessManager::current_ipcns(); match cmd { - // 查看信号量系统信息 + // Retrieve semaphore system information SemCtlCmd::IpcInfo | SemCtlCmd::SemInfo => { let (ret, sem_info) = { let guard = ipcns.sem.lock(); @@ -50,7 +51,7 @@ pub(super) fn do_kernel_semctl( user_buffer_writer.copy_one_to_user(&sem_info, 0)?; Ok(ret) } - // 查看 id 对应的信号量集合信息 + // Retrieve information for the semaphore set identified by ID SemCtlCmd::IpcStat | SemCtlCmd::SemStat | SemCtlCmd::SemStatAny => { let (ret, sem_id_ds) = { let guard = ipcns.sem.lock(); @@ -64,7 +65,7 @@ pub(super) fn do_kernel_semctl( user_buffer_writer.copy_one_to_user(&sem_id_ds, 0)?; Ok(ret) } - // 设置权限 + // Set permissions SemCtlCmd::IpcSet => { let mut sem_id_ds = PosixSemIdDs::default(); let user_buffer_reader = UserBufferReader::new( @@ -77,25 +78,25 @@ pub(super) fn do_kernel_semctl( guard.ipc_set(semid, sem_id_ds)?; Ok(0) } - // 删除信号量集合 + // Remove the semaphore set SemCtlCmd::IpcRmid => { let mut guard = ipcns.sem.lock(); guard.ipc_rmid(semid)?; Ok(0) } - // 单信号量查询 + // Query a single semaphore SemCtlCmd::GetVal | SemCtlCmd::GetPid | SemCtlCmd::GetNcnt | SemCtlCmd::GetZcnt => { let guard = ipcns.sem.lock(); guard.sem_get_value(semid, semnum, cmd) } - // 设置单个信号量的值(arg 直接为数值,非指针) + // Set a single semaphore value (`arg` is a value, not a pointer) SemCtlCmd::SetVal => { let val = arg as u32 as i32; let mut guard = ipcns.sem.lock(); guard.setval(semid, semnum, val)?; Ok(0) } - // 获取集合内所有信号量的值 + // Get values of all semaphores in the set SemCtlCmd::GetAll => { let vals = { let guard = ipcns.sem.lock(); @@ -111,7 +112,7 @@ pub(super) fn do_kernel_semctl( } Ok(0) } - // 设置集合内所有信号量的值 + // Set values of all semaphores in the set SemCtlCmd::SetAll => { let token = { let guard = ipcns.sem.lock(); @@ -130,7 +131,7 @@ pub(super) fn do_kernel_semctl( guard.setall(token, &vals)?; Ok(0) } - // 无效操作码 + // Invalid command SemCtlCmd::Default => Err(SystemError::EINVAL), } } diff --git a/kernel/src/ipc/syscall/sys_semget.rs b/kernel/src/ipc/syscall/sys_semget.rs index 4c8e410937..1bcc920fa9 100644 --- a/kernel/src/ipc/syscall/sys_semget.rs +++ b/kernel/src/ipc/syscall/sys_semget.rs @@ -12,18 +12,18 @@ use system_error::SystemError; pub struct SysSemgetHandle; -/// # SYS_SEMGET 系统调用:创建或获取信号量集合 +/// # SYS_SEMGET syscall: create or get a semaphore set /// -/// ## 参数 +/// ## Parameters /// -/// - `key`: 信号量集合键值 -/// - `nsems`: 集合内信号量数量 -/// - `semflg`: 标志位(IPC_CREAT/IPC_EXCL/权限位) +/// - `key`: semaphore-set key +/// - `nsems`: number of semaphores in the set +/// - `semflg`: flags (IPC_CREAT/IPC_EXCL/permission bits) /// -/// ## 返回值 +/// ## Return value /// -/// 成功:信号量集合 id -/// 失败:错误码 +/// On success: semaphore set ID. +/// On failure: error code impl Syscall for SysSemgetHandle { fn num_args(&self) -> usize { 3 diff --git a/kernel/src/ipc/syscall/sys_semop.rs b/kernel/src/ipc/syscall/sys_semop.rs index 7ee0adecfa..e853141959 100644 --- a/kernel/src/ipc/syscall/sys_semop.rs +++ b/kernel/src/ipc/syscall/sys_semop.rs @@ -14,20 +14,20 @@ use super::sys_semtimedop::do_kernel_semtimedop; pub struct SysSemopHandle; -/// # SYS_SEMOP 系统调用:原子执行一组信号量操作(无限等待) +/// # SYS_SEMOP syscall: atomically execute a group of semaphore operations indefinitely /// -/// 与 SYS_SEMTIMEDOP 共享实现,timeout 为 NULL。 +/// Shares its implementation with SYS_SEMTIMEDOP with a NULL timeout. /// -/// ## 参数 +/// ## Parameters /// -/// - `semid`: 信号量集合 id -/// - `sops`: 用户态 sembuf 数组指针 -/// - `nsops`: 操作数 +/// - `semid`: semaphore set ID +/// - `sops`: userspace `sembuf` array pointer +/// - `nsops`: number of operations /// -/// ## 返回值 +/// ## Return value /// -/// 成功:0 -/// 失败:错误码 +/// On success: 0. +/// On failure: error code impl Syscall for SysSemopHandle { fn num_args(&self) -> usize { 3 diff --git a/kernel/src/ipc/syscall/sys_semtimedop.rs b/kernel/src/ipc/syscall/sys_semtimedop.rs index fb2d4c896d..c3c3a79df7 100644 --- a/kernel/src/ipc/syscall/sys_semtimedop.rs +++ b/kernel/src/ipc/syscall/sys_semtimedop.rs @@ -15,19 +15,20 @@ use system_error::SystemError; pub struct SysSemtimedopHandle; -/// # SYS_SEMTIMEDOP 系统调用:原子执行一组信号量操作,可指定超时 +/// # SYS_SEMTIMEDOP syscall: atomically execute a group of semaphore operations with an +/// optional timeout /// -/// ## 参数 +/// ## Parameters /// -/// - `semid`: 信号量集合 id -/// - `sops`: 用户态 sembuf 数组指针 -/// - `nsops`: 操作数 -/// - `timeout`: 指向 struct timespec 的指针,为 NULL 时无限等待 +/// - `semid`: semaphore set ID +/// - `sops`: userspace `sembuf` array pointer +/// - `nsops`: number of operations +/// - `timeout`: pointer to `struct timespec`; NULL waits indefinitely /// -/// ## 返回值 +/// ## Return value /// -/// 成功:0 -/// 失败:错误码(EAGAIN 超时、EINTR 被信号中断等) +/// On success: 0. +/// On failure: error code (EAGAIN on timeout, EINTR on signal interruption, etc.) pub(super) fn do_kernel_semtimedop( semid: SemId, sops: &[PosixSemBuf], @@ -85,7 +86,7 @@ impl Syscall for SysSemtimedopHandle { let sops_ptr = Self::sops(args); let from_user = frame.is_from_user(); - // 与 Linux 一致:先于任何分配校验 nsops 范围 + // Match Linux: validate nsops before making any allocation. if nsops == 0 { return Err(SystemError::EINVAL); } diff --git a/kernel/src/process/namespace/ipc_namespace.rs b/kernel/src/process/namespace/ipc_namespace.rs index 36378436b0..298048ccb5 100644 --- a/kernel/src/process/namespace/ipc_namespace.rs +++ b/kernel/src/process/namespace/ipc_namespace.rs @@ -8,21 +8,21 @@ use crate::process::namespace::{ }; use crate::process::ProcessManager; -// 根 IPC 命名空间 +// Root IPC namespace lazy_static::lazy_static! { pub static ref INIT_IPC_NAMESPACE: Arc = IpcNamespace::new_root(); } -/// DragonOS 的 IPC 命名空间 +/// DragonOS IPC namespace pub struct IpcNamespace { ns_common: NsCommon, self_ref: Weak, - /// 关联的 user namespace (权限判断使用) + /// Associated user namespace (used for permission checks) pub user_ns: Arc, - /// SysV SHM 管理器(阶段一:仅支持 per-ns shm) + /// SysV SHM manager (phase one: per-namespace SHM only) pub shm: SpinLock, - /// SysV 信号量管理器 + /// SysV semaphore manager pub sem: SpinLock, } @@ -43,7 +43,7 @@ impl IpcNamespace { }) } - /// 复制/创建 IPC 命名空间 + /// Copy or create an IPC namespace pub fn copy_ipc_ns( &self, clone_flags: &crate::process::fork::CloneFlags, @@ -53,7 +53,7 @@ impl IpcNamespace { if !clone_flags.contains(CloneFlags::CLONE_NEWIPC) { return self.self_ref.upgrade().unwrap(); } - // 创建新的 IPC 命名空间,SHM/SEM 空间独立 + // Create an independent IPC namespace with separate SHM and semaphore spaces. Arc::new_cyclic(|weak_self| IpcNamespace { ns_common: NsCommon::new(self.ns_common.level + 1, NamespaceType::Ipc), self_ref: weak_self.clone(), diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index 34675964b8..8adaabbc6b 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -67,7 +67,7 @@ class SemSet { SemSet(int nsems, int flags) : id_(SemGet(IPC_PRIVATE, nsems, flags)) {} - // 接管已存在的 id + // Adopt an existing ID SemSet(int existing_id, bool owns) : id_(existing_id), owns_(owns) {} ~SemSet() { @@ -211,7 +211,8 @@ void WaitChildOk(ChildGuard* child) { EXPECT_EQ(0, WEXITSTATUS(status)) << "child failed, status=" << status; } -// 等待指定信号量的等待者数量达到预期(用于确认子进程已阻塞) +// Wait until the target semaphore has the expected number of waiters, confirming that +// child processes have blocked. bool WaitForWaiters(int semid, int semnum, int expected, int timeout_ms = 5000) { const int deadline = timeout_ms * 1000; for (int elapsed = 0; elapsed < deadline; elapsed += 10000) { @@ -467,7 +468,7 @@ TEST(SysVSem, SetAllAtomicRangeError) { EXPECT_EQ(-1, SemCtl(sem.id(), 0, SETALL, reinterpret_cast(bad))); EXPECT_EQ(ERANGE, errno); - // 校验失败不得改变任何值 + // Validation failure must not change any value. unsigned short out[3] = {0, 0, 0}; ASSERT_EQ(0, SemCtl(sem.id(), 0, GETALL, reinterpret_cast(out))); EXPECT_EQ(1u, out[0]); @@ -530,7 +531,7 @@ TEST(SysVSem, IncrementOverflowIsErange) { ASSERT_TRUE(sem.valid()); ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 32766)); - // semval + op > SEMVMX:立即 ERANGE,不阻塞、不改变任何值 + // semval + op > SEMVMX: return ERANGE immediately without blocking or modifying values. struct sembuf inc = {0, 2, 0}; errno = 0; EXPECT_EQ(-1, SemOp(sem.id(), &inc, 1)); @@ -542,7 +543,7 @@ TEST(SysVSem, WaitForZero) { SemSet sem(1, IPC_CREAT | 0600); ASSERT_TRUE(sem.valid()); - // val == 0:立即成功 + // val == 0: succeed immediately struct sembuf op = {0, 0, 0}; ASSERT_EQ(0, SemOp(sem.id(), &op, 1)); @@ -569,7 +570,8 @@ TEST(SysVSem, AtomicMultiOpRollback) { ASSERT_TRUE(sem.valid()); ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 1)); - // op1 成功(1-1=0),op2 失败(0-1<0,IPC_NOWAIT)→ 整体 EAGAIN,op1 回滚 + // op1 succeeds (1 - 1 = 0), then op2 fails (0 - 1 < 0, IPC_NOWAIT): the whole + // group returns EAGAIN and rolls back op1. struct sembuf ops[2] = {{0, -1, 0}, {1, -1, IPC_NOWAIT}}; errno = 0; EXPECT_EQ(-1, SemOp(sem.id(), ops, 2)); @@ -666,7 +668,7 @@ TEST(SysVSem, BlockingWakeup) { ASSERT_GE(child, 0) << "fork failed: errno=" << errno; if (child == 0) { - // 子进程阻塞在 -1(val==0) + // The child blocks on -1 while val == 0. struct sembuf dec = {0, -1, 0}; if (SemOp(sem.id(), &dec, 1) != 0) { _exit(10); @@ -691,7 +693,7 @@ TEST(SysVSem, BlockingWakeupZero) { ASSERT_GE(child, 0); if (child == 0) { - // 子进程等待 val 变 0 + // The child waits for val to become zero. struct sembuf wait = {0, 0, 0}; if (SemOp(sem.id(), &wait, 1) != 0) { _exit(10); @@ -751,7 +753,7 @@ TEST(SysVSem, OneWakeAllMultiWaiters) { ASSERT_TRUE(WaitForWaiters(sem.id(), 0, kWaiters)) << "children did not block"; EXPECT_EQ(kWaiters, SemCtl(sem.id(), 0, GETNCNT, 0)); - // 一次 +kWaiters 唤醒全部等待者 + // A single +kWaiters operation wakes all waiters. struct sembuf inc = {0, kWaiters, 0}; ASSERT_EQ(0, SemOp(sem.id(), &inc, 1)) << "parent inc failed: errno=" << errno; for (int i = 0; i < kWaiters; ++i) { @@ -828,7 +830,7 @@ TEST(SysVSem, SignalInterruptsBlockedSemop) { struct sembuf dec = {0, -1, 0}; int ret = SemOp(sem.id(), &dec, 1); - // 阻塞被打断:必须返回 EINTR + // An interrupted blocked operation must return EINTR. if (ret == 0) { _exit(11); } @@ -964,7 +966,8 @@ TEST(SysVSem, ConcurrentWorkers) { children[i] = fork(); ASSERT_GE(children[i], 0); if (children[i] == 0) { - // 每对 (-1, +1):-1 可能阻塞,+1 释放,并发竞争测原子性与唤醒 + // Each (-1, +1) pair may block on -1 and release on +1; concurrent races + // exercise atomicity and wakeup behavior. struct sembuf dec = {0, -1, 0}; struct sembuf inc = {0, 1, 0}; for (int j = 0; j < 100; ++j) { From 0650199ea9eb5340e0d099bd644c89bd277d3492 Mon Sep 17 00:00:00 2001 From: yuming Date: Sun, 30 Aug 2026 20:56:50 +0800 Subject: [PATCH 06/34] fix(ipc): make IPC_SET permission updates atomic --- kernel/src/ipc/ipc_perm.rs | 7 +++-- .../suites/normal/sysv_sem_semantics.cc | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/kernel/src/ipc/ipc_perm.rs b/kernel/src/ipc/ipc_perm.rs index 99409dc1f1..0b3c08383a 100644 --- a/kernel/src/ipc/ipc_perm.rs +++ b/kernel/src/ipc/ipc_perm.rs @@ -124,8 +124,11 @@ impl IpcPerm { mode: u32, user_ns: &Arc, ) -> Result<(), SystemError> { - self.uid = make_kuid(user_ns, uid)?; - self.gid = make_kgid(user_ns, gid)?; + let uid = make_kuid(user_ns, uid)?; + let gid = make_kgid(user_ns, gid)?; + + self.uid = uid; + self.gid = gid; self.mode = mode & PERM_MASK; Ok(()) } diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index 8adaabbc6b..210486b7a3 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -360,6 +360,33 @@ TEST(SysVSem, IpcSetMode) { EXPECT_EQ(0644, static_cast(ds.sem_perm.mode & 0777)); } +TEST(SysVSem, IpcSetInvalidGidDoesNotPartiallyUpdate) { + if (geteuid() != 0) { + GTEST_SKIP() << "requires root to inspect the set after transferring ownership"; + } + + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct semid_ds before = {}; + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_STAT, reinterpret_cast(&before))); + + struct semid_ds update = before; + update.sem_perm.uid = 1234; + update.sem_perm.gid = UINT32_MAX; + update.sem_perm.mode = 0644; + + errno = 0; + EXPECT_EQ(-1, SemCtl(sem.id(), 0, IPC_SET, reinterpret_cast(&update))); + EXPECT_EQ(EINVAL, errno); + + struct semid_ds after = {}; + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_STAT, reinterpret_cast(&after))); + EXPECT_EQ(before.sem_perm.uid, after.sem_perm.uid); + EXPECT_EQ(before.sem_perm.gid, after.sem_perm.gid); + EXPECT_EQ(before.sem_perm.mode & 0777, after.sem_perm.mode & 0777); +} + TEST(SysVSem, OwnerCanControlModeZeroSet) { const pid_t child = fork(); ASSERT_GE(child, 0); From 4d4346cf1135634b37aafeb998752542d7e256c8 Mon Sep 17 00:00:00 2001 From: yuming Date: Sun, 30 Aug 2026 21:04:33 +0800 Subject: [PATCH 07/34] fix(ipc): make semaphore set allocation fallible --- kernel/src/ipc/id.rs | 88 +++++++++++++++++-- kernel/src/ipc/sem.rs | 27 +++++- kernel/src/ipc/shm.rs | 16 +++- .../suites/normal/sysv_sem_semantics.cc | 28 ++++++ 4 files changed, 144 insertions(+), 15 deletions(-) diff --git a/kernel/src/ipc/id.rs b/kernel/src/ipc/id.rs index b11d93b3bb..4a0a10ff60 100644 --- a/kernel/src/ipc/id.rs +++ b/kernel/src/ipc/id.rs @@ -1,4 +1,4 @@ -use ida::IdAllocator; +use bitmap::{traits::BitMapOps, StaticBitmap}; use system_error::SystemError; /// Linux-compatible SysV IPC id allocator. @@ -7,12 +7,17 @@ use system_error::SystemError; /// The low bits address the object table, and the high bits distinguish stale /// userspace ids after an index is reused. #[derive(Debug)] -pub struct IpcIdAllocator { - ida: IdAllocator, +pub struct FixedIpcIdAllocator { + used: StaticBitmap, + max_ids: usize, + next_idx: usize, seq: usize, last_idx: Option, } +pub type IpcIdAllocator = FixedIpcIdAllocator<32768, { bitmap::static_bitmap_size::<32768>() }>; +pub type ShmIpcIdAllocator = FixedIpcIdAllocator<4096, { bitmap::static_bitmap_size::<4096>() }>; + #[derive(Debug, Clone, Copy)] pub struct IpcId { pub raw: usize, @@ -20,26 +25,35 @@ pub struct IpcId { pub seq: usize, } -impl IpcIdAllocator { +impl FixedIpcIdAllocator { pub const IPC_ID_INDEX_BITS: usize = 15; pub const IPC_ID_IDX_MASK: usize = (1usize << Self::IPC_ID_INDEX_BITS) - 1; pub const IPC_ID_SEQ_SHIFT: usize = Self::IPC_ID_INDEX_BITS; pub const IPC_ID_SEQ_MAX: usize = (i32::MAX as usize) >> Self::IPC_ID_SEQ_SHIFT; pub fn new(max_ids: usize) -> Result { - if max_ids == 0 || max_ids > Self::IPC_ID_IDX_MASK + 1 { + if max_ids == 0 + || max_ids > CAPACITY + || CAPACITY > Self::IPC_ID_IDX_MASK + 1 + || WORDS != bitmap::static_bitmap_size::() + { return Err(SystemError::EINVAL); } Ok(Self { - ida: IdAllocator::new(0, max_ids).ok_or(SystemError::EINVAL)?, + used: StaticBitmap::new(), + max_ids, + next_idx: 0, seq: 0, last_idx: None, }) } pub fn alloc(&mut self) -> Result { - let idx = self.ida.alloc().ok_or(SystemError::ENOSPC)?; + let idx = self.find_free_idx().ok_or(SystemError::ENOSPC)?; + debug_assert_eq!(self.used.set(idx, true), Some(false)); + self.next_idx = if idx + 1 == self.max_ids { 0 } else { idx + 1 }; + if let Some(last_idx) = self.last_idx { if idx <= last_idx { self.seq += 1; @@ -57,8 +71,25 @@ impl IpcIdAllocator { }) } + fn find_free_idx(&self) -> Option { + if self.used.get(self.next_idx) == Some(false) { + return Some(self.next_idx); + } + + self.used + .next_false_index(self.next_idx) + .filter(|&idx| idx < self.max_ids) + .or_else(|| { + self.used + .first_false_index() + .filter(|&idx| idx < self.max_ids) + }) + } + pub fn free_idx(&mut self, idx: usize) { - self.ida.free(idx); + if idx < self.max_ids { + self.used.set(idx, false); + } } pub fn decode(raw: usize) -> Result { @@ -76,3 +107,44 @@ impl IpcIdAllocator { (seq << Self::IPC_ID_SEQ_SHIFT) | idx } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn allocates_until_capacity_then_returns_enospc() { + let mut allocator = IpcIdAllocator::new(2).unwrap(); + + assert_eq!(allocator.alloc().unwrap().idx, 0); + assert_eq!(allocator.alloc().unwrap().idx, 1); + assert_eq!(allocator.alloc().unwrap_err(), SystemError::ENOSPC); + } + + #[test] + fn reuses_freed_index_with_a_new_sequence() { + let mut allocator = IpcIdAllocator::new(2).unwrap(); + let old = allocator.alloc().unwrap(); + let retained = allocator.alloc().unwrap(); + allocator.free_idx(old.idx); + + let reused = allocator.alloc().unwrap(); + + assert_eq!(retained.idx, 1); + assert_eq!(reused.idx, old.idx); + assert_ne!(reused.raw, old.raw); + assert_eq!(reused.seq, old.seq + 1); + } + + #[test] + fn rejects_invalid_capacity() { + assert_eq!(IpcIdAllocator::new(0).unwrap_err(), SystemError::EINVAL); + assert_eq!( + IpcIdAllocator::new(IpcIdAllocator::IPC_ID_IDX_MASK + 2).unwrap_err(), + SystemError::EINVAL + ); + + type InvalidAllocator = FixedIpcIdAllocator<64, 0>; + assert_eq!(InvalidAllocator::new(64).unwrap_err(), SystemError::EINVAL); + } +} diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index 88875f1ed5..ad25b4ed36 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -323,10 +323,18 @@ pub struct KernelSemSet { } impl KernelSemSet { - pub fn new(kern_ipc_perm: IpcPerm, nsems: usize) -> Self { + fn try_allocate_sems(nsems: usize) -> Result, SystemError> { + let mut sems = Vec::new(); + sems.try_reserve_exact(nsems) + .map_err(|_| SystemError::ENOMEM)?; + sems.resize(nsems, KernelSem { val: 0, pid: None }); + Ok(sems) + } + + fn new(kern_ipc_perm: IpcPerm, sems: Vec) -> Self { KernelSemSet { kern_ipc_perm, - sems: core::iter::repeat_n(KernelSem { val: 0, pid: None }, nsems).collect(), + sems, sem_otime: 0, sem_ctime: PosixTimeSpec::now().tv_sec, waiters: VecDeque::new(), @@ -496,6 +504,16 @@ impl SemManager { return Err(SystemError::ENOSPC); } + self.id2sem + .try_reserve(1) + .map_err(|_| SystemError::ENOMEM)?; + if key != IPC_PRIVATE { + self.key2id + .try_reserve(1) + .map_err(|_| SystemError::ENOMEM)?; + } + let sems = KernelSemSet::try_allocate_sems(nsems)?; + let ipc_id = self.id_allocator.alloc()?; let sem_id = SemId::new(ipc_id.raw); let current_cred = ProcessManager::current_pcb().cred(); @@ -506,7 +524,7 @@ impl SemManager { semflg.bits() & SemFlags::PERM_MASK.bits(), ipc_id.seq, ); - let set = KernelSemSet::new(kern_ipc_perm, nsems); + let set = KernelSemSet::new(kern_ipc_perm, sems); if key != IPC_PRIVATE { self.key2id.insert(key, sem_id); @@ -929,7 +947,8 @@ mod tests { fn insert_test_set(manager: &mut SemManager, key: SemKey, vals: &[i32]) -> SemId { let ipc_id = manager.id_allocator.alloc().unwrap(); let id = SemId::new(ipc_id.raw); - let mut set = KernelSemSet::new(test_perm(id, key, ipc_id.seq), vals.len()); + let sems = KernelSemSet::try_allocate_sems(vals.len()).unwrap(); + let mut set = KernelSemSet::new(test_perm(id, key, ipc_id.seq), sems); for (sem, val) in set.sems.iter_mut().zip(vals.iter().copied()) { sem.val = val; } diff --git a/kernel/src/ipc/shm.rs b/kernel/src/ipc/shm.rs index 053806f17c..69028dcae5 100644 --- a/kernel/src/ipc/shm.rs +++ b/kernel/src/ipc/shm.rs @@ -9,7 +9,7 @@ use crate::{ }, }, ipc::{ - id::IpcIdAllocator, + id::{IpcIdAllocator, ShmIpcIdAllocator}, ipc_perm::{self, IpcPermView, PosixIpcPerm}, }, libs::mutex::Mutex, @@ -355,7 +355,7 @@ impl Drop for SysVShmAttachGuard { #[derive(Debug)] pub struct ShmManager { /// ShmId分配器 - id_allocator: IpcIdAllocator, + id_allocator: ShmIpcIdAllocator, /// 低位 IPC idx 映射共享内存信息表 id2shm: HashMap, /// ShmKey映射ShmId表 @@ -377,7 +377,7 @@ impl ShmManager { pub fn new() -> Self { ShmManager { - id_allocator: IpcIdAllocator::new(PosixShmMetaInfo::SHMMNI).unwrap(), + id_allocator: ShmIpcIdAllocator::new(PosixShmMetaInfo::SHMMNI).unwrap(), id2shm: HashMap::new(), key2id: HashMap::new(), total_pages: 0, @@ -455,6 +455,16 @@ impl ShmManager { .total_pages .checked_add(numpages) .ok_or(SystemError::ENOSPC)?; + + self.id2shm + .try_reserve(1) + .map_err(|_| SystemError::ENOMEM)?; + if key != IPC_PRIVATE { + self.key2id + .try_reserve(1) + .map_err(|_| SystemError::ENOMEM)?; + } + let ipc_id = self.id_allocator.alloc()?; let shm_id = ShmId::new(ipc_id.raw); diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index 210486b7a3..d5d1df02ca 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -447,6 +447,34 @@ TEST(SysVSem, IpcInfoAndSemInfo) { EXPECT_EQ(before.semaem + 5, info.semaem) << "SEM_INFO semaem is the semaphore count"; } +TEST(SysVSem, KeyedCreateRemovalRestoresAccountingAndAllowsReuse) { + struct seminfo before = {}; + ASSERT_GE(SemCtl(0, 0, SEM_INFO, reinterpret_cast(&before)), 0); + + const key_t key = UniqueKey(); + int semid = SemGet(key, 4, IPC_CREAT | IPC_EXCL | 0600); + ASSERT_GE(semid, 0) << "semget failed: errno=" << errno; + EXPECT_EQ(semid, SemGet(key, 0, 0)); + + struct seminfo created = {}; + ASSERT_GE(SemCtl(0, 0, SEM_INFO, reinterpret_cast(&created)), 0); + EXPECT_EQ(before.semusz + 1, created.semusz); + EXPECT_EQ(before.semaem + 4, created.semaem); + + ASSERT_EQ(0, SemCtl(semid, 0, IPC_RMID, 0)); + errno = 0; + EXPECT_EQ(-1, SemGet(key, 0, 0)); + EXPECT_EQ(ENOENT, errno); + + struct seminfo removed = {}; + ASSERT_GE(SemCtl(0, 0, SEM_INFO, reinterpret_cast(&removed)), 0); + EXPECT_EQ(before.semusz, removed.semusz); + EXPECT_EQ(before.semaem, removed.semaem); + + SemSet reused(key, 4, IPC_CREAT | IPC_EXCL | 0600); + ASSERT_TRUE(reused.valid()) << "key must be reusable after IPC_RMID: errno=" << errno; +} + TEST(SysVSem, SetValGetVal) { SemSet sem(1, IPC_CREAT | 0600); ASSERT_TRUE(sem.valid()); From 1257bce2ca64a52bb25825e78f5134c9c9b51dac Mon Sep 17 00:00:00 2001 From: yuming Date: Fri, 4 Sep 2026 17:32:03 +0800 Subject: [PATCH 08/34] feat(ipc): implement SysV SEM_UNDO lifecycle --- .../2026-09-03-sem-undo-implementation.md | 1277 +++++++++++++ .../specs/2026-09-03-sem-undo-design.md | 687 +++++++ kernel/src/ipc/mod.rs | 1 + kernel/src/ipc/sem.rs | 1612 ++++++++++++++++- kernel/src/ipc/sem_undo.rs | 983 ++++++++++ kernel/src/ipc/syscall/sys_semtimedop.rs | 30 +- kernel/src/process/fork.rs | 62 +- kernel/src/process/manager/exit.rs | 6 + kernel/src/process/namespace/nsproxy.rs | 275 ++- kernel/src/process/namespace/setns.rs | 108 +- kernel/src/process/namespace/unshare.rs | 109 +- kernel/src/process/pid.rs | 34 +- kernel/src/process/task.rs | 123 +- kernel/src/rcu/mod.rs | 101 ++ kernel/src/rcu/selftest.rs | 32 + .../suites/normal/sysv_sem_semantics.cc | 1018 ++++++++++- 16 files changed, 6265 insertions(+), 193 deletions(-) create mode 100644 docs/superpowers/plans/2026-09-03-sem-undo-implementation.md create mode 100644 docs/superpowers/specs/2026-09-03-sem-undo-design.md create mode 100644 kernel/src/ipc/sem_undo.rs diff --git a/docs/superpowers/plans/2026-09-03-sem-undo-implementation.md b/docs/superpowers/plans/2026-09-03-sem-undo-implementation.md new file mode 100644 index 0000000000..da1816be5a --- /dev/null +++ b/docs/superpowers/plans/2026-09-03-sem-undo-implementation.md @@ -0,0 +1,1277 @@ +# SEM_UNDO Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 DragonOS 的 System V semaphore 实现中,以 Linux 6.6 生命周期语义实现 `SEM_UNDO`,同时保证立即提交、排队重试、控制操作、fork/clone、namespace 切换和 task exit 的记录与回放均具备失败原子性。 + +**Architecture:** 每个 PCB 持有一个显式的 `SemUndoAttachment` slot;attachment 持有 `Arc`,group 绑定唯一 `Weak`,并以完整、含 generation 的 `SemId` 保存每个 semaphore-set 的 adjustment slice。每个 IPC namespace 的 `SemManager` 用 `Arc>>` registry snapshot 枚举仍活跃的 group;排队操作以不计入 task owner 数的强 `Arc` observer 固定原调用者。操作仍由既有 `IpcNamespace.sem: SpinLock` 串行化;本计划不引入 per-set lock、全局 namespace 重构或新的用户 ABI。 + +**Tech Stack:** Rust `no_std` kernel、`alloc::{Arc, Weak, Vec, Box}`、DragonOS `SpinLock`、System V semaphore syscalls、GoogleTest dunitest、QEMU dunitest runner。 + +## Global Constraints + +- 只在最终提交移除 [`kernel/src/ipc/sem.rs:683-688`](kernel/src/ipc/sem.rs#L683-L688) 的 `SEM_UNDO -> ENOSYS` 门禁;此前所有 `SEM_UNDO` syscall 仍必须返回 `ENOSYS`。 +- PCB `sem_undo` slot lock 不得与其他 SEM_UNDO 锁嵌套;跨对象方向固定为 `IpcNamespace.sem` manager lock → `SemUndoGroup.inner` → queue-entry 的 `undo_record` / `scratch` / `status` 锁。禁止任何 group → manager 反向获取。 +- 禁止在持有 `ipcns.sem` manager spinlock 时动态分配:不得保留 [`simulate_semop()` 的 `HashMap::with_capacity()`,sem.rs:539-580](kernel/src/ipc/sem.rs#L539-L580)、[`SemQueueEntry::new()` 的 `to_vec()`,sem.rs:264-276](kernel/src/ipc/sem.rs#L264-L276) 或 blocked 分支的 `Arc::new()`(sem.rs:702-735)。 +- 所有用户触发的增长都用 `Arc::try_new`、`Vec::try_reserve_exact` 等 fallible API,并映射为 `SystemError::ENOMEM`;任何 allocation error 均不得改变 semval、semadj、queue、record 或 registry。 +- record key 必须是完整 `SemId`,不得使用低 index;`adjustments.len()` 必须严格等于目标 `KernelSemSet.sems.len()`。 +- `SEMUME`、`SEMMNU` 只维持 [`PosixSemInfo::new()`](kernel/src/ipc/sem.rs#L210-L230) 的报告兼容,绝不参与 admission;操作数上限继续使用全局 `SEMOPM = 500`。 +- 维持一个 namespace 一把 manager lock 的既有结构;明确**不实现 per-set lock**。 +- 不能把 namespace/fork 全面 publication 重构作为 SEM_UNDO 主体;仅完成“child owner token 在任何 child publication 前安装、任何后续可失败路径先回滚 publication 再撤销 owner”的最小安全协议。 +- 所有 kernel 内部测试放在相关模块的 `#[cfg(test)] mod tests`;当前 kernel `Cargo.toml` 的 library 是 `staticlib`,根 `kernel/Makefile:test` 只跑 workspace crates 且排除 `dragonos_kernel`,因此新增的 kernel `#[test]` 需使用项目实际可运行的内核测试 target,不能把 `make -C kernel test` 误报为这些测试的执行证据。 +- dunitest 源文件已存在于 [`user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc`](user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc),且 whitelist 已含 `normal/sysv_sem_semantics`(`whitelist.txt:37`);无需新建 CMake 注册项。 + +--- + +## 实际落点与职责 + +| 文件 | 变更职责 | +|---|---| +| [`kernel/src/ipc/sem_undo.rs`](kernel/src/ipc/sem_undo.rs) | 新模块:attachment、group、record、owner 计数、record/registry prepare、SETVAL/SETALL/RMID cleanup、最后 owner replay 的可测试原语。 | +| [`kernel/src/ipc/mod.rs:1-12`](kernel/src/ipc/mod.rs#L1-L12) | 导出 `sem_undo`。 | +| [`kernel/src/ipc/sem.rs:6-1008`](kernel/src/ipc/sem.rs) | registry 字段;无分配 simulation/commit;queue 捕获 group;SETVAL/SETALL/RMID cleanup;semtimedop 最终接入。 | +| [`kernel/src/process/task.rs:59-237, 325-407, 1581-1594`](kernel/src/process/task.rs) | PCB `SpinLock>` 与语义 API。 | +| [`kernel/src/process/fork.rs:575-1100`](kernel/src/process/fork.rs) | `CLONE_NEWIPC|CLONE_SYSVSEM` 早拒绝;普通 fork 不继承;显式 SYSVSEM 共享、child attachment guard、最小 rollback protocol。 | +| [`kernel/src/process/manager/exit.rs:450-488, 662-704`](kernel/src/process/manager/exit.rs) | robust-list 后、`exit_mm` 前调用唯一 detach/replay;reap 只断言 slot 已空。 | +| [`kernel/src/process/namespace/nsproxy.rs:294-343`](kernel/src/process/namespace/nsproxy.rs) | 将 IPC detach 作为 namespace install 的明确输入;prepare 完成后、安装新 nsproxy 前同步 detach。 | +| [`kernel/src/process/namespace/unshare.rs:19-53`](kernel/src/process/namespace/unshare.rs) | `CLONE_SYSVSEM` 和 `CLONE_NEWIPC` 设置 detach 意图,并接入统一 install helper。 | +| [`kernel/src/process/namespace/setns.rs:96-267`](kernel/src/process/namespace/setns.rs) | pidfd / namespace-fd 两条路径均传递 `detach_sysvsem`,same-Arc IPC setns 仍 detach。 | +| [`kernel/src/process/namespace/ipc_namespace.rs:17-64`](kernel/src/process/namespace/ipc_namespace.rs) | 仅增加已审计的不变量断言,或在无法证明时加明确调用上下文的 `destroy_all()`;禁止在 `Drop` 取 manager lock。 | +| [`kernel/src/process/pid.rs:630-681`](kernel/src/process/pid.rs) | 如 replay 需要以 group namespace 可见的 TGID 更新 `sempid`,将现有私有 `task_pid_nr_ns(PidType::TGID, ...)` 以最小方式开放给 process/IPС 内部调用者;不能用 raw PID 或 `task_pid_vnr()` 替代。 | +| [`user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc:41-226, 706-714`](user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc) | 删除 `SemUndoRejected`,改为公开 ABI SEM_UNDO 语义测试。 | +| [`user/apps/tests/dunitest/Makefile:24-105`](user/apps/tests/dunitest/Makefile) | 不应修改;已有 `wildcard suites/*/*.cc` 自动构建。 | +| [`user/apps/tests/dunitest/whitelist.txt:36-38`](user/apps/tests/dunitest/whitelist.txt#L36-L38) | 不应修改;已有 test binary 白名单。 | + +--- + +### Task 1: 固化 SEM_UNDO 所有权模型和 PCB slot,但保持 ABI 关闭 + +**Files:** +- Create: `kernel/src/ipc/sem_undo.rs` +- Modify: `kernel/src/ipc/mod.rs:1-12` +- Modify: `kernel/src/process/task.rs:5-56, 59-237, 325-407` +- Modify: `kernel/src/ipc/sem.rs:388-418` +- Test: `kernel/src/ipc/sem_undo.rs` 的 `#[cfg(test)] mod tests` +- Test: `kernel/src/ipc/sem.rs:930-1008` + +**Interfaces:** +- Produces: + ```rust + pub struct SemUndoAttachment { /* group: Arc */ } + + pub struct SemUndoGroup { + /* ipc_ns: Weak, + inner: SpinLock */ + } + + pub(crate) struct UnpublishedSemUndoAttachmentGuard { /* fork-local */ } + + impl ProcessControlBlock { + pub fn sem_undo_group(&self) -> Option>; + pub fn ensure_sem_undo_group( + &self, + ipc_ns: &Arc, + ) -> Result, SystemError>; + pub fn take_sem_undo_attachment(&self) -> Option; + } + ``` +- Consumes: `IpcNamespace` 的 `Arc`/`Weak`([`ipc_namespace.rs:17-64`](kernel/src/process/namespace/ipc_namespace.rs#L17-L64))、`SpinLock`、`SystemError::ENOMEM`。 +- Does not produce any user-visible SEM_UNDO behavior in this task. + +- [ ] **Step 1: 写失败的 ownership 单测。** + + 在新模块 `sem_undo.rs` 的 `#[cfg(test)]` 中先覆盖以下不可变行为: + + ```rust + #[test] + fn observer_arc_does_not_change_task_owner_count() { + let group = SemUndoGroup::new_for_test(); + let attachment = SemUndoAttachment::new_for_test(group.clone()); + let observer = attachment.group_for_test(); + assert_eq!(group.task_owners_for_test(), 1); + drop(observer); + assert_eq!(group.task_owners_for_test(), 1); + } + + #[test] + fn attachment_is_taken_once_and_drop_never_replays() { + let attachment = SemUndoAttachment::new_for_test(SemUndoGroup::new_for_test()); + let mut slot = Some(attachment); + assert!(slot.take().is_some()); + assert!(slot.take().is_none()); + } + + #[test] + fn group_rejects_different_ipc_namespace() { + let group = SemUndoGroup::new_for_test_bound_to_first_namespace(); + assert_eq!( + group.verify_ipc_ns_for_test(second_test_ipc_ns()), + Err(SystemError::EINVAL) + ); + } + ``` + + 测试 hook 只能在 `cfg(test)` 存在;产品路径不允许公开 fake namespace 构造器或 failpoint ABI。 + +- [ ] **Step 2: 运行并确认测试失败。** + + Run(选择 DragonOS 实际 kernel-test 编译命令,以下为必须执行的 direct target): + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib ipc::sem_undo::tests --target x86_64-unknown-linux-gnu + ``` + + Expected: 编译失败,原因是 `ipc::sem_undo`、`SemUndoGroup` 和测试 helper 尚不存在。 + + 同时记录基线门禁: + + ```bash + grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs + ``` + + Expected: 仍能看到 `return Err(SystemError::ENOSYS);`。 + +- [ ] **Step 3: 实现最小数据模型和 PCB slot。** + + 在 `sem_undo.rs` 实现且只实现以下所有权基础: + + ```rust + pub struct SemUndoAttachment { + group: Arc, + } + + pub struct SemUndoGroup { + ipc_ns: Weak, + inner: SpinLock, + } + + struct SemUndoGroupState { + task_owners: usize, + records: Vec, + } + + struct SemUndoRecord { + semid: SemId, + adjustments: Box<[i16]>, + } + ``` + + 具体规则: + + 1. `SemUndoAttachment` 不派生 `Clone`。 + 2. `SemUndoGroup::new(ipc_ns: &Arc) -> Result, SystemError>` 使用 `Arc::try_new`,初始 `task_owners = 1`,`records = Vec::new()`。 + 3. PCB 新字段为: + ```rust + pub(super) sem_undo: SpinLock>, + ``` + 在 [`ProcessControlBlock::do_create_pcb()` 的 struct literal,task.rs:325-397](kernel/src/process/task.rs#L325-L397) 初始化为 `SpinLock::new(None)`。 + 4. `sem_undo_group()` 只短暂获取 PCB slot lock、clone `Arc` 后释放;不能返回 attachment。 + 5. `ensure_sem_undo_group()`:slot lock 检查为空后释放;锁外 `SemUndoGroup::new()`;再次取得 slot lock,已有 group 胜出时丢弃 candidate,否则安装 `SemUndoAttachment`。不嵌套 slot lock 与 group/manager lock。 + 6. `take_sem_undo_attachment()` 仅 `self.sem_undo.lock_irqsave().take()`;无 replay、无 manager lock。 + 7. `Drop for SemUndoAttachment` 仅 debug assertion / diagnostic,绝不可 replay 或获取 manager lock。 + 8. 给 `SemManager` 增加: + ```rust + undo_groups: Arc>>, + ``` + 并在 [`SemManager::new()`,sem.rs:411-418](kernel/src/ipc/sem.rs#L411-L418) 初始化为空 slice snapshot。此任务不注册任何 group。 + +- [ ] **Step 4: 运行测试并确认通过,且 ABI 仍关闭。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib ipc::sem_undo::tests --target x86_64-unknown-linux-gnu + ``` + + Expected: 新 ownership tests PASS。 + + Run: + + ```bash + grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs + ``` + + Expected: `ENOSYS` gate 仍存在。 + +- [ ] **Step 5: 编译目标内核。** + + Run: + + ```bash + cd . + make kernel + ``` + + Expected: `Kernel Build Done.`,无 SEM_UNDO 相关编译或 clippy error。 + +- [ ] **Step 6: Commit。** + + ```bash + git add kernel/src/ipc/mod.rs kernel/src/ipc/sem.rs kernel/src/ipc/sem_undo.rs kernel/src/process/task.rs + git commit -m "feat(ipc): add private sem undo ownership model" + ``` + +--- + +### Task 2: 接入 clone owner sharing,并建立最小 child rollback 安全协议 + +**Files:** +- Modify: `kernel/src/ipc/sem_undo.rs` +- Modify: `kernel/src/process/task.rs` +- Modify: `kernel/src/process/fork.rs:575-1100` +- Modify: `kernel/src/process/namespace/nsproxy.rs:125-200` +- Test: `kernel/src/ipc/sem_undo.rs` 的 `#[cfg(test)]` +- Test: `kernel/src/process/fork.rs` 的现有或新增 `#[cfg(test)]` 模块 + +**Interfaces:** +- Produces: + ```rust + impl ProcessControlBlock { + pub(crate) fn prepare_shared_sem_undo_attachment( + &self, + ipc_ns: &Arc, + ) -> Result; + } + + impl UnpublishedSemUndoAttachmentGuard { + pub(crate) fn install_into( + &mut self, + child: &ProcessControlBlock, + ); + pub(crate) fn disarm(self); + } + ``` +- Consumes: Task 1 的 attachment/group/PCB API、`CloneFlags::CLONE_SYSVSEM`([`fork.rs:37-93`](kernel/src/process/fork.rs#L37-L93))。 + +- [ ] **Step 1: 先写失败测试。** + + 覆盖以下最小协议,而不是用 `Arc::strong_count()` 推断 owner: + + ```rust + #[test] + fn ordinary_fork_child_starts_without_attachment() { + let parent = test_pcb_with_group(); + let child = test_unpublished_child(); + assert!(child.sem_undo_group().is_none()); + assert!(parent.sem_undo_group().is_some()); + } + + #[test] + fn sysvsem_guard_increments_once_then_install_moves_token() { + let parent = test_pcb_with_group(); + let group = parent.sem_undo_group().unwrap(); + let child = test_unpublished_child(); + + let mut guard = parent.prepare_shared_sem_undo_attachment(test_ipc_ns()).unwrap(); + assert_eq!(group.task_owners_for_test(), 2); + guard.install_into(&child); + assert!(child.sem_undo_group().is_some()); + guard.disarm(); + assert_eq!(group.task_owners_for_test(), 2); + } + + #[test] + fn installed_guard_rollback_takes_child_slot_and_only_drops_owner() { + let parent = test_pcb_with_group(); + let group = parent.sem_undo_group().unwrap(); + let child = test_unpublished_child(); + + let mut guard = parent.prepare_shared_sem_undo_attachment(test_ipc_ns()).unwrap(); + guard.install_into(&child); + drop(guard); + + assert!(child.sem_undo_group().is_none()); + assert_eq!(group.task_owners_for_test(), 1); + assert_eq!(group.replay_count_for_test(), 0); + } + ``` + + 并为 clone flag 前置拒绝加测试: + + ```rust + #[test] + fn newipc_and_sysvsem_are_rejected_before_attachment_prepare() { + let flags = CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_SYSVSEM; + assert_eq!(validate_semundo_clone_flags_for_test(flags), Err(SystemError::EINVAL)); + } + ``` + +- [ ] **Step 2: 确认 RED。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib 'ipc::sem_undo::tests::(ordinary_fork|sysvsem_guard|installed_guard)|process::fork::tests::newipc' --target x86_64-unknown-linux-gnu + ``` + + Expected: 编译失败,因为 guard、owner increment 和 install API 尚不存在。 + +- [ ] **Step 3: 实现 guard 和 clone 接入。** + + 1. `UnpublishedSemUndoAttachmentGuard` 持有“额外 owner token”和待安装 attachment;其 `Drop`: + - 若已安装,调用 child `take_sem_undo_attachment()`; + - 在 group lock 下 `task_owners -= 1`; + - **不回放**; + - debug-assert 减少前 owner 大于 1(parent token 必须仍存在)。 + 2. `prepare_shared_sem_undo_attachment()`: + - 使用 parent 当前 IPC ns,调用 `ensure_sem_undo_group()`;parent 没有 group 时允许创建空 group; + - 验证 group 仍绑定该 IPC namespace; + - 仅 group lock 下 `task_owners += 1`; + - 以已有 group `Arc` 组成 guard; + - 不得从 parent slot `take()`,不得把 `CLONE_THREAD` 推导为 `CLONE_SYSVSEM`。 + 3. 在 [`ProcessManager::copy_process()`](kernel/src/process/fork.rs#L575-L1100) 的既有 early validation 中,在任何 child 状态/namespace 创建/owner 修改前,加入: + ```rust + if (clone_flags & (CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_SYSVSEM)) + == (CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_SYSVSEM) + { + return Err(SystemError::EINVAL); + } + ``` + 不能依赖 [`copy_namespaces()` 中较晚的相同检查,nsproxy.rs:166-177](kernel/src/process/namespace/nsproxy.rs#L166-L177)。 + 4. `copy_namespaces()` 后、PID relation / `thread_group_live` / pid links / `children` / `add_pcb()` / cgroup visibility / scheduler 可观察 child 前,若 flags 含 `CLONE_SYSVSEM`,创建 guard 并立即 `install_into(pcb)`。 + 5. 目前 `copy_process()` 在 PID links 与 `children` publication 后仍可能在 [`install_reserved_fd(...)?`,fork.rs:1061-1066](kernel/src/process/fork.rs#L1061-L1066) 返回错误;所以先将 pidfd install 移进 relation publication 前的 prepare 阶段,或将 install 改为在完整 publication 前、后续不可失败的阶段完成。 + 6. 最小 publication rollback 不可只复用 [`rollback_failed_fork()`,fork.rs:1103-1129](kernel/src/process/fork.rs#L1103-L1129),因为该函数当前仅回收 pidfd/cgroup charge,未撤销 PID links、thread-group membership、children 或 global PCB。 + 7. 本任务的提交目标是:将所有 guard 创建后的 fallible operation 移到 guard 创建前;guard install 后到 `copy_process()` 成功返回之间不得保留 `?` 或显式 `Err` 分支。`guard.disarm()` 在 PID/TGID/PGID/SID attach、thread group / children relation、pidfd、`ProcessManager::add_pcb()`、cgroup visible accounting 完成后执行;[`wake_up_new_task()`,manager/sched.rs:94-121](kernel/src/process/manager/sched.rs#L94-L121) 必须发生在 disarm 后。 + 8. 普通 fork 不碰 child slot,天然保持 `None`。 + +- [ ] **Step 4: 确认 GREEN。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib ipc::sem_undo::tests --target x86_64-unknown-linux-gnu + ``` + + Expected: owner、install、rollback、flag tests PASS;无 replay 发生于 child rollback。 + +- [ ] **Step 5: 验证 ABI 仍保持关闭。** + + Run: + + ```bash + cd . + make kernel + grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs + ``` + + Expected: kernel 成功构建,且 `ENOSYS` 仍存在。 + +- [ ] **Step 6: Commit。** + + ```bash + git add kernel/src/ipc/sem_undo.rs kernel/src/process/task.rs kernel/src/process/fork.rs kernel/src/process/namespace/nsproxy.rs + git commit -m "feat(process): track shared sem undo owners across clone" + ``` + +--- + +### Task 3: 实现 detach/replay 基础并接入 exit,仍不开放 syscall + +**Files:** +- Modify: `kernel/src/ipc/sem_undo.rs` +- Modify: `kernel/src/ipc/sem.rs:311-639, 764-780` +- Modify: `kernel/src/process/manager/exit.rs:450-488, 662-704` +- Modify: `kernel/src/process/pid.rs:649-681`(仅在现有 visibility 不够时) +- Test: `kernel/src/ipc/sem_undo.rs` 的 `#[cfg(test)]` +- Test: `kernel/src/ipc/sem.rs` 的 `#[cfg(test)]` + +**Interfaces:** +- Produces: + ```rust + pub(crate) fn detach_sem_undo( + pcb: &Arc, + ); + + impl SemUndoGroup { + fn detach_owner_and_take_last_records( + &self, + ) -> Option>; + } + ``` +- Consumes: Task 1/2 的 `take_sem_undo_attachment()`;`SemManager` checked full-ID lookup;queue rescan。 + +- [ ] **Step 1: 写失败的 replay 单测。** + + 在 `sem_undo.rs` / `sem.rs` 的内部测试中直接用现有 [`insert_test_set()`,sem.rs:948-959](kernel/src/ipc/sem.rs#L948-L959) 风格构造 manager/set,并覆盖: + + ```rust + #[test] + fn last_owner_replays_adjustment_with_clamp_and_removes_record() { + // semval = 32766, adjustment = 4 -> clamp to SEMVMX. + // last owner replay removes record and leaves semval = 32767. + } + + #[test] + fn non_last_owner_does_not_replay() { + // owner 1 detach: semval and record unchanged. + // owner 2 detach: exactly one replay. + } + + #[test] + fn stale_full_semid_does_not_touch_reused_index() { + // old set record -> RMID -> same low index, new generation. + // replay old record; new set remains unchanged. + } + + #[test] + fn replay_updates_otime_and_rescans_waiter() { + // record adjustment turns a blocked waiter Ready. + // replay must update sem_otime, set exiting visible TGID as sempid, + // then complete the waiter in the same manager critical section. + } + ``` + +- [ ] **Step 2: 确认 RED。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib 'ipc::sem_undo::tests::(last_owner|non_last_owner|stale_full_semid|replay_updates)' --target x86_64-unknown-linux-gnu + ``` + + Expected: 编译或 assertions 失败,因为 detach/replay 尚未实现。 + +- [ ] **Step 3: 实现 explicit detach 与 replay。** + + 1. 新增唯一入口 `detach_sem_undo(pcb)`: + - 首先 `pcb.take_sem_undo_attachment()`,立即释放 PCB slot lock; + - 仅 group lock 下递减 `task_owners`; + - 非最后 owner 直接返回; + - 最后 owner 成为唯一 drainer,取走 records;禁止依赖 `Arc::strong_count()`、thread group、leader 或 TGID 判断。 + 2. 将调用放到 [`RobustListHead::cleanup_robust_list(&pcb)` 后、`exit_mm` 前,exit.rs:450-463](kernel/src/process/manager/exit.rs#L450-L463)。 + 3. 对每条 record: + - 先仅持 group lock 取 full `SemId`; + - upgrade group 的 namespace Weak; + - 取得 `ipcns.sem` manager lock,再取得 group lock; + - full-ID `get_by_semid_checked_mut(record.semid)` 成功才回放;generation mismatch / RMID 仅删除 record。 + 4. 回放每个非零 adjustment: + ```rust + let next = (sem.val as i64 + adjustment as i64).clamp(0, SEMVMX as i64); + sem.val = next as i32; + sem.pid = exiting_tgid.clone(); + ``` + `exiting_tgid` 必须由 group 的 IPC namespace 对应 task 视角解析;当前 [`task_tgid_vnr()`,pid.rs:679-681](kernel/src/process/pid.rs#L679-L681) 使用 current task active pid ns,不可在跨 namespace replay 中直接使用。最小改动是将 [`task_pid_nr_ns()`,pid.rs:649-655](kernel/src/process/pid.rs#L649-L655) 提升为 `pub(crate)`,让 replay 使用 group namespace 关联的 PID namespace 获取 `PidType::TGID`。 + 5. replay 发生值变动时更新 set 的 `sem_otime`,并在同一 manager 临界区复用 queue rescan。 + 6. `ProcessManager::release()`([`exit.rs:662-704`](kernel/src/process/manager/exit.rs#L662-L704))不应首次 detach;仅加 debug assertion 确保 slot 已空。 + 7. namespace Weak upgrade 失败只能按“无 live namespace state、不再回放”删除 record 并 debug-log;不可访问失效 namespace。 + +- [ ] **Step 4: 确认 GREEN。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib 'ipc::sem_undo::tests|ipc::sem::tests' --target x86_64-unknown-linux-gnu + ``` + + Expected: clamp、single final drainer、generation guard、`sem_otime` / queue rescan tests PASS。 + +- [ ] **Step 5: 编译并确认 user ABI 未发布。** + + Run: + + ```bash + cd . + make kernel + grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs + ``` + + Expected: build PASS,`ENOSYS` gate 仍在。 + +- [ ] **Step 6: Commit。** + + ```bash + git add kernel/src/ipc/sem_undo.rs kernel/src/ipc/sem.rs kernel/src/process/manager/exit.rs kernel/src/process/pid.rs + git commit -m "feat(ipc): replay sem undo on final task exit" + ``` + +--- + +### Task 4: 实现 namespace detach transaction,并保持 SEM_UNDO syscall 关闭 + +**Files:** +- Modify: `kernel/src/process/namespace/nsproxy.rs:294-371` +- Modify: `kernel/src/process/namespace/unshare.rs:19-53, 140-190` +- Modify: `kernel/src/process/namespace/setns.rs:96-267` +- Modify: `kernel/src/ipc/sem_undo.rs` +- Test: `kernel/src/process/namespace/nsproxy.rs` 或同目录对应 `#[cfg(test)]` +- Test: `kernel/src/ipc/sem_undo.rs` 的 owner/replay tests + +**Interfaces:** +- Produces: + ```rust + pub(crate) struct PreparedNamespaceInstall { + new_nsproxy: Arc, + new_fs: Option>, + new_cred: Option>, + detach_sysvsem: bool, + } + + impl PreparedNamespaceInstall { + pub(crate) fn commit( + self, + tsk: &Arc, + fs_refs: &FsRefsReadGuard, + ) -> Result<(), SystemError>; + } + ``` + +**Scope boundary:** 此 task 不重写 namespace subsystem;只将已有 `setns` / `unshare` 已完成的 fallible prepare 与 publication 组合为无失败 commit,并把 SEM_UNDO detach 定义为显式 bool。 + +- [ ] **Step 1: 写失败测试。** + + ```rust + #[test] + fn unshare_sysvsem_detaches_even_when_ipc_namespace_is_unchanged() { + // old attachment is drained before syscall returns; + // subsequent SEM_UNDO ensure produces a different empty group. + } + + #[test] + fn unshare_newipc_detaches_once_when_sysvsem_is_also_present() { + // no double decrement/replay. + } + + #[test] + fn setns_newipc_detaches_even_for_same_arc_target() { + // force installation request with CLONE_NEWIPC and same Arc target; + // old attachment is detached. + } + + #[test] + fn namespace_prepare_error_preserves_attachment_and_old_state() { + // induced prepare failure: no fs/nsproxy/cred/attachment mutation. + } + ``` + +- [ ] **Step 2: 确认 RED。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib 'process::namespace::.*::tests|ipc::sem_undo::tests' --target x86_64-unknown-linux-gnu + ``` + + Expected: tests fail because `detach_sysvsem` is not represented or committed atomically. + +- [ ] **Step 3: 实现 prepare/commit 边界。** + + 1. 现状中 [`ksys_unshare()`](kernel/src/process/namespace/unshare.rs#L19-L53) 在 nsproxy/fs publication 后还可能在 `set_cred()?` 失败;[`switch_task_namespaces_inner()`](kernel/src/process/namespace/nsproxy.rs#L318-L343) 又在 `tsk.set_nsproxy()` 前修改 fs。将所有 permission、target lookup、mount path projection、new `FsStruct`、new `NsProxy`、new `Cred` 的失败工作放进 prepare。 + 2. `PreparedNamespaceInstall` 显式保留 `detach_sysvsem`;不得使用 old/new IPC `Arc::ptr_eq()` 推导: + - `unshare(CLONE_SYSVSEM)`:true; + - `unshare(CLONE_NEWIPC)`:true; + - 两者同时:true,且 commit 只 detach 一次; + - pidfd 或 namespace-fd `setns`:若已验证的 installation flags 含 `CLONE_NEWIPC`,即使 target IPC Arc 与 old Arc 相同也 true; + - 其他 UTS/NET/MNT/CGROUP/PID-for-children switch:false。 + 3. commit 开始后按以下顺序执行,无 allocation、无 user-memory access、无 permission check、无新的 fallible work: + 1. 若 `detach_sysvsem`,调用 Task 3 的 synchronous `detach_sem_undo(tsk)`; + 2. 安装已准备好的 fs root/pwd 或 `FsStruct`; + 3. `tsk.set_nsproxy(new_nsproxy)`; + 4. 若有,`tsk.set_cred(new_cred)`;虽然现有 [`set_cred()`,task.rs:862-866](kernel/src/process/task.rs#L862-L866) 类型为 `Result`,实现恒为 `Ok(())`,在该 commit 中应改为或包裹为无法失败的内部 install API,避免 PONR 后错误返回。 + 4. pidfd-setns 与 namespace-fd setns 在 [`ksys_setns()`](kernel/src/process/namespace/setns.rs#L96-L267) 的两条分支都改为创建同一 prepared install 后 commit。 + 5. `exec_task_namespaces()`([`nsproxy.rs:283-291`](kernel/src/process/namespace/nsproxy.rs#L283-L291))传 `detach_sysvsem = false`,确保 exec 保留 attachment。 + 6. 保持 `CLONE_NEWIPC | CLONE_SYSVSEM` 在 fork early check 拒绝;不要尝试把 unshare 的两个标志误判为非法组合。 + +- [ ] **Step 4: 确认 GREEN。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib 'process::namespace::.*::tests|ipc::sem_undo::tests' --target x86_64-unknown-linux-gnu + ``` + + Expected: four namespace detach tests PASS;prepare failure 不改变 old attachment。 + +- [ ] **Step 5: 编译并确认 ENOSYS 仍在。** + + Run: + + ```bash + cd . + make kernel + grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs + ``` + + Expected: kernel PASS;门禁仍未移除。 + +- [ ] **Step 6: Commit。** + + ```bash + git add kernel/src/process/namespace/nsproxy.rs kernel/src/process/namespace/unshare.rs kernel/src/process/namespace/setns.rs kernel/src/process/task.rs kernel/src/ipc/sem_undo.rs + git commit -m "fix(process): detach sem undo during namespace transitions" + ``` + +--- + +### Task 5: 重构 semop 为 lock 外 prepare + 无分配 simulation/commit + +**Files:** +- Modify: `kernel/src/ipc/sem.rs:6-10, 249-309, 376-639, 663-761` +- Modify: `kernel/src/ipc/sem_undo.rs` +- Test: `kernel/src/ipc/sem.rs:930-1008` +- Test: `kernel/src/ipc/sem_undo.rs` + +**Interfaces:** +- Produces: + ```rust + struct SemopScratch { + // fixed-capacity, pre-reserved ordered virtual state + } + + enum SemopOutcome { + Ready(SemopSimulation), + Blocked(SemBlockedOp), + } + + impl SemManager { + fn simulate_semop( + set: &KernelSemSet, + sops: &[PosixSemBuf], + undo: Option<&mut SemUndoRecord>, + scratch: &mut SemopScratch, + ) -> Result; + } + ``` +- Changes: + ```rust + struct SemQueueEntry { + sops: Vec, + pid: Option>, + undo_group: Option>, + waker: Arc, + status: SpinLock, + } + ``` + +- [ ] **Step 1: 写 RED tests:逐项 transactional simulation。** + + 先在 `sem.rs` 的测试模块中扩展已有 `insert_test_set()`,覆盖: + + ```rust + #[test] + fn ordered_mixed_undo_ops_apply_each_adjustment_step() { + // Same semnum, e.g. +3|UNDO, -1 without UNDO, -2|UNDO. + // Verify virtual semval and semadj after every element, not only net sum. + } + + #[test] + fn intermediate_adjustment_overflow_is_erange_even_if_later_op_cancels_it() { + // Begin adjustment at 32767; operation sequence +1|UNDO, -1|UNDO. + // Must return ERANGE and mutate neither semval nor semadj. + } + + #[test] + fn blocked_or_nowait_failure_does_not_commit_semval_or_semadj_prefix() { + // First op is successful UNDO; second op blocks/NOWAIT. + // Both semval and record adjustment remain original. + } + + #[test] + fn zero_undo_op_can_prepare_zero_record_without_adjustment() { + // sem_op = 0 | SEM_UNDO has group/record preparation but adjustment remains 0. + } + ``` + +- [ ] **Step 2: 确认 RED。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib 'ipc::sem::tests::(ordered_mixed|intermediate_adjustment|blocked_or_nowait|zero_undo)' --target x86_64-unknown-linux-gnu + ``` + + Expected: tests fail because old simulation neither receives undo record nor validates `semadj` incrementally. + +- [ ] **Step 3: 实现 scratch、record prepare 和立即路径 commit。** + + 1. 删除 `hashbrown::HashMap` 对 `SemopSimulation` 的依赖;现有 [`SemopSimulation { values: HashMap }`,sem.rs:376-380](kernel/src/ipc/sem.rs#L376-L380) 改为 lock 外 `try_reserve_exact(sops.len())` 的有界 ordered scratch。每个 unique `sem_num` 记录: + ```rust + struct SemopScratchEntry { + semnum: usize, + initial_val: i32, + virtual_val: i32, + initial_adj: i16, + virtual_adj: i16, + } + ``` + 使用线性查找即可,最多 `SEMOPM = 500`,不要在 manager lock 里分配 HashMap。 + 2. 只要任意 op 带 `SEM_UNDO`(含 `sem_op == 0`),在 manager lock 外: + - 读取 PCB group;没有时 `ensure_sem_undo_group(&ipcns)`; + - group 必须绑定本 syscall IPC namespace; + - manager lock 下先验证 full semid、读取 `nsems`,随后释放锁; + - group lock 下检查 record;缺失时锁外以 `Vec::try_reserve_exact(nsems)` + `resize` 制造 `Box<[i16]>`; + - 仅 group lock 下预留 records 一项容量;释放; + - 采用 Task 6 定义的 registry snapshot replacement; + - 重新 `manager -> group` 验证 full semid / generation / nsems 并无失败插入。 + 3. simulation 按原始 `sops` 顺序: + - 先每项验证 `sem_num`; + - `sem_op == 0`:virtual semval 非零返回 `Blocked(Zero)`; + - nonzero:每步 `next_val = current + sem_op`,大于 `SEMVMX` 返回 `ERANGE`,小于 0 返回 `Blocked(Increase)`; + - 若该项有 `SEM_UNDO` 且 nonzero,计算宽类型: + ```rust + let next_adj = current_adj as i32 - op.sem_op as i32; + ``` + 每步要求 `-32768..=32767`,否则 `ERANGE`; + - 任一 Blocked/Error 不写共享 semval/record。 + 4. `Ready` 时唯一 commit: + - 按 scratch 写入 semval; + - 写最终 adjustment; + - 写受影响 `KernelSem.pid`; + - 更新 `KernelSemSet.sem_otime`; + - 随后 queue rescan。 + 5. 仅 Task 5 结束时,直接 ready `SEM_UNDO` 仍不向用户开放,因为 syscall gate 必须保留;调用路径可以受 `cfg(test)` / internal entry 驱动验证。 + +- [ ] **Step 4: 确认 GREEN。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib 'ipc::sem::tests|ipc::sem_undo::tests' --target x86_64-unknown-linux-gnu + ``` + + Expected: all immediate transaction tests PASS。 + +- [ ] **Step 5: 审计临界区 allocation。** + + Run: + + ```bash + cd . + grep -nE 'HashMap::with_capacity|\\.to_vec\\(\\)|Arc::new\\(' kernel/src/ipc/sem.rs + ``` + + Expected: 在 semtimedop manager-locked section、`simulate_semop()` 和 queue creation path 中不再出现这些分配;如还有其他合理非临界区分配,提交说明必须逐个给出其 lock context。 + +- [ ] **Step 6: 保持门禁并编译。** + + Run: + + ```bash + make kernel + grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs + ``` + + Expected: build PASS;`ENOSYS` 仍存在。 + +- [ ] **Step 7: Commit。** + + ```bash + git add kernel/src/ipc/sem.rs kernel/src/ipc/sem_undo.rs + git commit -m "feat(ipc): prepare atomic sem undo accounting" + ``` + +--- + +### Task 6: 实现 namespace registry、queue-origin attribution 与 queued commit + +**Files:** +- Modify: `kernel/src/ipc/sem.rs:249-309, 311-639, 663-761` +- Modify: `kernel/src/ipc/sem_undo.rs` +- Modify: `kernel/src/process/namespace/ipc_namespace.rs:17-64`(仅为 lifecycle invariant/debug assertion 或明确 `destroy_all()`) +- Test: `kernel/src/ipc/sem.rs` 的 `#[cfg(test)]` +- Test: `kernel/src/ipc/sem_undo.rs` 的 `#[cfg(test)]` + +**Interfaces:** +- Produces: + ```rust + impl SemManager { + fn prepare_undo_record_and_registry( + &mut self, + group: &Arc, + semid: SemId, + ) -> Result<(), SystemError>; + + fn prune_and_apply_setval_undo( + &mut self, + semid: SemId, + semnum: usize, + ); + + fn prune_and_apply_setall_undo( + &mut self, + semid: SemId, + ); + + fn remove_undo_records_for_rmid( + &mut self, + semid: SemId, + ); + } + ``` + +- [ ] **Step 1: 写 RED tests。** + + ```rust + #[test] + fn queued_undo_commits_to_captured_group_not_waker_current_task() { + // Queue A with SEM_UNDO; change a test-current PCB before rescan. + // Ready retry updates A's captured group only. + } + + #[test] + fn queued_timeout_signal_and_rmid_never_commit_adjustment() { + // Timeout -> EAGAIN; signal -> EINTR; RMID -> EIDRM. + // Every result preserves record adjustment. + } + + #[test] + fn first_record_is_registry_visible_before_setval_cleanup() { + // Create first record, run SETVAL cleanup, then assert its target slot == 0. + } + + #[test] + fn stale_weak_entries_are_compacted_without_losing_live_group() { + // Snapshot contains stale + live + candidate. + // Replacement preserves live/candidate exactly once. + } + + #[test] + fn namespace_lifecycle_invariant_has_no_live_record_at_final_drop() { + // Test-only invariant: last namespace state cannot coexist with live owner, + // waiter, or registered group record. + } + ``` + +- [ ] **Step 2: 确认 RED。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib 'ipc::sem::tests::(queued_|first_record|stale_weak)|ipc::sem_undo::tests::namespace_lifecycle' --target x86_64-unknown-linux-gnu + ``` + + Expected: failures because `SemQueueEntry` has no `undo_group` and registry manipulation does not exist. + +- [ ] **Step 3: 实现 registry replacement protocol。** + + 1. `SemManager.undo_groups` 是 `Arc>>` snapshot。 + 2. record 第一次插入前,manager lock 内只 clone old snapshot Arc 并记录 identity;锁外: + - upgrade each weak; + - 去掉 stale; + - 保留 live; + - 若当前 group 不在 live set,append downgrade(group); + - 用 `Vec::try_reserve_exact` 及 `Arc::try_new` 创建 replacement slice; + 3. 回到 manager lock,只有 `Arc::ptr_eq(&self.undo_groups, &old_snapshot)` 时,按 `manager -> group` 二次检查 full semid、nsems、record presence,然后无失败 swap snapshot + insert record;snapshot 已变化时释放锁、重新 snapshot/rebuild,绝不能在不一致 snapshot 下只插 record。 + 4. registry scans 始终在 manager lock 中逐个 upgrade,一个 group 一次,处理后释放 group lock;不得在 group lock 内取 manager lock。 + 5. stale weak 压缩也采用 snapshot replacement;不得原地 `Vec` mutate / allocate。 + +- [ ] **Step 4: 实现 queue ownership 和 retry commit。** + + 1. `SemQueueEntry` 新增: + ```rust + undo_group: Option>, + ``` + 2. 将 [`SemQueueEntry::new()`,sem.rs:264-276](kernel/src/ipc/sem.rs#L264-L276) 改为接收预先复制的 `Vec`、预先 `Arc::try_new(Waker)` 结果和 captured strong observer;它本身不调用 `to_vec()`。 + 3. `semtimedop()` 在进 manager lock 前: + - prepare queue `sops` copy; + - `Arc::try_new(SemQueueEntry { ... })`; + - timer/waker allocation; + - 准备失败立即 `ENOMEM` 且无 queue/semadj 变化。 + 4. [`update_queue()`,sem.rs:600-639](kernel/src/ipc/sem.rs#L600-L639) 改为 manager lock 内使用 Task 5 同一 simulation/commit: + - entry 带 `Arc` observer 时,在 `manager -> captured group` 方向锁 record; + - `Weak::upgrade()` 失败是 internal invariant violation:完成 entry 的内部错误路径,绝不能给 current/waker task 记账; + - 只有 `Ready` 同时提交 semval+semadj,随后 remove/complete/wake; + - `Blocked`、NOWAIT、timeout、signal、RMID 都只 remove/complete,不记 adjustment。 + 5. 确认 waiter syscall 存续时持有强 group(来自 PCB attachment / semtimedop local observer);last owner detach 加 debug assertion,不能存在仍可能对该 group commit 的 queue entry。 + 6. namespace lifecycle: + - 先证明 `IpcNamespace` 的 `Arc` 被 task nsproxy / blocked syscall 保持; + - group 只持 `Weak`; + - namespace switch 在发布前完成 detach/replay; + - 若该证明无法在 test/debug assertion 中建立,才新增明确调用者的 `SemManager::destroy_all()`,在 manager lock 下 delete record、不回放、完成 waiter `EIDRM`、移除 sets,最后 free index; + - 不在 `IpcNamespace::Drop` 取 manager lock。 + +- [ ] **Step 5: 确认 GREEN。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib 'ipc::sem::tests|ipc::sem_undo::tests' --target x86_64-unknown-linux-gnu + ``` + + Expected: queue owner, cancellation, registry, lifecycle tests PASS。 + +- [ ] **Step 6: 审计 manager lock 下无新增 allocation。** + + Run: + + ```bash + cd . + grep -nE 'HashMap::with_capacity|\\.to_vec\\(\\)|Arc::new\\(|try_reserve|Vec::new' kernel/src/ipc/sem.rs kernel/src/ipc/sem_undo.rs + ``` + + Expected: 所有可能分配操作都位于 manager lock 外的 prepare/replacement 阶段;检查每个命中行的调用上下文。 + +- [ ] **Step 7: 保持 ABI 关闭并构建。** + + Run: + + ```bash + make kernel + grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs + ``` + + Expected: `ENOSYS` gate remains。 + +- [ ] **Step 8: Commit。** + + ```bash + git add kernel/src/ipc/sem.rs kernel/src/ipc/sem_undo.rs kernel/src/process/namespace/ipc_namespace.rs + git commit -m "feat(ipc): commit queued sem undo to originating group" + ``` + +--- + +### Task 7: 接入 SETVAL、SETALL、IPC_RMID 的 undo cleanup + +**Files:** +- Modify: `kernel/src/ipc/sem.rs:763-780, 868-927` +- Modify: `kernel/src/ipc/sem_undo.rs` +- Test: `kernel/src/ipc/sem.rs:930-1008` + +**Interfaces:** +- Produces: + ```rust + impl SemManager { + fn clear_undo_for_setval(&mut self, id: SemId, semnum: usize); + fn clear_undo_for_setall(&mut self, id: SemId); + fn discard_undo_for_rmid(&mut self, id: SemId); + } + ``` + +- [ ] **Step 1: 写 RED tests。** + + ```rust + #[test] + fn setval_clears_only_target_sem_adjustment_across_all_groups() { + // Two groups, one multi-sem record. + // SETVAL semnum 0 clears slot 0 but preserves slot 1 in both groups. + } + + #[test] + fn setall_clears_entire_full_semid_record_across_all_groups() { + // SETALL zeros every adjustment in matching records only. + } + + #[test] + fn setval_cleanup_precedes_value_write_and_queue_rescan() { + // SETVAL wakes queued undo operation. + // The newly-ready operation creates new debt that survives this cleanup. + } + + #[test] + fn rmid_discards_record_before_index_can_be_reused() { + // Old full ID record is removed; reuse low index with new generation; + // old adjustment cannot affect replacement. + } + ``` + +- [ ] **Step 2: 确认 RED。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib 'ipc::sem::tests::(setval_clears|setall_clears|setval_cleanup|rmid_discards)' --target x86_64-unknown-linux-gnu + ``` + + Expected: tests fail because existing `setval()` / `setall()` only write values, and [`ipc_rmid()`,sem.rs:764-780](kernel/src/ipc/sem.rs#L764-L780) frees index before undo cleanup. + +- [ ] **Step 3: 实现 cleanup order。** + + 1. `setval(id, semnum, val)` 保持已有 errno order:value range → semnum → permission([`sem.rs:869-880`](kernel/src/ipc/sem.rs#L869-L880))。 + 2. 完整 semid/permission 验证后,在同一 manager lock 内: + 1. 扫描 registry 中 live groups; + 2. 对 matching full `SemId` record 执行 `adjustments[semnum] = 0`; + 3. 写 semval、sempid、ctime; + 4. queue rescan。 + 3. `setall(token, vals)` 保持既有 [`prepare_setall()` 两阶段 token 语义,sem.rs:921-927](kernel/src/ipc/sem.rs#L921-L927):最终 full-ID token 复核成功后,先清 matching record 的整个 slice,再写全部 sem value/pid/ctime,最后 rescan。 + 4. `ipc_rmid(id)`: + 1. full-ID 与 permission 验证; + 2. manager lock 下扫描 live group,删除 matching full-ID record,不回放; + 3. `complete_all_removed()` 给每个 waiter `EIDRM`; + 4. remove key/id table、更新 total; + 5. 最后 `id_allocator.free_idx(decoded.idx)`。 + 5. 所有这些 scan 都遵循 `manager -> group`;不能借助 record 的 low index 判断。 + +- [ ] **Step 4: 确认 GREEN。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib ipc::sem::tests --target x86_64-unknown-linux-gnu + ``` + + Expected: SETVAL/SETALL/RMID cleanup + 既有 stale setall token tests PASS。 + +- [ ] **Step 5: 构建并确认门禁仍存在。** + + Run: + + ```bash + cd . + make kernel + grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs + ``` + + Expected: build PASS;`ENOSYS` 未删除。 + +- [ ] **Step 6: Commit。** + + ```bash + git add kernel/src/ipc/sem.rs kernel/src/ipc/sem_undo.rs + git commit -m "feat(ipc): clear sem undo state on semaphore controls" + ``` + +--- + +### Task 8: 增加公开 dunitest ABI 覆盖,但暂时保留 ENOSYS + +**Files:** +- Modify: `user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc:41-226, 706-714` +- Test: `user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc` +- Do not modify: `user/apps/tests/dunitest/whitelist.txt` +- Do not modify: `user/apps/tests/dunitest/Makefile` + +**Interfaces:** +- Replaces: + ```cpp + TEST(SysVSem, SemUndoRejected) + ``` +- Adds helpers using only existing public syscalls: `semget`, `semop`, `semtimedop`, `semctl`, `fork`, `clone`, `exec`, `unshare`, `setns`, pipe/eventfd/futex-style synchronization as already available to the environment. + +- [ ] **Step 1: 先将当前 ENOSYS test 改为失败的 future ABI tests。** + + 删除 [`SemUndoRejected`,sysv_sem_semantics.cc:706-714](user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc#L706-L714),增加下列 12 组(每组可包含多个 gtest case): + + 1. **`SemUndoBasicAccumulationAndSign`** + child 对初值 0 做 `+3 | SEM_UNDO` 与 `-1 | SEM_UNDO`;parent 在 child exit 前读到 2,exit 后读回 0;初值 3 的 `-2 | SEM_UNDO` exit 后回到 3。 + 2. **`SemUndoArrayOrderAndRollback`** + 同 semnum 重复、混合 flags;前项 UNDO 成功但后项 NOWAIT blocked 或 ERANGE 时,调用后和 child exit 后均不出现 prefix effect。 + 3. **`SemUndoAdjustmentBounds`** + 到达 `32767` / `-32768` 成功;下一项超界 `ERANGE`;同一数组中先超界而后抵消仍必须 `ERANGE`。 + 4. **`SemUndoQueueCapturedOwner`** + A `-1|SEM_UNDO` 排队;用 `GETNCNT` 确认;B 普通 `+1` 唤醒;A 成功后退出才回放,不是 B 的 exit。 + 5. **`SemUndoUncommittedPaths`** + NOWAIT、超时、signal、wait 中 RMID 分别验证 `EAGAIN/EINTR/EIDRM` 与无 undo 回放。原有 `WaitForWaiters()` 使用轮询;新增 case 必须改为 `GETNCNT/GETZCNT` 明确握手而不是 sleep 猜测。 + 6. **`SemUndoSetvalSetallClearDebt`** + 两 group、多 sem:SETVAL 只清目标 sem;SETALL 清该 set 全 slice;SETVAL 唤醒后由 queue 新提交的 debt 仍在 exit replay。 + 7. **`SemUndoRmidDiscardsDebt`** + 成功 UNDO 后 RMID、owner exit,确认不会回放,且无 crash。 + 8. **`SemUndoForkAndCloneOwners`** + 普通 fork 不继承;`clone(CLONE_SYSVSEM | SIGCHLD)` share;先 exit 不回放、最后 owner 仅回放一次;`CLONE_THREAD` 不含 SYSVSEM 时不共享;`CLONE_NEWIPC | CLONE_SYSVSEM` 返回 `EINVAL`。 + 9. **`SemUndoExecPreservesAttachment`** + child 做 UNDO 后 `exec` 当前 binary 的 helper mode,helper exit 后观察 replay;失败 exec 后仍保留 attachment 并在 exit 回放。 + 10. **`SemUndoExitClampAndWake`** + 正向/负向 clamp;用 `GETNCNT`/`GETZCNT` 确认 waiter 入队;final-owner replay 变值触发 waiter completion。 + 11. **`SemUndoUnshareSysvsem`** + share owners 中一方 `unshare(CLONE_SYSVSEM)` 后,新 UNDO 进入新 group;剩余 owner drain 旧 group;唯一 owner unshare 在 syscall return 前 drain。 + 12. **`SemUndoIpcNamespaceAndErrnos`** + `unshare(CLONE_NEWIPC)`、namespace-fd `setns`、pidfd `setns` 全 detach;same-Arc IPC setns 也 detach;权限/invalid target 的 prepare failure 保留 old attachment;回归 `EINVAL/E2BIG/EFAULT/EFBIG/EACCES`。 + + 注:用户 ABI 测试不得通过循环创建数万 semaphore 强迫 index reuse,也不得尝试 `SEMMNU + 1` admission;generation reuse 与 allocation failure 留给 kernel-internal tests。 + +- [ ] **Step 2: 确认 dunitest 编译通过而运行仍 RED。** + + Run: + + ```bash + cd user/apps/tests/dunitest + make build-suites + ``` + + Expected: `bin/normal/sysv_sem_semantics_test` 编译成功。 + + Run: + + ```bash + ./bin/normal/sysv_sem_semantics_test --gtest_filter='SysVSem.SemUndo*' + ``` + + Expected: FAIL;当前 kernel 在 `SEM_UNDO` 首次操作返回 `ENOSYS`,证明 tests 不是误绿。 + +- [ ] **Step 3: 不修改内核门禁,只整理 ABI tests。** + + 确保: + - 使用已有 `SemSet` RAII cleanup; + - 多进程测试使用 `ChildGuard` / `WaitChildOk`; + - 所有 blocked rendezvous 用 `GETNCNT` / `GETZCNT`、pipe 或 eventfd,不依赖固化 sleep; + - fork/exec helper 通过 argv/环境变量选择 helper mode,保持单一 test binary; + - QEMU 平台不支持某一 capability 时,不得无声 skip;设计应使用现有 DragonOS 支持路径。 + +- [ ] **Step 4: 再确认 RED。** + + Run: + + ```bash + ./bin/normal/sysv_sem_semantics_test --gtest_filter='SysVSem.SemUndo*' + ``` + + Expected: failing cases 的共同直接原因是 `ENOSYS`,而不是 test synchronization 超时或不确定 race。 + +- [ ] **Step 5: Commit。** + + ```bash + git add user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc + git commit -m "test(ipc): add sem undo ABI conformance cases" + ``` + +--- + +### Task 9: 最终开放 SEM_UNDO ABI,并执行全量门禁验证 + +**Files:** +- Modify: `kernel/src/ipc/sem.rs:671-761` +- Modify: `user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc`(仅修正因真实 ABI 暴露而发现的确定性测试问题) +- No new APIs. +- No per-set lock. +- No namespace tunable. +- No global publication refactor beyond Task 2 的最小安全协议。 + +**Hard prerequisites:** +- Task 1–8 所有 internal tests 已通过。 +- immediate simulation/commit、queued retry、exit replay、SETVAL/SETALL、RMID、full-ID reuse protection、fork/clone owner、exec、unshare、same-Arc setns、namespace lifecycle、prepare/rollback 都有真实实现和测试。 +- 审计确认没有 `group -> manager` lock acquisition、没有 PCB slot 与 manager/group lock nesting。 +- `SEM_UNDO` 仍只在唯一 gate 被拦截。 + +- [ ] **Step 1: 最后写 RED publication assertion。** + + 在 `sysv_sem_semantics.cc` 保留至少一个明确断言: + + ```cpp + TEST(SysVSem, SemUndoBasicAccumulationAndSign) { + // ... + ASSERT_EQ(0, SemOp(sem.id(), &undo_inc, 1)) + << "SEM_UNDO must be published only after all lifecycle prerequisites"; + } + ``` + + 这不是新测试;它确保删除 gate 是 ABI publication 而非仅 internals 完成。 + +- [ ] **Step 2: 运行全部 kernel-internal gates。** + + Run: + + ```bash + cd kernel + cargo +nightly-2026-02-24 test --lib 'ipc::sem::tests|ipc::sem_undo::tests|process::namespace::.*::tests|process::fork::tests' --target x86_64-unknown-linux-gnu + ``` + + Expected: PASS。若项目的实际 kernel test runner 不支持 host target,改用已验证的 DragonOS CI kernel-test target;不能以 `make -C kernel test` 代替,因为它明确 `--exclude dragonos_kernel`。 + +- [ ] **Step 3: 删除唯一 ENOSYS gate。** + + 在 [`SemManager::semtimedop()`,sem.rs:671-688](kernel/src/ipc/sem.rs#L671-L688) 删除: + + ```rust + if sops + .iter() + .any(|op| (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() != 0) + { + return Err(SystemError::ENOSYS); + } + ``` + + 其余 input validation (`sops.is_empty()`、`SEMOPM`) 保持原位。随后让 Task 5/6 的 lock-out prepare、record prepare、queue preallocation 和 manager-locked no-fail commit 成为正常路径。 + +- [ ] **Step 4: 确认 GREEN:局部 ABI test。** + + 先本地编译: + + ```bash + cd user/apps/tests/dunitest + make build-suites + ``` + + 再在 DragonOS QEMU dunitest 环境执行目标 binary: + + ```bash + /opt/tests/dunitest/bin/normal/sysv_sem_semantics_test --gtest_filter='SysVSem.SemUndo*' + ``` + + Expected: `PASSED`,所有 `SemUndo*` cases 无 skipped、无 timeout。 + +- [ ] **Step 5: 构建与 QEMU dunitest 回归。** + + Run: + + ```bash + cd . + make kernel + make test-dunit + ``` + + Expected: + - `make kernel` 成功; + - dunitest runner 中 `normal/sysv_sem_semantics` PASS; + - 既有 `SysVSem` 非-UNDO tests 无回归。 + + 如开发环境适合走 Nix,等价完整命令为: + + ```bash + nix develop --command make test-dunit + ``` + +- [ ] **Step 6: 静态锁/分配审计。** + + Run: + + ```bash + cd . + grep -RIn --include='*.rs' 'sem_undo' kernel/src/ipc kernel/src/process + grep -nE 'HashMap::with_capacity|\\.to_vec\\(\\)|Arc::new\\(' kernel/src/ipc/sem.rs + ``` + + Expected: + - `sem_undo` 仅通过明确语义 API 修改 PCB slot/owner; + - manager-locked semop/queue paths 无 allocation; + - 没有 per-set lock 字段或 lock acquisition; + - 没有以 `Arc::strong_count()` 判定 SEM_UNDO 最后 owner。 + +- [ ] **Step 7: Commit final ABI publication。** + + ```bash + git add kernel/src/ipc/sem.rs user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc + git commit -m "feat(ipc): enable Linux-compatible sem undo" + ``` + +--- + +## Planned Commit Boundaries + +1. `feat(ipc): add private sem undo ownership model` +2. `feat(process): track shared sem undo owners across clone` +3. `feat(ipc): replay sem undo on final task exit` +4. `fix(process): detach sem undo during namespace transitions` +5. `feat(ipc): prepare atomic sem undo accounting` +6. `feat(ipc): commit queued sem undo to originating group` +7. `feat(ipc): clear sem undo state on semaphore controls` +8. `test(ipc): add sem undo ABI conformance cases` +9. `feat(ipc): enable Linux-compatible sem undo` + +`ENOSYS` 必须贯穿提交 1–8;第 9 个提交是唯一 publication point。 + +## Plan Self-Review + +- **规格覆盖:** 覆盖 `SemUndoGroup/Attachment`、PCB slot、`CLONE_SYSVSEM`、普通 fork、child rollback、task exit、exec 保留、unshare/setns detach、immediate transaction、queued transaction、full generation semid、SETVAL、SETALL、RMID、namespace lifetime、ENOMEM failure atomicity、dunitest。 +- **明确排除:** 不实现 per-set lock;不新增 namespace-local semaphore tunable;不以 `SEMUME/SEMMNU` admission;不扩大全局 namespace/fork 重构。 +- **现有事实锚点:** 当前 ENOSYS gate 位于 `sem.rs:683-688`;manager lock 覆盖当前 semop `sem.rs:702-735`;当前 queue copy 在 `sem.rs:264-276`;exit hook 位于 `exit.rs:450-463`;fork publication 后 PIDFD installation 仍可能失败于 `fork.rs:1061-1066`;setns/unshare 当前确有 prepare/publication 分离缺口。 +- **测试可执行性注意:** dunitest 有明确构建/运行路径:`make build-suites`、`make test-dunit`。内核 `#[cfg(test)]` 必须使用能编译 `dragonos_kernel` 本体的实际 test target;不可声称 `kernel/Makefile:test` 覆盖它,因为该命令明示排除 `dragonos_kernel`。 + +### Critical Files for Implementation +- `kernel/src/ipc/sem.rs` +- `kernel/src/ipc/sem_undo.rs` +- `kernel/src/process/task.rs` +- `kernel/src/process/fork.rs` +- `kernel/src/process/manager/exit.rs` diff --git a/docs/superpowers/specs/2026-09-03-sem-undo-design.md b/docs/superpowers/specs/2026-09-03-sem-undo-design.md new file mode 100644 index 0000000000..95d12a6580 --- /dev/null +++ b/docs/superpowers/specs/2026-09-03-sem-undo-design.md @@ -0,0 +1,687 @@ +# DragonOS `SEM_UNDO` 实现规格(Linux 6.6 兼容) + +## 1. 结论与范围 + +采用当前实现的最小可发布模型:**PCB 显式 attachment → `Arc`;每个 group 绑定一个 IPC namespace;`SemManager` 维护 namespace 级 `Arc>>` registry;record 只以完整 generation-bearing `semid` 为键。** + +```puml +@startuml SemUndoOwnership +skinparam classAttributeIconSize 0 +skinparam linetype ortho +left to right direction + +class ProcessControlBlock { + sem_undo: SpinLock> +} + +class SemUndoAttachment { + group: Arc +} + +class SemUndoGroup { + ipc_ns: Weak + inner: SpinLock +} + +class SemUndoGroupState { + task_owners: usize + records: Vec + reserved_records: usize + absence_generation: u64 + retired: bool + records_taken: bool +} + +class SemUndoRecord { + semid: SemId + adjustments: Box<[i16]> + revision: u64 + prepared_state: PreparedSemUndoRecordState + reservation: Option +} + +class IpcNamespace { + sem: SpinLock +} + +class SemManager { + undo_groups: Arc>> +} + +ProcessControlBlock "1" *-- "0..1" SemUndoAttachment : PCB 显式 attachment +SemUndoAttachment *-- "1" SemUndoGroup : group +SemUndoGroup --> "1" IpcNamespace : ipc_ns,每组唯一 +SemUndoGroup *-- "1" SemUndoGroupState : inner +SemUndoGroupState *-- "0..*" SemUndoRecord : records +IpcNamespace *-- "1" SemManager : sem +SemManager o-- "0..*" SemUndoGroup : undo_groups 弱引用注册 + +@enduml +``` + +不引入 per-set lock、RCU 式 namespace teardown 或额外用户 ABI。为实现无分配 record publication 与 namespace/fork 事务,当前实现使用局部 prepared-record state、record reservation、final-owner `retired` 状态和预分配 RCU retirement;这些不是第二套 group/namespace 身份模型。`NsCommon.nsid` 仍只提供诊断身份。 + +本规格覆盖 Linux 6.6 的 `semop`/`semtimedop`、queue、`SETVAL`、`SETALL`、`IPC_RMID`、fork/clone、exec、task exit、`unshare(CLONE_SYSVSEM)`、IPC namespace unshare/setns 语义。namespace 与 fork publication 的事务修复是开放 ABI 前置条件;当前实现以 prepared namespace install、预分配 RCU retirement 和 child attachment guard 达成该前提。 + +在执行、等待重试、退出回放、控制操作、RMID、fork/clone、unshare/setns 和 namespace 生命周期路径实现完成后,已删除 `SEM_UNDO -> ENOSYS` 门禁;这是本实现的用户可见 publication point。后续变更不得重新引入仅部分路径可用的 ABI。 + +## 2. 当前实现基线 + +- 实现前的基线在 `kernel/src/ipc/sem.rs` 对 `SEM_UNDO` 返回 `ENOSYS`;当前最终实现已删除该门禁并接入 lifecycle accounting。 +- `SemQueueEntry::new()` 的 `to_vec()` 已改为锁外 fallible 的预准备 sops copy。 +- `simulate_semop()` 的临时 `HashMap` 已改为锁外预留、临界区内固定容量的 scratch。 +- blocked 路径的 queue `Arc`、sops、waker/timer 和 scratch 均在 manager lock 外预准备。 +- 一个 namespace 的全部 semaphore set、queue 和 ID 表仍由 `IpcNamespace.sem: SpinLock` 串行化。 +- `SEMOPM` 是 `kernel/src/ipc/sem.rs` 的全局常量 500;不存在 namespace-local `sc_semopm`。 +- namespace switch/unshare 已汇入 `PreparedNamespaceInstall`,并在 commit 前准备 RCU retirement。 +- fork 已将 pidfd 安装置于 child relation publication 之前,并用 child attachment guard 覆盖 publication 区间。 + +## 3. Linux 6.6 行为基准 + +权威参考: + +- [`ipc/sem.c` 数据结构](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L143-L166) +- [原子执行与 adjustment](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L630-L784) +- [queued retry](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L934-L1067) +- [`IPC_RMID`](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L1139-L1193) +- [`SETVAL`/`SETALL`](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L1343-L1527) +- [undo 分配与 semop 主路径](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L1840-L2220) +- [`copy_semundo()` / `exit_sem()`](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L2302-L2446) +- [UAPI 常量](https://github.com/torvalds/linux/blob/v6.6/include/uapi/linux/sem.h#L68-L91) +- [namespace clone 约束与切换](https://github.com/torvalds/linux/blob/v6.6/kernel/nsproxy.c#L151-L187) +- [`unshare()`](https://github.com/torvalds/linux/blob/v6.6/kernel/fork.c#L3378-L3460) + +核心不变量: + +```text +adjustment(group, semid, sem_num) + = -Σ(该 group 成功提交且带 SEM_UNDO 的非零 sem_op) +``` + +对成功的 `sem_op = x`: + +```text +semval' = semval + x +semadj' = semadj - x +``` + +必须保持: + +1. 用户数组按原顺序逐项模拟;同一 `sem_num` 可重复,带/不带 `SEM_UNDO` 可混合。 +2. `sem_op == 0 | SEM_UNDO` 可触发空 group/record 的惰性创建,但 adjustment 不变。 +3. 每一步 `semval` 必须在 `0..=32767`;每一步 `semadj` 必须在 `-32768..=32767`,不能只检查最终净值。 +4. 任一项阻塞或失败时,整个数组不修改 semval 或 semadj。 +5. NOWAIT 阻塞和超时返回 `EAGAIN`,信号取消返回 `EINTR`,等待期间 RMID 返回 `EIDRM`;均不记账。 +6. 普通 fork 不继承;显式 `CLONE_SYSVSEM` 共享;`CLONE_THREAD` 不隐含 `CLONE_SYSVSEM`;exec 保留。 +7. 最后 task owner detach 时执行 `semval = clamp(semval + semadj, 0, 32767)`,不等待,并按 Linux `exit_sem()` 经 `do_smart_update(..., otime=1, ...)` 更新 `sem_otime`。 +8. `SETVAL` 清所有 groups 对目标 semaphore 的旧 adjustment;`SETALL` 清该 set 的全部旧 adjustment。 +9. `IPC_RMID` 删除 record 而不回放,waiter 得到 `EIDRM`,且清理完成后才允许低 index 复用。 +10. `SEMUME`、`SEMMNU` 仅作为兼容报告字段,不作为 admission limit。 + +## 4. 数据模型与所有权 + +新增 `kernel/src/ipc/sem_undo.rs`: + +```rust +pub struct SemUndoAttachment { + group: Arc, +} + +pub struct SemUndoGroup { + ipc_ns: Weak, + inner: SpinLock, +} + +struct SemUndoGroupState { + task_owners: usize, + records: Vec, + reserved_records: usize, + absence_generation: u64, + retired: bool, + records_taken: bool, +} + +struct SemUndoRecord { + semid: SemId, + adjustments: Box<[i16]>, + revision: u64, + prepared_state: PreparedSemUndoRecordState, + reservation: Option, +} +``` + +`PreparedSemUndoRecordState` 区分已存在 record 的 revision、缺失 record 的 absence generation 和已发布 record;`PendingSemUndoRecordReservation` 仅为锁外准备保留 `records` 容量,不能成为 owner。 + +规则: + +- queue 保存 captured `Arc`、预准备 `SemUndoRecord` 和 scratch;该 strong reference 只保障阻塞操作的归属与完成,不参与 `task_owners`,也不替代 PCB attachment。 +- group 创建时绑定当前 `Weak`,之后不得换绑;每条 record 因此无需重复保存 namespace 或 namespace ID。 +- record 的 `semid` 必须是完整 ID,不得只保存低 index。 +- adjustment buffer 长度创建后严格等于对应 set 的 `nsems`。 +- `task_owners` 只由显式 share、rollback、detach 修改;禁止使用 `Arc::strong_count()`。 +- attachment 的 `Drop` 不回放、不取 manager lock。所有正常路径必须显式 take/detach;debug assertion 用于发现遗漏。 +- group 首次获得 record 时在所属 namespace 的 `SemManager.undo_groups` 注册一次;registry 以 snapshot replacement 为主,并可在 `Arc::get_mut()` 成功时无分配压缩 stale weak。 +- 最后 owner 将 group 标记为 `retired`,一次性取走 records;retired group 拒绝新的 owner、record prepare 和 publication,`records_taken` 保证回放仅发生一次。 + +- `sem_undo_group() -> Option>`:observer。 +- `ensure_sem_undo_group(ipc_ns) -> Result, ENOMEM>`:二次检查并安装 `task_owners=1` 的空 group。 +- `take_sem_undo_attachment() -> Option`:原子清空 slot。 +- child guard 的 install/rollback API;调用者不得直接改 owner 数。 + +普通 fork 的 child slot 初始化为 `None`。group 首次获得 record 时在所属 namespace 的 `SemManager.undo_groups` 注册一次;stale weak 通过 snapshot replacement 或独占 registry 的 `Arc::get_mut()` 路径压缩。 + +## 5. 锁与分配契约 + +联合锁约束如下: + +```puml +@startuml SemUndoLockOrder +skinparam linetype ortho +left to right direction + +rectangle "PCB sem_undo slot 锁" as PcbSlot +rectangle "ipcns.sem 管理器锁" as Manager +rectangle "SemUndoGroup.inner" as Group +rectangle "队列局部锁\nundo_record / scratch / status" as QueueLocks + +PcbSlot -[hidden]-> Manager +Manager --> Group : 获取顺序 +Group --> QueueLocks : 获取顺序 +Group -[#red,dashed]-> Manager : 禁止反向获取 + +note bottom of PcbSlot + 不与其余锁嵌套 +end note + +note bottom of Manager + 每次最多锁一个 group +end note +@enduml +``` + +禁止持 group lock 获取 manager lock。manager lock 下每次只锁一个 group;queue entry 的 `undo_record`、`scratch` 与 `status` 均为 entry 局部锁,必须避免反向取得 manager/group 锁。 + +所有用户可触发增长必须 fallible,失败映射 `ENOMEM`。共享 semval、semadj、registry 或 queue 状态修改前,所有内存与容量必须准备好。不得在 manager spinlock 内调用 `HashMap::with_capacity`、`to_vec()`、`Arc::new()` 或其他可能分配的 API。 + +具体准备策略: + +- 使用 `Arc::try_new` 创建 group、queue entry、waker-owned 对象。 +- adjustment 使用 `Vec::try_reserve_exact(nsems)`、`resize(0)`、`into_boxed_slice()`。 +- simulation scratch 与 queue-owned sops 在 manager lock 外完成 fallible reserve/copy。 +- group record storage 必须分阶段准备:先在 `manager -> group` 下验证并快照 `required_capacity`、record revision/absence generation,随后释放全部锁;在锁外对 replacement storage 执行 fallible reserve;再按 `manager -> group` 重新获取锁并复核 full semid、`nsems`、容量需求及准备 token。仅当复核仍成立时,才通过无分配的 `append`/`swap` 安装已准备 storage 和 record;不得在 group lock 下 reserve。 +- manager registry 使用 immutable `Arc>>` 快照。manager lock 内只 clone 快照 `Arc` 并记录其身份;锁外 fallibly 构造“压缩 stale weak + 保留 live group + 必要时追加当前 group”的 replacement;重新获取 manager lock,只有旧快照仍 `Arc::ptr_eq` 时才无失败 swap,否则重试。当 registry 无其他 snapshot 持有者时,允许用 `Arc::get_mut()` 在 manager lock 内无分配压缩 stale weak。 +- 当前 `simulate_semop()` 的 `HashMap` 已替换为锁外预留的有界 scratch;queue 的 sops copy、queue `Arc`、waker/timer 和 queue-owned scratch 也在 manager 临界区外完成。 + +不新增 per-set lock 或额外的通用生命周期 guard 契约;本文已定义的 child attachment guard、prepared-record reservation 和 RCU retirement 是实现所需的局部机制。性能优化必须另行设计。 + +## 6. group 与 record prepare + +只要 sops 任一项带 `SEM_UNDO`,包括零操作,就执行 prepare。 + +### 6.1 group prepare + +1. 读取 PCB slot;已有 group 时验证它绑定当前 IPC namespace并返回 observer。 +2. 为空时,锁外 fallibly 构造 `task_owners=1` 的 candidate group。 +3. 再锁 PCB slot二次检查;已有者胜出,candidate直接丢弃;否则安装 candidate attachment。 +4. semop 后续失败时允许保留空 group,匹配 Linux 惰性分配行为。 + +### 6.2 record 与 registry prepare + +1. manager lock 下以完整 semid 验证 set并读取 `nsems`,同时取得 registry snapshot;再获取 group lock,检查 record 状态并快照 `required_capacity`、revision/absence generation,然后释放全部锁。 +2. 锁外构造全零 adjustment buffer、simulation/queue 所需对象以及 registry replacement;缺失 record 时同时 fallibly reserve 足以容纳快照容量的 replacement record storage。 +3. 获取 manager lock,再获取 group lock;重新验证完整 semid、`nsems`、registry snapshot 和 prepared record 的 revision/absence generation。 +4. 已有 record 在 prepare 后发生 `SETVAL`/`SETALL`/竞争更新时,以当前 record 内容刷新 prepared record;缺失 record 仅在 absence generation 与容量需求均一致时,才可使用锁外准备的 storage 发布。 +5. registry snapshot 已变化、RMID/reuse 或 record preparation 已失效时,释放锁并重试或返回既有 checked lookup errno;不得留下半个 record/registry。 +6. 所有验证通过后,无失败地 swap registry;缺失 record 通过 allocation-free `append`/`swap` 安装 prepared storage 并发布 prepared record,已有 record 则保留或刷新。该路径不得在 manager/group 临界区分配。 + +```puml +@startuml SemUndoRecordPrepare +skinparam sequenceArrowThickness 1.5 +autonumber + +participant "SemManager" as Manager +participant "SemUndoGroup.inner" as Group +participant "锁外预备存储" as Prepared + +Manager -> Manager : 验证 full semid,读取 nsems\n取得 registry snapshot +Manager -> Group : 按 SemManager -> SemUndoGroup.inner 获取锁 +Group -> Group : 快照 required_capacity\nrevision / absence_generation +Group --> Manager : 释放 group lock + +Manager -> Prepared : 释放 manager lock 后开始准备 +Prepared -> Prepared : 可失败地 reserve 替换存储\n构造 adjustment / scratch / queue / registry replacement + +Manager -> Group : 重新按 SemManager -> SemUndoGroup.inner 获取锁 +Group -> Group : 复核 full semid / nsems / registry snapshot\nrevision / absence_generation / required_capacity +alt 快照失效或 RMID/reuse + Group --> Manager : 不安装,释放锁 + Manager -> Manager : 重试或返回既有查找 errno +else 复核通过 + Prepared -> Group : 以无分配 append/swap\n安装预备存储与 record + Group -> Manager : 无失败 swap registry + Group --> Manager : 释放 group lock +end + +note over Group, Prepared + reserve 只能发生在全部锁释放之后; + group lock 内不得 reserve +end note +@enduml +``` + +允许留下空 group或全零 record;不允许 record 存在而 registry 缺失,也不允许 allocation error 发生在 semval/semadj 修改后。 + +## 7. semop 事务与 queue + +### 7.1 锁外准备 + +`SemManager::semtimedop()` 在获取 manager lock 前完成: + +1. 现有 nsops、timeout、用户复制和 flag 校验;上限使用全局 `SEMOPM`。 +2. 若含 `SEM_UNDO`,准备当前 namespace-bound group和目标 record。 +3. fallibly准备容量至多为 sops 唯一 semnum数的 simulation scratch。 +4. 若调用可能阻塞,fallibly准备 queue-owned sops、queue entry、waker/timer所需对象;尚不入队。 + +### 7.2 无分配 simulate/commit + +获取 manager lock后: + +1. 重新验证 full semid、semnum边界和权限;semnum越界保持先于权限检查。 +2. 若需要 undo,按 `manager -> group` 查找 record。 +3. 统一执行器按用户数组顺序在 scratch中模拟 semval和semadj。 +4. `Ready` 时一次不可失败 commit:写最终 semval、最终 adjustment、受影响 semaphore 的 `sempid`,并更新正常成功 semop 的 `sem_otime`。 +5. `Blocked` 或错误时不写任何共享值。 +6. commit 后调用 queue rescan;blocked 时依据 NOWAIT/timeout立即返回或把已准备 entry无失败入队。 + +每个带 `SEM_UNDO` 的非零 op 使用宽类型计算: + +```text +next_adjustment = current_virtual_adjustment - sem_op +``` + +每一步检查 `-32768..=32767`。semval每一步检查 `0..=32767`;负结果是阻塞,不是部分提交。 + +### 7.3 queued operation + +`SemQueueEntry` 增加: + +```rust +undo_group: Option> +undo_record: SpinLock> +scratch: SpinLock +``` + +```puml +@startuml SemUndoQueuedTransaction +skinparam sequenceArrowThickness 1.5 +autonumber + +participant "原调用任务" as Caller +participant SemQueueEntry as Entry +participant "SemManager\nupdate_queue()" as Manager +participant "SemUndoGroup.inner" as Group +participant "信号量集合" as Set +participant "等待任务" as Sleeper + +Caller -> Entry : 锁外准备并捕获\nundo_group / undo_record / scratch +Caller -> Manager : 无失败入队 +Manager -> Entry : 状态设为 Queued + +Manager -> Group : 按 SemManager -> SemUndoGroup.inner 获取锁 +activate Group +Manager -> Entry : 锁定局部 undo_record / scratch +Manager -> Manager : 刷新 prepared record 并模拟 +alt Ready + Manager -> Set : 原子提交 semval / semadj + Manager -> Entry : status = Completed(result) +else 仍然 Blocked + Manager -> Entry : 保持 Queued,不写共享值 +else 超时 / 信号 / RMID + Manager -> Entry : 移除并完成,不提交 adjustment +end +deactivate Group +Manager -> Manager : 释放 group 与 manager lock +Manager -> Sleeper : 锁外实际唤醒 + +Sleeper -> Entry : 只消费 Completed(result) + +note over Caller, Entry + NOWAIT 阻塞不进入 queue; + 队列捕获的 Arc 不增加 task_owners +end note +@enduml +``` + +规则: + +- 捕获原调用者 group,不读取 waker 的 `current_pcb()`;queue 的 `Arc` 仅保持 captured group 可用,不增加 `task_owners`。 +- queue entry 保存预准备 record;retry 在 manager lock 内通过 captured group 的 prepared-record protocol 刷新、保留或发布该 record。 +- `update_queue()` 复用同一无分配执行器;只有 retry 真正 `Ready` 时才同时提交 semval/semadj;sleeper 醒来只消费 `Completed(result)`,不再执行。 +- NOWAIT、timeout、signal、RMID均只移除/完成 entry,不修改 adjustment。 +- task exit 前阻塞 syscall必须已正常完成或取消;debug assertion验证最后 owner drain时没有该 group可提交的 queued entry。 + +## 8. 控制操作与 RMID + +`SemManager.undo_groups` 的扫描均在 manager lock内进行;逐个 upgrade Weak、锁一个 group、处理后释放,并压缩 stale weak。registry replacement必须按第5节在锁外准备,manager临界区只做无失败 swap。 + +### 8.1 `SETVAL` + +1. 验证 full semid、权限和值。 +2. 扫描所有 live groups;若存在该 full semid record,将目标 `adjustments[semnum]` 清零。 +3. 写管理员值并更新现有 `sempid`/`sem_ctime`。 +4. rescan queue。 + +顺序必须是先清旧债、再写值、最后 rescan。被新值唤醒后提交的 `SEM_UNDO` 是新债务,不得被本次清理删除。 + +### 8.2 `SETALL` + +用户数组复制和验证保持现有两阶段 token协议。最终 full semid token复核成功后,在同一 manager临界区清所有 groups 对该 set record的整个 adjustment slice,再写全部值、更新时间/sempid并 rescan queue。 + +### 8.3 `IPC_RMID` + +在 manager lock内: + +1. 验证 full semid/generation和权限。 +2. 扫描 live groups并删除匹配 full semid的 record;不实施 adjustment。 +3. 将全部 waiter完成为 `EIDRM`。 +4. 从 key/ID表删除 set、更新计数。 +5. 最后释放 index供复用。 + +旧 record不得作用于相同低 index的新 generation。 + +## 9. fork、clone、exec 与 exit + +### 9.1 clone flags 与共享规则 + +- `CLONE_NEWIPC | CLONE_SYSVSEM` 在 `copy_process()` 的早期纯 flag检查阶段返回 `EINVAL`,早于 group创建或 owner变化。 +- 无 `CLONE_SYSVSEM`:child slot保持 `None`。 +- 有 `CLONE_SYSVSEM`:parent无 group时可惰性创建空 group并保留;group必须绑定parent当前 IPC namespace。随后为 child增加一个 owner token。 +- `CLONE_THREAD` 不自动推导 `CLONE_SYSVSEM`。 + +### 9.2 child attachment install、rollback 与 publication + +`UnpublishedSemUndoAttachmentGuard` 只表示一个尚未发布 child 的 owner token,不是 attachment状态或未来生命周期 guard;它是 fork局部回滚对象。 + +精确协议: + +1. 完成 `CLONE_NEWIPC | CLONE_SYSVSEM` 拒绝和 child namespace创建后,在 parent group lock下增加一次 `task_owners`并创建 guard;此时 attachment尚未进入 child PCB。 +2. 在任何使其他任务能通过 PID表、TGID/PGID/SID关系、线程组、parent children list、全局PCB表、pidfd、cgroup accounting或scheduler观察/运行 child 的操作之前,调用 `guard.install_into(child_pcb)`。该操作只把已准备 attachment移动到确认为空的 child slot,不分配、不失败、不获取 group lock。 +3. install 后 guard仍保留 rollback authority,不得立即 disarm。 +4. fork publication prerequisite 必须先独立修复:pidfd安装移到关系 publication之前,或为 PID links、thread-group live、group task、children、全局PCB、cgroup accounting等所有 side effect提供完整逆序 rollback;逐项枚举 guard创建后的每个 `?` 和显式错误返回。 +5. publication commit结束点是 PID/TGID/PGID/SID attach、线程组/children插入、pidfd安装、`ProcessManager::add_pcb()`、cgroup task/accounting全部完成,且到 `copy_process()` 成功返回间不再存在可失败操作。只有此处调用 `guard.disarm()`。 +6. `wake_up_new_task()` 必须晚于 disarm;可运行 child始终已经安装 attachment。 +7. 任一错误先完整撤销 child publication,再由 guard从 child slot取回attachment(若已安装)并在 group lock下只减少该 unpublished owner。不得回放;parent owner仍存活,计数降到零是内核 bug。 + +### 9.3 exec/de-thread + +- exec成功和可恢复失败都保留当前 task attachment。 +- non-leader exec survivor保留自己的 attachment,不从旧 leader搬运。 +- siblings和旧 leader通过各自真实 exit路径逐个detach。 +- PID/TID身份交换不改变owner关系。 + +### 9.4 task detach 与回放 + +exit在 robust-list后、`exit_mm`前调用统一 detach: + +1. PCB slot短锁内 `take()` attachment,释放slot锁。 +2. 仅持 group lock执行 `task_owners -= 1`;非最后 owner立即返回。 +3. 减到零的调用者将 group 标记为 `retired`,因此不再接受 share、prepare 或 record publication;它是唯一 drainer。 +4. 解析退出者在正确 PID namespace可见的 TGID,复用 canonical `task_tgid_vnr()` 等价 helper;禁止 raw PID/TID。 +5. 一次性从 retired group 取走全部 records,并以 `records_taken` 保证仅取一次;释放 group lock 后 upgrade group唯一的 namespace Weak。 +6. namespace Weak 已失效时,仅 debug 记录并丢弃取出的 records,不得访问失效 namespace。 +7. namespace 仍存活时,在 manager lock 下逐条回放:set 仍存在则对每个非零 adjustment 执行 `semval = clamp(semval + adjustment, 0, 32767)`,更新该 semaphore的`sempid`为退出者可见TGID、更新 `sem_otime` 并 rescan queue;set 已被 RMID 或 generation 不匹配时只丢弃 record。 +8. `release()`/reap只断言slot已空,不能首次detach。 + +```puml +@startuml SemUndoFinalOwnerReplay +skinparam activityDiamondBackgroundColor white + +start +:从 PCB slot take attachment; +:释放 PCB slot lock; +:在 SemUndoGroup.inner 下执行 task_owners -= 1; +if (最后 owner?) then (否) + :释放 SemUndoGroup.inner; + stop +else (是) + :retired = true; + :解析命名空间可见 TGID\n使用 task_tgid_vnr() 等价 helper; + :records_taken = true\n一次性 take 全部 records; + :释放 SemUndoGroup.inner; +endif + +if (ipc_ns.upgrade() 成功?) then (否) + :debug 记录并丢弃 records; + stop +else (是) + :获取 ipcns.sem manager lock; +endif + +while (仍有 SemUndoRecord?) is (是) + if (完整 semid / generation 仍有效?) then (是) + :按 Linux exit_sem() 语义回放; + :更新 sempid / sem_otime\n并 rescan queue; + else (否) + :丢弃该 record; + endif +endwhile (否) +:释放 manager lock; +stop +@enduml +``` + +每个真实task只detach一次;不得用thread group live、leader、TGID或`Arc::strong_count()`判断最后owner。 + +## 10. namespace transition 的强制前置事务 + +当前 namespace切换不满足“全部可失败prepare → 不可失败commit”。开放 `SEM_UNDO` 前必须先建立统一 `PreparedNamespaceInstall`: + +- prepare只计算并持有新 `NsProxy`、新cred、目标mount root/pwd、新或复用的`FsStruct`、安装flags及所有fd/权限/用户内存结果;不得修改PCB、fs、cred或undo attachment。 +- prepare结果显式携带`detach_sysvsem`,不得由old/new `Arc::ptr_eq()`推导。 +- commit在现有namespace/fs publication串行化边界内按固定顺序执行:detach并同步完成旧group回放;无失败安装fs root/pwd与FsStruct;无失败安装nsproxy;无失败安装已构造cred。 +- commit开始后不得分配、访问用户内存、做权限检查或返回错误。 +- pidfd setns、namespace-fd setns和unshare必须汇入同一prepare/commit helper。 + +具体 detach 判定: + +- `unshare(CLONE_SYSVSEM)`:置位,哪怕IPC namespace不变。成功后slot为空,后续UNDO创建新group。 +- `unshare(CLONE_NEWIPC)`:置位;若同时含`CLONE_SYSVSEM`仍只detach一次。 +- setns:已验证的installation set包含`CLONE_NEWIPC`就置位,即使目标IPC namespace与当前是同一个`Arc`。 +- 只切换UTS、NET、MNT、CGROUP或PID-for-children不detach。 + +任何prepare错误发生在detach之前,必须保持旧namespace、fs、cred、attachment及债务完全不变。 + +```puml +@startuml PreparedNamespaceInstallLifecycle +skinparam activityDiamondBackgroundColor white + +start +:unshare() / setns(); + +partition "prepare(允许失败)" { + :验证 fd / 权限 / 用户内存; + :构造新的 NsProxy / cred / FsStruct\n以及 root / pwd / 安装 flags; + :显式计算 detach_sysvsem; + if (prepare 失败?) then (是) + :返回 errno;旧命名空间 / fs / cred /\nattachment / 债务保持不变; + stop + else (否) + endif +} + +partition "commit(不可失败)" { + :进入命名空间 / fs 发布串行化边界; + if (detach_sysvsem?) then (是) + :同步 detach 旧 SemUndoGroup\n并完成 final-owner replay; + endif + :无失败安装 FsStruct 与 root / pwd; + :无失败安装 nsproxy; + :无失败安装 cred; + note right + commit 开始后不得分配、访问用户内存、 + 做权限检查或返回错误 + end note +} + +:新的 IpcNamespace 对任务可见; +note right + 任务 / nsproxy 与阻塞 syscall 持有 Arc; + SemUndoGroup 只持 Weak +end note +:任务离开且阻塞 syscall / RMID 完成后\n释放各自强引用; +if (最后 Arc?) then (是) + :在 IpcNamespace::Drop 前验证\n无存活 owner / waiter / registered record; +endif +stop +@enduml +``` + +## 11. namespace 生命周期 + +当前实现不在 `IpcNamespace::Drop` 中取 manager lock、RMID set或唤醒 waiter,也不把 Drop 上下文作为 teardown 实现。依赖并验证以下生命周期约束: + +1. task/nsproxy和blocked syscall持有`Arc`; +2. group只持namespace Weak,不能延长namespace生命周期; +3. task离开IPC namespace前同步detach,最后owner回放完成后才发布新namespace; +4. blocked syscall结束或RMID完成前,其namespace强引用仍在; +5. 因此最后`Arc`析构时不存在live task、blocked waiter、live group owner或有record的registered group。 + +若该不变量被后续引用路径变更打破,必须在引入明确调用者和安全上下文后增加 `destroy_all()`:在 manager lock 内对每个 set 执行“删 group records、不回放、waiter 完成 `EIDRM`、删除 ID、最后释放 index”。不得把未知 Drop 上下文当作 teardown 实现。当前 group 的 `retired`/`records_taken` 只保证 final-owner records 单次消费,不构成 namespace teardown。 + +## 12. errno 与失败原子性 + +| 条件 | 结果 | 共享状态 | +|---|---:|---| +| 空sops、负semid、非法timeout | `EINVAL` | 不变 | +| `nsops > SEMOPM` | `E2BIG` | 不变 | +| 用户复制失败 | `EFAULT` | 不变 | +| semnum越界 | `EFBIG` | 不变 | +| 权限/安全检查失败 | `EACCES`或原errno | 不变 | +| semval或semadj逐步越界 | `ERANGE` | 整组不变 | +| NOWAIT阻塞、超时 | `EAGAIN` | 不变 | +| signal取消 | `EINTR` | 不变 | +| 等待期间RMID | `EIDRM` | 不变 | +| group/record/adjustment/registry/scratch/queue分配失败 | `ENOMEM` | 不变 | +| 初始无效semid | 现有checked lookup errno,通常`EINVAL` | 不变 | +| prepare后RMID或generation变化 | `EIDRM` | 不变 | + +commit必须无分配、无失败;blocked/cancelled queue不得commit;控制清账、写值和rescan处于同一manager临界区;RMID在index复用前清完record。 + +## 13. 历史实施分期与当前状态 + +以下分期记录实现时的提交边界,不再描述当前 tree 的提交状态;最终实现已经合并 ownership、namespace transaction、record accounting、ABI 开放和测试。 + +### 13.1 历史前置提交:process publication事务化 + +文件: + +- `kernel/src/process/namespace/nsproxy.rs` +- `kernel/src/process/namespace/unshare.rs` +- `kernel/src/process/namespace/setns.rs` +- `kernel/src/process/fork.rs` +- 必要的 `kernel/src/process/task.rs`、cred/fs helper文件 + +内容: + +- 引入namespace纯prepare/不可失败commit;两条setns和unshare汇流。 +- 修复fork中pidfd与PID/线程组/children/global PCB/cgroup publication顺序或完整rollback。 + +验收:每个commit前可失败点都有测试;失败不留下半发布namespace、fs、cred、PID关系、pidfd或child。 + +### 13.2 历史提交1:最小undo所有权 + +文件: + +- 新增 `kernel/src/ipc/sem_undo.rs` +- `kernel/src/ipc/mod.rs` +- `kernel/src/ipc/sem.rs` +- `kernel/src/process/task.rs` +- `kernel/src/process/fork.rs` +- `kernel/src/process/manager/exit.rs` +- `kernel/src/process/namespace/nsproxy.rs` +- `kernel/src/process/namespace/unshare.rs` +- `kernel/src/process/namespace/setns.rs` + +内容:group、non-Clone attachment、PCB slot、owner计数、namespace weak registry骨架、精确child guard、fork/exec/exit/unshare/setns attach/detach;records为空。用户态仍返回`ENOSYS`。 + +验收:owner/observer分离,普通fork不继承,SYSVSEM共享,guard安装/回滚/publication点和namespace detach内部测试通过;独立构建通过。 + +### 13.3 历史提交2:完整但未发布的记账路径 + +文件: + +- `kernel/src/ipc/sem_undo.rs` +- `kernel/src/ipc/sem.rs` +- `kernel/src/process/manager/exit.rs` +- `kernel/src/process/namespace/ipc_namespace.rs`(仅生命周期不变量或经证明必需的显式teardown) + +内容:full-semid records、锁外fallible prepare、`Arc>>` registry replacement、无分配simulate/commit、queue原group归属、SETVAL/SETALL/RMID清账、最后owner replay/clamp/sempid/rescan,以及namespace lifecycle不变量或经明确调用上下文实现的`destroy_all()`。该历史阶段的用户态仍返回`ENOSYS`。 + +验收:所有执行、replay、控制、RMID、queue、namespace lifecycle内部测试通过;manager lock内无新增分配;独立构建通过。 + +### 13.4 历史提交3:一次性开放ABI与测试 + +文件: + +- `kernel/src/ipc/sem.rs` +- `user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc` +- 同目录必要的最小syscall/helper代码 +- kernel现有测试模块 + +内容:仅在全部 prerequisite通过后删除`SEM_UNDO -> ENOSYS`;替换`SemUndoRejected`;加入下述两层测试。禁止夹带per-set lock、namespace tunable或无关重构。 + +验收:格式、目标内核构建、kernel-internal目标及QEMU dunitest全部通过。 + +删除门禁的硬 prerequisite:立即执行、queued retry、exit replay、SETVAL/SETALL、RMID、full-ID复用保护、fork/clone owner、exec、unshare/setns(含same-Arc IPC install)、namespace lifecycle、所有prepare/rollback路径均已实现并通过内部测试。缺一项就不得开放。 + +## 14. 测试矩阵 + +测试明确分层: + +- **QEMU dunitest ABI层**只使用公开syscall和pipe/eventfd/futex及`GETNCNT`/`GETZCNT`握手,不依赖sleep猜时序,不依赖内核私有failpoint。 +- **kernel-internal层**可用测试allocator、指定generation ID和lock gate;注入只存在于`cfg(test)`或等价非产品构建,不新增用户调试ABI。 + +### 14.1 QEMU ABI(12组) + +1. **基本累计与符号**:child依次`+3|UNDO`、`-1|UNDO`,退出前净+2、退出后恢复;另测初值3上的`-2|UNDO`。 +2. **数组顺序与回滚**:重复semnum、混合flag;前项UNDO成功但后项NOWAIT阻塞或ERANGE时,调用后和退出后均无前缀效果。 +3. **semadj边界**:分别到达32767和-32768成功,下一步越界ERANGE;单数组中间越界即使后续可抵消仍失败。 +4. **queue归属与提交点**:A排队UNDO,`GETNCNT`确认;B普通操作使Ready;债务只由A退出回放。 +5. **未提交路径**:timeout、signal、NOWAIT、等待期间RMID分别验证EAGAIN/EINTR/EAGAIN/EIDRM且无回放。 +6. **SETVAL/SETALL**:两个独立groups、多sem清账;SETVAL仅清目标,SETALL清全set;SETVAL唤醒后形成的新债务仍回放。 +7. **RMID**:成功UNDO后RMID再退出,不回放、不崩溃。generation定向复用放内部层。 +8. **fork/clone owner**:普通fork不继承;`clone(CLONE_SYSVSEM|SIGCHLD)`双方操作,首个退出不回放,最后owner回放一次;不带SYSVSEM的thread flags不共享;NEWIPC|SYSVSEM返回EINVAL。 +9. **exec与退出**:成功exec到同binary helper mode后退出仍回放;可恢复exec失败后债务仍在。non-leader exec/group-exit作为基础设施稳定后的同组回归,不阻塞首版独立门禁。 +10. **exit clamp与唤醒**:上下界clamp;用GETNCNT/GETZCNT确认负/零waiter入队,最后owner退出改变值后waiter提交。 +11. **unshare SYSVSEM**:共享owners之一unshare后,新UNDO进入新group,旧group由剩余owner结算;唯一owner unshare在syscall返回前完成旧债结算。 +12. **IPC namespace与errno**:已覆盖 namespace-fd setns、pidfd setns(含 same-Arc IPC install)及 prepare 失败保持旧 attachment;回归 `EINVAL`/`E2BIG`/`EFAULT`/`EFBIG`/`EACCES`。**follow-up:补充 `unshare(CLONE_NEWIPC)` 的 QEMU ABI 用例;当前该路径仅有 kernel-internal 覆盖。** + +### 14.2 kernel-internal(3组) + +1. **owner/publication**:attachment不可Clone;observer不改变owner;share只加一次;每task take/detach至多一次;最后owner唯一drain;child guard在安装前、安装后、最终disarm前失败时先publication rollback再撤owner。 +2. **fallible事务与竞态**:已覆盖 record reservation/capacity、stale prepared record、registry snapshot、queue retry 和 RCU retirement prepare 的失败原子性,以及 SETVAL/RMID/exit 的合法串行结果。**follow-up:为 group、adjustment、registry replacement、simulation scratch、queue Arc/sops 的 allocator failure 增加可控 failpoint,逐类验证 `ENOMEM` 与零部分状态。** +3. **ID与算法**:直接构造旧set RMID后同index新generation,旧group不污染新set;覆盖逐项semadj边界、重复semnum、混合flag、SETVAL局部清账、SETALL/RMID全record清账、stale Weak压缩;代码路径断言不读取SEMUME/SEMMNU作admission判断。 + +所有case独立创建并RMID自己的set。用户态不得通过不确定循环强迫ID复用,也不得尝试创建`SEMMNU + 1`对象。 + +## 15. 最终验收 + +1. ABI 已在前置事务、ownership、记账与测试路径合入后开放;后续变更不得倒退为部分 lifecycle 支持。 +2. 上述已实现的 QEMU 与 kernel-internal 覆盖必须由明确构建目标通过;`unshare(CLONE_NEWIPC)` ABI 与逐类 allocator failpoint 保持为第 14 节标注的 follow-up,不得误报为已覆盖。 +3. `semadj = -Σ(successful nonzero SEM_UNDO sem_op)`在立即、queued和退出路径一致;逐项边界准确。 +4. blocked、errno、timeout、signal、RMID和已覆盖的 `ENOMEM` 路径均不留下semval/semadj前缀或半个registry/record/queue变更。 +5. 所有用户可触发增长保持 fallible;manager lock内无动态分配。 +6. 普通fork不继承,显式SYSVSEM共享,最后显式owner唯一回放;observer和 queue capture 的强引用都不影响 owner 结算。 +7. child attachment在任何publication前安装,guard持续到完整不可失败publication结束;失败先撤publication再撤owner。 +8. exec保留;每个真实task在robust-list后、exit_mm前只detach一次。 +9. replay clamp到`[0,32767]`、使用namespace-visible TGID更新sempid、更新`sem_otime`并rescan queue。 +10. SETVAL/SETALL先清旧债、再写值、后rescan;RMID删债不回放且先于index复用。 +11. record只用full generation-bearing semid;定向复用测试证明旧债不污染新set。 +12. queued operation归属原调用者group,只在Ready时记账;queue 保存的 `Arc` 与 prepared record 仅保障该归属,不是 task owner。 +13. `unshare(CLONE_SYSVSEM)`及两条setns路径通过;setns安装same-Arc IPC namespace仍detach;prepare失败不改变旧状态。`unshare(CLONE_NEWIPC)` 的 ABI 测试为 follow-up。 +14. namespace最后引用不变量经审计和debug测试维持;若将来无法满足,必须实现具备明确调用上下文的`destroy_all()`。 +15. 锁依赖符合第5节:不存在`group -> manager`,PCB slot不嵌套;queue 的 `undo_record`、`scratch` 与 `status` 遵循 entry 局部锁约束。 +16. `SEMUME`、`SEMMNU`不参与admission,操作数限制继续使用全局`SEMOPM`。 +17. 非`SEM_UNDO`行为无回归;kernel格式、目标架构构建、kernel-internal测试和QEMU dunitest必须持续通过。 + +本规格已同步当前最终实现:不保留待选数据模型;局部 prepared record/reservation/revision/retired、queue strong capture 与预分配 RCU retirement 均为既定实现机制。第14节明确标注的 `unshare(CLONE_NEWIPC)` ABI 和 allocator failpoint 覆盖是 follow-up,不得作为已完成验收宣称。 diff --git a/kernel/src/ipc/mod.rs b/kernel/src/ipc/mod.rs index 5923f2381a..a03bfaaa74 100644 --- a/kernel/src/ipc/mod.rs +++ b/kernel/src/ipc/mod.rs @@ -4,6 +4,7 @@ pub mod ipc_perm; pub mod kill; pub mod pipe; pub mod sem; +pub mod sem_undo; pub mod shm; pub mod sighand; pub mod signal; diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index ad25b4ed36..c5024bafea 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -1,9 +1,13 @@ // SPDX-License-Identifier: GPL-2.0-or-later // // System V semaphore subsystem, tracking Linux 6.6 `ipc/sem.c` observable -// behavior. SEM_UNDO is intentionally unsupported and rejected with ENOSYS. +// behavior, including SEM_UNDO lifecycle accounting. -use alloc::{collections::VecDeque, sync::Arc, vec::Vec}; +use alloc::{ + collections::VecDeque, + sync::{Arc, Weak}, + vec::Vec, +}; use core::fmt; use hashbrown::HashMap; @@ -13,6 +17,7 @@ use crate::{ ipc::{ id::IpcIdAllocator, ipc_perm::{self, IpcPerm, IpcPermView, PosixIpcPerm}, + sem_undo::{PreparedSemUndoRecordAction, SemUndoGroup, SemUndoRecord}, }, libs::{ spinlock::SpinLock, @@ -256,25 +261,73 @@ enum SemQueueStatus { struct SemQueueEntry { sops: Vec, pid: Option>, + undo_group: Option>, + undo_record: SpinLock>, waker: Arc, + scratch: SpinLock, status: SpinLock, } impl SemQueueEntry { - fn new( - sops: &[PosixSemBuf], + fn new_prepared( + sops: Vec, pid: Option>, + undo_group: Option>, + undo_record: Option, waker: Arc, + scratch: SemopScratch, blocker: SemBlockedOp, ) -> Self { + debug_assert_eq!( + undo_group.is_some(), + undo_record.is_some(), + "queued SEM_UNDO group and prepared record must be captured together" + ); + debug_assert!( + undo_group.is_some() + || sops + .iter() + .all(|op| (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() == 0), + "queued SEM_UNDO entry requires a captured undo group" + ); Self { - sops: sops.to_vec(), + scratch: SpinLock::new(scratch), + sops, pid, + undo_group, + undo_record: SpinLock::new(undo_record), waker, status: SpinLock::new(SemQueueStatus::Queued(blocker)), } } + fn prepare_sops(sops: &[PosixSemBuf]) -> Result, SystemError> { + let mut owned_sops = Vec::new(); + owned_sops + .try_reserve_exact(sops.len()) + .map_err(|_| SystemError::ENOMEM)?; + owned_sops.extend_from_slice(sops); + Ok(owned_sops) + } + + #[cfg(test)] + fn new( + sops: &[PosixSemBuf], + pid: Option>, + waker: Arc, + blocker: SemBlockedOp, + ) -> Self { + Self::new_prepared( + Self::prepare_sops(sops).unwrap(), + pid, + None, + None, + waker, + SemopScratch::try_new(sops.len()).unwrap(), + blocker, + ) + } + fn completed_result(&self) -> Option> { match &*self.status.lock() { SemQueueStatus::Queued(_) => None, @@ -372,9 +425,76 @@ impl KernelSemSet { } } +#[derive(Debug)] +struct SemopScratchEntry { + semnum: usize, + initial_val: i32, + virtual_val: i32, + initial_adj: i16, + virtual_adj: i16, +} + +#[derive(Debug)] +struct SemopScratch { + entries: Vec, +} + +impl SemopScratch { + fn try_new(capacity: usize) -> Result { + let mut entries = Vec::new(); + entries + .try_reserve_exact(capacity) + .map_err(|_| SystemError::ENOMEM)?; + Ok(Self { entries }) + } + + fn clear(&mut self) { + self.entries.clear(); + } + + fn entry_for( + &mut self, + set: &KernelSemSet, + semnum: usize, + undo: Option<&SemUndoRecord>, + ) -> Result<&mut SemopScratchEntry, SystemError> { + if let Some(index) = self.entries.iter().position(|entry| entry.semnum == semnum) { + return Ok(&mut self.entries[index]); + } + + if self.entries.len() == self.entries.capacity() { + return Err(SystemError::ENOMEM); + } + + let initial_val = set.sems[semnum].val; + let initial_adj = undo + .map(|record| record.adjustment(semnum)) + .unwrap_or_default(); + self.entries.push(SemopScratchEntry { + semnum, + initial_val, + virtual_val: initial_val, + initial_adj, + virtual_adj: initial_adj, + }); + Ok(self + .entries + .last_mut() + .expect("SEM_UNDO scratch entry was just inserted")) + } +} + +/// Fixed-capacity virtual semaphore state produced by `SemopScratch`. #[derive(Debug)] struct SemopSimulation { - values: HashMap, + entry_count: usize, +} + +impl SemopSimulation { + #[cfg(test)] + fn empty_for_test() -> Self { + Self { entry_count: 0 } + } } /// Result of an attempted `semop` execution @@ -384,6 +504,16 @@ enum SemopOutcome { Blocked(SemBlockedOp), } +impl SemopOutcome { + #[cfg(test)] + fn ready_for_test(self) -> SemopSimulation { + match self { + Self::Ready(simulation) => simulation, + Self::Blocked(_) => panic!("expected ready semop outcome"), + } + } +} + /// Semaphore manager #[derive(Debug)] pub struct SemManager { @@ -395,6 +525,8 @@ pub struct SemManager { key2id: HashMap, /// Total semaphores in the namespace (Linux semmns accounting) total_sems: usize, + #[allow(dead_code)] + undo_groups: Arc>>, } impl Default for SemManager { @@ -413,6 +545,7 @@ impl SemManager { id2sem: HashMap::new(), key2id: HashMap::new(), total_sems: 0, + undo_groups: Arc::new(Vec::new()), } } @@ -442,6 +575,208 @@ impl SemManager { self.id2sem.get(&idx).ok_or(SystemError::EINVAL) } + #[allow(dead_code)] + fn validate_semid_nsems(&self, semid: SemId) -> Result { + Ok(self.get_by_semid_checked(semid)?.sems.len()) + } + + #[allow(dead_code)] + fn build_undo_registry_replacement( + snapshot: &Arc>>, + group: &Arc, + ) -> Result>>, SystemError> { + let mut live = Vec::new(); + live.try_reserve_exact(snapshot.len().saturating_add(1)) + .map_err(|_| SystemError::ENOMEM)?; + let mut contains_group = false; + + for weak in snapshot.iter() { + let Some(existing) = weak.upgrade() else { + continue; + }; + contains_group |= Arc::ptr_eq(&existing, group); + if !live + .iter() + .any(|item: &Weak| item.ptr_eq(weak)) + { + live.push(Arc::downgrade(&existing)); + } + } + + if !contains_group { + live.push(Arc::downgrade(group)); + } + + Arc::try_new(live).map_err(|_| SystemError::ENOMEM) + } + + #[allow(dead_code)] + pub(crate) fn prepare_undo_record_and_registry( + ipcns: &Arc, + group: &Arc, + semid: SemId, + ) -> Result<(), SystemError> { + loop { + let (nsems, snapshot) = { + let guard = ipcns.sem.lock(); + ( + guard.validate_semid_nsems(semid)?, + guard.undo_groups.clone(), + ) + }; + + let replacement = Self::build_undo_registry_replacement(&snapshot, group)?; + let record = group.prepare_record(semid, nsems)?; + + let mut guard = ipcns.sem.lock(); + if !Arc::ptr_eq(&guard.undo_groups, &snapshot) { + drop(record); + continue; + } + + let current_nsems = guard.validate_semid_nsems(semid)?; + if current_nsems != nsems || record.adjustment_count() != current_nsems { + drop(record); + continue; + } + + guard.undo_groups = replacement; + match group.commit_prepared_record_noalloc(record) { + Ok(()) => return Ok(()), + Err(SystemError::EAGAIN_OR_EWOULDBLOCK) => continue, + Err(error) => return Err(error), + } + } + } + + fn with_undo_record_mut( + group: &Arc, + semid: SemId, + f: impl FnOnce(&mut SemUndoRecord) -> R, + ) -> Option { + group.with_record_mut(semid, f) + } + + fn compact_undo_registry(&mut self) { + if let Some(groups) = Arc::get_mut(&mut self.undo_groups) { + groups.retain(|weak| weak.strong_count() != 0); + } + } + + pub(crate) fn clear_undo_for_setval(&mut self, semid: SemId, semnum: usize) { + let mut saw_stale = false; + for weak in self.undo_groups.iter() { + let Some(group) = weak.upgrade() else { + saw_stale = true; + continue; + }; + Self::with_undo_record_mut(&group, semid, |record| { + if semnum < record.adjustment_count() { + record.clear_adjustment(semnum); + } + }); + } + if saw_stale { + self.compact_undo_registry(); + } + } + + pub(crate) fn clear_undo_for_setall(&mut self, semid: SemId) { + let mut saw_stale = false; + for weak in self.undo_groups.iter() { + let Some(group) = weak.upgrade() else { + saw_stale = true; + continue; + }; + Self::with_undo_record_mut(&group, semid, |record| record.clear_all_adjustments()); + } + if saw_stale { + self.compact_undo_registry(); + } + } + + pub(crate) fn discard_undo_for_rmid(&mut self, semid: SemId) { + let mut saw_stale = false; + for weak in self.undo_groups.iter() { + let Some(group) = weak.upgrade() else { + saw_stale = true; + continue; + }; + group.remove_record(semid); + } + if saw_stale { + self.compact_undo_registry(); + } + } + + #[allow(dead_code)] + pub(crate) fn prune_and_apply_setval_undo(&mut self, semid: SemId, semnum: usize) { + self.clear_undo_for_setval(semid, semnum); + } + + #[allow(dead_code)] + pub(crate) fn prune_and_apply_setall_undo(&mut self, semid: SemId) { + self.clear_undo_for_setall(semid); + } + + #[allow(dead_code)] + pub(crate) fn remove_undo_records_for_rmid(&mut self, semid: SemId) { + self.discard_undo_for_rmid(semid); + } + + #[cfg(test)] + fn update_queue_for_test(&mut self, semid: SemId) { + let Ok(set) = self.get_by_semid_checked_mut(semid) else { + return; + }; + Self::update_queue(set, semid); + } + + #[cfg(test)] + fn live_undo_group_count_for_test(&self) -> usize { + self.undo_groups + .iter() + .filter(|weak| weak.upgrade().is_some()) + .count() + } + + #[cfg(test)] + fn undo_registry_contains_for_test(&self, group: &Arc) -> bool { + self.undo_groups + .iter() + .any(|weak| weak.ptr_eq(&Arc::downgrade(group))) + } + + #[cfg(test)] + fn namespace_lifecycle_invariant_for_test(&self) -> bool { + self.undo_groups.iter().all(|weak| { + weak.upgrade() + .is_none_or(|group| group.record_count_for_test() == 0) + }) + } + + #[cfg(test)] + pub(crate) fn prepare_undo_record_and_registry_for_test( + &mut self, + group: &Arc, + semid: SemId, + ) -> Result<(), SystemError> { + let nsems = self.validate_semid_nsems(semid)?; + let snapshot = self.undo_groups.clone(); + let replacement = Self::build_undo_registry_replacement(&snapshot, group)?; + let record = group.prepare_record(semid, nsems)?; + if !Arc::ptr_eq(&self.undo_groups, &snapshot) { + drop(record); + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + if self.validate_semid_nsems(semid)? != nsems { + drop(record); + return Err(SystemError::EINVAL); + } + self.undo_groups = replacement; + group.commit_prepared_record_noalloc(record) + } + fn current_max_index(&self) -> usize { self.id2sem.keys().copied().max().unwrap_or(0) } @@ -535,12 +870,14 @@ impl SemManager { Ok(sem_id.data()) } - /// Simulate sops in order without changing the real semaphore values. + /// Simulate sops in order without changing shared semaphore values or undo records. fn simulate_semop( set: &KernelSemSet, sops: &[PosixSemBuf], + undo: Option<&mut SemUndoRecord>, + scratch: &mut SemopScratch, ) -> Result { - let mut values = HashMap::with_capacity(sops.len()); + scratch.clear(); for op in sops { let idx = op.sem_num as usize; @@ -548,8 +885,9 @@ impl SemManager { return Err(SystemError::EFBIG); } - let value = values.entry(idx).or_insert(set.sems[idx].val); - let current = *value; + let has_undo = (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() != 0; + let entry = scratch.entry_for(set, idx, undo.as_deref())?; + let current = entry.virtual_val; if op.sem_op == 0 { if current != 0 { return Ok(SemopOutcome::Blocked(SemBlockedOp { @@ -572,40 +910,106 @@ impl SemManager { nowait: (op.sem_flg as u32) & SemFlags::IPC_NOWAIT.bits() != 0, })); } - *value = result as i32; + + if has_undo { + let next_adj = entry.virtual_adj as i32 - op.sem_op as i32; + if !(i16::MIN as i32..=i16::MAX as i32).contains(&next_adj) { + return Err(SystemError::ERANGE); + } + entry.virtual_adj = next_adj as i16; + } + entry.virtual_val = result as i32; } - Ok(SemopOutcome::Ready(SemopSimulation { values })) + Ok(SemopOutcome::Ready(SemopSimulation { + entry_count: scratch.entries.len(), + })) } /// Commit a successful simulation while the manager lock is held. fn commit_semop( set: &mut KernelSemSet, simulation: SemopSimulation, + scratch: &SemopScratch, pid: Option>, + mut undo: Option<&mut SemUndoRecord>, ) -> bool { let mut values_changed = false; - for (idx, value) in simulation.values { - let sem = &mut set.sems[idx]; - values_changed |= sem.val != value; - sem.val = value; + for entry in scratch.entries.iter().take(simulation.entry_count) { + let sem = &mut set.sems[entry.semnum]; + values_changed |= entry.initial_val != entry.virtual_val; + sem.val = entry.virtual_val; sem.pid = pid.clone(); + if entry.virtual_adj != entry.initial_adj { + if let Some(record) = undo.as_deref_mut() { + record.set_adjustment(entry.semnum, entry.virtual_adj); + } + } } set.sem_otime = PosixTimeSpec::now().tv_sec; values_changed } /// Complete every executable queue entry without head-of-line blocking. - fn update_queue(set: &mut KernelSemSet) { + fn update_queue(set: &mut KernelSemSet, _semid: SemId) { loop { let mut values_changed = false; let mut index = 0; while index < set.waiters.len() { let entry = set.waiters[index].clone(); - match Self::simulate_semop(set, &entry.sops) { + + if let Some(group) = entry.undo_group.as_ref() { + let result = { + let mut record_slot = entry.undo_record.lock_irqsave(); + let Some(record) = record_slot.take() else { + set.waiters.remove(index); + entry.complete(Err(SystemError::EINVAL)); + entry.waker.wake(); + continue; + }; + match group.with_prepared_record_noalloc(record, |record| { + match Self::retry_queued_undo_entry(set, &entry, record) { + Ok(Some(changed)) => { + PreparedSemUndoRecordAction::Publish(Ok(Some(changed))) + } + Ok(None) => PreparedSemUndoRecordAction::Keep(Ok(None)), + Err(error) => PreparedSemUndoRecordAction::Keep(Err(error)), + } + }) { + Ok((result, kept_record)) => { + *record_slot = kept_record; + result + } + Err(error) => Err(error), + } + }; + + match result { + Ok(Some(changed)) => { + values_changed |= changed; + set.waiters.remove(index); + entry.complete(Ok(0)); + entry.waker.wake(); + if changed { + break; + } + } + Ok(None) => index += 1, + Err(error) => { + set.waiters.remove(index); + entry.complete(Err(error)); + entry.waker.wake(); + } + } + continue; + } + + let mut scratch = entry.scratch.lock(); + match Self::simulate_semop(set, &entry.sops, None, &mut scratch) { Ok(SemopOutcome::Ready(simulation)) => { - let changed = Self::commit_semop(set, simulation, entry.pid.clone()); + let changed = + Self::commit_semop(set, simulation, &scratch, entry.pid.clone(), None); values_changed |= changed; set.waiters.remove(index); entry.complete(Ok(0)); @@ -637,6 +1041,59 @@ impl SemManager { } } + fn retry_queued_undo_entry( + set: &mut KernelSemSet, + entry: &Arc, + record: &mut SemUndoRecord, + ) -> Result, SystemError> { + if record.adjustment_count() != set.sems.len() { + return Err(SystemError::EINVAL); + } + let mut scratch = entry.scratch.lock(); + match Self::simulate_semop(set, &entry.sops, Some(record), &mut scratch) { + Ok(SemopOutcome::Ready(simulation)) => Ok(Some(Self::commit_semop( + set, + simulation, + &scratch, + entry.pid.clone(), + Some(record), + ))), + Ok(SemopOutcome::Blocked(blocker)) => { + if blocker.nowait { + Err(SystemError::EAGAIN_OR_EWOULDBLOCK) + } else { + entry.update_blocker(blocker); + Ok(None) + } + } + Err(error) => Err(error), + } + } + + pub(crate) fn replay_sem_undo_adjustments( + &mut self, + semid: SemId, + adjustments: &[i16], + exiting_tgid: Option>, + ) { + let Ok(set) = self.get_by_semid_checked_mut(semid) else { + return; + }; + + for (sem, adjustment) in set.sems.iter_mut().zip(adjustments.iter().copied()) { + if adjustment == 0 { + continue; + } + + let next = (sem.val as i64 + adjustment as i64).clamp(0, SEMVMX as i64) as i32; + sem.val = next; + sem.pid = exiting_tgid.clone(); + } + + set.sem_otime = PosixTimeSpec::now().tv_sec; + Self::update_queue(set, semid); + } + fn cancel_queued_entry( &mut self, semid: SemId, @@ -647,16 +1104,28 @@ impl SemManager { return result; } - let set = match self.get_by_semid_checked_mut(semid) { - Ok(set) => set, - Err(_) => { - entry.complete(Err(SystemError::EIDRM)); - return Err(SystemError::EIDRM); + if let Ok(set) = self.get_by_semid_checked_mut(semid) { + if let Some(result) = entry.completed_result() { + return result; } - }; - set.remove_waiter(entry); - entry.complete(Err(error.clone())); - Err(error) + set.remove_waiter(entry); + if entry.complete(Err(error.clone())) { + return Err(error); + } + return entry + .completed_result() + .expect("completed semaphore queue entry lost its terminal result"); + } + + if let Some(result) = entry.completed_result() { + return result; + } + if entry.complete(Err(SystemError::EIDRM)) { + return Err(SystemError::EIDRM); + } + entry + .completed_result() + .expect("completed semaphore queue entry lost its terminal result") } /// # semtimedop: execute `sops` atomically, blocking if necessary @@ -679,28 +1148,19 @@ impl SemManager { if sops.len() > SEMOPM { return Err(SystemError::E2BIG); } - if sops - .iter() - .any(|op| (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() != 0) - { - return Err(SystemError::ENOSYS); - } + let non_blocking = timeout == Some(Duration::ZERO); + let has_undo = sops + .iter() + .any(|op| (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() != 0); // Check read permission only for all-zero waits; otherwise check write permission // to match Linux semantics. let alter = sops.iter().any(|op| op.sem_op != 0); let target_user_ns = ipcns.user_ns.clone(); - let deadline_ticks = timeout.map(|d| next_n_us_timer_jiffies(d.total_micros())); - - let (waiter, waker) = Waiter::new_pair(); - let timer = - deadline_ticks.map(|deadline| Timer::new(TimeoutWaker::new(waker.clone()), deadline)); - let pid = ProcessManager::current_pcb().task_pid_ptr(PidType::TGID); - - let entry = { - let mut guard = ipcns.sem.lock(); - let set = guard.get_by_semid_checked_mut(semid)?; + { + let guard = ipcns.sem.lock(); + let set = guard.get_by_semid_checked(semid)?; // Match Linux: check semnum bounds (EFBIG) before permissions (EACCES). if sops.iter().any(|op| op.sem_num as usize >= set.sems.len()) { return Err(SystemError::EFBIG); @@ -714,23 +1174,167 @@ impl SemManager { }, &target_user_ns, )?; + } + + let deadline_ticks = timeout.map(|d| next_n_us_timer_jiffies(d.total_micros())); + let (waiter, waker) = Waiter::new_pair(); + let timer = + deadline_ticks.map(|deadline| Timer::new(TimeoutWaker::new(waker.clone()), deadline)); + + let current = ProcessManager::current_pcb(); + let pid = current.task_pid_ptr(PidType::TGID); + let undo_group = if has_undo { + Some(current.ensure_sem_undo_group(ipcns)?) + } else { + None + }; + let mut immediate_scratch = SemopScratch::try_new(sops.len())?; + let plain_prepared_entry = if has_undo { + None + } else { + Some( + Arc::try_new(SemQueueEntry::new_prepared( + SemQueueEntry::prepare_sops(sops)?, + pid.clone(), + None, + None, + waker.clone(), + SemopScratch::try_new(sops.len())?, + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }, + )) + .map_err(|_| SystemError::ENOMEM)?, + ) + }; - match Self::simulate_semop(set, sops)? { - SemopOutcome::Ready(simulation) => { - Self::commit_semop(set, simulation, pid); - Self::update_queue(set); - return Ok(0); + let entry = loop { + let (nsems, snapshot) = { + let guard = ipcns.sem.lock(); + let set = guard.get_by_semid_checked(semid)?; + // Match Linux: check semnum bounds (EFBIG) before permissions (EACCES). + if sops.iter().any(|op| op.sem_num as usize >= set.sems.len()) { + return Err(SystemError::EFBIG); + } + ipc_perm::ipc_permission( + &set.kern_ipc_perm, + if alter { + Self::IPC_WRITE + } else { + Self::IPC_READ + }, + &target_user_ns, + )?; + (set.sems.len(), guard.undo_groups.clone()) + }; + + let prepared_undo = if let Some(group) = undo_group.as_ref() { + let replacement = Self::build_undo_registry_replacement(&snapshot, group)?; + let record = group.prepare_record(semid, nsems)?; + let entry = Arc::try_new(SemQueueEntry::new_prepared( + SemQueueEntry::prepare_sops(sops)?, + pid.clone(), + Some(group.clone()), + Some(record), + waker.clone(), + SemopScratch::try_new(sops.len())?, + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }, + )) + .map_err(|_| SystemError::ENOMEM)?; + Some((replacement, entry)) + } else { + None + }; + + let mut guard = ipcns.sem.lock(); + if !Arc::ptr_eq(&guard.undo_groups, &snapshot) { + continue; + } + if guard.validate_semid_nsems(semid)? != nsems { + continue; + } + if let Some((replacement, prepared_entry)) = prepared_undo { + let mut record_slot = prepared_entry.undo_record.lock_irqsave(); + let prepared_record = record_slot + .take() + .expect("prepared SEM_UNDO entry owns its record"); + if prepared_record.adjustment_count() != nsems { + *record_slot = Some(prepared_record); + continue; } - SemopOutcome::Blocked(blocker) => { - if blocker.nowait || non_blocking { - return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + guard.undo_groups = replacement; + let set = guard.get_by_semid_checked_mut(semid)?; + let (outcome, kept_record) = undo_group + .as_ref() + .expect("SEM_UNDO operation has a current group") + .with_prepared_record_noalloc(prepared_record, |record| { + let outcome = + Self::simulate_semop(set, sops, Some(record), &mut immediate_scratch); + match outcome { + Ok(SemopOutcome::Ready(simulation)) => { + Self::commit_semop( + set, + simulation, + &immediate_scratch, + pid.clone(), + Some(record), + ); + PreparedSemUndoRecordAction::Publish(Ok(None)) + } + Ok(SemopOutcome::Blocked(blocker)) => { + PreparedSemUndoRecordAction::Keep(Ok(Some(blocker))) + } + Err(error) => PreparedSemUndoRecordAction::Keep(Err(error)), + } + })?; + *record_slot = kept_record; + match outcome? { + None => { + drop(record_slot); + Self::update_queue(set, semid); + return Ok(0); } - if deadline_ticks.is_some_and(|deadline| clock() >= deadline) { - return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + Some(blocker) => { + if blocker.nowait || non_blocking { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + if deadline_ticks.is_some_and(|deadline| clock() >= deadline) { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + drop(record_slot); + prepared_entry.update_blocker(blocker); + set.enqueue_waiter(prepared_entry.clone()); + break prepared_entry; + } + } + } else { + let set = guard.get_by_semid_checked_mut(semid)?; + match Self::simulate_semop(set, sops, None, &mut immediate_scratch)? { + SemopOutcome::Ready(simulation) => { + Self::commit_semop(set, simulation, &immediate_scratch, pid, None); + Self::update_queue(set, semid); + return Ok(0); + } + SemopOutcome::Blocked(blocker) => { + if blocker.nowait || non_blocking { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + if deadline_ticks.is_some_and(|deadline| clock() >= deadline) { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + let prepared_entry = plain_prepared_entry + .as_ref() + .expect("plain queued semop entry is preallocated"); + prepared_entry.update_blocker(blocker); + set.enqueue_waiter(prepared_entry.clone()); + break prepared_entry.clone(); } - let entry = Arc::new(SemQueueEntry::new(sops, pid, waker.clone(), blocker)); - set.enqueue_waiter(entry.clone()); - entry } } }; @@ -768,14 +1372,15 @@ impl SemManager { ipc_perm::check_control_permission(&set.kern_ipc_perm, &target_user_ns)?; set.kern_ipc_perm.key }; + self.discard_undo_for_rmid(id); let mut set = self .id2sem .remove(&decoded.idx) .ok_or(SystemError::EINVAL)?; self.key2id.remove(&SemKey::new(key)); - self.id_allocator.free_idx(decoded.idx); self.total_sems = self.total_sems.saturating_sub(set.sems.len()); set.complete_all_removed(); + self.id_allocator.free_idx(decoded.idx); Ok(()) } @@ -871,33 +1476,45 @@ impl SemManager { if !(0..=SEMVMX).contains(&val) { return Err(SystemError::ERANGE); } - let set = self.get_by_semid_checked_mut(id)?; - if semnum >= set.sems.len() { - return Err(SystemError::EINVAL); - } - let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_WRITE, &target_user_ns)?; + let nsems = { + let set = self.get_by_semid_checked(id)?; + if semnum >= set.sems.len() { + return Err(SystemError::EINVAL); + } + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_WRITE, &target_user_ns)?; + set.sems.len() + }; + debug_assert!(semnum < nsems); + self.clear_undo_for_setval(id, semnum); + let set = self.get_by_semid_checked_mut(id)?; let sem = &mut set.sems[semnum]; sem.val = val; sem.pid = ProcessManager::current_pcb().task_pid_ptr(PidType::TGID); set.sem_ctime = PosixTimeSpec::now().tv_sec; - Self::update_queue(set); + Self::update_queue(set, id); Ok(()) } /// # SETALL: set values of all semaphores in the set without changes on validation failure pub fn setall(&mut self, token: SemSetAllToken, vals: &[u16]) -> Result<(), SystemError> { - let set = self - .get_by_semid_checked_mut(token.id) - .map_err(|_| SystemError::EIDRM)?; - if vals.len() != token.nsems || vals.len() != set.sems.len() { + let set_nsems = self + .get_by_semid_checked(token.id) + .map_err(|_| SystemError::EIDRM)? + .sems + .len(); + if vals.len() != token.nsems || vals.len() != set_nsems { return Err(SystemError::EINVAL); } if vals.iter().any(|&v| v as i32 > SEMVMX) { return Err(SystemError::ERANGE); } + self.clear_undo_for_setall(token.id); + let set = self + .get_by_semid_checked_mut(token.id) + .map_err(|_| SystemError::EIDRM)?; let pid = ProcessManager::current_pcb().task_pid_ptr(PidType::TGID); for (i, &v) in vals.iter().enumerate() { let sem = &mut set.sems[i]; @@ -905,7 +1522,7 @@ impl SemManager { sem.pid = pid.clone(); } set.sem_ctime = PosixTimeSpec::now().tv_sec; - Self::update_queue(set); + Self::update_queue(set, token.id); Ok(()) } @@ -929,7 +1546,16 @@ impl SemManager { #[cfg(test)] mod tests { use super::*; - use crate::process::cred::{Kgid, Kuid}; + use crate::{ + ipc::sem_undo::detach_sem_undo, + process::{ + cred::{Kgid, Kuid}, + fork::CloneFlags, + namespace::ipc_namespace::INIT_IPC_NAMESPACE, + namespace::pid_namespace::INIT_PID_NAMESPACE, + KernelStack, ProcessControlBlock, RawPid, + }, + }; fn test_perm(id: SemId, key: SemKey, seq: usize) -> IpcPerm { IpcPerm { @@ -976,6 +1602,850 @@ mod tests { .collect() } + fn test_ipc_ns() -> Arc { + INIT_IPC_NAMESPACE.copy_ipc_ns( + &CloneFlags::CLONE_NEWIPC, + INIT_IPC_NAMESPACE.user_ns.clone(), + ) + } + + fn test_pcb_with_group( + ipc_ns: &Arc, + ) -> (Arc, Arc) { + let pcb = ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()); + let group = pcb.ensure_sem_undo_group(ipc_ns).unwrap(); + (pcb, group) + } + + fn enqueue_test_waiter( + set: &mut KernelSemSet, + sops: &[PosixSemBuf], + blocker: SemBlockedOp, + ) -> Arc { + let (_waiter, waker) = Waiter::new_pair(); + let entry = Arc::new(SemQueueEntry::new(sops, None, waker, blocker)); + set.enqueue_waiter(entry.clone()); + entry + } + + #[test] + fn last_owner_replays_adjustment_with_clamp_and_removes_record() { + let ipc_ns = test_ipc_ns(); + let semid = { + let mut manager = ipc_ns.sem.lock(); + insert_test_set(&mut manager, SemKey::new(31), &[32766]) + }; + let (pcb, group) = test_pcb_with_group(&ipc_ns); + group.insert_test_record(semid, &[4]); + + detach_sem_undo(&pcb); + + let manager = ipc_ns.sem.lock(); + assert_eq!(sem_values(&manager, semid), vec![SEMVMX]); + assert_eq!(group.record_count_for_test(), 0); + assert!(pcb.sem_undo_group().is_none()); + } + + #[test] + fn non_last_owner_does_not_replay() { + let ipc_ns = test_ipc_ns(); + let semid = { + let mut manager = ipc_ns.sem.lock(); + insert_test_set(&mut manager, SemKey::new(32), &[10]) + }; + let (owner_one, group) = test_pcb_with_group(&ipc_ns); + let owner_two = ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()); + let mut guard = owner_one + .prepare_shared_sem_undo_attachment(&ipc_ns) + .unwrap(); + guard.install_into(&owner_two); + guard.disarm(); + group.insert_test_record(semid, &[4]); + + detach_sem_undo(&owner_one); + + assert_eq!(sem_values(&ipc_ns.sem.lock(), semid), vec![10]); + assert_eq!(group.record_count_for_test(), 1); + + detach_sem_undo(&owner_two); + + assert_eq!(sem_values(&ipc_ns.sem.lock(), semid), vec![14]); + assert_eq!(group.record_count_for_test(), 0); + } + + #[test] + fn stale_full_semid_does_not_touch_reused_index() { + let ipc_ns = test_ipc_ns(); + let old_semid = { + let mut manager = ipc_ns.sem.lock(); + manager.id_allocator = IpcIdAllocator::new(2).unwrap(); + insert_test_set(&mut manager, SemKey::new(33), &[7]) + }; + let (pcb, group) = test_pcb_with_group(&ipc_ns); + group.insert_test_record(old_semid, &[9]); + + let new_semid = { + let mut manager = ipc_ns.sem.lock(); + insert_test_set(&mut manager, SemKey::new(34), &[5]); + remove_test_set(&mut manager, old_semid); + insert_test_set(&mut manager, SemKey::new(35), &[21]) + }; + assert_ne!(old_semid, new_semid); + assert_eq!( + old_semid.data() & IpcIdAllocator::IPC_ID_IDX_MASK, + new_semid.data() & IpcIdAllocator::IPC_ID_IDX_MASK + ); + + detach_sem_undo(&pcb); + + assert_eq!(sem_values(&ipc_ns.sem.lock(), new_semid), vec![21]); + assert_eq!(group.record_count_for_test(), 0); + } + + #[test] + fn replay_updates_otime_and_rescans_waiter() { + let ipc_ns = test_ipc_ns(); + let (semid, entry) = { + let mut manager = ipc_ns.sem.lock(); + let semid = insert_test_set(&mut manager, SemKey::new(35), &[1, 2]); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + set.sem_otime = -1; + + let (_waiter, waker) = Waiter::new_pair(); + let blocker = SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }; + let entry = Arc::new(SemQueueEntry::new( + &[PosixSemBuf { + sem_num: 0, + sem_op: 0, + sem_flg: 0, + }], + None, + waker, + blocker, + )); + set.enqueue_waiter(entry.clone()); + (semid, entry) + }; + let (pcb, group) = test_pcb_with_group(&ipc_ns); + let exiting_tgid = Pid::new_for_test(RawPid::new(4242), INIT_PID_NAMESPACE.clone()); + pcb.install_pid_identity_for_test(exiting_tgid.clone()); + group.insert_test_record(semid, &[-1, 1]); + + detach_sem_undo(&pcb); + + let mut manager = ipc_ns.sem.lock(); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let values = [set.sems[0].val, set.sems[1].val]; + let waiter_pid_was_applied = set.sems[0].pid.is_none(); + let replay_pid_was_applied = set.sems[1] + .pid + .as_ref() + .is_some_and(|pid| Arc::ptr_eq(pid, &exiting_tgid)); + let replay_pid_vnr = set.sems[1] + .pid + .as_ref() + .map(|pid| pid.pid_nr_ns(&INIT_PID_NAMESPACE)); + let sem_otime = set.sem_otime; + let waiters_are_empty = set.waiters.is_empty(); + let completed_result = entry.completed_result(); + let record_count = group.record_count_for_test(); + + for sem in &mut set.sems { + sem.pid = None; + } + drop(manager); + pcb.clear_pid_identity_for_test(); + exiting_tgid.clear_numbers_for_test(); + + assert_eq!(values, [0, 3]); + assert!(waiter_pid_was_applied); + assert!(replay_pid_was_applied); + assert_eq!(replay_pid_vnr, Some(RawPid::new(4242))); + assert_ne!(sem_otime, -1); + assert!(waiters_are_empty); + assert_eq!(completed_result, Some(Ok(0))); + assert_eq!(record_count, 0); + } + + #[test] + fn replay_rescans_and_updates_otime_when_clamp_does_not_change_value() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(36), &[SEMVMX, SEMVMX]); + let entry = { + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + set.sem_otime = -1; + enqueue_test_waiter( + set, + &[PosixSemBuf { + sem_num: 0, + sem_op: -1, + sem_flg: 0, + }], + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: false, + }, + ) + }; + let exiting_tgid = Pid::new_for_test(RawPid::new(4243), INIT_PID_NAMESPACE.clone()); + + manager.replay_sem_undo_adjustments(semid, &[1, 1], Some(exiting_tgid.clone())); + + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let values = [set.sems[0].val, set.sems[1].val]; + let replay_pid_was_applied = set.sems[1] + .pid + .as_ref() + .is_some_and(|pid| Arc::ptr_eq(pid, &exiting_tgid)); + let sem_otime = set.sem_otime; + let waiters_are_empty = set.waiters.is_empty(); + let completed_result = entry.completed_result(); + for sem in &mut set.sems { + sem.pid = None; + } + exiting_tgid.clear_numbers_for_test(); + + assert_eq!(values, [SEMVMX - 1, SEMVMX]); + assert!(replay_pid_was_applied); + assert_ne!(sem_otime, -1); + assert!(waiters_are_empty); + assert_eq!(completed_result, Some(Ok(0))); + } + + #[test] + fn valid_all_zero_record_still_updates_otime_and_rescans_queue() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(37), &[0, 5]); + let entry = { + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + set.sem_otime = -1; + enqueue_test_waiter( + set, + &[PosixSemBuf { + sem_num: 0, + sem_op: 0, + sem_flg: 0, + }], + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }, + ) + }; + let exiting_tgid = Pid::new_for_test(RawPid::new(4244), INIT_PID_NAMESPACE.clone()); + + manager.replay_sem_undo_adjustments(semid, &[0, 0], Some(exiting_tgid.clone())); + + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let values = [set.sems[0].val, set.sems[1].val]; + let untouched_pid = set.sems[1].pid.is_none(); + let sem_otime = set.sem_otime; + let waiters_are_empty = set.waiters.is_empty(); + let completed_result = entry.completed_result(); + for sem in &mut set.sems { + sem.pid = None; + } + exiting_tgid.clear_numbers_for_test(); + + assert_eq!(values, [0, 5]); + assert!(untouched_pid); + assert_ne!(sem_otime, -1); + assert!(waiters_are_empty); + assert_eq!(completed_result, Some(Ok(0))); + } + + #[test] + fn new_manager_starts_with_empty_undo_registry() { + assert!(SemManager::new().undo_groups.is_empty()); + } + + #[test] + fn queued_undo_commits_to_captured_group_not_waker_current_task() { + let ipc_ns = test_ipc_ns(); + let group_a = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let group_b = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let mut manager = ipc_ns.sem.lock(); + let semid = insert_test_set(&mut manager, SemKey::new(46), &[0]); + manager + .prepare_undo_record_and_registry_for_test(&group_a, semid) + .unwrap(); + manager + .prepare_undo_record_and_registry_for_test(&group_b, semid) + .unwrap(); + + let (_waiter, waker) = Waiter::new_pair(); + let entry = Arc::new(SemQueueEntry::new_prepared( + SemQueueEntry::prepare_sops(&[undo_sop(0, -1)]).unwrap(), + None, + Some(group_a.clone()), + Some(group_a.prepare_record_for_test(semid, 1).unwrap()), + waker, + SemopScratch::try_new(1).unwrap(), + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: false, + }, + )); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + set.enqueue_waiter(entry.clone()); + set.sems[0].val = 1; + + manager.update_queue_for_test(semid); + + assert_eq!(entry.completed_result(), Some(Ok(0))); + assert_eq!(sem_values(&manager, semid), vec![0]); + assert_eq!(group_a.adjustment_for_test(semid, 0), 1); + assert_eq!(group_b.adjustment_for_test(semid, 0), 0); + } + + #[test] + fn queued_timeout_signal_and_rmid_never_commit_adjustment() { + let ipc_ns = test_ipc_ns(); + let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let mut manager = ipc_ns.sem.lock(); + let timeout_semid = insert_test_set(&mut manager, SemKey::new(47), &[0]); + let signal_semid = insert_test_set(&mut manager, SemKey::new(48), &[0]); + let rmid_semid = insert_test_set(&mut manager, SemKey::new(49), &[0]); + for semid in [timeout_semid, signal_semid, rmid_semid] { + manager + .prepare_undo_record_and_registry_for_test(&group, semid) + .unwrap(); + } + + let timeout_entry = enqueue_undo_waiter_for_test(&mut manager, timeout_semid, &group); + assert_eq!( + manager.cancel_queued_entry( + timeout_semid, + &timeout_entry, + SystemError::EAGAIN_OR_EWOULDBLOCK, + ), + Err(SystemError::EAGAIN_OR_EWOULDBLOCK) + ); + assert_eq!(group.adjustment_for_test(timeout_semid, 0), 0); + + let signal_entry = enqueue_undo_waiter_for_test(&mut manager, signal_semid, &group); + assert_eq!( + manager.cancel_queued_entry(signal_semid, &signal_entry, SystemError::EINTR), + Err(SystemError::EINTR) + ); + assert_eq!(group.adjustment_for_test(signal_semid, 0), 0); + + let rmid_entry = enqueue_undo_waiter_for_test(&mut manager, rmid_semid, &group); + manager.ipc_rmid(rmid_semid).unwrap(); + assert_eq!(rmid_entry.completed_result(), Some(Err(SystemError::EIDRM))); + assert_eq!(group.adjustment_for_test(rmid_semid, 0), 0); + } + + #[test] + fn first_record_is_registry_visible_before_future_cleanup() { + let ipc_ns = test_ipc_ns(); + let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let mut manager = ipc_ns.sem.lock(); + let semid = insert_test_set(&mut manager, SemKey::new(50), &[3]); + + manager + .prepare_undo_record_and_registry_for_test(&group, semid) + .unwrap(); + + assert_eq!(manager.live_undo_group_count_for_test(), 1); + assert!(manager.undo_registry_contains_for_test(&group)); + assert_eq!(group.adjustment_for_test(semid, 0), 0); + } + + #[test] + fn stale_weak_entries_are_compacted_without_losing_live_group() { + let ipc_ns = test_ipc_ns(); + let live = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let candidate = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let stale = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let stale_weak = Arc::downgrade(&stale); + drop(stale); + + let mut manager = ipc_ns.sem.lock(); + let semid = insert_test_set(&mut manager, SemKey::new(51), &[1]); + manager.undo_groups = Arc::new(vec![stale_weak, Arc::downgrade(&live)]); + + manager + .prepare_undo_record_and_registry_for_test(&candidate, semid) + .unwrap(); + + assert_eq!(manager.live_undo_group_count_for_test(), 2); + assert!(manager.undo_registry_contains_for_test(&live)); + assert!(manager.undo_registry_contains_for_test(&candidate)); + } + + #[test] + fn queued_undo_entry_retains_group_after_external_owner_drops() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(52), &[1]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + manager + .prepare_undo_record_and_registry_for_test(&group, semid) + .unwrap(); + let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); + let group_weak = Arc::downgrade(&group); + drop(group); + assert!(group_weak.upgrade().is_some()); + manager.get_by_semid_checked_mut(semid).unwrap().sems[0].val = 1; + + manager.update_queue_for_test(semid); + + assert_eq!(entry.completed_result(), Some(Ok(0))); + assert_eq!(sem_values(&manager, semid), vec![0]); + } + + #[test] + fn queued_undo_record_length_mismatch_completes_with_internal_error() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(53), &[1, 1]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[0]); + let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); + manager.get_by_semid_checked_mut(semid).unwrap().sems[0].val = 1; + + manager.update_queue_for_test(semid); + + assert_eq!(entry.completed_result(), Some(Err(SystemError::EINVAL))); + assert_eq!(sem_values(&manager, semid), vec![1, 1]); + assert_eq!(group.adjustment_for_test(semid, 0), 0); + } + + #[test] + fn final_owner_detach_replays_against_setval_as_a_single_serial_order() { + let ipc_ns = test_ipc_ns(); + let semid = { + let mut manager = ipc_ns.sem.lock(); + insert_test_set(&mut manager, SemKey::new(62), &[2]) + }; + let (pcb, group) = test_pcb_with_group(&ipc_ns); + group.insert_test_record(semid, &[3]); + drop(pcb.take_sem_undo_attachment().unwrap()); + assert!(group.detach_last_owner_for_test()); + + { + let mut manager = ipc_ns.sem.lock(); + manager.setval(semid, 0, 7).unwrap(); + } + group.replay_marked_records_for_test(&pcb); + + let manager = ipc_ns.sem.lock(); + assert_eq!(sem_values(&manager, semid), vec![7]); + assert_eq!(group.record_count_for_test(), 0); + assert_eq!(group.replay_count_for_test(), 1); + assert!(pcb.sem_undo_group().is_none()); + } + + #[test] + fn rmid_before_final_owner_replay_skips_detached_record_once() { + let ipc_ns = test_ipc_ns(); + let semid = { + let mut manager = ipc_ns.sem.lock(); + insert_test_set(&mut manager, SemKey::new(63), &[2]) + }; + let (pcb, group) = test_pcb_with_group(&ipc_ns); + group.insert_test_record(semid, &[3]); + drop(pcb.take_sem_undo_attachment().unwrap()); + assert!(group.detach_last_owner_for_test()); + + { + let mut manager = ipc_ns.sem.lock(); + manager.ipc_rmid(semid).unwrap(); + } + group.replay_marked_records_for_test(&pcb); + + let manager = ipc_ns.sem.lock(); + assert_eq!( + manager.get_by_semid_checked(semid), + Err(SystemError::EINVAL) + ); + assert_eq!(group.record_count_for_test(), 0); + assert_eq!(group.replay_count_for_test(), 1); + assert!(pcb.sem_undo_group().is_none()); + } + + #[test] + fn prepare_existing_undo_record_length_mismatch_returns_einval_without_mutation() { + let semid = SemId::new(64); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[5]); + + let err = group.prepare_record_for_test(semid, 2).unwrap_err(); + + assert_eq!(err, SystemError::EINVAL); + assert_eq!(group.adjustment_for_test(semid, 0), 5); + assert_eq!(group.record_count_for_test(), 1); + assert_eq!(group.pending_record_reservations_for_test(), 0); + } + + #[test] + fn setval_clears_only_target_sem_adjustment_across_all_groups() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(54), &[4, 5]); + let other_semid = insert_test_set(&mut manager, SemKey::new(55), &[6, 7]); + let group_a = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let group_b = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group_a.insert_test_record(semid, &[11, 12]); + group_b.insert_test_record(semid, &[21, 22]); + group_a.insert_test_record(other_semid, &[31, 32]); + manager.undo_groups = Arc::new(vec![Arc::downgrade(&group_a), Arc::downgrade(&group_b)]); + + manager.setval(semid, 0, 9).unwrap(); + + assert_eq!(sem_values(&manager, semid), vec![9, 5]); + assert_eq!(group_a.adjustment_for_test(semid, 0), 0); + assert_eq!(group_a.adjustment_for_test(semid, 1), 12); + assert_eq!(group_b.adjustment_for_test(semid, 0), 0); + assert_eq!(group_b.adjustment_for_test(semid, 1), 22); + assert_eq!(group_a.adjustment_for_test(other_semid, 0), 31); + } + + #[test] + fn setall_clears_entire_full_semid_record_across_all_groups() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(56), &[1, 2, 3]); + let other_semid = insert_test_set(&mut manager, SemKey::new(57), &[4, 5, 6]); + let group_a = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let group_b = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group_a.insert_test_record(semid, &[1, 2, 3]); + group_b.insert_test_record(semid, &[4, 5, 6]); + group_b.insert_test_record(other_semid, &[7, 8, 9]); + manager.undo_groups = Arc::new(vec![Arc::downgrade(&group_a), Arc::downgrade(&group_b)]); + let token = SemSetAllToken::new(semid, 3); + + manager.setall(token, &[10, 11, 12]).unwrap(); + + assert_eq!(sem_values(&manager, semid), vec![10, 11, 12]); + assert_eq!(group_a.adjustment_for_test(semid, 0), 0); + assert_eq!(group_a.adjustment_for_test(semid, 1), 0); + assert_eq!(group_a.adjustment_for_test(semid, 2), 0); + assert_eq!(group_b.adjustment_for_test(semid, 0), 0); + assert_eq!(group_b.adjustment_for_test(semid, 1), 0); + assert_eq!(group_b.adjustment_for_test(semid, 2), 0); + assert_eq!(group_b.adjustment_for_test(other_semid, 1), 8); + } + + #[test] + fn setval_cleanup_precedes_value_write_and_queue_rescan() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(58), &[0]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[7]); + manager.undo_groups = Arc::new(vec![Arc::downgrade(&group)]); + let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); + + manager.setval(semid, 0, 1).unwrap(); + + assert_eq!(entry.completed_result(), Some(Ok(0))); + assert_eq!(sem_values(&manager, semid), vec![0]); + assert_eq!(group.adjustment_for_test(semid, 0), 1); + } + + #[test] + fn rmid_discards_record_before_index_can_be_reused() { + let mut manager = SemManager::new(); + manager.id_allocator = IpcIdAllocator::new(2).unwrap(); + let old_semid = insert_test_set(&mut manager, SemKey::new(59), &[3]); + let filler_semid = insert_test_set(&mut manager, SemKey::new(60), &[4]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(old_semid, &[9]); + manager.undo_groups = Arc::new(vec![Arc::downgrade(&group)]); + manager.ipc_rmid(old_semid).unwrap(); + + let new_semid = insert_test_set(&mut manager, SemKey::new(61), &[5]); + + assert_ne!(old_semid, new_semid); + assert_eq!( + old_semid.data() & IpcIdAllocator::IPC_ID_IDX_MASK, + new_semid.data() & IpcIdAllocator::IPC_ID_IDX_MASK + ); + assert_eq!(sem_values(&manager, new_semid), vec![5]); + assert_eq!(group.record_count_for_test(), 0); + assert_eq!(sem_values(&manager, filler_semid), vec![4]); + } + + #[test] + fn setval_clear_between_prepare_and_commit_refreshes_stale_existing_record() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(62), &[4]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[7]); + manager.undo_groups = Arc::new(vec![Arc::downgrade(&group)]); + + let record = group.prepare_record_for_test(semid, 1).unwrap(); + manager.clear_undo_for_setval(semid, 0); + let mut scratch = SemopScratch::try_new(1).unwrap(); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + + let result = group.with_prepared_record_noalloc(record, |record| { + let outcome = + SemManager::simulate_semop(set, &[undo_sop(0, -1)], Some(record), &mut scratch) + .unwrap() + .ready_for_test(); + SemManager::commit_semop(set, outcome, &scratch, None, Some(record)); + PreparedSemUndoRecordAction::Publish(()) + }); + + assert!(result.is_ok()); + assert_eq!(sem_values(&manager, semid), vec![3]); + assert_eq!(group.adjustment_for_test(semid, 0), 1); + } + + #[test] + fn setall_clear_between_prepare_and_commit_refreshes_stale_existing_record() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(63), &[4, 5]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[7, -3]); + manager.undo_groups = Arc::new(vec![Arc::downgrade(&group)]); + + let record = group.prepare_record_for_test(semid, 2).unwrap(); + manager.clear_undo_for_setall(semid); + let mut scratch = SemopScratch::try_new(1).unwrap(); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + + let result = group.with_prepared_record_noalloc(record, |record| { + let outcome = + SemManager::simulate_semop(set, &[undo_sop(0, -1)], Some(record), &mut scratch) + .unwrap() + .ready_for_test(); + SemManager::commit_semop(set, outcome, &scratch, None, Some(record)); + PreparedSemUndoRecordAction::Publish(()) + }); + + assert!(result.is_ok()); + assert_eq!(sem_values(&manager, semid), vec![3, 5]); + assert_eq!(group.adjustment_for_test(semid, 0), 1); + assert_eq!(group.adjustment_for_test(semid, 1), 0); + } + + #[test] + fn stale_existing_prepared_record_refreshes_before_immediate_commit() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(65), &[4]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[7]); + manager.undo_groups = Arc::new(vec![Arc::downgrade(&group)]); + + let record = group.prepare_record_for_test(semid, 1).unwrap(); + manager.clear_undo_for_setval(semid, 0); + let mut scratch = SemopScratch::try_new(1).unwrap(); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + + let result = group.with_prepared_record_noalloc(record, |record| { + let outcome = + SemManager::simulate_semop(set, &[undo_sop(0, -1)], Some(record), &mut scratch) + .unwrap() + .ready_for_test(); + SemManager::commit_semop(set, outcome, &scratch, None, Some(record)); + PreparedSemUndoRecordAction::Publish(()) + }); + + assert!(result.is_ok()); + assert_eq!(sem_values(&manager, semid), vec![3]); + assert_eq!(group.adjustment_for_test(semid, 0), 1); + } + + #[test] + fn queued_stale_existing_record_retries_and_completes_without_error() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(66), &[0]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[7]); + manager.undo_groups = Arc::new(vec![Arc::downgrade(&group)]); + let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); + + manager.clear_undo_for_setval(semid, 0); + manager.get_by_semid_checked_mut(semid).unwrap().sems[0].val = 2; + manager.update_queue_for_test(semid); + + assert_eq!(entry.completed_result(), Some(Ok(0))); + assert_eq!(sem_values(&manager, semid), vec![1]); + assert_eq!(group.adjustment_for_test(semid, 0), 1); + } + + #[test] + fn consecutive_sem_undo_on_unchanged_existing_record_still_accumulates() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(64), &[5]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[2]); + + let mut record = group.prepare_record_for_test(semid, 1).unwrap(); + let mut scratch = SemopScratch::try_new(1).unwrap(); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let outcome = + SemManager::simulate_semop(set, &[undo_sop(0, -2)], Some(&mut record), &mut scratch) + .unwrap() + .ready_for_test(); + SemManager::commit_semop(set, outcome, &scratch, None, Some(&mut record)); + group.commit_prepared_record_noalloc(record).unwrap(); + + assert_eq!(group.adjustment_for_test(semid, 0), 4); + } + + fn enqueue_undo_waiter_for_test( + manager: &mut SemManager, + semid: SemId, + group: &Arc, + ) -> Arc { + let (_waiter, waker) = Waiter::new_pair(); + let entry = Arc::new(SemQueueEntry::new_prepared( + SemQueueEntry::prepare_sops(&[undo_sop(0, -1)]).unwrap(), + None, + Some(Arc::clone(group)), + Some(group.prepare_record_for_test(semid, 1).unwrap()), + waker, + SemopScratch::try_new(1).unwrap(), + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: false, + }, + )); + manager + .get_by_semid_checked_mut(semid) + .unwrap() + .enqueue_waiter(entry.clone()); + entry + } + + fn undo_sop(sem_num: u16, sem_op: i16) -> PosixSemBuf { + PosixSemBuf { + sem_num, + sem_op, + sem_flg: SemFlags::SEM_UNDO.bits() as i16, + } + } + + fn plain_sop(sem_num: u16, sem_op: i16) -> PosixSemBuf { + PosixSemBuf { + sem_num, + sem_op, + sem_flg: 0, + } + } + + fn nowait_sop(sem_num: u16, sem_op: i16) -> PosixSemBuf { + PosixSemBuf { + sem_num, + sem_op, + sem_flg: SemFlags::IPC_NOWAIT.bits() as i16, + } + } + + #[test] + fn ordered_mixed_undo_ops_apply_each_adjustment_step() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(41), &[4]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let mut record = group.prepare_record_for_test(semid, 1).unwrap(); + let mut scratch = SemopScratch::try_new(3).unwrap(); + let sops = [undo_sop(0, 3), plain_sop(0, -1), undo_sop(0, -2)]; + + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let outcome = SemManager::simulate_semop(set, &sops, Some(&mut record), &mut scratch); + assert!(matches!(outcome, Ok(SemopOutcome::Ready(_)))); + SemManager::commit_semop( + set, + outcome.unwrap().ready_for_test(), + &scratch, + None, + Some(&mut record), + ); + + assert_eq!(set.sems[0].val, 4); + assert_eq!(record.adjustment_for_test(0), -1); + } + + #[test] + fn intermediate_adjustment_overflow_is_erange_even_if_later_op_cancels_it() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(42), &[10]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let mut record = group.prepare_record_for_test(semid, 1).unwrap(); + record.set_adjustment_for_test(0, i16::MAX); + let mut scratch = SemopScratch::try_new(2).unwrap(); + let sops = [undo_sop(0, -1), undo_sop(0, 1)]; + + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + assert!(matches!( + SemManager::simulate_semop(set, &sops, Some(&mut record), &mut scratch), + Err(SystemError::ERANGE) + )); + + assert_eq!(set.sems[0].val, 10); + assert_eq!(record.adjustment_for_test(0), i16::MAX); + } + + #[test] + fn blocked_or_nowait_failure_does_not_commit_semval_or_semadj_prefix() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(43), &[2]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let mut record = group.prepare_record_for_test(semid, 1).unwrap(); + let mut scratch = SemopScratch::try_new(2).unwrap(); + let sops = [undo_sop(0, -1), nowait_sop(0, -2)]; + + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + assert!(matches!( + SemManager::simulate_semop(set, &sops, Some(&mut record), &mut scratch), + Ok(SemopOutcome::Blocked(SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: true, + })) + )); + + assert_eq!(set.sems[0].val, 2); + assert_eq!(record.adjustment_for_test(0), 0); + } + + #[test] + fn zero_undo_op_can_prepare_zero_record_without_adjustment() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(44), &[0]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let mut record = group.prepare_record_for_test(semid, 1).unwrap(); + let mut scratch = SemopScratch::try_new(1).unwrap(); + let sops = [undo_sop(0, 0)]; + + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let outcome = SemManager::simulate_semop(set, &sops, Some(&mut record), &mut scratch); + assert!(matches!(outcome, Ok(SemopOutcome::Ready(_)))); + SemManager::commit_semop( + set, + outcome.unwrap().ready_for_test(), + &scratch, + None, + Some(&mut record), + ); + group.commit_record(record).unwrap(); + + assert_eq!(set.sems[0].val, 0); + assert_eq!(group.record_count_for_test(), 1); + } + + #[test] + fn scratch_entry_for_returns_enomem_instead_of_extending_past_capacity() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(45), &[1, 1]); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let mut scratch = SemopScratch::try_new(1).unwrap(); + let sops = [plain_sop(0, -1), plain_sop(1, -1)]; + + assert!(matches!( + SemManager::simulate_semop(set, &sops, None, &mut scratch), + Err(SystemError::ENOMEM) + )); + } + #[test] fn prepared_setall_token_returns_eidrm_after_rmid() { let mut manager = SemManager::new(); diff --git a/kernel/src/ipc/sem_undo.rs b/kernel/src/ipc/sem_undo.rs new file mode 100644 index 0000000000..b94dd887a6 --- /dev/null +++ b/kernel/src/ipc/sem_undo.rs @@ -0,0 +1,983 @@ +use alloc::{ + boxed::Box, + sync::{Arc, Weak}, + vec::Vec, +}; +use system_error::SystemError; + +use crate::{ + ipc::sem::{SemId, SemManager}, + libs::spinlock::SpinLock, + process::{namespace::ipc_namespace::IpcNamespace, pid::PidType, ProcessControlBlock}, +}; + +#[derive(Debug)] +pub struct SemUndoAttachment { + group: Arc, +} + +#[derive(Debug)] +pub struct SemUndoGroup { + ipc_ns: Weak, + #[allow(dead_code)] + inner: SpinLock, +} + +#[allow(dead_code)] +#[derive(Debug)] +struct SemUndoGroupState { + task_owners: usize, + records: Vec, + reserved_records: usize, + absence_generation: u64, + retired: bool, + records_taken: bool, + #[cfg(test)] + replay_count: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PreparedSemUndoRecordState { + Existing { expected_revision: u64 }, + Missing { expected_absence_generation: u64 }, + Live, +} + +#[derive(Debug)] +struct PendingSemUndoRecordReservation { + group: Weak, + active: bool, +} + +#[derive(Debug)] +pub(crate) struct SemUndoRecord { + semid: SemId, + adjustments: Box<[i16]>, + revision: u64, + prepared_state: PreparedSemUndoRecordState, + reservation: Option, +} + +pub(crate) enum PreparedSemUndoRecordAction { + Publish(R), + Keep(R), +} + +impl PendingSemUndoRecordReservation { + fn new(group: &Arc) -> Self { + Self { + group: Arc::downgrade(group), + active: true, + } + } + + fn disarm(&mut self) { + self.active = false; + } +} + +impl Drop for PendingSemUndoRecordReservation { + fn drop(&mut self) { + if !self.active { + return; + } + let Some(group) = self.group.upgrade() else { + return; + }; + let mut state = group.inner.lock_irqsave(); + state.reserved_records = state + .reserved_records + .checked_sub(1) + .expect("SEM_UNDO record reservation count underflow"); + self.active = false; + } +} + +fn prepare_records_storage_capacity( + records: &mut Vec, + required_capacity: usize, +) -> Result<(), SystemError> { + if records.capacity() >= required_capacity { + return Ok(()); + } + + let additional = required_capacity + .checked_sub(records.len()) + .ok_or(SystemError::ENOMEM)?; + records + .try_reserve_exact(additional) + .map_err(|_| SystemError::ENOMEM)?; + if records.capacity() < required_capacity { + return Err(SystemError::ENOMEM); + } + Ok(()) +} + +impl SemUndoGroupState { + fn bump_absence_generation(&mut self) { + self.absence_generation = self.absence_generation.wrapping_add(1); + } +} + +impl SemUndoRecord { + #[cfg(test)] + fn new_live(semid: SemId, adjustments: Box<[i16]>) -> Self { + Self { + semid, + adjustments, + revision: 0, + prepared_state: PreparedSemUndoRecordState::Live, + reservation: None, + } + } + + fn bump_revision(&mut self) { + self.revision = self.revision.wrapping_add(1); + } + + fn publish_from(&mut self, record: &SemUndoRecord) { + self.adjustments.copy_from_slice(&record.adjustments); + self.bump_revision(); + } + + fn refresh_existing(&mut self, existing: &SemUndoRecord) { + self.adjustments.copy_from_slice(&existing.adjustments); + self.revision = existing.revision; + self.prepared_state = PreparedSemUndoRecordState::Existing { + expected_revision: existing.revision, + }; + } + + fn refresh_missing(&mut self, absence_generation: u64) { + self.adjustments.fill(0); + self.revision = 0; + self.prepared_state = PreparedSemUndoRecordState::Missing { + expected_absence_generation: absence_generation, + }; + } + + fn mark_live(&mut self) { + self.prepared_state = PreparedSemUndoRecordState::Live; + } + + pub(crate) fn adjustment(&self, semnum: usize) -> i16 { + self.adjustments[semnum] + } + + pub(crate) fn set_adjustment(&mut self, semnum: usize, adjustment: i16) { + if self.adjustments[semnum] != adjustment { + self.adjustments[semnum] = adjustment; + self.bump_revision(); + } + } + + pub(crate) fn clear_adjustment(&mut self, semnum: usize) { + self.set_adjustment(semnum, 0); + } + + pub(crate) fn clear_all_adjustments(&mut self) { + if self.adjustments.iter().any(|&adjustment| adjustment != 0) { + self.adjustments.fill(0); + self.bump_revision(); + } + } + + pub(crate) fn adjustment_count(&self) -> usize { + self.adjustments.len() + } + + #[cfg(test)] + pub(crate) fn adjustment_for_test(&self, semnum: usize) -> i16 { + self.adjustment(semnum) + } + + #[cfg(test)] + pub(crate) fn set_adjustment_for_test(&mut self, semnum: usize, adjustment: i16) { + self.set_adjustment(semnum, adjustment); + } +} + +pub(crate) struct UnpublishedSemUndoAttachmentGuard { + group: Arc, + attachment: Option, + installed_child: Option>, + armed: bool, +} + +impl SemUndoAttachment { + pub(crate) fn new(group: Arc) -> Self { + Self { group } + } + + pub(crate) fn group(&self) -> Arc { + self.group.clone() + } + + #[cfg(test)] + fn new_for_test(group: Arc) -> Self { + Self::new(group) + } + + #[cfg(test)] + fn group_for_test(&self) -> Arc { + self.group() + } +} + +impl Drop for SemUndoAttachment { + // Replay is an explicit lifecycle operation and must never run from Drop. + fn drop(&mut self) {} +} + +impl SemUndoGroup { + pub(crate) fn new(ipc_ns: &Arc) -> Result, SystemError> { + Arc::try_new(Self { + ipc_ns: Arc::downgrade(ipc_ns), + inner: SpinLock::new(SemUndoGroupState { + task_owners: 1, + records: Vec::new(), + reserved_records: 0, + absence_generation: 0, + retired: false, + records_taken: false, + #[cfg(test)] + replay_count: 0, + }), + }) + .map_err(|_| SystemError::ENOMEM) + } + + pub(crate) fn verify_ipc_ns(&self, ipc_ns: &Arc) -> Result<(), SystemError> { + let state = self.inner.lock_irqsave(); + if state.task_owners == 0 || state.retired { + return Err(SystemError::EINVAL); + } + if self.ipc_ns.ptr_eq(&Arc::downgrade(ipc_ns)) { + Ok(()) + } else { + Err(SystemError::EINVAL) + } + } + + fn detach_owner_and_mark_last(&self) -> bool { + let mut state = self.inner.lock_irqsave(); + debug_assert!( + state.task_owners > 0, + "SEM_UNDO detach requires an attached task owner" + ); + if state.task_owners == 0 { + return false; + } + + state.task_owners -= 1; + if state.task_owners != 0 { + return false; + } + + state.retired = true; + true + } + + fn take_retired_records(&self) -> Vec { + let mut state = self.inner.lock_irqsave(); + if !state.retired || state.records_taken { + return Vec::new(); + } + state.records_taken = true; + #[cfg(test)] + { + state.replay_count += 1; + } + core::mem::take(&mut state.records) + } + + #[cfg(test)] + fn new_for_test() -> Arc { + Self::new(&crate::process::namespace::ipc_namespace::INIT_IPC_NAMESPACE).unwrap() + } + + #[cfg(test)] + fn new_for_test_bound_to_first_namespace() -> Arc { + Self::new_for_test() + } + + #[cfg(test)] + pub(crate) fn new_for_test_bound_to( + ipc_ns: &Arc, + ) -> Result, SystemError> { + Self::new(ipc_ns) + } + + #[allow(dead_code)] + pub(crate) fn prepare_record( + self: &Arc, + semid: SemId, + nsems: usize, + ) -> Result { + let mut adjustments = Vec::new(); + adjustments + .try_reserve_exact(nsems) + .map_err(|_| SystemError::ENOMEM)?; + adjustments.resize(nsems, 0); + + let mut reserved_storage = Vec::new(); + + loop { + let mut state = self.inner.lock_irqsave(); + if state.task_owners == 0 || state.retired { + return Err(SystemError::EINVAL); + } + + if let Some(existing) = state.records.iter().find(|item| item.semid == semid) { + if existing.adjustments.len() != nsems { + return Err(SystemError::EINVAL); + } + adjustments.copy_from_slice(&existing.adjustments); + return Ok(SemUndoRecord { + semid, + adjustments: adjustments.into_boxed_slice(), + revision: existing.revision, + prepared_state: PreparedSemUndoRecordState::Existing { + expected_revision: existing.revision, + }, + reservation: None, + }); + } + + let required_capacity = state + .records + .len() + .checked_add(state.reserved_records) + .and_then(|capacity| capacity.checked_add(1)) + .ok_or(SystemError::ENOMEM)?; + + if state.records.capacity() < required_capacity { + if reserved_storage.capacity() < required_capacity { + drop(state); + prepare_records_storage_capacity(&mut reserved_storage, required_capacity)?; + continue; + } + + reserved_storage.append(&mut state.records); + core::mem::swap(&mut state.records, &mut reserved_storage); + } + + state.reserved_records = state + .reserved_records + .checked_add(1) + .ok_or(SystemError::ENOMEM)?; + return Ok(SemUndoRecord { + semid, + adjustments: adjustments.into_boxed_slice(), + revision: 0, + prepared_state: PreparedSemUndoRecordState::Missing { + expected_absence_generation: state.absence_generation, + }, + reservation: Some(PendingSemUndoRecordReservation::new(self)), + }); + } + } + + #[allow(dead_code)] + pub(crate) fn commit_record(&self, record: SemUndoRecord) -> Result<(), SystemError> { + self.commit_prepared_record_noalloc(record) + } + + pub(crate) fn commit_prepared_record_noalloc( + &self, + record: SemUndoRecord, + ) -> Result<(), SystemError> { + self.with_prepared_record_noalloc( + record, + |_record| PreparedSemUndoRecordAction::Publish(()), + ) + .map(|((), _)| ()) + } + + pub(crate) fn with_prepared_record_noalloc( + &self, + mut record: SemUndoRecord, + f: impl FnOnce(&mut SemUndoRecord) -> PreparedSemUndoRecordAction, + ) -> Result<(R, Option), SystemError> { + let mut state = self.inner.lock_irqsave(); + if state.task_owners == 0 || state.retired { + return Err(SystemError::EINVAL); + } + + let existing_index = state + .records + .iter() + .position(|item| item.semid == record.semid); + + if let Some(index) = existing_index { + if state.records[index].adjustments.len() != record.adjustments.len() { + return Err(SystemError::EINVAL); + } + let current_revision = state.records[index].revision; + let stale = !matches!( + record.prepared_state, + PreparedSemUndoRecordState::Existing { expected_revision } + if current_revision == expected_revision + ) && !matches!(record.prepared_state, PreparedSemUndoRecordState::Live); + if stale { + record.refresh_existing(&state.records[index]); + } + if let Some(mut reservation) = record.reservation.take() { + state.reserved_records = state + .reserved_records + .checked_sub(1) + .expect("SEM_UNDO record reservation count underflow"); + reservation.disarm(); + } + + return match f(&mut record) { + PreparedSemUndoRecordAction::Publish(result) => { + state.records[index].publish_from(&record); + Ok((result, None)) + } + PreparedSemUndoRecordAction::Keep(result) => Ok((result, Some(record))), + }; + } + + match record.prepared_state { + PreparedSemUndoRecordState::Missing { + expected_absence_generation, + } if expected_absence_generation == state.absence_generation => {} + PreparedSemUndoRecordState::Live => {} + _ => record.refresh_missing(state.absence_generation), + } + + if state.records.len() >= state.records.capacity() { + return Err(SystemError::ENOMEM); + } + + match f(&mut record) { + PreparedSemUndoRecordAction::Publish(result) => { + if let Some(mut reservation) = record.reservation.take() { + state.reserved_records = state + .reserved_records + .checked_sub(1) + .expect("SEM_UNDO record reservation count underflow"); + reservation.disarm(); + } + record.mark_live(); + state.records.push(record); + state.bump_absence_generation(); + Ok((result, None)) + } + PreparedSemUndoRecordAction::Keep(result) => Ok((result, Some(record))), + } + } + + pub(crate) fn with_record_mut( + &self, + semid: SemId, + f: impl FnOnce(&mut SemUndoRecord) -> R, + ) -> Option { + let mut state = self.inner.lock_irqsave(); + state + .records + .iter_mut() + .find(|record| record.semid == semid) + .map(f) + } + + pub(crate) fn remove_record(&self, semid: SemId) { + let mut state = self.inner.lock_irqsave(); + state.records.retain(|record| record.semid != semid); + state.bump_absence_generation(); + } + + #[cfg(test)] + pub(crate) fn adjustment_for_test(&self, semid: SemId, semnum: usize) -> i16 { + self.inner + .lock_irqsave() + .records + .iter() + .find(|record| record.semid == semid) + .map(|record| record.adjustment(semnum)) + .unwrap_or_default() + } + + #[cfg(test)] + pub(crate) fn has_live_records_in_namespace_for_test( + &self, + ipc_ns: &Arc, + ) -> bool { + self.verify_ipc_ns(ipc_ns).is_ok() && !self.inner.lock_irqsave().records.is_empty() + } + + #[cfg(test)] + pub(crate) fn prepare_record_for_test( + self: &Arc, + semid: SemId, + nsems: usize, + ) -> Result { + self.prepare_record(semid, nsems) + } + + #[cfg(test)] + pub(crate) fn task_owners_for_test(&self) -> usize { + self.inner.lock_irqsave().task_owners + } + + #[cfg(test)] + pub(crate) fn replay_count_for_test(&self) -> usize { + self.inner.lock_irqsave().replay_count + } + + #[cfg(test)] + fn verify_ipc_ns_for_test(&self, ipc_ns: Arc) -> Result<(), SystemError> { + self.verify_ipc_ns(&ipc_ns) + } + + #[cfg(test)] + pub(crate) fn insert_test_record(&self, semid: SemId, adjustments: &[i16]) { + let mut state = self.inner.lock_irqsave(); + state.records.push(SemUndoRecord::new_live( + semid, + adjustments.to_vec().into_boxed_slice(), + )); + state.bump_absence_generation(); + } + + #[cfg(test)] + pub(crate) fn record_count_for_test(&self) -> usize { + self.inner.lock_irqsave().records.len() + } + + #[cfg(test)] + pub(crate) fn record_capacity_for_test(&self) -> usize { + self.inner.lock_irqsave().records.capacity() + } + + #[cfg(test)] + pub(crate) fn set_record_capacity_for_test(&self, capacity: usize) { + let mut state = self.inner.lock_irqsave(); + assert!(state.records.is_empty()); + state.records = Vec::with_capacity(capacity); + assert_eq!(state.records.capacity(), capacity); + } + + #[cfg(test)] + pub(crate) fn pending_record_reservations_for_test(&self) -> usize { + self.inner.lock_irqsave().reserved_records + } + + #[cfg(test)] + pub(crate) fn detach_last_owner_for_test(&self) -> bool { + self.detach_owner_and_mark_last() + } + + #[cfg(test)] + pub(crate) fn replay_marked_records_for_test(self: &Arc, pcb: &Arc) { + replay_marked_records(pcb, self); + } +} + +pub(crate) fn detach_sem_undo(pcb: &Arc) { + let Some(attachment) = pcb.take_sem_undo_attachment() else { + return; + }; + let group = attachment.group(); + drop(attachment); + + if !group.detach_owner_and_mark_last() { + return; + } + + replay_marked_records(pcb, &group); +} + +fn replay_marked_records(pcb: &Arc, group: &Arc) { + let exiting_tgid = pcb.try_active_pid_ns().and_then(|pid_ns| { + pcb.task_pid_nr_ns(PidType::TGID, Some(pid_ns)) + .filter(|tgid| tgid.data() != 0)?; + pcb.task_pid_ptr(PidType::TGID) + }); + + let Some(ipc_ns) = group.ipc_ns.upgrade() else { + for record in group.take_retired_records() { + log::debug!( + "dropping SEM_UNDO record for semid {} after IPC namespace teardown", + record.semid.data() + ); + } + return; + }; + + let mut manager = ipc_ns.sem.lock(); + let records = group.take_retired_records(); + for record in records { + SemManager::replay_sem_undo_adjustments( + &mut manager, + record.semid, + &record.adjustments, + exiting_tgid.clone(), + ); + } +} + +impl UnpublishedSemUndoAttachmentGuard { + pub(crate) fn new(group: Arc) -> Self { + { + let mut state = group.inner.lock_irqsave(); + debug_assert!( + state.task_owners > 0 && !state.retired, + "SEM_UNDO shared owner must be acquired before final retirement" + ); + state.task_owners = state + .task_owners + .checked_add(1) + .expect("SEM_UNDO task owner count overflow"); + } + + Self { + attachment: Some(SemUndoAttachment::new(group.clone())), + group, + installed_child: None, + armed: true, + } + } + + pub(crate) fn install_into(&mut self, child: &ProcessControlBlock) { + assert!(self.armed, "cannot install a disarmed SEM_UNDO guard"); + assert!( + self.installed_child.is_none(), + "SEM_UNDO guard can only be installed once" + ); + let attachment = self + .attachment + .take() + .expect("SEM_UNDO guard attachment token is missing"); + self.installed_child = Some(child.install_unpublished_sem_undo_attachment(attachment)); + } + + pub(crate) fn disarm(mut self) { + debug_assert!( + self.attachment.is_none() && self.installed_child.is_some(), + "only an installed SEM_UNDO guard can be disarmed" + ); + self.armed = false; + } +} + +impl Drop for UnpublishedSemUndoAttachmentGuard { + fn drop(&mut self) { + if !self.armed { + return; + } + + if let Some(child) = self.installed_child.take() { + let attachment = child.take_sem_undo_attachment(); + debug_assert!(attachment.is_some(), "installed SEM_UNDO slot is empty"); + if let Some(attachment) = attachment { + debug_assert!(Arc::ptr_eq(&attachment.group, &self.group)); + drop(attachment); + } + } else { + drop(self.attachment.take()); + } + + let mut state = self.group.inner.lock_irqsave(); + debug_assert!( + state.task_owners > 1, + "unpublished SEM_UNDO rollback requires the parent owner" + ); + state.task_owners -= 1; + } +} + +#[cfg(test)] +mod tests { + use alloc::sync::Arc; + + use super::{ + detach_sem_undo, PreparedSemUndoRecordState, SemUndoAttachment, SemUndoGroup, + SemUndoRecord, UnpublishedSemUndoAttachmentGuard, + }; + use crate::ipc::sem::SemId; + use crate::process::{ + fork::CloneFlags, + namespace::ipc_namespace::{IpcNamespace, INIT_IPC_NAMESPACE}, + KernelStack, ProcessControlBlock, + }; + use system_error::SystemError; + + fn test_ipc_ns() -> &'static Arc { + &INIT_IPC_NAMESPACE + } + + fn test_unpublished_child() -> Arc { + ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()) + } + + fn test_pcb_with_group() -> Arc { + let pcb = test_unpublished_child(); + pcb.ensure_sem_undo_group(test_ipc_ns()).unwrap(); + pcb + } + + fn second_test_ipc_ns() -> Arc { + INIT_IPC_NAMESPACE.copy_ipc_ns( + &CloneFlags::CLONE_NEWIPC, + INIT_IPC_NAMESPACE.user_ns.clone(), + ) + } + + #[test] + fn missing_reservation_cannot_publish_after_final_drain() { + let pcb = test_pcb_with_group(); + let group = pcb.sem_undo_group().unwrap(); + let semid = SemId::new(208); + let record = group.prepare_record_for_test(semid, 1).unwrap(); + + detach_sem_undo(&pcb); + + assert_eq!(group.task_owners_for_test(), 0); + assert_eq!(group.commit_record(record), Err(SystemError::EINVAL)); + assert_eq!(group.record_count_for_test(), 0); + assert_eq!(group.pending_record_reservations_for_test(), 0); + assert_eq!( + group.prepare_record_for_test(semid, 1), + Err(SystemError::EINVAL) + ); + } + + #[test] + fn retired_group_rejects_shared_owner_and_replays_only_once() { + let pcb = test_pcb_with_group(); + let group = pcb.sem_undo_group().unwrap(); + let child = test_unpublished_child(); + group.insert_test_record(SemId::new(209), &[1]); + + detach_sem_undo(&pcb); + + assert_eq!(group.replay_count_for_test(), 1); + assert_eq!(group.task_owners_for_test(), 0); + assert!(matches!( + pcb.prepare_shared_sem_undo_attachment(test_ipc_ns()), + Err(SystemError::EINVAL) + )); + group.replay_marked_records_for_test(&pcb); + detach_sem_undo(&child); + assert_eq!(group.replay_count_for_test(), 1); + } + + #[test] + fn prepared_existing_and_missing_records_cannot_publish_after_final_drain() { + let pcb = test_pcb_with_group(); + let group = pcb.sem_undo_group().unwrap(); + let existing_semid = SemId::new(210); + let missing_semid = SemId::new(211); + group.insert_test_record(existing_semid, &[1]); + let existing = group.prepare_record_for_test(existing_semid, 1).unwrap(); + let missing = group.prepare_record_for_test(missing_semid, 1).unwrap(); + + detach_sem_undo(&pcb); + + assert_eq!(group.commit_record(existing), Err(SystemError::EINVAL)); + assert_eq!(group.commit_record(missing), Err(SystemError::EINVAL)); + assert_eq!(group.record_count_for_test(), 0); + assert_eq!(group.pending_record_reservations_for_test(), 0); + } + + #[test] + fn prepare_existing_record_is_rejected_after_final_drain() { + let pcb = test_pcb_with_group(); + let group = pcb.sem_undo_group().unwrap(); + let semid = SemId::new(210); + group.insert_test_record(semid, &[1]); + + detach_sem_undo(&pcb); + + assert_eq!(group.record_count_for_test(), 0); + assert_eq!( + group.prepare_record_for_test(semid, 1), + Err(SystemError::EINVAL) + ); + } + + #[test] + fn namespace_lifecycle_invariant_has_no_live_record_at_final_drop() { + let ipc_ns = second_test_ipc_ns(); + let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let mut manager = ipc_ns.sem.lock(); + let semid = manager + .semget( + crate::ipc::sem::IPC_PRIVATE, + 1, + crate::ipc::sem::SemFlags::IPC_CREAT, + ) + .unwrap(); + manager + .prepare_undo_record_and_registry_for_test(&group, SemId::new(semid)) + .unwrap(); + + assert!(group.has_live_records_in_namespace_for_test(&ipc_ns)); + drop(manager); + drop(group); + assert!(ipc_ns.sem.lock().namespace_lifecycle_invariant_for_test()); + } + + #[test] + fn prepare_existing_record_copies_current_adjustments() { + let group = SemUndoGroup::new_for_test(); + let semid = SemId::new(101); + group.insert_test_record(semid, &[7, -3]); + + let record = group.prepare_record_for_test(semid, 2).unwrap(); + + assert_eq!(record.adjustment_for_test(0), 7); + assert_eq!(record.adjustment_for_test(1), -3); + } + + #[test] + fn concurrent_prepare_for_distinct_semids_reserves_each_missing_record_slot() { + let group = SemUndoGroup::new_for_test(); + let semid_one = SemId::new(201); + let semid_two = SemId::new(202); + + let record_one = group.prepare_record_for_test(semid_one, 1).unwrap(); + let record_two = group.prepare_record_for_test(semid_two, 1).unwrap(); + + assert_eq!(group.pending_record_reservations_for_test(), 2); + assert!(group.record_capacity_for_test() >= 2); + group.commit_record(record_one).unwrap(); + group.commit_record(record_two).unwrap(); + assert_eq!(group.record_count_for_test(), 2); + assert_eq!(group.pending_record_reservations_for_test(), 0); + } + + #[test] + fn prepare_missing_record_reserves_capacity_for_two_outstanding_reservations() { + let group = SemUndoGroup::new_for_test(); + group.set_record_capacity_for_test(1); + + let record_one = group + .prepare_record_for_test(SemId::new(204), 1) + .expect("first reservation must fit in the single free slot"); + let record_two = group + .prepare_record_for_test(SemId::new(205), 1) + .expect("second reservation must grow physical capacity"); + + assert_eq!(group.pending_record_reservations_for_test(), 2); + assert!(group.record_capacity_for_test() >= 2); + group.commit_record(record_one).unwrap(); + group.commit_record(record_two).unwrap(); + assert_eq!(group.record_count_for_test(), 2); + } + + #[test] + fn missing_record_reservation_loses_to_competing_insert_with_retry() { + let group = SemUndoGroup::new_for_test(); + let semid = SemId::new(206); + let mut stale = group.prepare_record_for_test(semid, 1).unwrap(); + stale.set_adjustment_for_test(0, 4); + group.insert_test_record(semid, &[9]); + + assert_eq!(group.commit_record(stale), Ok(())); + assert_eq!(group.adjustment_for_test(semid, 0), 9); + assert_eq!(group.pending_record_reservations_for_test(), 0); + } + + #[test] + fn missing_record_reservation_loses_to_rmid_generation_with_retry() { + let group = SemUndoGroup::new_for_test(); + let semid = SemId::new(207); + let mut stale = group.prepare_record_for_test(semid, 1).unwrap(); + stale.set_adjustment_for_test(0, 4); + group.remove_record(semid); + + assert_eq!(group.commit_record(stale), Ok(())); + assert_eq!(group.record_count_for_test(), 1); + assert_eq!(group.adjustment_for_test(semid, 0), 0); + assert_eq!(group.pending_record_reservations_for_test(), 0); + } + + #[test] + fn commit_unreserved_missing_record_returns_enomem_without_allocating() { + let group = SemUndoGroup::new_for_test(); + let before_capacity = group.record_capacity_for_test(); + let record = SemUndoRecord { + semid: SemId::new(203), + adjustments: alloc::vec![0].into_boxed_slice(), + revision: 0, + prepared_state: PreparedSemUndoRecordState::Live, + reservation: None, + }; + + assert_eq!(group.commit_record(record), Err(SystemError::ENOMEM)); + assert_eq!(group.record_count_for_test(), 0); + assert_eq!(group.record_capacity_for_test(), before_capacity); + } + + #[test] + fn observer_arc_does_not_change_task_owner_count() { + let group = SemUndoGroup::new_for_test(); + let attachment = SemUndoAttachment::new_for_test(group.clone()); + let observer = attachment.group_for_test(); + assert_eq!(group.task_owners_for_test(), 1); + drop(observer); + assert_eq!(group.task_owners_for_test(), 1); + } + + #[test] + fn attachment_is_taken_once_and_drop_never_replays() { + let attachment = SemUndoAttachment::new_for_test(SemUndoGroup::new_for_test()); + let mut slot = Some(attachment); + assert!(slot.take().is_some()); + assert!(slot.take().is_none()); + } + + #[test] + fn group_rejects_different_ipc_namespace() { + let group = SemUndoGroup::new_for_test_bound_to_first_namespace(); + assert_eq!( + group.verify_ipc_ns_for_test(second_test_ipc_ns()), + Err(SystemError::EINVAL) + ); + } + + #[test] + fn ordinary_fork_child_starts_without_attachment() { + let parent = test_pcb_with_group(); + let child = test_unpublished_child(); + + assert!(child.sem_undo_group().is_none()); + assert!(parent.sem_undo_group().is_some()); + } + + #[test] + fn sysvsem_guard_increments_once_then_install_moves_token() { + let parent = test_pcb_with_group(); + let group = parent.sem_undo_group().unwrap(); + let child = test_unpublished_child(); + + let mut guard = parent + .prepare_shared_sem_undo_attachment(test_ipc_ns()) + .unwrap(); + assert_eq!(group.task_owners_for_test(), 2); + guard.install_into(&child); + assert!(child.sem_undo_group().is_some()); + guard.disarm(); + assert_eq!(group.task_owners_for_test(), 2); + } + + #[test] + fn installed_guard_rollback_takes_child_slot_and_only_drops_owner() { + let parent = test_pcb_with_group(); + let group = parent.sem_undo_group().unwrap(); + let child = test_unpublished_child(); + + let mut guard = parent + .prepare_shared_sem_undo_attachment(test_ipc_ns()) + .unwrap(); + guard.install_into(&child); + drop(guard); + + assert!(child.sem_undo_group().is_none()); + assert_eq!(group.task_owners_for_test(), 1); + assert_eq!(group.replay_count_for_test(), 0); + } +} diff --git a/kernel/src/ipc/syscall/sys_semtimedop.rs b/kernel/src/ipc/syscall/sys_semtimedop.rs index c3c3a79df7..6b4bfa85d9 100644 --- a/kernel/src/ipc/syscall/sys_semtimedop.rs +++ b/kernel/src/ipc/syscall/sys_semtimedop.rs @@ -84,9 +84,23 @@ impl Syscall for SysSemtimedopHandle { let semid = Self::semid(args); let nsops = Self::nsops(args); let sops_ptr = Self::sops(args); + let timeout_ptr = Self::timeout(args); let from_user = frame.is_from_user(); - // Match Linux: validate nsops before making any allocation. + let timeout = if timeout_ptr.is_null() { + None + } else { + let mut ts = PosixTimeSpec::new(0, 0); + let ts_reader = UserBufferReader::new( + timeout_ptr, + core::mem::size_of::(), + from_user, + )?; + ts_reader.copy_one_from_user(&mut ts, 0)?; + Some(ts) + }; + + // Match Linux: after copying a non-null timeout, validate nsops before allocating sops. if nsops == 0 { return Err(SystemError::EINVAL); } @@ -111,20 +125,6 @@ impl Syscall for SysSemtimedopHandle { sops_reader.copy_one_from_user(op, i * core::mem::size_of::())?; } - let timeout_ptr = Self::timeout(args); - let timeout = if timeout_ptr.is_null() { - None - } else { - let mut ts = PosixTimeSpec::new(0, 0); - let ts_reader = UserBufferReader::new( - timeout_ptr, - core::mem::size_of::(), - from_user, - )?; - ts_reader.copy_one_from_user(&mut ts, 0)?; - Some(ts) - }; - do_kernel_semtimedop(semid, &sops, timeout) } diff --git a/kernel/src/process/fork.rs b/kernel/src/process/fork.rs index 8774d3b955..a30178cd43 100644 --- a/kernel/src/process/fork.rs +++ b/kernel/src/process/fork.rs @@ -97,6 +97,21 @@ bitflags! { } } +fn validate_semundo_clone_flags(clone_flags: CloneFlags) -> Result<(), SystemError> { + if (clone_flags & (CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_SYSVSEM)) + == (CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_SYSVSEM) + { + Err(SystemError::EINVAL) + } else { + Ok(()) + } +} + +#[cfg(test)] +fn validate_semundo_clone_flags_for_test(clone_flags: CloneFlags) -> Result<(), SystemError> { + validate_semundo_clone_flags(clone_flags) +} + /// ## clone与clone3系统调用的参数载体 /// /// 因为这两个系统调用的参数很多,所以有这样一个载体更灵活 @@ -592,6 +607,7 @@ impl ProcessManager { current_trapframe: &TrapFrame, ) -> Result, SystemError> { let clone_flags = clone_args.flags; + validate_semundo_clone_flags(clone_flags)?; // 不允许与不同namespace的进程共享根目录 // exec 去线程化期间不允许创建新线程 @@ -818,6 +834,16 @@ impl ProcessManager { ) }); + // Pre-create the parent's group while allocation failures are still + // harmless. The extra child owner is acquired only at publication. + let shared_sem_undo_ipc_ns = if clone_flags.contains(CloneFlags::CLONE_SYSVSEM) { + let ipc_ns = current_pcb.nsproxy().ipc_ns.clone(); + current_pcb.ensure_sem_undo_group(&ipc_ns)?; + Some(ipc_ns) + } else { + None + }; + // alloc_pid if pcb.raw_pid() == RawPid::UNASSIGNED { // 分层PID分配:在父进程的子PID namespace中为新任务分配PID @@ -937,6 +963,16 @@ impl ProcessManager { Some(PtraceEvent::Fork) }; + let mut sem_undo_guard = None; + let mut attach_sem_undo = || { + if let Some(ipc_ns) = shared_sem_undo_ipc_ns.as_ref() { + let mut guard = current_pcb + .prepare_shared_sem_undo_attachment(ipc_ns) + .expect("parent undo group was prepared before publication"); + guard.install_into(pcb); + sem_undo_guard = Some(guard); + } + }; let mut inherited_ptrace_session = None; let publish_result: Result<(), SystemError> = loop { // Keep PGID/SID publication ordered before the relation lock, the @@ -1007,7 +1043,8 @@ impl ProcessManager { // thread publication have a single linearization order. let inherited_tty = current_pcb.sig_info_irqsave().tty(); pcb.sig_info_mut().set_tty(inherited_tty); - current_pcb.sighand().with_group_exec_check(|| { + if let Err(err) = current_pcb.sighand().with_group_exec_check(|| { + attach_sem_undo(); let live = pcb .threads_read_irqsave() .thread_group_live @@ -1018,13 +1055,16 @@ impl ProcessManager { .threads_write_irqsave() .group_tasks .push(Arc::downgrade(pcb)); - })?; + }) { + break Err(err); + } let leader_tgid_pid = group_leader.pid(); pcb.init_task_pid(PidType::TGID, leader_tgid_pid.clone()); pcb.attach_pid(PidType::TGID); Ok(()) } else { + attach_sem_undo(); if clone_flags.contains(CloneFlags::CLONE_PARENT) { let inherited_parent = current_pcb.parent_pcb.read_irqsave().clone(); let inherited_real_parent = current_pcb.real_parent_pcb.read_irqsave().clone(); @@ -1153,6 +1193,10 @@ impl ProcessManager { return Err(err); } + if let Some(guard) = sem_undo_guard { + guard.disarm(); + } + let published_cgroup = if pcb.raw_pid() > RawPid(0) { let cgroup = pcb.task_cgroup_node(); let needs_oom_score_adj_sync = Self::needs_oom_score_adj_clone_vm_sync(&clone_flags); @@ -1331,6 +1375,20 @@ impl ProcessManager { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn newipc_and_sysvsem_are_rejected_before_attachment_prepare() { + let flags = CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_SYSVSEM; + assert_eq!( + validate_semundo_clone_flags_for_test(flags), + Err(SystemError::EINVAL) + ); + } +} + impl ProcessControlBlock { /// https://code.dragonos.org.cn/xref/linux-6.6.21/kernel/fork.c#1959 pub(super) fn init_task_pid(&self, pid_type: PidType, pid: Arc) { diff --git a/kernel/src/process/manager/exit.rs b/kernel/src/process/manager/exit.rs index 37e6c44de2..470151e9a1 100644 --- a/kernel/src/process/manager/exit.rs +++ b/kernel/src/process/manager/exit.rs @@ -15,6 +15,7 @@ use crate::{ driver::tty::tty_job_control::TtyJobCtrlManager, exception::InterruptArch, ipc::{ + sem_undo::detach_sem_undo, sighand::{NaturalParentNotifyToken, ReapTransition}, signal_types::{SigCode, SigInfo, SigType}, }, @@ -457,6 +458,7 @@ impl ProcessManager { compiler_fence(Ordering::SeqCst); RobustListHead::cleanup_robust_list(&pcb); + detach_sem_undo(&pcb); // If this process was created via vfork, complete the completion. if let Some(vd) = vfork_done { vd.complete_all(); @@ -663,6 +665,10 @@ impl ProcessManager { pub(crate) unsafe fn release(pid: RawPid) { let pcb = ProcessManager::find(pid); if let Some(ref pcb) = pcb { + debug_assert!( + pcb.sem_undo_group().is_none(), + "SEM_UNDO attachment must be detached before ProcessManager::release" + ); ProcessManager::exit_ptrace(pcb); ProcessManager::ptrace_unlink_tracee(pcb); diff --git a/kernel/src/process/namespace/nsproxy.rs b/kernel/src/process/namespace/nsproxy.rs index b8a38c1cd6..2c8a759065 100644 --- a/kernel/src/process/namespace/nsproxy.rs +++ b/kernel/src/process/namespace/nsproxy.rs @@ -4,6 +4,7 @@ use system_error::SystemError; use crate::{ filesystem::{fs::FsStruct, vfs::IndexNode}, + ipc::sem_undo::detach_sem_undo, process::{ cred::Cred, fork::CloneFlags, @@ -15,6 +16,7 @@ use crate::{ }, FsRefsReadGuard, ProcessControlBlock, ProcessManager, }, + rcu::PreparedRcuArcRetire, }; use core::{fmt::Debug, intrinsics::likely}; @@ -169,6 +171,10 @@ impl ProcessManager { * namespace are unreachable. In clone parlance, CLONE_SYSVSEM * means share undolist with parent, so we must forbid using * it along with CLONE_NEWIPC. + * + * copy_process() rejects this combination before touching child + * state. Keep this check only as a defensive boundary for callers of + * copy_namespaces(); fork correctness must not depend on reaching it. */ if *clone_flags & (CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_SYSVSEM) @@ -286,70 +292,141 @@ pub fn exec_task_namespaces() -> Result<(), SystemError> { let new_nsproxy = create_new_namespaces(&CloneFlags::empty(), &tsk, user_ns)?; // todo: time_ns的逻辑 let fs_refs = crate::process::lock_fs_refs_copy(); - switch_task_namespaces(&tsk, new_nsproxy, &fs_refs)?; + let prepared = + PreparedNamespaceInstall::prepare_for_setns(&tsk, new_nsproxy, None, false, &fs_refs)?; + prepared.commit(&tsk, &fs_refs)?; return Ok(()); } -pub(crate) fn switch_task_namespaces( - tsk: &Arc, +pub(crate) struct PreparedNamespaceInstall { new_nsproxy: Arc, - _fs_refs: &FsRefsReadGuard, -) -> Result<(), SystemError> { - // Check sharing before taking our temporary Arc below. Counting after - // cloning the Arc would mistake this function's own reference for a - // CLONE_FS peer and reject an otherwise private fs_struct. - let fs_is_shared = tsk.fs_struct_is_shared(); - let fs = tsk.fs_struct(); - switch_task_namespaces_inner(tsk, &fs, new_nsproxy, fs_is_shared, false) + new_fs: Option>, + new_cred: Option>, + detach_sysvsem: bool, + nsproxy_retire: PreparedRcuArcRetire, + cred_retire: Option>, } -pub(crate) fn switch_task_namespaces_with_fs( - tsk: &Arc, - fs: &Arc, - new_nsproxy: Arc, - _fs_refs: &FsRefsReadGuard, -) -> Result<(), SystemError> { - // unshare(CLONE_NEWNS) passes either a freshly copied fs_struct or the - // task's proven-private one, so there is no CLONE_FS peer to reject. - switch_task_namespaces_inner(tsk, fs, new_nsproxy, false, true) +#[derive(Clone, Copy)] +enum MountPathPreparation { + ProjectCopySource, + UseNamespaceRoot, } -fn switch_task_namespaces_inner( - tsk: &Arc, - fs: &Arc, - new_nsproxy: Arc, - fs_is_shared: bool, - project_copy_source: bool, -) -> Result<(), SystemError> { - if !Arc::ptr_eq(tsk.nsproxy().mnt_namespace(), &new_nsproxy.mnt_ns) { - if fs_is_shared { - return Err(SystemError::EINVAL); +impl PreparedNamespaceInstall { + pub(crate) fn prepare_for_unshare( + tsk: &Arc, + new_nsproxy: Arc, + new_fs: Option>, + new_cred: Option>, + detach_sysvsem: bool, + fs_refs: &FsRefsReadGuard, + ) -> Result { + Self::prepare( + tsk, + new_nsproxy, + new_fs, + new_cred, + detach_sysvsem, + MountPathPreparation::ProjectCopySource, + fs_refs, + ) + } + + pub(crate) fn prepare_for_setns( + tsk: &Arc, + new_nsproxy: Arc, + new_cred: Option>, + detach_sysvsem: bool, + fs_refs: &FsRefsReadGuard, + ) -> Result { + Self::prepare( + tsk, + new_nsproxy, + None, + new_cred, + detach_sysvsem, + MountPathPreparation::UseNamespaceRoot, + fs_refs, + ) + } + + fn prepare( + tsk: &Arc, + new_nsproxy: Arc, + mut new_fs: Option>, + new_cred: Option>, + detach_sysvsem: bool, + mount_paths: MountPathPreparation, + _fs_refs: &FsRefsReadGuard, + ) -> Result { + if !Arc::ptr_eq(tsk.nsproxy().mnt_namespace(), &new_nsproxy.mnt_ns) { + if new_fs.is_none() { + // Check sharing before taking the temporary fs Arc below. + if tsk.fs_struct_is_shared() { + return Err(SystemError::EINVAL); + } + let current_fs = tsk.fs_struct(); + new_fs = Some(Arc::new((*current_fs).clone())); + } + + let prepared_fs = new_fs + .as_ref() + .expect("mount namespace switch must prepare an fs_struct"); + let (new_root, new_pwd) = match mount_paths { + MountPathPreparation::ProjectCopySource => { + resolve_fs_paths_for_new_mntns(prepared_fs, &new_nsproxy.mnt_ns)? + } + MountPathPreparation::UseNamespaceRoot => { + let root = new_nsproxy.mnt_ns.root_inode(); + (root.clone(), root) + } + }; + prepared_fs.set_root(new_root); + prepared_fs.set_pwd(new_pwd); } - if project_copy_source { - prepare_fs_for_new_mntns(fs, &new_nsproxy.mnt_ns)?; + + let nsproxy_retire = PreparedRcuArcRetire::prepare().map_err(|_| SystemError::ENOMEM)?; + let cred_retire = if new_cred.is_some() { + Some(PreparedRcuArcRetire::prepare().map_err(|_| SystemError::ENOMEM)?) } else { - // setns installs the target namespace root as both root and cwd; - // it must not preserve paths merely because the target happens to - // have been cloned from the current namespace. - let root = new_nsproxy.mnt_ns.root_inode(); - fs.set_root(root.clone()); - fs.set_pwd(root); - } - } + None + }; - tsk.set_nsproxy(new_nsproxy); - Ok(()) -} + Ok(Self { + new_nsproxy, + new_fs, + new_cred, + detach_sysvsem, + nsproxy_retire, + cred_retire, + }) + } -pub(crate) fn prepare_fs_for_new_mntns( - fs: &Arc, - new_mntns: &Arc, -) -> Result<(), SystemError> { - let (new_root, new_pwd) = resolve_fs_paths_for_new_mntns(fs, new_mntns)?; - fs.set_root(new_root); - fs.set_pwd(new_pwd); - Ok(()) + pub(crate) fn commit( + self, + tsk: &Arc, + fs_refs: &FsRefsReadGuard, + ) -> Result<(), SystemError> { + let Self { + new_nsproxy, + new_fs, + new_cred, + detach_sysvsem, + nsproxy_retire, + cred_retire, + } = self; + if detach_sysvsem { + detach_sem_undo(tsk); + } + if let Some(new_fs) = new_fs { + tsk.set_fs_struct(new_fs, fs_refs); + } + let prepared_cred = new_cred.zip(cred_retire); + tsk.install_prepared_namespace_state(new_nsproxy, nsproxy_retire, prepared_cred); + Ok(()) + } } type ReboundFsPaths = (Arc, Arc); @@ -370,3 +447,99 @@ fn resolve_fs_paths_for_new_mntns( let namespace_root = new_mntns.root_inode(); Ok((namespace_root.clone(), namespace_root)) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::process::KernelStack; + + fn test_pcb() -> Arc { + ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()) + } + + #[test] + fn namespace_prepare_error_preserves_attachment_and_old_state() { + let pcb = test_pcb(); + let old_nsproxy = pcb.nsproxy(); + let old_fs = pcb.fs_struct(); + let old_cred = pcb.cred(); + let old_group = pcb.ensure_sem_undo_group(&old_nsproxy.ipc_ns).unwrap(); + + let mut target = old_nsproxy.clone_inner(); + target.mnt_ns = old_nsproxy + .mnt_ns + .copy_mnt_ns(&CloneFlags::CLONE_NEWNS, old_cred.user_ns.clone()) + .unwrap(); + let prepared_cred = Cred::new_arc((*old_cred).clone()); + let fs_refs = crate::process::lock_fs_refs_copy(); + + let result = PreparedNamespaceInstall::prepare_for_setns( + &pcb, + Arc::new(target), + Some(prepared_cred), + true, + &fs_refs, + ); + + assert_eq!(result.err(), Some(SystemError::EINVAL)); + assert!(Arc::ptr_eq(&pcb.nsproxy(), &old_nsproxy)); + assert!(Arc::ptr_eq(&pcb.fs_struct(), &old_fs)); + assert!(Arc::ptr_eq(&pcb.cred(), &old_cred)); + assert!(Arc::ptr_eq(&pcb.sem_undo_group().unwrap(), &old_group)); + assert_eq!(old_group.task_owners_for_test(), 1); + assert_eq!(old_group.replay_count_for_test(), 0); + } + + #[test] + fn rcu_publication_prepare_error_preserves_attachment_and_old_state() { + let pcb = test_pcb(); + let old_nsproxy = pcb.nsproxy(); + let old_fs = pcb.fs_struct(); + let old_cred = pcb.cred(); + let old_group = pcb.ensure_sem_undo_group(&old_nsproxy.ipc_ns).unwrap(); + let prepared_cred = Cred::new_arc((*old_cred).clone()); + let fs_refs = crate::process::lock_fs_refs_copy(); + crate::rcu::fail_next_prepared_arc_retire_for_test(); + + let result = PreparedNamespaceInstall::prepare_for_setns( + &pcb, + old_nsproxy.clone(), + Some(prepared_cred), + true, + &fs_refs, + ); + + assert_eq!(result.err(), Some(SystemError::ENOMEM)); + assert!(Arc::ptr_eq(&pcb.nsproxy(), &old_nsproxy)); + assert!(Arc::ptr_eq(&pcb.fs_struct(), &old_fs)); + assert!(Arc::ptr_eq(&pcb.cred(), &old_cred)); + assert!(Arc::ptr_eq(&pcb.sem_undo_group().unwrap(), &old_group)); + assert_eq!(old_group.task_owners_for_test(), 1); + assert_eq!(old_group.replay_count_for_test(), 0); + } + + #[test] + fn namespace_commit_uses_only_prepared_rcu_publications() { + let pcb = test_pcb(); + let old_nsproxy = pcb.nsproxy(); + pcb.ensure_sem_undo_group(&old_nsproxy.ipc_ns).unwrap(); + let prepared_cred = Cred::new_arc((*pcb.cred()).clone()); + let fs_refs = crate::process::lock_fs_refs_copy(); + let prepared = PreparedNamespaceInstall::prepare_for_setns( + &pcb, + old_nsproxy, + Some(prepared_cred), + true, + &fs_refs, + ) + .unwrap(); + let unprepared_before = pcb.namespace_unprepared_rcu_stores_for_test(); + + prepared.commit(&pcb, &fs_refs).unwrap(); + + assert_eq!( + pcb.namespace_unprepared_rcu_stores_for_test(), + unprepared_before + ); + } +} diff --git a/kernel/src/process/namespace/setns.rs b/kernel/src/process/namespace/setns.rs index 5a8d1b0f77..6ef59f5356 100644 --- a/kernel/src/process/namespace/setns.rs +++ b/kernel/src/process/namespace/setns.rs @@ -9,11 +9,11 @@ use crate::{ fork::CloneFlags, lock_fs_refs_copy, pid::PidType, - ProcessManager, + FsRefsReadGuard, ProcessControlBlock, ProcessManager, }, }; -use super::nsproxy::{switch_task_namespaces, NsProxy}; +use super::nsproxy::{NsProxy, PreparedNamespaceInstall}; fn can_setns_cgroup(target: &crate::process::namespace::cgroup_namespace::CgroupNamespace) -> bool { let current = ProcessManager::current_pcb(); @@ -31,6 +31,33 @@ fn flags_match(flags: CloneFlags, expected: CloneFlags) -> bool { flags.is_empty() || flags == expected } +fn validate_namespace_fd_flags( + flags: CloneFlags, + expected: CloneFlags, +) -> Result { + if flags_match(flags, expected) { + Ok(expected) + } else { + Err(SystemError::EINVAL) + } +} + +fn prepare_setns_install( + current: &Arc, + new_nsproxy: Arc, + new_cred: Option>, + installation_flags: CloneFlags, + fs_refs: &FsRefsReadGuard, +) -> Result { + PreparedNamespaceInstall::prepare_for_setns( + current, + new_nsproxy, + new_cred, + installation_flags.contains(CloneFlags::CLONE_NEWIPC), + fs_refs, + ) +} + fn can_setns_target_userns( target_user_ns: &Arc, ) -> bool { @@ -185,7 +212,8 @@ pub fn ksys_setns(fd: i32, nstype: i32) -> Result<(), SystemError> { let new_nsproxy = Arc::new(new_inner); let fs_refs = lock_fs_refs_copy(); - switch_task_namespaces(¤t, new_nsproxy, &fs_refs)?; + let prepared = prepare_setns_install(¤t, new_nsproxy, None, flags, &fs_refs)?; + prepared.commit(¤t, &fs_refs)?; return Ok(()); } @@ -205,11 +233,14 @@ pub fn ksys_setns(fd: i32, nstype: i32) -> Result<(), SystemError> { } } + let installation_flags; + let mut new_cred = None; match ns_fd { NamespaceFilePrivateData::Ipc(ns) => { if !flags_match(flags, CloneFlags::CLONE_NEWIPC) { return Err(SystemError::EINVAL); } + installation_flags = CloneFlags::CLONE_NEWIPC; new_inner.ipc_ns = ns; } NamespaceFilePrivateData::Uts(ns) => { @@ -219,33 +250,34 @@ pub fn ksys_setns(fd: i32, nstype: i32) -> Result<(), SystemError> { if !can_setns_uts(&ns) { return Err(SystemError::EPERM); } + installation_flags = CloneFlags::CLONE_NEWUTS; new_inner.uts_ns = ns; } NamespaceFilePrivateData::Mnt(ns) => { if !flags_match(flags, CloneFlags::CLONE_NEWNS) { return Err(SystemError::EINVAL); } + installation_flags = CloneFlags::CLONE_NEWNS; new_inner.mnt_ns = ns; } NamespaceFilePrivateData::Net(ns) => { if !flags_match(flags, CloneFlags::CLONE_NEWNET) { return Err(SystemError::EINVAL); } + installation_flags = CloneFlags::CLONE_NEWNET; new_inner.net_ns = ns; } NamespaceFilePrivateData::Pid(ns) | NamespaceFilePrivateData::PidForChildren(ns) => { if !flags_match(flags, CloneFlags::CLONE_NEWPID) { return Err(SystemError::EINVAL); } + installation_flags = CloneFlags::CLONE_NEWPID; // 仅影响子进程 PID namespace,保持与 Linux 语义一致 new_inner.pid_ns_for_children = ns; } NamespaceFilePrivateData::User(ns) => { - if !flags.is_empty() && !flags.contains(CloneFlags::CLONE_NEWUSER) { - return Err(SystemError::EINVAL); - } - userns_install(¤t, ns)?; - return Ok(()); + installation_flags = validate_namespace_fd_flags(flags, CloneFlags::CLONE_NEWUSER)?; + new_cred = Some(prepare_userns_cred(¤t, ns)?); } NamespaceFilePrivateData::Cgroup(ns) => { if !flags_match(flags, CloneFlags::CLONE_NEWCGROUP) { @@ -254,6 +286,7 @@ pub fn ksys_setns(fd: i32, nstype: i32) -> Result<(), SystemError> { if !can_setns_cgroup(&ns) { return Err(SystemError::EPERM); } + installation_flags = CloneFlags::CLONE_NEWCGROUP; new_inner.cgroup_ns = ns; } } @@ -262,16 +295,23 @@ pub fn ksys_setns(fd: i32, nstype: i32) -> Result<(), SystemError> { // 5. 原子切换当前任务的 namespace 代理 let fs_refs = lock_fs_refs_copy(); - switch_task_namespaces(¤t, new_nsproxy, &fs_refs)?; + let prepared = prepare_setns_install( + ¤t, + new_nsproxy, + new_cred, + installation_flags, + &fs_refs, + )?; + prepared.commit(¤t, &fs_refs)?; Ok(()) } /// 安装(切换)user namespace(对应 Linux userns_install) -fn userns_install( +fn prepare_userns_cred( current: &Arc, user_ns: Arc, -) -> Result<(), SystemError> { +) -> Result, SystemError> { // 1. 不能与当前 ns 相同(防止重复获得能力) if Arc::ptr_eq(¤t.cred().user_ns, &user_ns) { return Err(SystemError::EINVAL); @@ -292,10 +332,50 @@ fn userns_install( return Err(SystemError::EPERM); } - // 5. 先准备新的 cred,全部校验通过后再提交 + // 5. Prepare new credentials after every validation has passed. let mut new_cred = (*current.cred()).clone(); crate::process::cred::set_cred_user_ns(&mut new_cred, user_ns); - current.commit_cred(Cred::new_arc(new_cred))?; + Ok(Cred::new_arc(new_cred)) +} - Ok(()) +#[cfg(test)] +mod tests { + use super::*; + use crate::process::{KernelStack, ProcessControlBlock}; + + #[test] + fn setns_newipc_detaches_even_for_same_arc_target() { + let pcb = ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()); + let old_nsproxy = pcb.nsproxy(); + let old_group = pcb.ensure_sem_undo_group(&old_nsproxy.ipc_ns).unwrap(); + let fs_refs = lock_fs_refs_copy(); + + let prepared = prepare_setns_install( + &pcb, + old_nsproxy.clone(), + None, + CloneFlags::CLONE_NEWIPC, + &fs_refs, + ) + .unwrap(); + prepared.commit(&pcb, &fs_refs).unwrap(); + + assert!(Arc::ptr_eq(&pcb.nsproxy(), &old_nsproxy)); + assert!(pcb.sem_undo_group().is_none()); + let replacement = pcb.ensure_sem_undo_group(&old_nsproxy.ipc_ns).unwrap(); + assert!(!Arc::ptr_eq(&replacement, &old_group)); + assert_eq!(old_group.task_owners_for_test(), 0); + assert_eq!(old_group.replay_count_for_test(), 1); + } + + #[test] + fn user_namespace_fd_rejects_combined_newuser_newipc() { + assert_eq!( + validate_namespace_fd_flags( + CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWIPC, + CloneFlags::CLONE_NEWUSER, + ), + Err(SystemError::EINVAL) + ); + } } diff --git a/kernel/src/process/namespace/unshare.rs b/kernel/src/process/namespace/unshare.rs index 214d91ffc0..8f9010d5d4 100644 --- a/kernel/src/process/namespace/unshare.rs +++ b/kernel/src/process/namespace/unshare.rs @@ -8,10 +8,8 @@ use crate::{ cred::{ns_capable, CAPFlags, Cred}, fork::CloneFlags, lock_fs_refs_copy, - namespace::nsproxy::{ - create_new_namespaces, switch_task_namespaces, switch_task_namespaces_with_fs, NsProxy, - }, - ProcessManager, + namespace::nsproxy::{create_new_namespaces, NsProxy, PreparedNamespaceInstall}, + FsRefsReadGuard, ProcessControlBlock, ProcessManager, }, }; @@ -23,28 +21,14 @@ pub fn ksys_unshare(flags: CloneFlags) -> Result<(), SystemError> { check_unshare_flags(flags)?; let current_pcb = ProcessManager::current_pcb(); - let mut new_cred = unshare_user_cred(flags, ¤t_pcb)?; + let new_cred = unshare_user_cred(flags, ¤t_pcb)?.map(Cred::new_arc); let fs_refs = lock_fs_refs_copy(); let new_fs = unshare_fs_struct(flags, ¤t_pcb, &fs_refs)?; let new_nsproxy = - unshare_nsproxy_namespaces(flags, ¤t_pcb, new_cred.as_ref(), new_fs.as_ref())?; - - if let Some(new_nsproxy) = new_nsproxy { - if let Some(new_fs) = new_fs.as_ref() { - switch_task_namespaces_with_fs(¤t_pcb, new_fs, new_nsproxy, &fs_refs)?; - } else { - switch_task_namespaces(¤t_pcb, new_nsproxy, &fs_refs)?; - } - } - - if let Some(new_fs) = new_fs { - current_pcb.set_fs_struct(new_fs, &fs_refs); - } - drop(fs_refs); - - if let Some(new_cred) = new_cred.take() { - current_pcb.commit_cred(Cred::new_arc(new_cred))?; - } + unshare_nsproxy_namespaces(flags, ¤t_pcb, new_cred.as_deref(), new_fs.as_ref())?; + let prepared = + prepare_unshare_install(¤t_pcb, flags, new_fs, new_nsproxy, new_cred, &fs_refs)?; + prepared.commit(¤t_pcb, &fs_refs)?; // TODO: 处理其他命名空间的 unshare 操作 // CLONE_FS, CLONE_FILES, CLONE_SIGHAND, CLONE_VM, CLONE_THREAD, CLONE_SYSVSEM, @@ -53,6 +37,26 @@ pub fn ksys_unshare(flags: CloneFlags) -> Result<(), SystemError> { Ok(()) } +fn prepare_unshare_install( + current: &Arc, + flags: CloneFlags, + new_fs: Option>, + new_nsproxy: Option>, + new_cred: Option>, + fs_refs: &FsRefsReadGuard, +) -> Result { + let new_nsproxy = new_nsproxy.unwrap_or_else(|| current.nsproxy()); + let detach_sysvsem = flags.intersects(CloneFlags::CLONE_SYSVSEM | CloneFlags::CLONE_NEWIPC); + PreparedNamespaceInstall::prepare_for_unshare( + current, + new_nsproxy, + new_fs, + new_cred, + detach_sysvsem, + fs_refs, + ) +} + #[inline(always)] fn normalize_unshare_flags(mut flags: CloneFlags) -> CloneFlags { if flags.contains(CloneFlags::CLONE_NEWUSER) { @@ -189,3 +193,62 @@ fn check_unshare_flags(flags: CloneFlags) -> Result<(), SystemError> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::process::{KernelStack, ProcessControlBlock}; + + fn test_pcb() -> Arc { + ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()) + } + + #[test] + fn unshare_sysvsem_detaches_even_when_ipc_namespace_is_unchanged() { + let pcb = test_pcb(); + let ipc_ns = pcb.nsproxy().ipc_ns.clone(); + let old_group = pcb.ensure_sem_undo_group(&ipc_ns).unwrap(); + let fs_refs = lock_fs_refs_copy(); + + let prepared = + prepare_unshare_install(&pcb, CloneFlags::CLONE_SYSVSEM, None, None, None, &fs_refs) + .unwrap(); + prepared.commit(&pcb, &fs_refs).unwrap(); + + assert!(pcb.sem_undo_group().is_none()); + let replacement = pcb.ensure_sem_undo_group(&ipc_ns).unwrap(); + assert!(!Arc::ptr_eq(&replacement, &old_group)); + assert_eq!(old_group.task_owners_for_test(), 0); + assert_eq!(old_group.replay_count_for_test(), 1); + } + + #[test] + fn unshare_newipc_detaches_once_when_sysvsem_is_also_present() { + let pcb = test_pcb(); + let old_nsproxy = pcb.nsproxy(); + let old_group = pcb.ensure_sem_undo_group(&old_nsproxy.ipc_ns).unwrap(); + let mut new_inner = old_nsproxy.clone_inner(); + new_inner.ipc_ns = old_nsproxy.ipc_ns.copy_ipc_ns( + &CloneFlags::CLONE_NEWIPC, + old_nsproxy.ipc_ns.user_ns.clone(), + ); + let new_ipc_ns = new_inner.ipc_ns.clone(); + let fs_refs = lock_fs_refs_copy(); + + let prepared = prepare_unshare_install( + &pcb, + CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_SYSVSEM, + None, + Some(Arc::new(new_inner)), + None, + &fs_refs, + ) + .unwrap(); + prepared.commit(&pcb, &fs_refs).unwrap(); + + assert!(pcb.sem_undo_group().is_none()); + assert!(Arc::ptr_eq(&pcb.nsproxy().ipc_ns, &new_ipc_ns)); + assert_eq!(old_group.task_owners_for_test(), 0); + assert_eq!(old_group.replay_count_for_test(), 1); + } +} diff --git a/kernel/src/process/pid.rs b/kernel/src/process/pid.rs index fe05444661..29da4d18f9 100644 --- a/kernel/src/process/pid.rs +++ b/kernel/src/process/pid.rs @@ -100,6 +100,20 @@ impl Pid { pid } + #[cfg(test)] + pub(crate) fn new_for_test(nr: RawPid, ns: Arc) -> Arc { + let pid = Self::new(ns.level()); + pid.numbers.lock()[ns.level() as usize] = Some(UPid::new(nr, ns)); + pid + } + + #[cfg(test)] + pub(crate) fn clear_numbers_for_test(&self) { + for number in self.numbers.lock().iter_mut() { + number.take(); + } + } + pub fn dead(&self) -> bool { self.dead.load(core::sync::atomic::Ordering::Relaxed) } @@ -302,6 +316,24 @@ impl ProcessControlBlock { self.thread_pid.read().clone().unwrap() } + #[cfg(test)] + pub(crate) fn install_pid_identity_for_test(&self, pid: Arc) { + let raw_pid = pid + .first_upid() + .expect("test PID identity must have a namespace number") + .nr; + self.pid.store(raw_pid, Ordering::Release); + self.thread_pid.write().replace(pid.clone()); + self.sighand().set_pid(PidType::TGID, Some(pid)); + } + + #[cfg(test)] + pub(crate) fn clear_pid_identity_for_test(&self) { + self.sighand().set_pid(PidType::TGID, None); + self.thread_pid.write().take(); + self.pid.store(RawPid::new(0), Ordering::Release); + } + /// 强制设置当前进程的raw_pid /// 注意:这个函数应该在创建进程时调用,不能在运行时随意调用 pub(super) unsafe fn force_set_raw_pid(&self, pid: RawPid) { @@ -646,7 +678,7 @@ impl ProcessControlBlock { /// # 特殊情况 /// * 如果进程的raw_pid为0(通常是空闲进程),则直接返回raw_pid #[allow(dead_code)] - pub(super) fn task_pid_nr_ns( + pub(crate) fn task_pid_nr_ns( &self, pid_type: PidType, ns: Option>, diff --git a/kernel/src/process/task.rs b/kernel/src/process/task.rs index c6eb5f72aa..864fa40a6e 100644 --- a/kernel/src/process/task.rs +++ b/kernel/src/process/task.rs @@ -28,6 +28,7 @@ use crate::{ }, }, ipc::{ + sem_undo::{SemUndoAttachment, SemUndoGroup, UnpublishedSemUndoAttachmentGuard}, sighand::{NaturalParentNotifyPhase, SigHand}, signal::RestartBlock, }, @@ -42,7 +43,7 @@ use crate::{ process::{ cred::{cred_cap_issubset, Cred, INIT_CRED, SUID_DUMPABLE, SUID_DUMP_DISABLE}, kthread::WorkerPrivate, - namespace::nsproxy::NsProxy, + namespace::{ipc_namespace::IpcNamespace, nsproxy::NsProxy}, pid::{Pid, PidLink, PidType}, resource::{RLimit64, RLimitID, RUsage}, timer::AlarmTimer, @@ -50,7 +51,7 @@ use crate::{ ProcessFlags, ProcessItimers, ProcessManager, ProcessSchedulerInfo, ProcessSignalInfo, ProcessState, RawPid, ThreadInfo, PTRACE_RELATION_LOCK, }, - rcu::RcuArcSlot, + rcu::{PreparedRcuArcRetire, RcuArcSlot}, }; use crate::process::{posix_timer, ptrace, rseq, seccomp}; @@ -147,6 +148,8 @@ pub struct ProcessControlBlock { /// The cgroup (v2) this task belongs to. pub(super) task_cgroup: RwLock, + pub(super) sem_undo: SpinLock>, + pub(super) basic: RwLock, pub(super) exec_update_lock: RwSem<()>, pub(super) preempt_count: AtomicUsize, @@ -360,6 +363,66 @@ impl ProcessControlBlock { vm.try_acquire() } + pub fn sem_undo_group(&self) -> Option> { + self.sem_undo + .lock_irqsave() + .as_ref() + .map(SemUndoAttachment::group) + } + + pub fn ensure_sem_undo_group( + &self, + ipc_ns: &Arc, + ) -> Result, SystemError> { + if let Some(group) = self.sem_undo_group() { + group.verify_ipc_ns(ipc_ns)?; + return Ok(group); + } + + let candidate = SemUndoGroup::new(ipc_ns)?; + let group = { + let mut slot = self.sem_undo.lock_irqsave(); + if let Some(attachment) = slot.as_ref() { + attachment.group() + } else { + *slot = Some(SemUndoAttachment::new(candidate.clone())); + candidate + } + }; + group.verify_ipc_ns(ipc_ns)?; + Ok(group) + } + + pub fn take_sem_undo_attachment(&self) -> Option { + self.sem_undo.lock_irqsave().take() + } + + pub(crate) fn prepare_shared_sem_undo_attachment( + &self, + ipc_ns: &Arc, + ) -> Result { + let group = self.ensure_sem_undo_group(ipc_ns)?; + group.verify_ipc_ns(ipc_ns)?; + Ok(UnpublishedSemUndoAttachmentGuard::new(group)) + } + + pub(crate) fn install_unpublished_sem_undo_attachment( + &self, + attachment: SemUndoAttachment, + ) -> Arc { + let child = self + .self_ref + .upgrade() + .expect("unpublished PCB lost its owning Arc"); + let mut slot = self.sem_undo.lock_irqsave(); + assert!( + slot.is_none(), + "unpublished child already has a SEM_UNDO attachment" + ); + *slot = Some(attachment); + child + } + #[inline(never)] fn do_create_pcb( name: String, @@ -436,6 +499,7 @@ impl ProcessControlBlock { pid_links: core::array::from_fn(|_| PidLink::default()), nsproxy: RcuArcSlot::new(nsproxy), task_cgroup: RwLock::new(task_cgroup), + sem_undo: SpinLock::new(None), preempt_count, pagefault_disabled, rcu_read_depth, @@ -986,9 +1050,44 @@ impl ProcessControlBlock { /// - Uses irqsave write lock for concurrency safety. /// - Returns `Result` so that callers can extend error handling as needed. pub fn set_cred(&self, new: Arc) -> Result<(), SystemError> { + self.install_cred(new); + Ok(()) + } + + /// Install credentials after every fallible preparation step has completed. + pub(crate) fn install_cred(&self, new: Arc) { let _task_guard = self.task_lock.lock_irqsave(); self.cred.store_deferred(new); - Ok(()) + } + + /// Publish prepared namespace state without allocating under task_lock. + pub(crate) fn install_prepared_namespace_state( + &self, + new_nsproxy: Arc, + nsproxy_retire: PreparedRcuArcRetire, + new_cred: Option<(Arc, PreparedRcuArcRetire)>, + ) { + let _exec_guard = self.exec_update_read(); + let active_mm = self.basic().user_vm(); + let (nsproxy_retirement, cred_retirement) = { + let _task_guard = self.task_lock.lock_irqsave(); + let nsproxy_retirement = self.nsproxy.swap_prepared(new_nsproxy, nsproxy_retire); + let cred_retirement = new_cred.map(|(cred, retire)| { + self.commit_cred_side_effects(&self.cred(), &cred, active_mm.as_ref()); + self.cred.swap_prepared(cred, retire) + }); + (nsproxy_retirement, cred_retirement) + }; + + nsproxy_retirement.enqueue(); + if let Some(retirement) = cred_retirement { + retirement.enqueue(); + } + } + + #[cfg(test)] + pub(crate) fn namespace_unprepared_rcu_stores_for_test(&self) -> usize { + self.nsproxy.unprepared_store_count_for_test() + self.cred.unprepared_store_count_for_test() } /// Commit new creds, updating dumpability accordingly @@ -1000,23 +1099,33 @@ impl ProcessControlBlock { let active_mm = self.basic().user_vm(); let _task_guard = self.task_lock.lock_irqsave(); let old = self.cred(); + self.commit_cred_side_effects(&old, &new, active_mm.as_ref()); + self.cred.store_deferred(new); + Ok(()) + } + + /// Caller holds exec_update_lock and task_lock; publish these effects before creds. + fn commit_cred_side_effects( + &self, + old: &Cred, + new: &Cred, + active_mm: Option<&Arc>, + ) { // Trigger: any of euid/egid/fsuid/fsgid changes, or new permitted is not a subset of old (privilege raise) if old.euid != new.euid || old.egid != new.egid || old.fsuid != new.fsuid || old.fsgid != new.fsgid - || !cred_cap_issubset(&old, &new) + || !cred_cap_issubset(old, new) { // Publish dumpability before creds; with the read-side fence, checks never see new creds with old dumpable - if let Some(mm) = active_mm.as_ref() { + if let Some(mm) = active_mm { mm.set_dumpable(SUID_DUMPABLE.load(Ordering::SeqCst) as u8); } // Also clear the parent-death signal on identity change self.set_pdeath_signal(Signal::INVALID); fence(Ordering::Release); } - self.cred.store_deferred(new); - Ok(()) } pub fn set_execute_path(&self, path: String) { diff --git a/kernel/src/rcu/mod.rs b/kernel/src/rcu/mod.rs index 57e48d814a..d9a7959ece 100644 --- a/kernel/src/rcu/mod.rs +++ b/kernel/src/rcu/mod.rs @@ -107,6 +107,84 @@ impl Drop for RcuReadGuard { } } +/// Preallocate an intrusive callback before an irreversible publication. +/// Enqueueing uses the existing per-CPU raw callback queue without allocation. +pub(crate) struct PreparedRcuArcRetire { + call: Box>, +} + +pub(crate) struct RcuArcRetirement { + call: Option>>, +} + +#[repr(C)] +struct RetiredArc { + head: RcuHead, + value: Option>, +} + +#[cfg(test)] +static FAIL_NEXT_PREPARED_ARC_RETIRE: AtomicBool = AtomicBool::new(false); + +#[cfg(test)] +pub(crate) fn fail_next_prepared_arc_retire_for_test() { + FAIL_NEXT_PREPARED_ARC_RETIRE.store(true, Ordering::Release); +} + +impl PreparedRcuArcRetire { + pub(crate) fn prepare() -> Result { + #[cfg(test)] + if FAIL_NEXT_PREPARED_ARC_RETIRE.swap(false, Ordering::AcqRel) { + return Err(()); + } + Ok(Self { + call: Box::try_new(RetiredArc { + head: RcuHead::new(), + value: None, + }) + .map_err(|_| ())?, + }) + } + + fn bind_removed(mut self, old: Arc) -> RcuArcRetirement { + self.call.value = Some(old); + RcuArcRetirement { + call: Some(self.call), + } + } +} + +unsafe fn reclaim_retired_arc(head: NonNull) { + // SAFETY: RetiredArc is repr(C), head is first, and enqueue transfers its Box. + drop(unsafe { Box::from_raw(head.as_ptr().cast::>()) }); +} + +impl RcuArcRetirement { + pub(crate) fn enqueue(mut self) { + self.submit(); + } + + fn submit(&mut self) { + if let Some(call) = self.call.take() { + let raw = Box::into_raw(call); + // SAFETY: callback owns the allocation until the grace period ends. + unsafe { + call_rcu_raw( + NonNull::new_unchecked(core::ptr::addr_of_mut!((*raw).head)), + reclaim_retired_arc::, + ); + } + } + } +} + +impl Drop for RcuArcRetirement { + fn drop(&mut self) { + // An accidentally dropped publication token must still honor RCU readers. + self.submit(); + } +} + #[derive(Debug)] /// An RCU-published non-null `Arc` slot. /// @@ -118,6 +196,8 @@ where T: Send + Sync + 'static, { ptr: AtomicPtr, + #[cfg(test)] + unprepared_stores: AtomicUsize, } unsafe fn defer_drop_slot_arc_raw(raw: *mut T) @@ -143,6 +223,8 @@ where pub fn new(initial: Arc) -> Self { Self { ptr: AtomicPtr::new(Arc::into_raw(initial) as *mut T), + #[cfg(test)] + unprepared_stores: AtomicUsize::new(0), } } @@ -200,12 +282,31 @@ where } pub fn store_deferred(&self, new: Arc) { + #[cfg(test)] + self.unprepared_stores.fetch_add(1, Ordering::Relaxed); // SAFETY: the removed slot reference is immediately transferred to // the RCU deferred-drop queue. let old = unsafe { self.swap(new) }; rcu_defer_drop(old); } + pub(crate) fn swap_prepared( + &self, + new: Arc, + prepared: PreparedRcuArcRetire, + ) -> RcuArcRetirement { + // SAFETY: the prepared retirement retains the removed slot reference + // until its preallocated callback is enqueued after the publication + // lock is released. + let old = unsafe { self.swap(new) }; + prepared.bind_removed(old) + } + + #[cfg(test)] + pub(crate) fn unprepared_store_count_for_test(&self) -> usize { + self.unprepared_stores.load(Ordering::Relaxed) + } + pub fn swap_deferred(&self, new: Arc) -> Arc { // SAFETY: the clone submitted below keeps the removed allocation alive // through a grace period even if the caller drops the returned Arc. diff --git a/kernel/src/rcu/selftest.rs b/kernel/src/rcu/selftest.rs index f6f4bd32f2..89ffe4b548 100644 --- a/kernel/src/rcu/selftest.rs +++ b/kernel/src/rcu/selftest.rs @@ -1601,6 +1601,38 @@ fn run_pr2_selftest() -> Result<(), &'static str> { return Err("current slot object was not dropped after slot destruction grace period"); } + let prepared_old_drops = Arc::new(AtomicUsize::new(0)); + let prepared_new_drops = Arc::new(AtomicUsize::new(0)); + let prepared_slot = RcuArcSlot::new(Arc::new(RcuSelftestDropProbe { + id: 11, + drops: prepared_old_drops.clone(), + })); + let prepared_retire = PreparedRcuArcRetire::prepare() + .map_err(|_| "prepared RCU retirement reservation failed")?; + let retirement = prepared_slot.swap_prepared( + Arc::new(RcuSelftestDropProbe { + id: 12, + drops: prepared_new_drops.clone(), + }), + prepared_retire, + ); + if prepared_old_drops.load(Ordering::SeqCst) != 0 { + return Err("prepared RCU swap dropped the old object before enqueue"); + } + retirement.enqueue(); + rcu_barrier(); + if prepared_old_drops.load(Ordering::SeqCst) != 1 { + return Err("prepared RCU retirement did not drop the old object after a grace period"); + } + if prepared_slot.load().id != 12 { + return Err("prepared RCU swap did not publish the replacement object"); + } + drop(prepared_slot); + rcu_barrier(); + if prepared_new_drops.load(Ordering::SeqCst) != 1 { + return Err("prepared RCU replacement was not dropped after slot destruction"); + } + let with_read_old_drops = Arc::new(AtomicUsize::new(0)); let with_read_new_drops = Arc::new(AtomicUsize::new(0)); let with_read_slot = RcuArcSlot::new(Arc::new(RcuSelftestDropProbe { diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index d5d1df02ca..2d511de099 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include #ifndef SYS_semget @@ -30,6 +32,15 @@ #ifndef SYS_semtimedop #define SYS_semtimedop 220 #endif +#ifndef SYS_unshare +#define SYS_unshare 272 +#endif +#ifndef SYS_setns +#define SYS_setns 308 +#endif +#ifndef SYS_pidfd_open +#define SYS_pidfd_open 434 +#endif #ifndef SEM_STAT #define SEM_STAT 18 @@ -37,6 +48,12 @@ #ifndef SEM_STAT_ANY #define SEM_STAT_ANY 20 #endif +#ifndef CLONE_SYSVSEM +#define CLONE_SYSVSEM 0x00040000 +#endif +#ifndef CLONE_NEWIPC +#define CLONE_NEWIPC 0x08000000 +#endif // ============ helpers ============ @@ -212,18 +229,106 @@ void WaitChildOk(ChildGuard* child) { } // Wait until the target semaphore has the expected number of waiters, confirming that -// child processes have blocked. +// child processes have blocked. GETNCNT and GETZCNT are the semaphore ABI handshake. +// A bounded exponential pause prevents a tight spin; correctness depends only on the +// observed counter and a monotonic deadline, never on a guessed child run interval. bool WaitForWaiters(int semid, int semnum, int expected, int timeout_ms = 5000) { - const int deadline = timeout_ms * 1000; - for (int elapsed = 0; elapsed < deadline; elapsed += 10000) { + struct timespec start = {}; + clock_gettime(CLOCK_MONOTONIC, &start); + long pause_ns = 1000; + for (;;) { int ncnt = SemCtl(semid, semnum, GETNCNT, 0); int zcnt = SemCtl(semid, semnum, GETZCNT, 0); if (ncnt >= 0 && ncnt + zcnt >= expected) { return true; } - usleep(10000); + struct timespec now = {}; + clock_gettime(CLOCK_MONOTONIC, &now); + const long elapsed_ms = (now.tv_sec - start.tv_sec) * 1000 + + (now.tv_nsec - start.tv_nsec) / 1000000; + if (elapsed_ms >= timeout_ms) { + return false; + } + const struct timespec pause = {0, pause_ns}; + nanosleep(&pause, nullptr); + if (pause_ns < 1000000) { + pause_ns *= 2; + } + } +} + +bool ReadExact(int fd, void* buffer, size_t size) { + char* cursor = static_cast(buffer); + while (size != 0) { + ssize_t received = read(fd, cursor, size); + if (received < 0 && errno == EINTR) { + continue; + } + if (received <= 0) { + return false; + } + cursor += received; + size -= static_cast(received); + } + return true; +} + +bool WriteExact(int fd, const void* buffer, size_t size) { + const char* cursor = static_cast(buffer); + while (size != 0) { + ssize_t written = write(fd, cursor, size); + if (written < 0 && errno == EINTR) { + continue; + } + if (written <= 0) { + return false; + } + cursor += written; + size -= static_cast(written); + } + return true; +} + +bool WaitForNcnt(int semid, int semnum, int expected) { + return WaitForWaiters(semid, semnum, expected) && + SemCtl(semid, semnum, GETNCNT, 0) >= expected; +} + +bool WaitForZcnt(int semid, int semnum, int expected) { + return WaitForWaiters(semid, semnum, expected) && + SemCtl(semid, semnum, GETZCNT, 0) >= expected; +} + +bool WaitForSemValue(int semid, int semnum, int expected, int timeout_ms = 5000) { + struct timespec start = {}; + clock_gettime(CLOCK_MONOTONIC, &start); + long pause_ns = 1000; + for (;;) { + if (SemCtl(semid, semnum, GETVAL, 0) == expected) { + return true; + } + struct timespec now = {}; + clock_gettime(CLOCK_MONOTONIC, &now); + const long elapsed_ms = (now.tv_sec - start.tv_sec) * 1000 + + (now.tv_nsec - start.tv_nsec) / 1000000; + if (elapsed_ms >= timeout_ms) { + return false; + } + const struct timespec pause = {0, pause_ns}; + nanosleep(&pause, nullptr); + if (pause_ns < 1000000) { + pause_ns *= 2; + } } - return false; +} + +bool SemOpMustSucceed(int semid, struct sembuf* ops, size_t count) { + return SemOp(semid, ops, count) == 0; +} + +bool SemUndoOpMustSucceed(int semid, unsigned short semnum, short delta) { + struct sembuf op = {semnum, delta, SEM_UNDO}; + return SemOpMustSucceed(semid, &op, 1); } // ============ creation & lookup ============ @@ -703,14 +808,905 @@ TEST(SysVSem, InvalidSemid) { EXPECT_EQ(EINVAL, errno); } -TEST(SysVSem, SemUndoRejected) { +// ============ SEM_UNDO ABI conformance ============ +// +// Every case starts with an immediately-completing undo operation. Until the +// kernel gate is enabled this is deliberately the first assertion to fail with +// ENOSYS, rather than allowing a later queue rendezvous to time out. +void RequireSemUndoAvailable(const SemSet& sem) { + struct sembuf probe = {0, 1, SEM_UNDO}; + ASSERT_EQ(0, SemOp(sem.id(), &probe, 1)) + << "SEM_UNDO must be published only after all lifecycle prerequisites; errno=" << errno; + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 0)) << "SETVAL must clear probe debt"; +} + +TEST(SysVSem, SemUndoBasicAccumulationAndSign) { SemSet sem(1, IPC_CREAT | 0600); ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + + int ready[2]; + int release[2]; + ASSERT_EQ(0, pipe(ready)); + ASSERT_EQ(0, pipe(release)); + FdGuard ready_read(ready[0]); + FdGuard ready_write(ready[1]); + FdGuard release_read(release[0]); + FdGuard release_write(release[1]); + pid_t child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + ready_read.Close(); + release_write.Close(); + struct sembuf ops[] = {{0, 3, SEM_UNDO}, {0, -1, SEM_UNDO}}; + const int result = SemOp(sem.id(), ops, 2); + const int saved_errno = errno; + if (!WriteExact(ready_write.get(), &result, sizeof(result)) || + !WriteExact(ready_write.get(), &saved_errno, sizeof(saved_errno))) { + _exit(10); + } + char token; + _exit(result == 0 && ReadExact(release_read.get(), &token, sizeof(token)) ? 0 : 11); + } + ready_write.Close(); + release_read.Close(); + int result; + int child_errno; + ASSERT_TRUE(ReadExact(ready_read.get(), &result, sizeof(result))); + ASSERT_TRUE(ReadExact(ready_read.get(), &child_errno, sizeof(child_errno))); + ASSERT_EQ(0, result) << "child SEM_UNDO ops failed: errno=" << child_errno; + EXPECT_EQ(2, SemCtl(sem.id(), 0, GETVAL, 0)); + const char token = 1; + ASSERT_TRUE(WriteExact(release_write.get(), &token, sizeof(token))); + WaitChildOk(child); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); - struct sembuf op = {0, 1, SEM_UNDO}; + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 3)); + child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + _exit(SemUndoOpMustSucceed(sem.id(), 0, -2) ? 0 : 12); + } + WaitChildOk(child); + EXPECT_EQ(3, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, SemUndoArrayOrderAndRollback) { + SemSet sem(2, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + + struct sembuf blocked[] = {{0, 1, SEM_UNDO}, {1, -1, SEM_UNDO | IPC_NOWAIT}}; errno = 0; - EXPECT_EQ(-1, SemOp(sem.id(), &op, 1)); - EXPECT_EQ(ENOSYS, errno) << "SEM_UNDO unsupported"; + EXPECT_EQ(-1, SemOp(sem.id(), blocked, 2)); + EXPECT_EQ(EAGAIN, errno); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); + + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 32767)); + struct sembuf overflow[] = {{0, -1, SEM_UNDO}, {0, 1, SEM_UNDO}, {0, 1, SEM_UNDO}}; + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), overflow, 3)); + EXPECT_EQ(ERANGE, errno); + EXPECT_EQ(32767, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, SemUndoAdjustmentBounds) { + SemSet negative(1, IPC_CREAT | 0600); + SemSet positive(1, IPC_CREAT | 0600); + ASSERT_TRUE(negative.valid()); + ASSERT_TRUE(positive.valid()); + RequireSemUndoAvailable(negative); + RequireSemUndoAvailable(positive); + + // semadj is the inverse of sem_op. One max-sized undo operation plus a + // one-unit undo operation reaches -32768 while the plain reverse keeps + // semval at zero. The following operation must exceed the signed bound. + pid_t child = fork(); + ASSERT_GE(child, 0); + ChildGuard negative_child(child); + if (child == 0) { + struct sembuf to_minus_32767[] = {{0, 32767, SEM_UNDO}, {0, -32767, 0}}; + struct sembuf reach_minus_32768[] = {{0, 1, SEM_UNDO}, {0, -1, 0}}; + struct sembuf below_min[] = {{0, 1, SEM_UNDO}, {0, -1, 0}}; + if (SemOp(negative.id(), to_minus_32767, 2) != 0 || + SemOp(negative.id(), reach_minus_32768, 2) != 0) { + _exit(40); + } + errno = 0; + _exit(SemOp(negative.id(), below_min, 2) == -1 && errno == ERANGE ? 0 : 41); + } + WaitChildOk(&negative_child); + EXPECT_EQ(0, SemCtl(negative.id(), 0, GETVAL, 0)); + + // Start at SEMVMX. A max-sized negative undo plus a plain reverse reaches + // +32767 semadj without blocking; the following IPC_NOWAIT decrement must + // fail for semadj overflow rather than queueing. + ASSERT_EQ(0, SemCtl(positive.id(), 0, SETVAL, 32767)); + child = fork(); + ASSERT_GE(child, 0); + ChildGuard positive_child(child); + if (child == 0) { + struct sembuf to_plus_32767[] = {{0, -32767, SEM_UNDO | IPC_NOWAIT}, {0, 32767, 0}}; + struct sembuf above_max[] = {{0, -1, SEM_UNDO | IPC_NOWAIT}, {0, 1, 0}}; + if (SemOp(positive.id(), to_plus_32767, 2) != 0) { + _exit(42); + } + errno = 0; + _exit(SemOp(positive.id(), above_max, 2) == -1 && errno == ERANGE ? 0 : 43); + } + WaitChildOk(&positive_child); + EXPECT_EQ(32767, SemCtl(positive.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, SemUndoQueueCapturedOwner) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + + int ready[2]; + int release[2]; + ASSERT_EQ(0, pipe(ready)); + ASSERT_EQ(0, pipe(release)); + pid_t owner = fork(); + ASSERT_GE(owner, 0); + if (owner == 0) { + close(ready[0]); + close(release[1]); + struct sembuf dec = {0, -1, SEM_UNDO}; + int result = SemOp(sem.id(), &dec, 1); + if (!WriteExact(ready[1], &result, sizeof(result))) { + _exit(20); + } + char token; + _exit(result == 0 && ReadExact(release[0], &token, sizeof(token)) ? 0 : 21); + } + FdGuard ready_read(ready[0]); + FdGuard ready_write(ready[1]); + FdGuard release_read(release[0]); + FdGuard release_write(release[1]); + ready_write.Close(); + release_read.Close(); + ASSERT_TRUE(WaitForNcnt(sem.id(), 0, 1)); + pid_t waker = fork(); + ASSERT_GE(waker, 0); + if (waker == 0) { + struct sembuf inc = {0, 1, 0}; + _exit(SemOp(sem.id(), &inc, 1) == 0 ? 0 : 22); + } + WaitChildOk(waker); + int result; + ASSERT_TRUE(ReadExact(ready_read.get(), &result, sizeof(result))); + ASSERT_EQ(0, result); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)) << "ordinary waker exit must not replay A debt"; + char token = 1; + ASSERT_TRUE(WriteExact(release_write.get(), &token, sizeof(token))); + WaitChildOk(owner); + EXPECT_EQ(1, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, SemUndoUncommittedPaths) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + + struct sembuf nowait = {0, -1, SEM_UNDO | IPC_NOWAIT}; + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), &nowait, 1)); + EXPECT_EQ(EAGAIN, errno); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); + + // A real blocking semtimedop must time out and leave no queued debt. + struct sembuf timed = {0, -1, SEM_UNDO}; + const struct timespec timeout = {0, 20 * 1000 * 1000}; + errno = 0; + EXPECT_EQ(-1, SemTimedOp(sem.id(), &timed, 1, &timeout)); + EXPECT_EQ(EAGAIN, errno); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETNCNT, 0)); + + int ready[2]; + ASSERT_EQ(0, pipe(ready)); + pid_t child = fork(); + ASSERT_GE(child, 0); + ChildGuard signal_child(child); + if (child == 0) { + close(ready[0]); + struct sigaction sa = {}; + sa.sa_handler = [](int) {}; + sigemptyset(&sa.sa_mask); + if (sigaction(SIGUSR1, &sa, nullptr) != 0) { + _exit(30); + } + struct sembuf dec = {0, -1, SEM_UNDO}; + const char entered = 1; + if (!WriteExact(ready[1], &entered, sizeof(entered))) { + _exit(31); + } + int result = SemOp(sem.id(), &dec, 1); + _exit(result == -1 && errno == EINTR ? 0 : 32); + } + FdGuard ready_read(ready[0]); + FdGuard ready_write(ready[1]); + ready_write.Close(); + char entered; + ASSERT_TRUE(ReadExact(ready_read.get(), &entered, sizeof(entered))); + ASSERT_TRUE(WaitForNcnt(sem.id(), 0, 1)); + ASSERT_EQ(0, kill(signal_child.pid(), SIGUSR1)); + WaitChildOk(&signal_child); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); + + // RMID must wake an uncommitted SEM_UNDO waiter with EIDRM and cannot replay debt. + SemSet removed(1, IPC_CREAT | 0600); + ASSERT_TRUE(removed.valid()); + int rmid_ready[2]; + ASSERT_EQ(0, pipe(rmid_ready)); + child = fork(); + ASSERT_GE(child, 0); + ChildGuard rmid_child(child); + if (child == 0) { + close(rmid_ready[0]); + struct sembuf dec = {0, -1, SEM_UNDO}; + const char queued = 1; + if (!WriteExact(rmid_ready[1], &queued, sizeof(queued))) { + _exit(33); + } + const int result = SemOp(removed.id(), &dec, 1); + _exit(result == -1 && errno == EIDRM ? 0 : 34); + } + FdGuard rmid_ready_read(rmid_ready[0]); + FdGuard rmid_ready_write(rmid_ready[1]); + rmid_ready_write.Close(); + ASSERT_TRUE(ReadExact(rmid_ready_read.get(), &entered, sizeof(entered))); + ASSERT_TRUE(WaitForNcnt(removed.id(), 0, 1)); + ASSERT_EQ(0, SemCtl(removed.id(), 0, IPC_RMID, 0)); + removed.release(); + WaitChildOk(&rmid_child); +} + +TEST(SysVSem, SemUndoSetvalSetallClearDebt) { + SemSet sem(2, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + + int ready[2]; + int release[2]; + ASSERT_EQ(0, pipe(ready)); + ASSERT_EQ(0, pipe(release)); + pid_t child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + close(ready[0]); + close(release[1]); + struct sembuf ops[] = {{0, 1, SEM_UNDO}, {1, 2, SEM_UNDO}}; + const int result = SemOp(sem.id(), ops, 2); + if (!WriteExact(ready[1], &result, sizeof(result))) { + _exit(40); + } + char token; + _exit(result == 0 && ReadExact(release[0], &token, sizeof(token)) ? 0 : 41); + } + FdGuard ready_read(ready[0]); + FdGuard ready_write(ready[1]); + FdGuard release_read(release[0]); + FdGuard release_write(release[1]); + ready_write.Close(); + release_read.Close(); + int result; + ASSERT_TRUE(ReadExact(ready_read.get(), &result, sizeof(result))); + ASSERT_EQ(0, result); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 7)); + char token = 1; + ASSERT_TRUE(WriteExact(release_write.get(), &token, sizeof(token))); + WaitChildOk(child); + EXPECT_EQ(7, SemCtl(sem.id(), 0, GETVAL, 0)) << "SETVAL clears only target debt"; + EXPECT_EQ(0, SemCtl(sem.id(), 1, GETVAL, 0)) << "un-cleared sem debt replays"; + + ASSERT_EQ(0, pipe(ready)); + ASSERT_EQ(0, pipe(release)); + child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + close(ready[0]); + close(release[1]); + struct sembuf ops[] = {{0, 1, SEM_UNDO}, {1, 1, SEM_UNDO}}; + const int child_result = SemOp(sem.id(), ops, 2); + if (!WriteExact(ready[1], &child_result, sizeof(child_result))) { + _exit(42); + } + char child_token; + _exit(child_result == 0 && ReadExact(release[0], &child_token, sizeof(child_token)) ? 0 : 43); + } + FdGuard all_ready_read(ready[0]); + FdGuard all_ready_write(ready[1]); + FdGuard all_release_read(release[0]); + FdGuard all_release_write(release[1]); + all_ready_write.Close(); + all_release_read.Close(); + ASSERT_TRUE(ReadExact(all_ready_read.get(), &result, sizeof(result))); + ASSERT_EQ(0, result); + unsigned short vals[] = {4, 5}; + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETALL, reinterpret_cast(vals))); + token = 1; + ASSERT_TRUE(WriteExact(all_release_write.get(), &token, sizeof(token))); + WaitChildOk(child); + EXPECT_EQ(4, SemCtl(sem.id(), 0, GETVAL, 0)) << "SETALL clears first adjustment"; + EXPECT_EQ(5, SemCtl(sem.id(), 1, GETVAL, 0)) << "SETALL clears complete set slice"; + // SETVAL wakes a queued UNDO request. Its undo debt is committed only when + // dequeued, so it must still replay when the waiter exits. + SemSet queued(1, IPC_CREAT | 0600); + ASSERT_TRUE(queued.valid()); + int queued_ready[2]; + int queued_release[2]; + ASSERT_EQ(0, pipe(queued_ready)); + ASSERT_EQ(0, pipe(queued_release)); + child = fork(); + ASSERT_GE(child, 0); + ChildGuard queued_child(child); + if (child == 0) { + close(queued_ready[0]); + close(queued_release[1]); + struct sembuf dec = {0, -1, SEM_UNDO}; + const char entered = 1; + if (!WriteExact(queued_ready[1], &entered, sizeof(entered)) || SemOp(queued.id(), &dec, 1) != 0) { + _exit(44); + } + char release_token; + _exit(ReadExact(queued_release[0], &release_token, sizeof(release_token)) ? 0 : 45); + } + FdGuard queued_ready_read(queued_ready[0]); + FdGuard queued_ready_write(queued_ready[1]); + FdGuard queued_release_read(queued_release[0]); + FdGuard queued_release_write(queued_release[1]); + queued_ready_write.Close(); + queued_release_read.Close(); + char queued_token; + ASSERT_TRUE(ReadExact(queued_ready_read.get(), &queued_token, sizeof(queued_token))); + ASSERT_TRUE(WaitForNcnt(queued.id(), 0, 1)); + ASSERT_EQ(0, SemCtl(queued.id(), 0, SETVAL, 1)); + EXPECT_EQ(0, SemCtl(queued.id(), 0, GETVAL, 0)); + queued_token = 1; + ASSERT_TRUE(WriteExact(queued_release_write.get(), &queued_token, sizeof(queued_token))); + WaitChildOk(&queued_child); + EXPECT_EQ(1, SemCtl(queued.id(), 0, GETVAL, 0)) + << "debt committed after SETVAL wake must replay at owner exit"; +} + +TEST(SysVSem, SemUndoRmidDiscardsDebt) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + int ready[2]; + int release[2]; + ASSERT_EQ(0, pipe(ready)); + ASSERT_EQ(0, pipe(release)); + pid_t child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + close(ready[0]); + close(release[1]); + const int result = SemUndoOpMustSucceed(sem.id(), 0, 1) ? 0 : -1; + if (!WriteExact(ready[1], &result, sizeof(result))) { + _exit(50); + } + char token; + _exit(result == 0 && ReadExact(release[0], &token, sizeof(token)) ? 0 : 51); + } + FdGuard ready_read(ready[0]); + FdGuard ready_write(ready[1]); + FdGuard release_read(release[0]); + FdGuard release_write(release[1]); + ready_write.Close(); + release_read.Close(); + int result; + ASSERT_TRUE(ReadExact(ready_read.get(), &result, sizeof(result))); + ASSERT_EQ(0, result); + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_RMID, 0)); + sem.release(); + char token = 1; + ASSERT_TRUE(WriteExact(release_write.get(), &token, sizeof(token))); + WaitChildOk(child); +} + +struct CloneUndoArgs { + int semid; + int ready_fd; + int release_fd; +}; + +int CloneSysvsemUndoChild(void* opaque) { + CloneUndoArgs* args = static_cast(opaque); + if (!SemUndoOpMustSucceed(args->semid, 0, 1)) { + return 1; + } + if (args->ready_fd >= 0) { + const char ready = 1; + if (!WriteExact(args->ready_fd, &ready, sizeof(ready))) { + return 2; + } + } + if (args->release_fd >= 0) { + char release; + if (!ReadExact(args->release_fd, &release, sizeof(release))) { + return 3; + } + } + return 0; +} + +TEST(SysVSem, SemUndoForkAndCloneOwners) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + + pid_t supervisor = fork(); + ASSERT_GE(supervisor, 0); + ChildGuard supervisor_guard(supervisor); + if (supervisor == 0) { + if (!SemUndoOpMustSucceed(sem.id(), 0, 1)) { + _exit(60); + } + pid_t ordinary = fork(); + if (ordinary < 0) { + _exit(61); + } + if (ordinary == 0) { + _exit(0); + } + int status; + if (waitpid(ordinary, &status, 0) != ordinary || !WIFEXITED(status) || WEXITSTATUS(status) || + SemCtl(sem.id(), 0, GETVAL, 0) != 1) { + _exit(62); + } + + alignas(16) char shared_stack[16384]; + CloneUndoArgs shared_args = {sem.id(), -1, -1}; + pid_t shared = clone(CloneSysvsemUndoChild, shared_stack + sizeof(shared_stack), + CLONE_SYSVSEM | SIGCHLD, &shared_args); + if (shared < 0 || waitpid(shared, &status, 0) != shared || !WIFEXITED(status) || WEXITSTATUS(status) || + SemCtl(sem.id(), 0, GETVAL, 0) != 2) { + _exit(63); + } + + int ready[2]; + int release[2]; + if (pipe(ready) != 0 || pipe(release) != 0) { + _exit(64); + } + alignas(16) char thread_stack[16384]; + CloneUndoArgs thread_args = {sem.id(), ready[1], release[0]}; + const int thread_flags = CLONE_VM | CLONE_SIGHAND | CLONE_THREAD; + if (clone(CloneSysvsemUndoChild, thread_stack + sizeof(thread_stack), thread_flags, &thread_args) < 0) { + _exit(65); + } + close(ready[1]); + close(release[0]); + char token; + if (!ReadExact(ready[0], &token, sizeof(token)) || SemCtl(sem.id(), 0, GETVAL, 0) != 3) { + _exit(66); + } + token = 1; + if (!WriteExact(release[1], &token, sizeof(token))) { + _exit(67); + } + close(release[1]); + // EOF is the explicit thread-exit handshake. The thread is the last + // writer, so EOF and the value together prove independent replay. + if (ReadExact(ready[0], &token, sizeof(token)) || SemCtl(sem.id(), 0, GETVAL, 0) != 2) { + _exit(68); + } + close(ready[0]); + _exit(0); + } + WaitChildOk(&supervisor_guard); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)) + << "only final CLONE_SYSVSEM owner replays its shared debt"; + + alignas(16) char invalid_stack[16384]; + CloneUndoArgs invalid_args = {sem.id(), -1, -1}; + errno = 0; + EXPECT_EQ(-1, clone(CloneSysvsemUndoChild, invalid_stack + sizeof(invalid_stack), + CLONE_NEWIPC | CLONE_SYSVSEM | SIGCHLD, &invalid_args)); + EXPECT_EQ(EINVAL, errno); +} + +const char* g_program_path = nullptr; + +TEST(SysVSem, SemUndoExecPreservesAttachment) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + ASSERT_NE(nullptr, g_program_path); + pid_t child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + if (!SemUndoOpMustSucceed(sem.id(), 0, 1)) { + _exit(70); + } + char* const argv[] = {const_cast(g_program_path), const_cast("--sem-undo-exec-helper"), nullptr}; + execv(g_program_path, argv); + _exit(71); + } + WaitChildOk(child); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); + + child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + if (!SemUndoOpMustSucceed(sem.id(), 0, 1)) { + _exit(72); + } + char* const argv[] = {const_cast("/missing-sem-undo-helper"), nullptr}; + execv(argv[0], argv); + _exit(errno == ENOENT ? 0 : 73); + } + WaitChildOk(child); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, SemUndoExitClampAndWake) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + + // Exit replay is saturating at SEMVMX: +1 debt meets a concurrently raised value. + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 32766)); + int ready[2]; + int release[2]; + ASSERT_EQ(0, pipe(ready)); + ASSERT_EQ(0, pipe(release)); + pid_t child = fork(); + ASSERT_GE(child, 0); + ChildGuard upper_child(child); + if (child == 0) { + close(ready[0]); + close(release[1]); + if (!SemUndoOpMustSucceed(sem.id(), 0, -1)) { + _exit(80); + } + const char token = 1; + if (!WriteExact(ready[1], &token, sizeof(token))) { + _exit(81); + } + char release_token; + _exit(ReadExact(release[0], &release_token, sizeof(release_token)) ? 0 : 82); + } + FdGuard upper_ready_read(ready[0]); + FdGuard upper_ready_write(ready[1]); + FdGuard upper_release_read(release[0]); + FdGuard upper_release_write(release[1]); + upper_ready_write.Close(); + upper_release_read.Close(); + char token; + ASSERT_TRUE(ReadExact(upper_ready_read.get(), &token, sizeof(token))); + struct sembuf raise_to_max = {0, 2, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &raise_to_max, 1)); + ASSERT_TRUE(WriteExact(upper_release_write.get(), &token, sizeof(token))); + WaitChildOk(&upper_child); + EXPECT_EQ(32767, SemCtl(sem.id(), 0, GETVAL, 0)); + + // Negative replay also saturates at zero: +1 debt meets a forced zero value. + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 1)); + ASSERT_EQ(0, pipe(ready)); + ASSERT_EQ(0, pipe(release)); + child = fork(); + ASSERT_GE(child, 0); + ChildGuard lower_child(child); + if (child == 0) { + close(ready[0]); + close(release[1]); + if (!SemUndoOpMustSucceed(sem.id(), 0, 1)) { + _exit(83); + } + const char child_token = 1; + if (!WriteExact(ready[1], &child_token, sizeof(child_token))) { + _exit(84); + } + char release_token; + _exit(ReadExact(release[0], &release_token, sizeof(release_token)) ? 0 : 85); + } + FdGuard lower_ready_read(ready[0]); + FdGuard lower_ready_write(ready[1]); + FdGuard lower_release_read(release[0]); + FdGuard lower_release_write(release[1]); + lower_ready_write.Close(); + lower_release_read.Close(); + ASSERT_TRUE(ReadExact(lower_ready_read.get(), &token, sizeof(token))); + struct sembuf lower_to_zero = {0, -2, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &lower_to_zero, 1)); + ASSERT_TRUE(WriteExact(lower_release_write.get(), &token, sizeof(token))); + WaitChildOk(&lower_child); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); + + // Final-owner replay must wake a zero waiter, validated by GETZCNT rather than timing. + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 0)); + ASSERT_EQ(0, pipe(ready)); + ASSERT_EQ(0, pipe(release)); + child = fork(); + ASSERT_GE(child, 0); + ChildGuard wake_owner(child); + if (child == 0) { + close(ready[0]); + close(release[1]); + if (!SemUndoOpMustSucceed(sem.id(), 0, 1)) { + _exit(86); + } + const char child_token = 1; + if (!WriteExact(ready[1], &child_token, sizeof(child_token))) { + _exit(87); + } + char release_token; + _exit(ReadExact(release[0], &release_token, sizeof(release_token)) ? 0 : 88); + } + FdGuard wake_ready_read(ready[0]); + FdGuard wake_ready_write(ready[1]); + FdGuard wake_release_read(release[0]); + FdGuard wake_release_write(release[1]); + wake_ready_write.Close(); + wake_release_read.Close(); + ASSERT_TRUE(ReadExact(wake_ready_read.get(), &token, sizeof(token))); + pid_t waiter = fork(); + ASSERT_GE(waiter, 0); + ChildGuard wake_waiter(waiter); + if (waiter == 0) { + struct sembuf zero = {0, 0, 0}; + _exit(SemOp(sem.id(), &zero, 1) == 0 ? 0 : 89); + } + ASSERT_TRUE(WaitForZcnt(sem.id(), 0, 1)); + ASSERT_TRUE(WriteExact(wake_release_write.get(), &token, sizeof(token))); + WaitChildOk(&wake_owner); + WaitChildOk(&wake_waiter); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, SemUndoUnshareSysvsem) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + + int phase[2]; + int supervisor_release[2]; + int old_owner_release[2]; + ASSERT_EQ(0, pipe(phase)); + ASSERT_EQ(0, pipe(supervisor_release)); + ASSERT_EQ(0, pipe(old_owner_release)); + pid_t supervisor = fork(); + ASSERT_GE(supervisor, 0); + ChildGuard supervisor_guard(supervisor); + if (supervisor == 0) { + close(phase[0]); + close(supervisor_release[1]); + close(old_owner_release[1]); + if (!SemUndoOpMustSucceed(sem.id(), 0, 1)) { + _exit(90); + } + alignas(16) char stack[16384]; + CloneUndoArgs args = {sem.id(), -1, old_owner_release[0]}; + pid_t old_owner = clone(CloneSysvsemUndoChild, stack + sizeof(stack), + CLONE_SYSVSEM | SIGCHLD, &args); + if (old_owner < 0 || unshare(CLONE_SYSVSEM) != 0 || + !SemUndoOpMustSucceed(sem.id(), 0, 1)) { + _exit(91); + } + const char detached = 1; + if (!WriteExact(phase[1], &detached, sizeof(detached))) { + _exit(92); + } + char release_supervisor; + if (!ReadExact(supervisor_release[0], &release_supervisor, sizeof(release_supervisor))) { + _exit(93); + } + // Do not wait for old_owner here: it is intentionally held by the parent + // until this process exits and replays the new group's debt. + _exit(0); + } + FdGuard phase_read(phase[0]); + FdGuard phase_write(phase[1]); + FdGuard supervisor_read(supervisor_release[0]); + FdGuard supervisor_write(supervisor_release[1]); + FdGuard old_owner_read(old_owner_release[0]); + FdGuard old_owner_write(old_owner_release[1]); + phase_write.Close(); + supervisor_read.Close(); + old_owner_read.Close(); + char token; + ASSERT_TRUE(ReadExact(phase_read.get(), &token, sizeof(token))); + EXPECT_EQ(3, SemCtl(sem.id(), 0, GETVAL, 0)) + << "new group debt must coexist with the still-live old shared group"; + ASSERT_TRUE(WriteExact(supervisor_write.get(), &token, sizeof(token))); + WaitChildOk(&supervisor_guard); + EXPECT_EQ(2, SemCtl(sem.id(), 0, GETVAL, 0)) + << "unshared owner exit replays only its new group debt"; + ASSERT_TRUE(WriteExact(old_owner_write.get(), &token, sizeof(token))); + ASSERT_TRUE(WaitForSemValue(sem.id(), 0, 0)); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)) + << "last old shared owner replays old-group debt after its release"; +} + +struct NamespaceUndoReport { + int setns_result; + int setns_errno; + int value_before_exit; + int inaccessible_sem_errno; + int precondition_errno; +}; + +int RunNamespaceSetnsChild(int semid, int setns_fd, int flags, int report_fd) { + NamespaceUndoReport report = {-1, 0, -1, 0, 0}; + if (!SemUndoOpMustSucceed(semid, 0, 1)) { + return 101; + } + errno = 0; + report.setns_result = static_cast(syscall(SYS_setns, setns_fd, flags)); + report.setns_errno = errno; + report.value_before_exit = SemCtl(semid, 0, GETVAL, 0); + return WriteExact(report_fd, &report, sizeof(report)) ? 0 : 102; +} + +bool ReadNamespaceReportAndReap(int report_fd, ChildGuard* child, NamespaceUndoReport* report) { + if (!ReadExact(report_fd, report, sizeof(*report))) { + return false; + } + WaitChildOk(child); + return true; +} + +TEST(SysVSem, SemtimedopCopiesNonNullTimeoutBeforeNsopsValidation) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + struct sembuf op = {0, 0, 0}; + const struct timespec* invalid_timeout = reinterpret_cast( + static_cast(1)); + + errno = 0; + EXPECT_EQ(-1, SemTimedOp(sem.id(), &op, 0, invalid_timeout)); + EXPECT_EQ(EFAULT, errno) + << "non-null timeout must be copied before nsops == 0 is rejected"; +} + +TEST(SysVSem, SemUndoIpcNamespaceAndErrnos) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + + struct sembuf invalid_semnum = {1, 1, SEM_UNDO}; + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), &invalid_semnum, 1)); + EXPECT_EQ(EFBIG, errno); + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), nullptr, 1)); + EXPECT_EQ(EFAULT, errno); + errno = 0; + EXPECT_EQ(-1, syscall(SYS_semop, sem.id(), &invalid_semnum, 501)); + EXPECT_EQ(E2BIG, errno); + + int report_pipe[2]; + ASSERT_EQ(0, pipe(report_pipe)); + pid_t child = fork(); + ASSERT_GE(child, 0); + ChildGuard invalid_child(child); + if (child == 0) { + close(report_pipe[0]); + _exit(RunNamespaceSetnsChild(sem.id(), -1, CLONE_NEWIPC | CLONE_SYSVSEM, report_pipe[1])); + } + FdGuard report_read(report_pipe[0]); + FdGuard report_write(report_pipe[1]); + report_write.Close(); + NamespaceUndoReport report = {}; + ASSERT_TRUE(ReadNamespaceReportAndReap(report_read.get(), &invalid_child, &report)); + EXPECT_EQ(-1, report.setns_result); + EXPECT_EQ(EINVAL, report.setns_errno); + EXPECT_EQ(1, report.value_before_exit); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)) + << "failed setns prepare must preserve old debt until child exit"; +} + +TEST(SysVSem, SemUndoNamespaceFdDetachesAttachment) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + + int nsfd = open("/proc/self/ns/ipc", O_RDONLY | O_CLOEXEC); + if (nsfd < 0) { + GTEST_SKIP() << "proc namespace-fd IPC setns unavailable (errno=" << errno + << "): requires FilePrivateData::Namespace; see " + "kernel/src/process/namespace/setns.rs:150-155"; + } + FdGuard namespace_fd(nsfd); + int report_pipe[2]; + ASSERT_EQ(0, pipe(report_pipe)); + pid_t child = fork(); + ASSERT_GE(child, 0); + ChildGuard nsfd_child(child); + if (child == 0) { + close(report_pipe[0]); + _exit(RunNamespaceSetnsChild(sem.id(), namespace_fd.get(), CLONE_NEWIPC, report_pipe[1])); + } + FdGuard report_read(report_pipe[0]); + FdGuard report_write(report_pipe[1]); + report_write.Close(); + NamespaceUndoReport report = {}; + ASSERT_TRUE(ReadNamespaceReportAndReap(report_read.get(), &nsfd_child, &report)); + if (report.setns_result == -1 && (report.setns_errno == EPERM || report.setns_errno == EACCES)) { + GTEST_SKIP() << "IPC namespace-fd setns needs CAP_SYS_ADMIN in target user namespace"; + } + ASSERT_EQ(0, report.setns_result) << "namespace-fd setns errno=" << report.setns_errno; + EXPECT_EQ(0, report.value_before_exit) + << "successful namespace-fd detach must replay and discard old attachment"; + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)) + << "namespace-fd detach must leave no old attachment for child exit"; +} + +TEST(SysVSem, SemUndoPidfdDetachesAttachment) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + RequireSemUndoAvailable(sem); + + int pidfd = static_cast(syscall(SYS_pidfd_open, getpid(), 0)); + if (pidfd < 0) { + GTEST_SKIP() << "pidfd_open unavailable errno=" << errno; + } + FdGuard pidfd_guard(pidfd); + int report_pipe[2]; + ASSERT_EQ(0, pipe(report_pipe)); + pid_t child = fork(); + ASSERT_GE(child, 0); + ChildGuard pidfd_child(child); + if (child == 0) { + close(report_pipe[0]); + _exit(RunNamespaceSetnsChild(sem.id(), pidfd_guard.get(), CLONE_NEWIPC, report_pipe[1])); + } + FdGuard report_read(report_pipe[0]); + FdGuard report_write(report_pipe[1]); + report_write.Close(); + NamespaceUndoReport report = {}; + ASSERT_TRUE(ReadNamespaceReportAndReap(report_read.get(), &pidfd_child, &report)); + if (report.setns_result == -1 && (report.setns_errno == EPERM || report.setns_errno == EACCES)) { + GTEST_SKIP() << "pidfd IPC setns needs capability/permission in target user namespace"; + } + ASSERT_EQ(0, report.setns_result) << "pidfd setns errno=" << report.setns_errno; + EXPECT_EQ(0, report.value_before_exit) + << "same-Arc pidfd setns must replay and discard old attachment"; + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)) + << "pidfd detach must leave no old attachment for child exit"; +} + +TEST(SysVSem, SemUndoCredentialDenied) { + SemSet probe(1, IPC_CREAT | 0600); + SemSet private_sem(1, IPC_CREAT | 0000); + ASSERT_TRUE(probe.valid()); + ASSERT_TRUE(private_sem.valid()); + RequireSemUndoAvailable(probe); + + int permission_pipe[2]; + ASSERT_EQ(0, pipe(permission_pipe)); + pid_t child = fork(); + ASSERT_GE(child, 0); + ChildGuard permission_child(child); + if (child == 0) { + close(permission_pipe[0]); + NamespaceUndoReport permission = {-1, 0, -1, 0, 0}; + if (setgid(1000) != 0 || setuid(1000) != 0) { + permission.precondition_errno = errno; + } else { + struct sembuf access = {0, 1, SEM_UNDO | IPC_NOWAIT}; + errno = 0; + SemOp(private_sem.id(), &access, 1); + permission.inaccessible_sem_errno = errno; + } + const bool wrote = WriteExact(permission_pipe[1], &permission, sizeof(permission)); + _exit(wrote ? 0 : 103); + } + FdGuard permission_read(permission_pipe[0]); + FdGuard permission_write(permission_pipe[1]); + permission_write.Close(); + NamespaceUndoReport report = {}; + ASSERT_TRUE(ReadNamespaceReportAndReap(permission_read.get(), &permission_child, &report)); + ASSERT_EQ(0, SemCtl(private_sem.id(), 0, IPC_RMID, 0)); + private_sem.release(); + if (report.precondition_errno != 0) { + GTEST_SKIP() << "credential-drop EACCES precondition unavailable errno=" << report.precondition_errno; + } + EXPECT_EQ(EACCES, report.inaccessible_sem_errno); } // ============ blocking & wakeup ============ @@ -1044,6 +2040,10 @@ TEST(SysVSem, ConcurrentWorkers) { } int main(int argc, char** argv) { + g_program_path = argv[0]; + if (argc == 2 && strcmp(argv[1], "--sem-undo-exec-helper") == 0) { + return 0; + } ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } From 7e9b503440dc8c51cb0f50ba268fe7172995bcc1 Mon Sep 17 00:00:00 2001 From: yuming Date: Sat, 5 Sep 2026 11:25:27 +0800 Subject: [PATCH 09/34] fix(ipc): mark allocated IDs in release builds --- kernel/src/ipc/id.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kernel/src/ipc/id.rs b/kernel/src/ipc/id.rs index 4a0a10ff60..077bcc05ab 100644 --- a/kernel/src/ipc/id.rs +++ b/kernel/src/ipc/id.rs @@ -51,7 +51,8 @@ impl FixedIpcIdAllocator Result { let idx = self.find_free_idx().ok_or(SystemError::ENOSPC)?; - debug_assert_eq!(self.used.set(idx, true), Some(false)); + let was_used = self.used.set(idx, true); + debug_assert_eq!(was_used, Some(false)); self.next_idx = if idx + 1 == self.max_ids { 0 } else { idx + 1 }; if let Some(last_idx) = self.last_idx { From 8678779687be4b5c455cb309ff1e3a5b91951292 Mon Sep 17 00:00:00 2001 From: yuming Date: Sat, 5 Sep 2026 12:01:56 +0800 Subject: [PATCH 10/34] fix(ipc): prioritize const semaphore waiters --- kernel/src/ipc/sem.rs | 253 ++++++++++++------ .../suites/normal/sysv_sem_semantics.cc | 37 +++ 2 files changed, 211 insertions(+), 79 deletions(-) diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index c5024bafea..3059414bbf 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -360,6 +360,13 @@ impl SemQueueEntry { } } +/// Selects one of the set-global pending queues. +#[derive(Debug, Clone, Copy)] +enum SemPendingQueue { + Const, + Alter, +} + /// Semaphore set #[derive(Debug)] pub struct KernelSemSet { @@ -371,8 +378,10 @@ pub struct KernelSemSet { pub sem_otime: i64, /// Time of the last metadata change pub sem_ctime: i64, - /// Waiter queue - waiters: VecDeque>, + /// Pending operation groups containing only zero-wait operations + pending_const: VecDeque>, + /// Pending operation groups containing at least one altering operation + pending_alter: VecDeque>, } impl KernelSemSet { @@ -390,36 +399,83 @@ impl KernelSemSet { sems, sem_otime: 0, sem_ctime: PosixTimeSpec::now().tv_sec, - waiters: VecDeque::new(), + pending_const: VecDeque::new(), + pending_alter: VecDeque::new(), + } + } + + fn pending_queue_for(sops: &[PosixSemBuf]) -> SemPendingQueue { + if sops.iter().any(|op| op.sem_op != 0) { + SemPendingQueue::Alter + } else { + SemPendingQueue::Const } } fn enqueue_waiter(&mut self, waiter: Arc) { - self.waiters.push_back(waiter); + match Self::pending_queue_for(&waiter.sops) { + SemPendingQueue::Const => self.pending_const.push_back(waiter), + SemPendingQueue::Alter => self.pending_alter.push_back(waiter), + } } fn remove_waiter(&mut self, target: &Arc) { - self.waiters.retain(|entry| !Arc::ptr_eq(entry, target)); + self.pending_const + .retain(|entry| !Arc::ptr_eq(entry, target)); + self.pending_alter + .retain(|entry| !Arc::ptr_eq(entry, target)); + } + + #[cfg(test)] + fn pending_is_empty(&self) -> bool { + self.pending_const.is_empty() && self.pending_alter.is_empty() + } + + fn pending_len(&self, queue: SemPendingQueue) -> usize { + match queue { + SemPendingQueue::Const => self.pending_const.len(), + SemPendingQueue::Alter => self.pending_alter.len(), + } + } + + fn pending_entry(&self, queue: SemPendingQueue, index: usize) -> Arc { + match queue { + SemPendingQueue::Const => self.pending_const[index].clone(), + SemPendingQueue::Alter => self.pending_alter[index].clone(), + } + } + + fn remove_pending(&mut self, queue: SemPendingQueue, index: usize) { + match queue { + SemPendingQueue::Const => self.pending_const.remove(index), + SemPendingQueue::Alter => self.pending_alter.remove(index), + }; } /// Complete and wake all entries during IPC_RMID under the manager lock. fn complete_all_removed(&mut self) { - for entry in self.waiters.drain(..) { + for entry in self.pending_const.drain(..) { + entry.complete(Err(SystemError::EIDRM)); + entry.waker.wake(); + } + for entry in self.pending_alter.drain(..) { entry.complete(Err(SystemError::EIDRM)); entry.waker.wake(); } } fn ncnt(&self, semnum: usize) -> usize { - self.waiters + self.pending_const .iter() + .chain(self.pending_alter.iter()) .filter(|entry| entry.is_waiting_on(semnum, SemWaitType::Increase)) .count() } fn zcnt(&self, semnum: usize) -> usize { - self.waiters + self.pending_const .iter() + .chain(self.pending_alter.iter()) .filter(|entry| entry.is_waiting_on(semnum, SemWaitType::Zero)) .count() } @@ -950,92 +1006,94 @@ impl SemManager { values_changed } - /// Complete every executable queue entry without head-of-line blocking. - fn update_queue(set: &mut KernelSemSet, _semid: SemId) { - loop { - let mut values_changed = false; - let mut index = 0; - - while index < set.waiters.len() { - let entry = set.waiters[index].clone(); - - if let Some(group) = entry.undo_group.as_ref() { - let result = { - let mut record_slot = entry.undo_record.lock_irqsave(); - let Some(record) = record_slot.take() else { - set.waiters.remove(index); - entry.complete(Err(SystemError::EINVAL)); - entry.waker.wake(); - continue; - }; - match group.with_prepared_record_noalloc(record, |record| { - match Self::retry_queued_undo_entry(set, &entry, record) { - Ok(Some(changed)) => { - PreparedSemUndoRecordAction::Publish(Ok(Some(changed))) - } - Ok(None) => PreparedSemUndoRecordAction::Keep(Ok(None)), - Err(error) => PreparedSemUndoRecordAction::Keep(Err(error)), - } - }) { - Ok((result, kept_record)) => { - *record_slot = kept_record; - result - } - Err(error) => Err(error), - } - }; + /// Scan one pending queue without head-of-line blocking. + fn scan_pending_queue(set: &mut KernelSemSet, queue: SemPendingQueue) -> bool { + let mut index = 0; + while index < set.pending_len(queue) { + let entry = set.pending_entry(queue, index); - match result { - Ok(Some(changed)) => { - values_changed |= changed; - set.waiters.remove(index); - entry.complete(Ok(0)); - entry.waker.wake(); - if changed { - break; + if let Some(group) = entry.undo_group.as_ref() { + let result = { + let mut record_slot = entry.undo_record.lock_irqsave(); + let Some(record) = record_slot.take() else { + set.remove_pending(queue, index); + entry.complete(Err(SystemError::EINVAL)); + entry.waker.wake(); + continue; + }; + match group.with_prepared_record_noalloc(record, |record| { + match Self::retry_queued_undo_entry(set, &entry, record) { + Ok(Some(changed)) => { + PreparedSemUndoRecordAction::Publish(Ok(Some(changed))) } + Ok(None) => PreparedSemUndoRecordAction::Keep(Ok(None)), + Err(error) => PreparedSemUndoRecordAction::Keep(Err(error)), } - Ok(None) => index += 1, - Err(error) => { - set.waiters.remove(index); - entry.complete(Err(error)); - entry.waker.wake(); + }) { + Ok((result, kept_record)) => { + *record_slot = kept_record; + result } + Err(error) => Err(error), } - continue; - } + }; - let mut scratch = entry.scratch.lock(); - match Self::simulate_semop(set, &entry.sops, None, &mut scratch) { - Ok(SemopOutcome::Ready(simulation)) => { - let changed = - Self::commit_semop(set, simulation, &scratch, entry.pid.clone(), None); - values_changed |= changed; - set.waiters.remove(index); + match result { + Ok(Some(changed)) => { + set.remove_pending(queue, index); entry.complete(Ok(0)); entry.waker.wake(); if changed { - break; + return true; } } - Ok(SemopOutcome::Blocked(blocker)) if blocker.nowait => { - set.waiters.remove(index); - entry.complete(Err(SystemError::EAGAIN_OR_EWOULDBLOCK)); - entry.waker.wake(); - } - Ok(SemopOutcome::Blocked(blocker)) => { - entry.update_blocker(blocker); - index += 1; - } + Ok(None) => index += 1, Err(error) => { - set.waiters.remove(index); + set.remove_pending(queue, index); entry.complete(Err(error)); entry.waker.wake(); } } + continue; + } + + let mut scratch = entry.scratch.lock(); + match Self::simulate_semop(set, &entry.sops, None, &mut scratch) { + Ok(SemopOutcome::Ready(simulation)) => { + let changed = + Self::commit_semop(set, simulation, &scratch, entry.pid.clone(), None); + set.remove_pending(queue, index); + entry.complete(Ok(0)); + entry.waker.wake(); + if changed { + return true; + } + } + Ok(SemopOutcome::Blocked(blocker)) if blocker.nowait => { + set.remove_pending(queue, index); + entry.complete(Err(SystemError::EAGAIN_OR_EWOULDBLOCK)); + entry.waker.wake(); + } + Ok(SemopOutcome::Blocked(blocker)) => { + entry.update_blocker(blocker); + index += 1; + } + Err(error) => { + set.remove_pending(queue, index); + entry.complete(Err(error)); + entry.waker.wake(); + } } + } + false + } - if !values_changed { + /// Complete executable const entries before altering entries. + fn update_queue(set: &mut KernelSemSet, _semid: SemId) { + loop { + let const_changed = Self::scan_pending_queue(set, SemPendingQueue::Const); + debug_assert!(!const_changed); + if !Self::scan_pending_queue(set, SemPendingQueue::Alter) { return; } } @@ -1750,7 +1808,7 @@ mod tests { .as_ref() .map(|pid| pid.pid_nr_ns(&INIT_PID_NAMESPACE)); let sem_otime = set.sem_otime; - let waiters_are_empty = set.waiters.is_empty(); + let waiters_are_empty = set.pending_is_empty(); let completed_result = entry.completed_result(); let record_count = group.record_count_for_test(); @@ -1803,7 +1861,7 @@ mod tests { .as_ref() .is_some_and(|pid| Arc::ptr_eq(pid, &exiting_tgid)); let sem_otime = set.sem_otime; - let waiters_are_empty = set.waiters.is_empty(); + let waiters_are_empty = set.pending_is_empty(); let completed_result = entry.completed_result(); for sem in &mut set.sems { sem.pid = None; @@ -1846,7 +1904,7 @@ mod tests { let values = [set.sems[0].val, set.sems[1].val]; let untouched_pid = set.sems[1].pid.is_none(); let sem_otime = set.sem_otime; - let waiters_are_empty = set.waiters.is_empty(); + let waiters_are_empty = set.pending_is_empty(); let completed_result = entry.completed_result(); for sem in &mut set.sems { sem.pid = None; @@ -2340,6 +2398,43 @@ mod tests { } } + #[test] + fn const_waiters_complete_before_altering_waiters() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(67), &[1]); + let (altering, constant) = { + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let altering = enqueue_test_waiter( + set, + &[plain_sop(0, 0), plain_sop(0, 1)], + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }, + ); + let constant = enqueue_test_waiter( + set, + &[plain_sop(0, 0)], + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }, + ); + set.sems[0].val = 0; + (altering, constant) + }; + + manager.update_queue_for_test(semid); + + let set = manager.get_by_semid_checked(semid).unwrap(); + assert_eq!(constant.completed_result(), Some(Ok(0))); + assert_eq!(altering.completed_result(), Some(Ok(0))); + assert!(set.pending_is_empty()); + assert_eq!(set.sems[0].val, 1); + } + #[test] fn ordered_mixed_undo_ops_apply_each_adjustment_step() { let mut manager = SemManager::new(); diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index 2d511de099..e98448846d 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -1902,6 +1902,43 @@ TEST(SysVSem, SignalInterruptsBlockedSemop) { << "an interrupted operation must not consume a future token"; } +TEST(SysVSem, ConstWaitersCompleteBeforeAlteringWaiters) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 1)); + + const pid_t altering_pid = fork(); + ASSERT_GE(altering_pid, 0); + ChildGuard altering(altering_pid); + if (altering_pid == 0) { + struct sembuf ops[] = {{0, 0, 0}, {0, 1, 0}}; + _exit(SemOp(sem.id(), ops, 2) == 0 ? 0 : 11); + } + ASSERT_TRUE(WaitForZcnt(sem.id(), 0, 1)); + ASSERT_EQ(1, SemCtl(sem.id(), 0, GETZCNT, 0)); + + const pid_t constant_pid = fork(); + ASSERT_GE(constant_pid, 0); + ChildGuard constant(constant_pid); + if (constant_pid == 0) { + struct sembuf wait_zero = {0, 0, 0}; + _exit(SemOp(sem.id(), &wait_zero, 1) == 0 ? 0 : 12); + } + ASSERT_TRUE(WaitForZcnt(sem.id(), 0, 2)); + ASSERT_EQ(2, SemCtl(sem.id(), 0, GETZCNT, 0)); + + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 0)); + const int zcnt_after_setval = SemCtl(sem.id(), 0, GETZCNT, 0); + if (zcnt_after_setval != 0) { + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 0)) + << "release a waiter left behind by incorrect queue ordering"; + } + EXPECT_EQ(0, zcnt_after_setval); + WaitChildOk(&altering); + WaitChildOk(&constant); + EXPECT_EQ(1, SemCtl(sem.id(), 0, GETVAL, 0)); +} + TEST(SysVSem, WakingSemopCompletesEligibleQueuedOperationsBeforeReturn) { SemSet sem(1, IPC_CREAT | 0600); ASSERT_TRUE(sem.valid()); From 26da36898638761240d955beac1cd487751e8dbd Mon Sep 17 00:00:00 2001 From: yuming Date: Sat, 5 Sep 2026 13:31:00 +0800 Subject: [PATCH 11/34] fix(process): preserve reaped PID identity --- kernel/src/process/pid.rs | 38 +++++++++------- .../suites/normal/sysv_sem_semantics.cc | 45 +++++++++++++++++++ 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/kernel/src/process/pid.rs b/kernel/src/process/pid.rs index 29da4d18f9..8e0df1f8ea 100644 --- a/kernel/src/process/pid.rs +++ b/kernel/src/process/pid.rs @@ -74,8 +74,10 @@ pub struct Pid { /// tasks[PidType::PID as usize] = 使用该PID作为进程ID的任务 /// tasks[PidType::TGID as usize] = 使用该PID作为线程组ID的任务 tasks: [SpinLock>>; PidType::PIDTYPE_MAX], - /// 在各个namespace中的PID值 + /// PID numbers in each namespace. These remain as identity until final drop. numbers: SpinLock>>, + /// Owns namespace map registrations and allocator slots until unregistration. + registered: Vec, /// pidfd poll/epoll waiters. Linux keeps this wakeup edge on struct pid. pidfd_epitems: EPollItemList, } @@ -94,6 +96,7 @@ impl Pid { level, tasks: core::array::from_fn(|_| SpinLock::new(Vec::new())), numbers: SpinLock::new(vec![None; level as usize + 1]), + registered: (0..=level).map(|_| AtomicBool::new(false)).collect(), pidfd_epitems: EPollItemList::default(), }); @@ -261,10 +264,14 @@ impl Eq for Pid {} impl Drop for Pid { fn drop(&mut self) { - // 清理numbers中的UPid引用 - let numbers_guard = self.numbers.lock(); - for upid in numbers_guard.iter().flatten() { - upid.ns.release_pid_in_ns(upid.nr); + let numbers = self.numbers.lock(); + for (level, upid) in numbers.iter().enumerate() { + if self.registered[level].swap(false, Ordering::AcqRel) { + let upid = upid + .as_ref() + .expect("registered PID namespace entry must have an identity"); + upid.ns.release_pid_in_ns(upid.nr); + } } } } @@ -519,6 +526,7 @@ pub(super) fn alloc_pid(ns: &Arc) -> Result, SystemError> let upid = UPid::new(nr, curr_ns.clone()); allocated_upids.push((level, upid.clone())); pid.numbers.lock()[level as usize] = Some(upid); + pid.registered[level as usize].store(true, Ordering::Release); current_ns = curr_ns.parent(); } Err(e) => { @@ -543,6 +551,7 @@ fn cleanup_allocated_pids(pid: Arc, mut allocated_upids: Vec<(isize, UPid)> let curr_ns = upid.ns; // 在当前namespace中释放UPid curr_ns.release_pid_in_ns(upid.nr); + pid.registered[level as usize].store(false, Ordering::Release); pid.numbers.lock()[level as usize] = None; } } @@ -556,18 +565,13 @@ pub(super) fn free_pid(pid: Arc) { // let raw_pid = pid.pid_vnr().data(); let mut level = 0; while level <= pid.level { - let upid = match pid.numbers.lock()[level as usize].take() { - Some(upid) => upid, - None => { - // PID numbers 已经被释放过了,这可能发生在进程被多次detach的场景 - // 虽然理论上不应该发生,但为了防御性编程,我们在这里直接返回 - log::warn!( - "PID numbers at level {} already freed, skipping remaining levels", - level - ); - return; - } - }; + if !pid.registered[level as usize].swap(false, Ordering::AcqRel) { + level += 1; + continue; + } + let upid = pid.numbers.lock()[level as usize] + .clone() + .expect("registered PID namespace entry must have an identity"); // log::debug!( // "Freeing pid: raw:{}, upid.nr:{}, level: {}", // raw_pid, diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index e98448846d..ef02d7cd76 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -671,6 +671,51 @@ TEST(SysVSem, GetPidTracksLastSemop) { EXPECT_EQ(getpid(), SemCtl(sem.id(), 0, GETPID, 0)) << "SETVAL also updates sempid"; } +TEST(SysVSem, GetPidPreservesSemUndoActorAfterWaitpidReap) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + + int ready_pipe[2]; + int release_pipe[2]; + ASSERT_EQ(0, pipe(ready_pipe)); + ASSERT_EQ(0, pipe(release_pipe)); + FdGuard ready_read(ready_pipe[0]); + FdGuard ready_write(ready_pipe[1]); + FdGuard release_read(release_pipe[0]); + FdGuard release_write(release_pipe[1]); + + const pid_t child_pid = fork(); + ASSERT_GE(child_pid, 0); + if (child_pid == 0) { + close(ready_pipe[0]); + close(release_pipe[1]); + char byte = 1; + if (!SemUndoOpMustSucceed(sem.id(), 0, 1) || + !WriteExact(ready_pipe[1], &byte, sizeof(byte)) || + !ReadExact(release_pipe[0], &byte, sizeof(byte))) { + _exit(1); + } + _exit(0); + } + ChildGuard child(child_pid); + ready_write.Close(); + release_read.Close(); + + char byte = 0; + ASSERT_TRUE(ReadExact(ready_read.get(), &byte, sizeof(byte))); + EXPECT_EQ(1, SemCtl(sem.id(), 0, GETVAL, 0)); + + struct sembuf parent_op = {0, 1, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &parent_op, 1)); + EXPECT_EQ(getpid(), SemCtl(sem.id(), 0, GETPID, 0)); + + ASSERT_TRUE(WriteExact(release_write.get(), &byte, sizeof(byte))); + WaitChildOk(&child); + EXPECT_EQ(1, SemCtl(sem.id(), 0, GETVAL, 0)) << "SEM_UNDO must preserve the parent delta"; + EXPECT_EQ(child_pid, SemCtl(sem.id(), 0, GETPID, 0)) + << "GETPID must retain the reaped SEM_UNDO actor identity"; +} + // ============ semop semantics ============ TEST(SysVSem, IncrementDecrement) { From 73467d16c831c017e760987ebb58f329b47eab22 Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 12:35:26 +0000 Subject: [PATCH 12/34] fix(process): preserve exec locking in prepared namespace publication Signed-off-by: longjin --- kernel/src/process/namespace/nsproxy.rs | 21 +++++++++++++++++++++ kernel/src/process/task.rs | 7 +++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/kernel/src/process/namespace/nsproxy.rs b/kernel/src/process/namespace/nsproxy.rs index 2c8a759065..57c4c99753 100644 --- a/kernel/src/process/namespace/nsproxy.rs +++ b/kernel/src/process/namespace/nsproxy.rs @@ -457,6 +457,27 @@ mod tests { ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()) } + #[test] + fn namespace_only_commit_does_not_reacquire_exec_lock() { + let pcb = test_pcb(); + let old_cred = pcb.cred(); + let new_nsproxy = Arc::new(pcb.nsproxy().clone_inner()); + let _exec_guard = pcb.exec_update_write(); + let fs_refs = crate::process::lock_fs_refs_copy(); + let prepared = PreparedNamespaceInstall::prepare_for_setns( + &pcb, + new_nsproxy.clone(), + None, + false, + &fs_refs, + ) + .unwrap(); + + prepared.commit(&pcb, &fs_refs).unwrap(); + assert!(Arc::ptr_eq(&pcb.nsproxy(), &new_nsproxy)); + assert!(Arc::ptr_eq(&pcb.cred(), &old_cred)); + } + #[test] fn namespace_prepare_error_preserves_attachment_and_old_state() { let pcb = test_pcb(); diff --git a/kernel/src/process/task.rs b/kernel/src/process/task.rs index 864fa40a6e..161d0274e1 100644 --- a/kernel/src/process/task.rs +++ b/kernel/src/process/task.rs @@ -1067,8 +1067,11 @@ impl ProcessControlBlock { nsproxy_retire: PreparedRcuArcRetire, new_cred: Option<(Arc, PreparedRcuArcRetire)>, ) { - let _exec_guard = self.exec_update_read(); - let active_mm = self.basic().user_vm(); + // Only credential publication needs to stabilize the active mm. Pure + // namespace publication also runs inside exec, which already owns the + // write side and must not recursively acquire this read lock. + let _exec_guard = new_cred.as_ref().map(|_| self.exec_update_read()); + let active_mm = new_cred.as_ref().and_then(|_| self.basic().user_vm()); let (nsproxy_retirement, cred_retirement) = { let _task_guard = self.task_lock.lock_irqsave(); let nsproxy_retirement = self.nsproxy.swap_prepared(new_nsproxy, nsproxy_retire); From 03c0e0354e58c7cc2f22d1824e42772b6b0a8c4b Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 13:01:47 +0000 Subject: [PATCH 13/34] fix(ipc): harden semaphore allocation and Linux syscall semantics Signed-off-by: longjin --- kernel/crates/rust-slabmalloc/src/lib.rs | 1 + kernel/crates/rust-slabmalloc/src/pages.rs | 10 +- .../rust-slabmalloc/tests/fallible_page.rs | 51 +++++ kernel/src/ipc/sem.rs | 192 ++++++++--------- kernel/src/ipc/sem_undo.rs | 14 ++ kernel/src/ipc/syscall/sys_semctl.rs | 8 +- kernel/src/ipc/syscall/sys_semget.rs | 7 +- kernel/src/ipc/syscall/sys_semop.rs | 43 +--- kernel/src/ipc/syscall/sys_semtimedop.rs | 75 ++++--- kernel/src/mm/allocator/slab.rs | 5 +- kernel/src/process/namespace/setns.rs | 17 +- .../suites/normal/sysv_sem_semantics.cc | 197 ++++++++++++++++-- 12 files changed, 409 insertions(+), 211 deletions(-) create mode 100644 kernel/crates/rust-slabmalloc/tests/fallible_page.rs diff --git a/kernel/crates/rust-slabmalloc/src/lib.rs b/kernel/crates/rust-slabmalloc/src/lib.rs index 29638cda19..5672f9a928 100644 --- a/kernel/crates/rust-slabmalloc/src/lib.rs +++ b/kernel/crates/rust-slabmalloc/src/lib.rs @@ -23,6 +23,7 @@ #![crate_name = "slabmalloc"] #![crate_type = "lib"] #![feature(maybe_uninit_as_bytes)] +#![feature(allocator_api)] #![deny(clippy::all)] #![allow(clippy::needless_return)] extern crate alloc; diff --git a/kernel/crates/rust-slabmalloc/src/pages.rs b/kernel/crates/rust-slabmalloc/src/pages.rs index bac4802633..820929c9c3 100644 --- a/kernel/crates/rust-slabmalloc/src/pages.rs +++ b/kernel/crates/rust-slabmalloc/src/pages.rs @@ -283,7 +283,13 @@ pub struct ObjectPage<'a> { } impl<'a> ObjectPage<'a> { pub fn new() -> Box> { - let mut page = Box::>::new_uninit(); + Self::try_new() + .unwrap_or_else(|_| alloc::alloc::handle_alloc_error(Layout::new::>())) + } + + /// Allocate a slab page without aborting when its backing allocation fails. + pub fn try_new() -> Result>, alloc::alloc::AllocError> { + let mut page = Box::>::try_new_uninit()?; unsafe { // The data area is intentionally uninitialized object storage. It // is wrapped in MaybeUninit so constructing ObjectPage is sound; @@ -297,7 +303,7 @@ impl<'a> ObjectPage<'a> { 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() + Ok(page.assume_init()) } } } diff --git a/kernel/crates/rust-slabmalloc/tests/fallible_page.rs b/kernel/crates/rust-slabmalloc/tests/fallible_page.rs new file mode 100644 index 0000000000..ba6a7cb5a6 --- /dev/null +++ b/kernel/crates/rust-slabmalloc/tests/fallible_page.rs @@ -0,0 +1,51 @@ +//! An isolated host allocator test: failure affects only the calling test thread. +use slabmalloc::ObjectPage; +use std::alloc::{GlobalAlloc, Layout, System}; +use std::cell::Cell; + +thread_local! { + static FAIL_NEXT_PAGE: Cell = const { Cell::new(false) }; +} + +struct PageFailureAllocator; + +unsafe impl GlobalAlloc for PageFailureAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let fail = layout == Layout::new::>() + && FAIL_NEXT_PAGE + .try_with(|armed| armed.replace(false)) + .unwrap_or(false); + if fail { + std::ptr::null_mut() + } else { + System.alloc(layout) + } + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + System.dealloc(ptr, layout); + } +} + +#[global_allocator] +static ALLOCATOR: PageFailureAllocator = PageFailureAllocator; + +#[test] +fn backing_page_failure_returns_error_and_can_retry() { + FAIL_NEXT_PAGE.with(|armed| armed.set(true)); + let failed_page = ObjectPage::try_new(); + let failure_consumed = FAIL_NEXT_PAGE.with(|armed| !armed.replace(false)); + assert!( + failure_consumed, + "the backing-page allocation was intercepted" + ); + assert!(failed_page.is_err(), "OOM must return instead of aborting"); + + let page = ObjectPage::try_new().expect("the next backing allocation succeeds"); + let addr = page.as_ref() as *const ObjectPage<'_> as usize; + assert_eq!(addr % std::mem::align_of::>(), 0); + drop(page); + + // Existing callers still use the same metadata initialization through new(). + drop(ObjectPage::new()); +} diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index 3059414bbf..b9c446b53e 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -412,11 +412,16 @@ impl KernelSemSet { } } - fn enqueue_waiter(&mut self, waiter: Arc) { - match Self::pending_queue_for(&waiter.sops) { - SemPendingQueue::Const => self.pending_const.push_back(waiter), - SemPendingQueue::Alter => self.pending_alter.push_back(waiter), - } + fn enqueue_waiter(&mut self, waiter: Arc) -> Result<(), SystemError> { + let queue = match Self::pending_queue_for(&waiter.sops) { + SemPendingQueue::Const => &mut self.pending_const, + SemPendingQueue::Alter => &mut self.pending_alter, + }; + // Preparation may fail, but publishing the waiter must not allocate. + // No semaphore values or undo adjustments have been committed here. + queue.try_reserve(1).map_err(|_| SystemError::ENOMEM)?; + queue.push_back(waiter); + Ok(()) } fn remove_waiter(&mut self, target: &Arc) { @@ -582,7 +587,7 @@ pub struct SemManager { /// Total semaphores in the namespace (Linux semmns accounting) total_sems: usize, #[allow(dead_code)] - undo_groups: Arc>>, + undo_groups: Vec>, } impl Default for SemManager { @@ -601,7 +606,7 @@ impl SemManager { id2sem: HashMap::new(), key2id: HashMap::new(), total_sems: 0, - undo_groups: Arc::new(Vec::new()), + undo_groups: Vec::new(), } } @@ -636,73 +641,24 @@ impl SemManager { Ok(self.get_by_semid_checked(semid)?.sems.len()) } - #[allow(dead_code)] - fn build_undo_registry_replacement( - snapshot: &Arc>>, - group: &Arc, - ) -> Result>>, SystemError> { - let mut live = Vec::new(); - live.try_reserve_exact(snapshot.len().saturating_add(1)) - .map_err(|_| SystemError::ENOMEM)?; - let mut contains_group = false; - - for weak in snapshot.iter() { - let Some(existing) = weak.upgrade() else { - continue; - }; - contains_group |= Arc::ptr_eq(&existing, group); - if !live - .iter() - .any(|item: &Weak| item.ptr_eq(weak)) - { - live.push(Arc::downgrade(&existing)); - } - } - - if !contains_group { - live.push(Arc::downgrade(group)); - } - - Arc::try_new(live).map_err(|_| SystemError::ENOMEM) - } - - #[allow(dead_code)] - pub(crate) fn prepare_undo_record_and_registry( - ipcns: &Arc, + /// Serialize registration with control operations using the manager lock. + /// A live group remains registered even when all of its debt is cleared. + fn ensure_undo_group_registered( + &mut self, group: &Arc, - semid: SemId, ) -> Result<(), SystemError> { - loop { - let (nsems, snapshot) = { - let guard = ipcns.sem.lock(); - ( - guard.validate_semid_nsems(semid)?, - guard.undo_groups.clone(), - ) - }; - - let replacement = Self::build_undo_registry_replacement(&snapshot, group)?; - let record = group.prepare_record(semid, nsems)?; - - let mut guard = ipcns.sem.lock(); - if !Arc::ptr_eq(&guard.undo_groups, &snapshot) { - drop(record); - continue; - } - - let current_nsems = guard.validate_semid_nsems(semid)?; - if current_nsems != nsems || record.adjustment_count() != current_nsems { - drop(record); - continue; - } - - guard.undo_groups = replacement; - match group.commit_prepared_record_noalloc(record) { - Ok(()) => return Ok(()), - Err(SystemError::EAGAIN_OR_EWOULDBLOCK) => continue, - Err(error) => return Err(error), - } + if group.registry_registered() { + return Ok(()); + } + if self.undo_groups.len() == self.undo_groups.capacity() { + self.compact_undo_registry(); } + self.undo_groups + .try_reserve(1) + .map_err(|_| SystemError::ENOMEM)?; + self.undo_groups.push(Arc::downgrade(group)); + group.mark_registry_registered(); + Ok(()) } fn with_undo_record_mut( @@ -714,9 +670,7 @@ impl SemManager { } fn compact_undo_registry(&mut self) { - if let Some(groups) = Arc::get_mut(&mut self.undo_groups) { - groups.retain(|weak| weak.strong_count() != 0); - } + self.undo_groups.retain(|weak| weak.strong_count() != 0); } pub(crate) fn clear_undo_for_setval(&mut self, semid: SemId, semnum: usize) { @@ -818,18 +772,8 @@ impl SemManager { semid: SemId, ) -> Result<(), SystemError> { let nsems = self.validate_semid_nsems(semid)?; - let snapshot = self.undo_groups.clone(); - let replacement = Self::build_undo_registry_replacement(&snapshot, group)?; let record = group.prepare_record(semid, nsems)?; - if !Arc::ptr_eq(&self.undo_groups, &snapshot) { - drop(record); - return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); - } - if self.validate_semid_nsems(semid)? != nsems { - drop(record); - return Err(SystemError::EINVAL); - } - self.undo_groups = replacement; + self.ensure_undo_group_registered(group)?; group.commit_prepared_record_noalloc(record) } @@ -1269,7 +1213,7 @@ impl SemManager { }; let entry = loop { - let (nsems, snapshot) = { + let nsems = { let guard = ipcns.sem.lock(); let set = guard.get_by_semid_checked(semid)?; // Match Linux: check semnum bounds (EFBIG) before permissions (EACCES). @@ -1285,11 +1229,10 @@ impl SemManager { }, &target_user_ns, )?; - (set.sems.len(), guard.undo_groups.clone()) + set.sems.len() }; let prepared_undo = if let Some(group) = undo_group.as_ref() { - let replacement = Self::build_undo_registry_replacement(&snapshot, group)?; let record = group.prepare_record(semid, nsems)?; let entry = Arc::try_new(SemQueueEntry::new_prepared( SemQueueEntry::prepare_sops(sops)?, @@ -1305,19 +1248,21 @@ impl SemManager { }, )) .map_err(|_| SystemError::ENOMEM)?; - Some((replacement, entry)) + Some(entry) } else { None }; let mut guard = ipcns.sem.lock(); - if !Arc::ptr_eq(&guard.undo_groups, &snapshot) { - continue; - } if guard.validate_semid_nsems(semid)? != nsems { continue; } - if let Some((replacement, prepared_entry)) = prepared_undo { + if let Some(prepared_entry) = prepared_undo { + guard.ensure_undo_group_registered( + undo_group + .as_ref() + .expect("SEM_UNDO operation has a current group"), + )?; let mut record_slot = prepared_entry.undo_record.lock_irqsave(); let prepared_record = record_slot .take() @@ -1326,7 +1271,6 @@ impl SemManager { *record_slot = Some(prepared_record); continue; } - guard.undo_groups = replacement; let set = guard.get_by_semid_checked_mut(semid)?; let (outcome, kept_record) = undo_group .as_ref() @@ -1367,7 +1311,7 @@ impl SemManager { } drop(record_slot); prepared_entry.update_blocker(blocker); - set.enqueue_waiter(prepared_entry.clone()); + set.enqueue_waiter(prepared_entry.clone())?; break prepared_entry; } } @@ -1390,7 +1334,7 @@ impl SemManager { .as_ref() .expect("plain queued semop entry is preallocated"); prepared_entry.update_blocker(blocker); - set.enqueue_waiter(prepared_entry.clone()); + set.enqueue_waiter(prepared_entry.clone())?; break prepared_entry.clone(); } } @@ -1682,7 +1626,7 @@ mod tests { ) -> Arc { let (_waiter, waker) = Waiter::new_pair(); let entry = Arc::new(SemQueueEntry::new(sops, None, waker, blocker)); - set.enqueue_waiter(entry.clone()); + set.enqueue_waiter(entry.clone()).unwrap(); entry } @@ -1785,7 +1729,7 @@ mod tests { waker, blocker, )); - set.enqueue_waiter(entry.clone()); + set.enqueue_waiter(entry.clone()).unwrap(); (semid, entry) }; let (pcb, group) = test_pcb_with_group(&ipc_ns); @@ -1952,7 +1896,7 @@ mod tests { }, )); let set = manager.get_by_semid_checked_mut(semid).unwrap(); - set.enqueue_waiter(entry.clone()); + set.enqueue_waiter(entry.clone()).unwrap(); set.sems[0].val = 1; manager.update_queue_for_test(semid); @@ -2028,7 +1972,9 @@ mod tests { let mut manager = ipc_ns.sem.lock(); let semid = insert_test_set(&mut manager, SemKey::new(51), &[1]); - manager.undo_groups = Arc::new(vec![stale_weak, Arc::downgrade(&live)]); + manager.undo_groups.push(stale_weak); + manager.ensure_undo_group_registered(&live).unwrap(); + manager.undo_groups.shrink_to_fit(); manager .prepare_undo_record_and_registry_for_test(&candidate, semid) @@ -2039,6 +1985,31 @@ mod tests { assert!(manager.undo_registry_contains_for_test(&candidate)); } + #[test] + fn live_group_registration_survives_debt_removal_without_duplicates() { + let ipc_ns = test_ipc_ns(); + let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let mut manager = ipc_ns.sem.lock(); + let semid = insert_test_set(&mut manager, SemKey::new(150), &[1]); + manager + .prepare_undo_record_and_registry_for_test(&group, semid) + .unwrap(); + let capacity = manager.undo_groups.capacity(); + manager.clear_undo_for_setall(semid); + manager.discard_undo_for_rmid(semid); + assert_eq!(group.record_count_for_test(), 0); + for _ in 0..32 { + manager.ensure_undo_group_registered(&group).unwrap(); + } + assert!(group.registry_registered()); + assert_eq!(manager.undo_groups.len(), 1); + assert_eq!(manager.undo_groups.capacity(), capacity); + manager + .prepare_undo_record_and_registry_for_test(&group, semid) + .unwrap(); + assert_eq!(manager.undo_groups.len(), 1); + } + #[test] fn queued_undo_entry_retains_group_after_external_owner_drops() { let mut manager = SemManager::new(); @@ -2152,7 +2123,8 @@ mod tests { group_a.insert_test_record(semid, &[11, 12]); group_b.insert_test_record(semid, &[21, 22]); group_a.insert_test_record(other_semid, &[31, 32]); - manager.undo_groups = Arc::new(vec![Arc::downgrade(&group_a), Arc::downgrade(&group_b)]); + manager.ensure_undo_group_registered(&group_a).unwrap(); + manager.ensure_undo_group_registered(&group_b).unwrap(); manager.setval(semid, 0, 9).unwrap(); @@ -2174,7 +2146,8 @@ mod tests { group_a.insert_test_record(semid, &[1, 2, 3]); group_b.insert_test_record(semid, &[4, 5, 6]); group_b.insert_test_record(other_semid, &[7, 8, 9]); - manager.undo_groups = Arc::new(vec![Arc::downgrade(&group_a), Arc::downgrade(&group_b)]); + manager.ensure_undo_group_registered(&group_a).unwrap(); + manager.ensure_undo_group_registered(&group_b).unwrap(); let token = SemSetAllToken::new(semid, 3); manager.setall(token, &[10, 11, 12]).unwrap(); @@ -2195,7 +2168,7 @@ mod tests { let semid = insert_test_set(&mut manager, SemKey::new(58), &[0]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(semid, &[7]); - manager.undo_groups = Arc::new(vec![Arc::downgrade(&group)]); + manager.ensure_undo_group_registered(&group).unwrap(); let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); manager.setval(semid, 0, 1).unwrap(); @@ -2213,7 +2186,7 @@ mod tests { let filler_semid = insert_test_set(&mut manager, SemKey::new(60), &[4]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(old_semid, &[9]); - manager.undo_groups = Arc::new(vec![Arc::downgrade(&group)]); + manager.ensure_undo_group_registered(&group).unwrap(); manager.ipc_rmid(old_semid).unwrap(); let new_semid = insert_test_set(&mut manager, SemKey::new(61), &[5]); @@ -2234,7 +2207,7 @@ mod tests { let semid = insert_test_set(&mut manager, SemKey::new(62), &[4]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(semid, &[7]); - manager.undo_groups = Arc::new(vec![Arc::downgrade(&group)]); + manager.ensure_undo_group_registered(&group).unwrap(); let record = group.prepare_record_for_test(semid, 1).unwrap(); manager.clear_undo_for_setval(semid, 0); @@ -2261,7 +2234,7 @@ mod tests { let semid = insert_test_set(&mut manager, SemKey::new(63), &[4, 5]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(semid, &[7, -3]); - manager.undo_groups = Arc::new(vec![Arc::downgrade(&group)]); + manager.ensure_undo_group_registered(&group).unwrap(); let record = group.prepare_record_for_test(semid, 2).unwrap(); manager.clear_undo_for_setall(semid); @@ -2289,7 +2262,7 @@ mod tests { let semid = insert_test_set(&mut manager, SemKey::new(65), &[4]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(semid, &[7]); - manager.undo_groups = Arc::new(vec![Arc::downgrade(&group)]); + manager.ensure_undo_group_registered(&group).unwrap(); let record = group.prepare_record_for_test(semid, 1).unwrap(); manager.clear_undo_for_setval(semid, 0); @@ -2316,7 +2289,7 @@ mod tests { let semid = insert_test_set(&mut manager, SemKey::new(66), &[0]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(semid, &[7]); - manager.undo_groups = Arc::new(vec![Arc::downgrade(&group)]); + manager.ensure_undo_group_registered(&group).unwrap(); let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); manager.clear_undo_for_setval(semid, 0); @@ -2370,7 +2343,8 @@ mod tests { manager .get_by_semid_checked_mut(semid) .unwrap() - .enqueue_waiter(entry.clone()); + .enqueue_waiter(entry.clone()) + .unwrap(); entry } diff --git a/kernel/src/ipc/sem_undo.rs b/kernel/src/ipc/sem_undo.rs index b94dd887a6..cf78ceb439 100644 --- a/kernel/src/ipc/sem_undo.rs +++ b/kernel/src/ipc/sem_undo.rs @@ -27,6 +27,9 @@ pub struct SemUndoGroup { #[derive(Debug)] struct SemUndoGroupState { task_owners: usize, + /// Published once in the bound IPC namespace's manager registry. Clearing + /// records does not unregister a live group; only dead Weak entries expire. + registry_registered: bool, records: Vec, reserved_records: usize, absence_generation: u64, @@ -235,6 +238,7 @@ impl SemUndoGroup { ipc_ns: Arc::downgrade(ipc_ns), inner: SpinLock::new(SemUndoGroupState { task_owners: 1, + registry_registered: false, records: Vec::new(), reserved_records: 0, absence_generation: 0, @@ -247,6 +251,16 @@ impl SemUndoGroup { .map_err(|_| SystemError::ENOMEM) } + /// The bound namespace's manager lock serializes registration callers. + pub(crate) fn registry_registered(&self) -> bool { + self.inner.lock_irqsave().registry_registered + } + + /// Called only after the manager has successfully inserted its Weak entry. + pub(crate) fn mark_registry_registered(&self) { + self.inner.lock_irqsave().registry_registered = true; + } + pub(crate) fn verify_ipc_ns(&self, ipc_ns: &Arc) -> Result<(), SystemError> { let state = self.inner.lock_irqsave(); if state.task_owners == 0 || state.retired { diff --git a/kernel/src/ipc/syscall/sys_semctl.rs b/kernel/src/ipc/syscall/sys_semctl.rs index 12a07b83aa..2c17f37375 100644 --- a/kernel/src/ipc/syscall/sys_semctl.rs +++ b/kernel/src/ipc/syscall/sys_semctl.rs @@ -139,17 +139,17 @@ pub(super) fn do_kernel_semctl( impl SysSemctlHandle { #[inline(always)] fn semid(args: &[usize]) -> SemId { - SemId::new(args[0]) + SemId::new(args[0] as u32 as usize) } #[inline(always)] fn semnum(args: &[usize]) -> usize { - args[1] + args[1] as u32 as usize } #[inline(always)] fn cmd(args: &[usize]) -> SemCtlCmd { - SemCtlCmd::from(args[2]) + SemCtlCmd::from(args[2] as u32 as usize) } #[inline(always)] @@ -164,7 +164,7 @@ impl Syscall for SysSemctlHandle { } fn handle(&self, args: &[usize], frame: &mut TrapFrame) -> Result { - if args[0] > i32::MAX as usize || args[2] > i32::MAX as usize { + if (args[0] as i32) < 0 { return Err(SystemError::EINVAL); } let semid = Self::semid(args); diff --git a/kernel/src/ipc/syscall/sys_semget.rs b/kernel/src/ipc/syscall/sys_semget.rs index 1bcc920fa9..d0cadbeac8 100644 --- a/kernel/src/ipc/syscall/sys_semget.rs +++ b/kernel/src/ipc/syscall/sys_semget.rs @@ -31,11 +31,14 @@ impl Syscall for SysSemgetHandle { fn handle(&self, args: &[usize], _frame: &mut TrapFrame) -> Result { let key = SemKey::new(args[0] as u32 as usize); - let nsems = args[1]; + let nsems = args[1] as i32; + if nsems < 0 { + return Err(SystemError::EINVAL); + } let semflg = SemFlags::from_bits_truncate(args[2] as u32); let ipcns = ProcessManager::current_ipcns(); let mut guard = ipcns.sem.lock(); - guard.semget(key, nsems, semflg) + guard.semget(key, nsems as usize, semflg) } fn entry_format(&self, args: &[usize]) -> Vec { diff --git a/kernel/src/ipc/syscall/sys_semop.rs b/kernel/src/ipc/syscall/sys_semop.rs index e853141959..09d90132de 100644 --- a/kernel/src/ipc/syscall/sys_semop.rs +++ b/kernel/src/ipc/syscall/sys_semop.rs @@ -1,16 +1,11 @@ use crate::alloc::vec::Vec; use crate::arch::interrupt::TrapFrame; use crate::syscall::table::FormattedSyscallParam; -use crate::{ - arch::syscall::nr::SYS_SEMOP, - ipc::sem::{PosixSemBuf, SemId}, - syscall::table::Syscall, - syscall::user_access::UserBufferReader, -}; +use crate::{arch::syscall::nr::SYS_SEMOP, syscall::table::Syscall}; use syscall_table_macros::declare_syscall; use system_error::SystemError; -use super::sys_semtimedop::do_kernel_semtimedop; +use super::sys_semtimedop::do_user_semtimedop; pub struct SysSemopHandle; @@ -34,35 +29,13 @@ impl Syscall for SysSemopHandle { } fn handle(&self, args: &[usize], frame: &mut TrapFrame) -> Result { - let semid = SemId::new(args[0]); - let nsops = args[2]; - let from_user = frame.is_from_user(); - - if nsops == 0 { - return Err(SystemError::EINVAL); - } - if nsops > crate::ipc::sem::SEMOPM { - return Err(SystemError::E2BIG); - } - - let mut sops: Vec = vec![ - PosixSemBuf { - sem_num: 0, - sem_op: 0, - sem_flg: 0, - }; - nsops - ]; - let sops_reader = UserBufferReader::new( + do_user_semtimedop( + args[0] as i32, args[1] as *const u8, - core::mem::size_of::() * nsops, - from_user, - )?; - for (i, op) in sops.iter_mut().enumerate() { - sops_reader.copy_one_from_user(op, i * core::mem::size_of::())?; - } - - do_kernel_semtimedop(semid, &sops, None) + args[2] as u32 as usize, + None, + frame.is_from_user(), + ) } fn entry_format(&self, args: &[usize]) -> Vec { diff --git a/kernel/src/ipc/syscall/sys_semtimedop.rs b/kernel/src/ipc/syscall/sys_semtimedop.rs index 6b4bfa85d9..2a4090ad4c 100644 --- a/kernel/src/ipc/syscall/sys_semtimedop.rs +++ b/kernel/src/ipc/syscall/sys_semtimedop.rs @@ -53,10 +53,50 @@ pub(super) fn do_kernel_semtimedop( SemManager::semtimedop(&ipcns, semid, sops, timeout) } +// Match Linux do_semtimedop: validate count and copy sops before rejecting +// a negative semid or validating the already-copied timeout value. +pub(super) fn do_user_semtimedop( + semid: i32, + sops_ptr: *const u8, + nsops: usize, + timeout: Option, + from_user: bool, +) -> Result { + if nsops > crate::ipc::sem::SEMOPM { + return Err(SystemError::E2BIG); + } + if nsops == 0 { + return Err(SystemError::EINVAL); + } + let mut sops = Vec::new(); + sops.try_reserve_exact(nsops) + .map_err(|_| SystemError::ENOMEM)?; + sops.resize( + nsops, + PosixSemBuf { + sem_num: 0, + sem_op: 0, + sem_flg: 0, + }, + ); + let reader = UserBufferReader::new( + sops_ptr, + core::mem::size_of::() * nsops, + from_user, + )?; + for (i, op) in sops.iter_mut().enumerate() { + reader.copy_one_from_user(op, i * core::mem::size_of::())?; + } + if semid < 0 { + return Err(SystemError::EINVAL); + } + do_kernel_semtimedop(SemId::new(semid as usize), &sops, timeout) +} + impl SysSemtimedopHandle { #[inline(always)] - fn semid(args: &[usize]) -> SemId { - SemId::new(args[0]) + fn semid(args: &[usize]) -> i32 { + args[0] as i32 } #[inline(always)] @@ -66,7 +106,7 @@ impl SysSemtimedopHandle { #[inline(always)] fn nsops(args: &[usize]) -> usize { - args[2] + args[2] as u32 as usize } #[inline(always)] @@ -100,37 +140,12 @@ impl Syscall for SysSemtimedopHandle { Some(ts) }; - // Match Linux: after copying a non-null timeout, validate nsops before allocating sops. - if nsops == 0 { - return Err(SystemError::EINVAL); - } - if nsops > crate::ipc::sem::SEMOPM { - return Err(SystemError::E2BIG); - } - - let mut sops: Vec = vec![ - PosixSemBuf { - sem_num: 0, - sem_op: 0, - sem_flg: 0, - }; - nsops - ]; - let sops_reader = UserBufferReader::new( - sops_ptr, - core::mem::size_of::() * nsops, - from_user, - )?; - for (i, op) in sops.iter_mut().enumerate() { - sops_reader.copy_one_from_user(op, i * core::mem::size_of::())?; - } - - do_kernel_semtimedop(semid, &sops, timeout) + do_user_semtimedop(semid, sops_ptr, nsops, timeout, from_user) } fn entry_format(&self, args: &[usize]) -> Vec { vec![ - FormattedSyscallParam::new("semid", format!("{}", Self::semid(args).data())), + FormattedSyscallParam::new("semid", format!("{}", Self::semid(args))), FormattedSyscallParam::new("sops", format!("{:#x}", Self::sops(args) as usize)), FormattedSyscallParam::new("nsops", format!("{}", Self::nsops(args))), FormattedSyscallParam::new("timeout", format!("{:#x}", Self::timeout(args) as usize)), diff --git a/kernel/src/mm/allocator/slab.rs b/kernel/src/mm/allocator/slab.rs index 49b408f876..61f7b37cf2 100644 --- a/kernel/src/mm/allocator/slab.rs +++ b/kernel/src/mm/allocator/slab.rs @@ -35,7 +35,10 @@ impl SlabAllocator { match self.zone.allocate(layout) { Ok(nptr) => nptr.as_ptr(), Err(AllocationError::OutOfMemory) => { - let boxed_page = ObjectPage::new(); + let Ok(boxed_page) = ObjectPage::try_new() else { + // Propagate backing-page OOM to fallible allocation callers. + return core::ptr::null_mut(); + }; assert_eq!( (boxed_page.as_ref() as *const ObjectPage as usize) & (MMArch::PAGE_SIZE - 1), 0 diff --git a/kernel/src/process/namespace/setns.rs b/kernel/src/process/namespace/setns.rs index 6ef59f5356..09fe49add7 100644 --- a/kernel/src/process/namespace/setns.rs +++ b/kernel/src/process/namespace/setns.rs @@ -121,6 +121,15 @@ fn nsfd_target_userns( /// - user namespace 当前仅支持通过 `/proc//ns/user` 这类 namespace fd 进入 #[inline(never)] pub fn ksys_setns(fd: i32, nstype: i32) -> Result<(), SystemError> { + // Linux resolves the fd before validating flags. Keep the file alive, but + // release the fd table lock before namespace preparation and permission checks. + let current = ProcessManager::current_pcb(); + let fd_table = current.fd_table(); + let file = fd_table + .read() + .get_file_by_fd(fd) + .ok_or(SystemError::EBADF)?; + // 1. 解析并校验 flag let flags = CloneFlags::from_bits(nstype as u64).ok_or(SystemError::EINVAL)?; @@ -139,14 +148,6 @@ pub fn ksys_setns(fd: i32, nstype: i32) -> Result<(), SystemError> { return Err(SystemError::EINVAL); } - // 2. 解析 fd,当前仅支持 pidfd - let current = ProcessManager::current_pcb(); - let fd_table = current.fd_table(); - let file = fd_table - .read() - .get_file_by_fd(fd) - .ok_or(SystemError::EBADF)?; - // 3. 根据 fd 类型决定 setns 模式:namespace fd / pidfd let ns_fd = { let pdata = file.private_data.lock(); diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index ef02d7cd76..84665ea46d 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #ifndef SYS_semget #define SYS_semget 64 @@ -1510,9 +1512,11 @@ TEST(SysVSem, SemUndoUnshareSysvsem) { int phase[2]; int supervisor_release[2]; int old_owner_release[2]; + int old_owner_ready[2]; ASSERT_EQ(0, pipe(phase)); ASSERT_EQ(0, pipe(supervisor_release)); ASSERT_EQ(0, pipe(old_owner_release)); + ASSERT_EQ(0, pipe(old_owner_ready)); pid_t supervisor = fork(); ASSERT_GE(supervisor, 0); ChildGuard supervisor_guard(supervisor); @@ -1524,10 +1528,13 @@ TEST(SysVSem, SemUndoUnshareSysvsem) { _exit(90); } alignas(16) char stack[16384]; - CloneUndoArgs args = {sem.id(), -1, old_owner_release[0]}; + CloneUndoArgs args = {sem.id(), old_owner_ready[1], old_owner_release[0]}; pid_t old_owner = clone(CloneSysvsemUndoChild, stack + sizeof(stack), CLONE_SYSVSEM | SIGCHLD, &args); - if (old_owner < 0 || unshare(CLONE_SYSVSEM) != 0 || + close(old_owner_ready[1]); + char ready; + if (old_owner < 0 || !ReadExact(old_owner_ready[0], &ready, sizeof(ready)) || + unshare(CLONE_SYSVSEM) != 0 || !SemUndoOpMustSucceed(sem.id(), 0, 1)) { _exit(91); } @@ -1552,6 +1559,8 @@ TEST(SysVSem, SemUndoUnshareSysvsem) { phase_write.Close(); supervisor_read.Close(); old_owner_read.Close(); + close(old_owner_ready[0]); + close(old_owner_ready[1]); char token; ASSERT_TRUE(ReadExact(phase_read.get(), &token, sizeof(token))); EXPECT_EQ(3, SemCtl(sem.id(), 0, GETVAL, 0)) @@ -1608,6 +1617,58 @@ TEST(SysVSem, SemtimedopCopiesNonNullTimeoutBeforeNsopsValidation) { << "non-null timeout must be copied before nsops == 0 is rejected"; } +TEST(SysVSem, RawSyscallIntArgumentsUseLow32Bits) { + static_assert(sizeof(unsigned long) == 8, "64-bit syscall ABI test"); + const unsigned long high = 1UL << 32; + int id = static_cast(syscall(SYS_semget, IPC_PRIVATE, high | 1, IPC_CREAT | 0600)); + ASSERT_GE(id, 0); + SemSet sem(id, true); + struct sembuf zero = {0, 0, 0}; + struct timespec timeout = {0, 0}; + EXPECT_EQ(0, syscall(SYS_semop, high | id, &zero, high | 1)); + EXPECT_EQ(0, syscall(SYS_semtimedop, high | id, &zero, high | 1, &timeout)); + EXPECT_EQ(0, syscall(SYS_semctl, high | id, high, high | GETVAL, 0UL)); + EXPECT_EQ(0, syscall(SYS_semctl, high | id, high, high | SETVAL, high | 7)); + EXPECT_EQ(7, SemCtl(id, 0, GETVAL, 0)); + unsigned short value = 0; + EXPECT_EQ(0, syscall(SYS_semctl, high | id, ~0UL, high | GETALL, &value)); + EXPECT_EQ(7, value) << "GETALL ignores even negative semnum and preserves pointer width"; + struct seminfo info = {}; + EXPECT_GE(syscall(SYS_semctl, 0UL, ~0UL, high | IPC_INFO, &info), 0); + errno = 0; + EXPECT_EQ(-1, syscall(SYS_semctl, 0xffffffffUL, 0UL, IPC_INFO, &info)); + EXPECT_EQ(EINVAL, errno); + errno = 0; + EXPECT_EQ(-1, syscall(SYS_semget, IPC_PRIVATE, high | 0xffffffffUL, IPC_CREAT | 0600)); + EXPECT_EQ(EINVAL, errno); +} + +TEST(SysVSem, RawSyscallCombinedErrorOrder) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + struct sembuf zero = {0, 0, 0}; + struct timespec invalid_value = {0, -1}; + auto bad_ops = reinterpret_cast(static_cast(1)); + const unsigned long negative_id = 0xffffffffUL; + errno = 0; + EXPECT_EQ(-1, syscall(SYS_semtimedop, sem.id(), &zero, 501UL, &invalid_value)); + EXPECT_EQ(E2BIG, errno); + errno = 0; + EXPECT_EQ(-1, syscall(SYS_semtimedop, sem.id(), bad_ops, 1UL, &invalid_value)); + EXPECT_EQ(EFAULT, errno); + for (long nr : {static_cast(SYS_semop), static_cast(SYS_semtimedop)}) { + errno = 0; + EXPECT_EQ(-1, syscall(nr, negative_id, &zero, 501UL, nullptr)); + EXPECT_EQ(E2BIG, errno); + errno = 0; + EXPECT_EQ(-1, syscall(nr, negative_id, bad_ops, 1UL, nullptr)); + EXPECT_EQ(EFAULT, errno); + errno = 0; + EXPECT_EQ(-1, syscall(nr, negative_id, &zero, 1UL, nullptr)); + EXPECT_EQ(EINVAL, errno); + } +} + TEST(SysVSem, SemUndoIpcNamespaceAndErrnos) { SemSet sem(1, IPC_CREAT | 0600); ASSERT_TRUE(sem.valid()); @@ -1624,25 +1685,28 @@ TEST(SysVSem, SemUndoIpcNamespaceAndErrnos) { EXPECT_EQ(-1, syscall(SYS_semop, sem.id(), &invalid_semnum, 501)); EXPECT_EQ(E2BIG, errno); - int report_pipe[2]; - ASSERT_EQ(0, pipe(report_pipe)); - pid_t child = fork(); - ASSERT_GE(child, 0); - ChildGuard invalid_child(child); - if (child == 0) { - close(report_pipe[0]); - _exit(RunNamespaceSetnsChild(sem.id(), -1, CLONE_NEWIPC | CLONE_SYSVSEM, report_pipe[1])); + for (bool valid_fd : {false, true}) { + int report_pipe[2]; + ASSERT_EQ(0, pipe(report_pipe)); + pid_t child = fork(); + ASSERT_GE(child, 0); + ChildGuard invalid_child(child); + if (child == 0) { + close(report_pipe[0]); + _exit(RunNamespaceSetnsChild(sem.id(), valid_fd ? report_pipe[1] : -1, + CLONE_NEWIPC | CLONE_SYSVSEM, report_pipe[1])); + } + FdGuard report_read(report_pipe[0]); + FdGuard report_write(report_pipe[1]); + report_write.Close(); + NamespaceUndoReport report = {}; + ASSERT_TRUE(ReadNamespaceReportAndReap(report_read.get(), &invalid_child, &report)); + EXPECT_EQ(-1, report.setns_result); + EXPECT_EQ(valid_fd ? EINVAL : EBADF, report.setns_errno); + EXPECT_EQ(1, report.value_before_exit); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)) + << "failed setns prepare must preserve old debt until child exit"; } - FdGuard report_read(report_pipe[0]); - FdGuard report_write(report_pipe[1]); - report_write.Close(); - NamespaceUndoReport report = {}; - ASSERT_TRUE(ReadNamespaceReportAndReap(report_read.get(), &invalid_child, &report)); - EXPECT_EQ(-1, report.setns_result); - EXPECT_EQ(EINVAL, report.setns_errno); - EXPECT_EQ(1, report.value_before_exit); - EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)) - << "failed setns prepare must preserve old debt until child exit"; } TEST(SysVSem, SemUndoNamespaceFdDetachesAttachment) { @@ -2088,6 +2152,99 @@ TEST(SysVSem, RemoveWhileWaiting) { // ============ concurrency ============ +TEST(SysVSem, WaitQueuesGrowForConstAndAlterOperations) { + constexpr int kWaiters = 32; + for (bool constant : {true, false}) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, constant ? 1 : 0)); + std::vector> children; + for (int i = 0; i < kWaiters; ++i) { + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + struct sembuf op = {0, static_cast(constant ? 0 : -1), 0}; + _exit(SemOp(sem.id(), &op, 1) == 0 ? 0 : 111); + } + children.emplace_back(new ChildGuard(pid)); + } + ASSERT_TRUE(constant ? WaitForZcnt(sem.id(), 0, kWaiters) + : WaitForNcnt(sem.id(), 0, kWaiters)); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, constant ? 0 : kWaiters)); + for (auto& child : children) { + WaitChildOk(child.get()); + } + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); + EXPECT_EQ(0, SemCtl(sem.id(), 0, constant ? GETZCNT : GETNCNT, 0)); + } +} + +TEST(SysVSem, IndependentUndoGroupsSteadyStateAndSetall) { + constexpr int kWorkers = 32; + std::vector> sets; + std::vector> children; + std::vector> ready_reads; + std::vector> release_writes; + for (int i = 0; i < kWorkers; ++i) { + sets.emplace_back(new SemSet(1, IPC_CREAT | 0600)); + ASSERT_TRUE(sets.back()->valid()); + int ready[2], release[2]; + ASSERT_EQ(0, pipe(ready)); + ASSERT_EQ(0, pipe(release)); + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + close(ready[0]); + close(release[1]); + const int id = sets.back()->id(); + char token = 1; + // Register every group before the parent starts the steady-state phase. + if (!SemUndoOpMustSucceed(id, 0, 1) || + !WriteExact(ready[1], &token, 1) || !ReadExact(release[0], &token, 1)) { + _exit(112); + } + for (int j = 0; j < 100; ++j) { + if (!SemUndoOpMustSucceed(id, 0, 1) || !SemUndoOpMustSucceed(id, 0, -1)) { + _exit(113); + } + } + if (!WriteExact(ready[1], &token, 1) || !ReadExact(release[0], &token, 1)) { + _exit(114); + } + _exit(0); + } + children.emplace_back(new ChildGuard(pid)); + close(ready[1]); + close(release[0]); + ready_reads.emplace_back(new FdGuard(ready[0])); + release_writes.emplace_back(new FdGuard(release[1])); + } + char token = 1; + for (auto& ready : ready_reads) { + ASSERT_TRUE(ReadExact(ready->get(), &token, 1)); + } + for (auto& release : release_writes) { + ASSERT_TRUE(WriteExact(release->get(), &token, 1)); + } + for (auto& ready : ready_reads) { + ASSERT_TRUE(ReadExact(ready->get(), &token, 1)); + } + for (auto& sem : sets) { + EXPECT_EQ(1, SemCtl(sem->id(), 0, GETVAL, 0)); + unsigned short value = 9; + ASSERT_EQ(0, SemCtl(sem->id(), 0, SETALL, reinterpret_cast(&value))); + } + for (auto& release : release_writes) { + ASSERT_TRUE(WriteExact(release->get(), &token, 1)); + } + for (auto& child : children) { + WaitChildOk(child.get()); + } + for (auto& sem : sets) { + EXPECT_EQ(9, SemCtl(sem->id(), 0, GETVAL, 0)) << "SETALL clears each group's exit debt"; + } +} + TEST(SysVSem, ConcurrentWorkers) { constexpr int kWorkers = 8; SemSet sem(1, IPC_CREAT | 0600); From 27c67d5284bc8b5b2f8110213a1694a327836395 Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 15:00:05 +0000 Subject: [PATCH 14/34] test(fuse): handle background writeback in direct-drain assertions Validate complete cached-page coverage before direct writes without assuming a fixed writeback partition. Cover tail-first writeback deterministically and close open files on failure to avoid contaminating later mount-isolation tests. Signed-off-by: longjin --- .../dunitest/suites/fuse/fuse_extended.cc | 127 ++++++++++++------ 1 file changed, 86 insertions(+), 41 deletions(-) diff --git a/user/apps/tests/dunitest/suites/fuse/fuse_extended.cc b/user/apps/tests/dunitest/suites/fuse/fuse_extended.cc index 4524b2c27d..810c7c68ff 100644 --- a/user/apps/tests/dunitest/suites/fuse/fuse_extended.cc +++ b/user/apps/tests/dunitest/suites/fuse/fuse_extended.cc @@ -576,7 +576,36 @@ static void fill_user_xattr_name(char *buf, size_t len) { buf[len] = '\0'; } -static int ext_test_p2_ops() { +// Background writeback may start at any dirty page. Check coverage and the +// cache-before-direct ordering, not a particular partition of cached writes. +static bool direct_drain_trace_valid(uint32_t count, const volatile uint64_t *offsets, + const volatile uint32_t *sizes, + const volatile uint32_t *flags, size_t length) { + if (length != 3 * 4096 + 17 || count < 4 || count > 6) + return false; + unsigned covered = 0; + const uint32_t cached_count = count - 2; + for (uint32_t i = 0; i < cached_count; ++i) { + const uint64_t offset = offsets[i]; + const uint32_t size = sizes[i]; + if (flags[i] != FUSE_WRITE_CACHE || offset >= length || offset % 4096 != 0 || + size == 0 || size > 8192 || size > length - offset || + (size % 4096 != 0 && offset + size != length)) + return false; + for (size_t page = offset / 4096; page <= (offset + size - 1) / 4096; ++page) { + if (covered & (1u << page)) + return false; + covered |= 1u << page; + } + } + return covered == 0xf && offsets[cached_count] == 0 && + sizes[cached_count] == 8192 && flags[cached_count] == FUSE_WRITE_LOCKOWNER && + offsets[cached_count + 1] == 8192 && + sizes[cached_count + 1] == length - 8192 && + flags[cached_count + 1] == FUSE_WRITE_LOCKOWNER; +} + +static int ext_test_p2_ops(bool preflush_direct_tail = false) { const char *mp = "/tmp/test_fuse_p2_ops"; int f = -1; int dfd = -1; @@ -632,9 +661,9 @@ static int ext_test_p2_ops() { volatile uint32_t last_write_flags_at_fsync = 0; volatile unsigned char extension_write_byte = 0; volatile uint64_t large_write_nodeid = 0; - volatile uint64_t write_offsets[4] = {0}; - volatile uint32_t write_sizes[4] = {0}; - volatile uint32_t write_flags[4] = {0}; + volatile uint64_t write_offsets[6] = {0}; + volatile uint32_t write_sizes[6] = {0}; + volatile uint32_t write_flags[6] = {0}; volatile int forced_write_errno = 0; volatile uint64_t forced_write_offset = UINT64_MAX; volatile uint64_t forced_short_write_offset = UINT64_MAX; @@ -669,7 +698,7 @@ static int ext_test_p2_ops() { args.write_offsets = write_offsets; args.write_sizes = write_sizes; args.write_flags = write_flags; - args.write_trace_capacity = 4; + args.write_trace_capacity = 6; args.forced_write_errno = &forced_write_errno; args.forced_write_offset = &forced_write_offset; args.forced_short_write_offset = &forced_short_write_offset; @@ -728,7 +757,6 @@ static int ext_test_p2_ops() { } if (fuseg_write_all_fd(f, "p2-data") != 0) { printf("[FAIL] write created file: %s (errno=%d)\n", strerror(errno), errno); - close(f); goto fail; } // LINK returns an attribute snapshot for the same inode. With @@ -737,25 +765,21 @@ static int ext_test_p2_ops() { snprintf(hard_path, sizeof(hard_path), "%s/p2_hard.txt", mp); if (link(created, hard_path) != 0) { printf("[FAIL] link dirty writeback file: %s (errno=%d)\n", strerror(errno), errno); - close(f); goto fail; } if (write_count != 0) { printf("[FAIL] writeback-cache write reached daemon before fsync: writes=%u\n", write_count); - close(f); goto fail; } if (fsync(f) != 0) { printf("[FAIL] fsync(file): %s (errno=%d)\n", strerror(errno), errno); - close(f); goto fail; } if (write_count_at_fsync == 0 || last_write_size != strlen("p2-data") || (last_write_flags_at_fsync & FUSE_WRITE_CACHE) == 0) { printf("[FAIL] fsync did not drain full cached write first: writes=%u size=%u flags=0x%x\n", write_count_at_fsync, last_write_size, last_write_flags_at_fsync); - close(f); goto fail; } @@ -767,22 +791,20 @@ static int ext_test_p2_ops() { write_count_at_fsync = 0; if (pwrite(f, &extension, 1, 200) != 1) { printf("[FAIL] extend dirty writeback file: %s (errno=%d)\n", strerror(errno), errno); - close(f); goto fail; } if (fsync(f) != 0) { printf("[FAIL] fsync(extended file): %s (errno=%d)\n", strerror(errno), errno); - close(f); goto fail; } if (write_count_at_fsync == 0 || last_write_size != 201 || extension_write_byte != (unsigned char)extension) { printf("[FAIL] fsync truncated extended cached page: writes=%u size=%u byte=%u\n", write_count_at_fsync, last_write_size, extension_write_byte); - close(f); goto fail; } close(f); + f = -1; // P3: four locally dirty pages (the last one partial) must be submitted in // max_write-sized batches rather than one request per page. @@ -799,8 +821,6 @@ static int ext_test_p2_ops() { if (f < 0 || write(f, batch_data, batch_size) != (ssize_t)batch_size) { printf("[FAIL] create batched write file: %s (errno=%d)\n", strerror(errno), errno); free(batch_data); - if (f >= 0) - close(f); goto fail; } large_write_nodeid = last_create_nodeid; @@ -811,7 +831,6 @@ static int ext_test_p2_ops() { if (fsync(f) != 0) { printf("[FAIL] fsync batched write: %s (errno=%d)\n", strerror(errno), errno); free(batch_data); - close(f); goto fail; } if (write_count != 2 || write_offsets[0] != 0 || write_sizes[0] != 8192 || @@ -825,10 +844,10 @@ static int ext_test_p2_ops() { write_flags[0], (unsigned long long)write_offsets[1], write_sizes[1], write_flags[1], (unsigned long long)large_write_nodeid, batch_size); free(batch_data); - close(f); goto fail; } close(f); + f = -1; // Exercise the asynchronous WRITE|WAIT_AFTER path. Failure of the second // max_write batch must be reported, leave only that failed batch dirty, @@ -840,8 +859,6 @@ static int ext_test_p2_ops() { if (f < 0 || write(f, batch_data, batch_size) != (ssize_t)batch_size) { printf("[FAIL] create async error file: %s (errno=%d)\n", strerror(errno), errno); free(batch_data); - if (f >= 0) - close(f); goto fail; } large_write_nodeid = last_create_nodeid; @@ -862,7 +879,6 @@ static int ext_test_p2_ops() { (unsigned long long)write_offsets[1], write_sizes[1]); forced_write_errno = 0; free(batch_data); - close(f); goto fail; } forced_write_errno = 0; @@ -876,7 +892,6 @@ static int ext_test_p2_ops() { printf("[FAIL] async EIO retry count=%u write=(%llu,%u) errno=%d\n", write_count, (unsigned long long)write_offsets[0], write_sizes[0], errno); free(batch_data); - close(f); goto fail; } @@ -885,7 +900,6 @@ static int ext_test_p2_ops() { if (pwrite(f, batch_data, batch_size, 0) != (ssize_t)batch_size) { printf("[FAIL] redirty async short-write file: %s (errno=%d)\n", strerror(errno), errno); free(batch_data); - close(f); goto fail; } write_count = 0; @@ -898,7 +912,6 @@ static int ext_test_p2_ops() { printf("[FAIL] async short reply errno/count=%d/%u\n", errno, write_count); forced_short_write_offset = UINT64_MAX; free(batch_data); - close(f); goto fail; } forced_short_write_offset = UINT64_MAX; @@ -911,7 +924,6 @@ static int ext_test_p2_ops() { printf("[FAIL] async short retry count=%u write=(%llu,%u) errno=%d\n", write_count, (unsigned long long)write_offsets[0], write_sizes[0], errno); free(batch_data); - close(f); goto fail; } close(f); @@ -919,12 +931,13 @@ static int ext_test_p2_ops() { // A direct write must drain overlapping cached dirty pages through the // same stable-size batch path before issuing direct FUSE_WRITE requests. - // With max_write=8192, both phases split as 8192 + 4113 and direct writes - // must not carry FUSE_WRITE_CACHE. Direct writes do carry the current + // Cached batches may be split by background writeback. Direct writes + // split as 8192 + 4113 and must not carry FUSE_WRITE_CACHE, but do carry the current // file lock owner, matching the ordinary FUSE direct-I/O path. memset(batch_backend, 0, sizeof(batch_backend)); large_write_nodeid = 0; snprintf(direct_path, sizeof(direct_path), "%s/p3_direct_drain.txt", mp); + write_count = 0; f = open(direct_path, O_CREAT | O_RDWR, 0644); if (f < 0 || write(f, batch_data, batch_size) != (ssize_t)batch_size) { printf("[FAIL] create dirty direct-drain file: %s (errno=%d)\n", strerror(errno), @@ -933,7 +946,15 @@ static int ext_test_p2_ops() { goto fail; } large_write_nodeid = last_create_nodeid; - write_count = 0; + // Deterministically exercise the tail-first writeback seen in CI. Keep + // these requests in the trace: resetting after write loses early I/O. + if (preflush_direct_tail && + syscall(__NR_sync_file_range, f, 12288, 17, + SYNC_FILE_RANGE_WRITE | SYNC_FILE_RANGE_WAIT_AFTER) != 0) { + printf("[FAIL] preflush direct-drain tail: %s (errno=%d)\n", strerror(errno), errno); + free(batch_data); + goto fail; + } dynamic_open_out_flags = FOPEN_DIRECT_IO; direct_fd = open(direct_path, O_WRONLY); if (direct_fd < 0 || pwrite(direct_fd, batch_data, batch_size, 0) != (ssize_t)batch_size) { @@ -946,13 +967,8 @@ static int ext_test_p2_ops() { dynamic_open_out_flags = 0; close(direct_fd); direct_fd = -1; - if (write_count != 4 || write_offsets[0] != 0 || write_sizes[0] != 8192 || - write_flags[0] != FUSE_WRITE_CACHE || write_offsets[1] != 8192 || - write_sizes[1] != batch_size - 8192 || write_flags[1] != FUSE_WRITE_CACHE || - write_offsets[2] != 0 || write_sizes[2] != 8192 || - write_flags[2] != FUSE_WRITE_LOCKOWNER || - write_offsets[3] != 8192 || write_sizes[3] != batch_size - 8192 || - write_flags[3] != FUSE_WRITE_LOCKOWNER || + if (!direct_drain_trace_valid(write_count, write_offsets, write_sizes, write_flags, + batch_size) || memcmp(batch_backend, batch_data, batch_size) != 0) { printf("[FAIL] direct drain trace count=%u cached=(%llu,%u,0x%x),(%llu,%u,0x%x) direct=(%llu,%u,0x%x),(%llu,%u,0x%x)\n", write_count, (unsigned long long)write_offsets[0], write_sizes[0], @@ -979,13 +995,11 @@ static int ext_test_p2_ops() { } if (pwrite(f, &sparse_marker, 1, sparse_offset) != 1) { printf("[FAIL] sparse cached extension: %s (errno=%d)\n", strerror(errno), errno); - close(f); goto fail; } sparse_contents = (unsigned char *)malloc(sparse_size); if (!sparse_contents) { printf("[FAIL] allocate sparse read buffer\n"); - close(f); goto fail; } memset(sparse_contents, 0xff, sparse_size); @@ -993,14 +1007,12 @@ static int ext_test_p2_ops() { printf("[FAIL] read sparse extension before fsync: %s (errno=%d)\n", strerror(errno), errno); free(sparse_contents); - close(f); goto fail; } for (size_t i = 0; i < sparse_size - 1; ++i) { if (sparse_contents[i] != 0) { printf("[FAIL] sparse hole byte %zu is %u\n", i, sparse_contents[i]); free(sparse_contents); - close(f); goto fail; } } @@ -1008,16 +1020,15 @@ static int ext_test_p2_ops() { printf("[FAIL] sparse dirty tail lost before fsync: got=%u\n", sparse_contents[sparse_size - 1]); free(sparse_contents); - close(f); goto fail; } free(sparse_contents); if (fsync(f) != 0) { printf("[FAIL] fsync sparse file: %s (errno=%d)\n", strerror(errno), errno); - close(f); goto fail; } close(f); + f = -1; if (unlink(hard_path) != 0) { printf("[FAIL] unlink dirty-link probe: %s (errno=%d)\n", strerror(errno), errno); @@ -1056,6 +1067,7 @@ static int ext_test_p2_ops() { } rn = read(f, rbuf, sizeof(rbuf) - 1); close(f); + f = -1; if (rn < (ssize_t)strlen("p2-data")) { printf("[FAIL] read hard link: %s (errno=%d)\n", strerror(errno), errno); goto fail; @@ -1072,6 +1084,7 @@ static int ext_test_p2_ops() { goto fail; } close(f); + f = -1; if (syscall(SYS_renameat2, AT_FDCWD, hard_path, AT_FDCWD, dst_exist, RENAME_NOREPLACE) == 0 || errno != EEXIST) { @@ -1093,10 +1106,10 @@ static int ext_test_p2_ops() { } if (fsync(dfd) != 0) { printf("[FAIL] fsync(dirfd): %s (errno=%d)\n", strerror(errno), errno); - close(dfd); goto fail; } close(dfd); + dfd = -1; usleep(100 * 1000); @@ -1119,6 +1132,12 @@ static int ext_test_p2_ops() { return 0; fail: + if (f >= 0) { + close(f); + } + if (dfd >= 0) { + close(dfd); + } if (direct_fd >= 0) { close(direct_fd); } @@ -11061,6 +11080,32 @@ TEST(FuseExtended, OpsAccessCreateSymlinkLinkRename2FlushFsync) { ASSERT_EQ(0, ext_test_p2_ops()); } +TEST(FuseExtended, DirectDrainAfterTailWriteback) { + ASSERT_EQ(0, ext_test_p2_ops(true)); +} + +TEST(FuseExtended, DirectDrainTraceRejectsIncompleteOrReorderedWrites) { + uint64_t offsets[] = {12288, 0, 8192, 0, 8192}; + uint32_t sizes[] = {17, 8192, 4096, 8192, 4113}; + uint32_t flags[] = {FUSE_WRITE_CACHE, FUSE_WRITE_CACHE, FUSE_WRITE_CACHE, + FUSE_WRITE_LOCKOWNER, FUSE_WRITE_LOCKOWNER}; + const size_t length = 3 * 4096 + 17; + ASSERT_TRUE(direct_drain_trace_valid(5, offsets, sizes, flags, length)); + sizes[0] = 16; + EXPECT_FALSE(direct_drain_trace_valid(5, offsets, sizes, flags, length)); + sizes[0] = 17; + offsets[2] = 0; + EXPECT_FALSE(direct_drain_trace_valid(5, offsets, sizes, flags, length)); + offsets[2] = 8192; + flags[2] = FUSE_WRITE_LOCKOWNER; + flags[3] = FUSE_WRITE_CACHE; + EXPECT_FALSE(direct_drain_trace_valid(5, offsets, sizes, flags, length)); + flags[2] = FUSE_WRITE_CACHE; + flags[3] = FUSE_WRITE_LOCKOWNER; + EXPECT_FALSE(direct_drain_trace_valid(4, offsets, sizes, flags, length)); + EXPECT_FALSE(direct_drain_trace_valid(7, offsets, sizes, flags, length)); +} + TEST(FuseExtended, DirtyMultibatchNotifyInvalidation) { ASSERT_EQ(0, ext_test_dirty_multibatch_notify_invalidation()); } From b3f637c7ab4e22959d062515630c89d6f31ecbc2 Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 15:00:05 +0000 Subject: [PATCH 15/34] fix(ipc): make bulk semaphore buffers fallible and prepare queue growth unlocked Return ENOMEM for SETALL and GETALL buffer allocation failures. Prepare spare waiting-queue capacity only after a blocked operation needs growth, then revalidate and publish without allocating under the namespace lock. Preserve FIFO order and release replaced storage outside the lock. Add a 32000-element SETALL/GETALL regression. Signed-off-by: longjin --- kernel/src/ipc/sem.rs | 66 +++++++++++++++++-- kernel/src/ipc/syscall/sys_semctl.rs | 5 +- .../suites/normal/sysv_sem_semantics.cc | 14 ++++ 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index b9c446b53e..63ec2aab8a 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -412,18 +412,44 @@ impl KernelSemSet { } } - fn enqueue_waiter(&mut self, waiter: Arc) -> Result<(), SystemError> { + /// Return the required capacity without publishing anything if the caller + /// must prepare storage outside the namespace lock. `spare` is empty and + /// retains the old allocation after a swap, to be freed outside the lock. + fn enqueue_waiter_prepared( + &mut self, + waiter: Arc, + spare: &mut VecDeque>, + ) -> Result<(), usize> { + debug_assert!(spare.is_empty()); let queue = match Self::pending_queue_for(&waiter.sops) { SemPendingQueue::Const => &mut self.pending_const, SemPendingQueue::Alter => &mut self.pending_alter, }; - // Preparation may fail, but publishing the waiter must not allocate. - // No semaphore values or undo adjustments have been committed here. - queue.try_reserve(1).map_err(|_| SystemError::ENOMEM)?; + if queue.len() == queue.capacity() { + if spare.capacity() <= queue.len() { + return Err(queue.len().saturating_mul(2).max(4)); + } + // Capacity covers the existing entries and the new waiter. Moving + // Arc handles preserves FIFO order and performs no allocation. + spare.extend(queue.drain(..)); + core::mem::swap(queue, spare); + } queue.push_back(waiter); Ok(()) } + #[cfg(test)] + fn enqueue_waiter(&mut self, waiter: Arc) -> Result<(), SystemError> { + let mut spare = VecDeque::new(); + if let Err(capacity) = self.enqueue_waiter_prepared(waiter.clone(), &mut spare) { + spare + .try_reserve(capacity) + .map_err(|_| SystemError::ENOMEM)?; + self.enqueue_waiter_prepared(waiter, &mut spare).unwrap(); + } + Ok(()) + } + fn remove_waiter(&mut self, target: &Arc) { self.pending_const .retain(|entry| !Arc::ptr_eq(entry, target)); @@ -1212,7 +1238,18 @@ impl SemManager { ) }; + let mut queue_spare = VecDeque::new(); + let mut queue_capacity_needed = 0; let entry = loop { + // Only a genuinely blocking operation requests queue storage. + // Revalidate the set and re-simulate after dropping the lock: a + // wakeup, RMID, or another enqueue may have changed the outcome. + if queue_capacity_needed != 0 { + queue_spare + .try_reserve(queue_capacity_needed) + .map_err(|_| SystemError::ENOMEM)?; + queue_capacity_needed = 0; + } let nsems = { let guard = ipcns.sem.lock(); let set = guard.get_by_semid_checked(semid)?; @@ -1311,7 +1348,12 @@ impl SemManager { } drop(record_slot); prepared_entry.update_blocker(blocker); - set.enqueue_waiter(prepared_entry.clone())?; + if let Err(capacity) = + set.enqueue_waiter_prepared(prepared_entry.clone(), &mut queue_spare) + { + queue_capacity_needed = capacity; + continue; + } break prepared_entry; } } @@ -1334,13 +1376,19 @@ impl SemManager { .as_ref() .expect("plain queued semop entry is preallocated"); prepared_entry.update_blocker(blocker); - set.enqueue_waiter(prepared_entry.clone())?; + if let Err(capacity) = + set.enqueue_waiter_prepared(prepared_entry.clone(), &mut queue_spare) + { + queue_capacity_needed = capacity; + continue; + } break prepared_entry.clone(); } } } }; + drop(queue_spare); if let Some(timer) = timer.as_ref() { timer.activate(); } @@ -1533,7 +1581,11 @@ impl SemManager { let set = self.get_by_semid_checked(id)?; let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_READ, &target_user_ns)?; - Ok(set.sems.iter().map(|s| s.val as u16).collect()) + let mut vals = Vec::new(); + vals.try_reserve_exact(set.sems.len()) + .map_err(|_| SystemError::ENOMEM)?; + vals.extend(set.sems.iter().map(|s| s.val as u16)); + Ok(vals) } /// Validate SETALL before the caller accesses the userspace array. diff --git a/kernel/src/ipc/syscall/sys_semctl.rs b/kernel/src/ipc/syscall/sys_semctl.rs index 2c17f37375..bb777c80dc 100644 --- a/kernel/src/ipc/syscall/sys_semctl.rs +++ b/kernel/src/ipc/syscall/sys_semctl.rs @@ -118,7 +118,10 @@ pub(super) fn do_kernel_semctl( let guard = ipcns.sem.lock(); guard.prepare_setall(semid)? }; - let mut vals: Vec = vec![0; token.nsems()]; + let mut vals = Vec::::new(); + vals.try_reserve_exact(token.nsems()) + .map_err(|_| SystemError::ENOMEM)?; + vals.resize(token.nsems(), 0); let user_buffer_reader = UserBufferReader::new( arg as *const u8, token.nsems() * core::mem::size_of::(), diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index 84665ea46d..8897048f4e 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -618,6 +618,20 @@ TEST(SysVSem, SetAllGetAll) { EXPECT_EQ(3u, out[2]); } +TEST(SysVSem, LargeSetAllGetAll) { + constexpr size_t count = 32000; + SemSet sem(count, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()) << "errno=" << errno; + std::vector values(count), actual(count); + for (size_t i = 0; i < count; ++i) + values[i] = static_cast(i % 32768); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETALL, + reinterpret_cast(values.data()))); + ASSERT_EQ(0, SemCtl(sem.id(), 0, GETALL, + reinterpret_cast(actual.data()))); + EXPECT_EQ(values, actual); +} + TEST(SysVSem, SetAllAtomicRangeError) { SemSet sem(3, IPC_CREAT | 0600); ASSERT_TRUE(sem.valid()); From dbcd42f799322412f9eec82eef4ad7f577d7f40d Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 15:13:57 +0000 Subject: [PATCH 16/34] fix(ipc): prepare undo registry growth outside the namespace lock Request spare capacity before publishing a first-time undo group. Allocate outside the manager lock, then revalidate the set and registration state before moving Weak entries into the prepared buffer. Keep replaced storage for unlocked disposal and preserve mark-after-insertion and duplicate prevention. Add coverage for competing growth and repeated registration. Signed-off-by: longjin --- kernel/src/ipc/sem.rs | 94 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index 63ec2aab8a..8679e7022a 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -669,24 +669,48 @@ impl SemManager { /// Serialize registration with control operations using the manager lock. /// A live group remains registered even when all of its debt is cleared. - fn ensure_undo_group_registered( + /// Insufficient spare capacity requests an unlocked preparation/retry. + /// On success, spare retains any replaced allocation for unlocked disposal. + fn ensure_undo_group_registered_prepared( &mut self, group: &Arc, - ) -> Result<(), SystemError> { + spare: &mut Vec>, + ) -> Result<(), usize> { + debug_assert!(spare.is_empty()); if group.registry_registered() { return Ok(()); } if self.undo_groups.len() == self.undo_groups.capacity() { self.compact_undo_registry(); } - self.undo_groups - .try_reserve(1) - .map_err(|_| SystemError::ENOMEM)?; + if self.undo_groups.len() == self.undo_groups.capacity() { + if spare.capacity() <= self.undo_groups.len() { + return Err(self.undo_groups.len().saturating_mul(2).max(4)); + } + spare.append(&mut self.undo_groups); + core::mem::swap(&mut self.undo_groups, spare); + } self.undo_groups.push(Arc::downgrade(group)); group.mark_registry_registered(); Ok(()) } + #[cfg(test)] + fn ensure_undo_group_registered( + &mut self, + group: &Arc, + ) -> Result<(), SystemError> { + let mut spare = Vec::new(); + if let Err(capacity) = self.ensure_undo_group_registered_prepared(group, &mut spare) { + spare + .try_reserve(capacity) + .map_err(|_| SystemError::ENOMEM)?; + self.ensure_undo_group_registered_prepared(group, &mut spare) + .unwrap(); + } + Ok(()) + } + fn with_undo_record_mut( group: &Arc, semid: SemId, @@ -1240,6 +1264,8 @@ impl SemManager { let mut queue_spare = VecDeque::new(); let mut queue_capacity_needed = 0; + let mut registry_spare = Vec::new(); + let mut registry_capacity_needed = 0; let entry = loop { // Only a genuinely blocking operation requests queue storage. // Revalidate the set and re-simulate after dropping the lock: a @@ -1250,6 +1276,12 @@ impl SemManager { .map_err(|_| SystemError::ENOMEM)?; queue_capacity_needed = 0; } + if registry_capacity_needed != 0 { + registry_spare + .try_reserve(registry_capacity_needed) + .map_err(|_| SystemError::ENOMEM)?; + registry_capacity_needed = 0; + } let nsems = { let guard = ipcns.sem.lock(); let set = guard.get_by_semid_checked(semid)?; @@ -1295,11 +1327,15 @@ impl SemManager { continue; } if let Some(prepared_entry) = prepared_undo { - guard.ensure_undo_group_registered( + if let Err(capacity) = guard.ensure_undo_group_registered_prepared( undo_group .as_ref() .expect("SEM_UNDO operation has a current group"), - )?; + &mut registry_spare, + ) { + registry_capacity_needed = capacity; + continue; + } let mut record_slot = prepared_entry.undo_record.lock_irqsave(); let prepared_record = record_slot .take() @@ -1389,6 +1425,7 @@ impl SemManager { }; drop(queue_spare); + drop(registry_spare); if let Some(timer) = timer.as_ref() { timer.activate(); } @@ -1919,6 +1956,49 @@ mod tests { assert!(SemManager::new().undo_groups.is_empty()); } + #[test] + fn prepared_registry_growth_rechecks_registration_and_capacity() { + let mut manager = SemManager::new(); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let mut spare = Vec::new(); + let capacity = manager + .ensure_undo_group_registered_prepared(&group, &mut spare) + .unwrap_err(); + assert!(!group.registry_registered()); + assert!(manager.undo_groups.is_empty()); + spare.try_reserve(capacity).unwrap(); + + // Simulate other first-time groups consuming the prepared capacity + // while this caller has dropped the manager lock. + let mut owners = Vec::new(); + while manager.undo_groups.len() < spare.capacity() { + let owner = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + manager.ensure_undo_group_registered(&owner).unwrap(); + owners.push(owner); + } + let capacity = manager + .ensure_undo_group_registered_prepared(&group, &mut spare) + .unwrap_err(); + assert!(!group.registry_registered()); + spare.try_reserve(capacity).unwrap(); + manager + .ensure_undo_group_registered_prepared(&group, &mut spare) + .unwrap(); + assert!(spare.is_empty()); + assert!(group.registry_registered()); + assert_eq!(manager.undo_groups.len(), owners.len() + 1); + for (weak, owner) in manager.undo_groups.iter().zip(&owners) { + assert!(weak.ptr_eq(&Arc::downgrade(owner))); + } + + // A concurrent CLONE_SYSVSEM sharer already registered this group. + let mut empty_spare = Vec::new(); + manager + .ensure_undo_group_registered_prepared(&group, &mut empty_spare) + .unwrap(); + assert_eq!(manager.undo_groups.len(), owners.len() + 1); + } + #[test] fn queued_undo_commits_to_captured_group_not_waker_current_task() { let ipc_ns = test_ipc_ns(); From e1503021361b836ee94cbfa8a2a9da8800403270 Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 15:56:43 +0000 Subject: [PATCH 17/34] fix(ipc): scope semaphore undo cleanup and defer wakeups Associate undo groups with each semaphore set so control operations no longer scan unrelated groups. Reuse published records for the registration fast path and preserve unlocked spare-capacity preparation. Publish queued results under the namespace lock and drain an allocation-free completion batch after unlocking across semop, semctl and exit replay. Keep completed entries alive and detach links iteratively. Add set-local undo and bulk removal regressions. Validate with make kernel, kernel workspace tests, 69 Linux host tests, 69 DragonOS guest tests and 20 complete guest repeats. Signed-off-by: longjin --- kernel/src/ipc/sem.rs | 493 +++++++++++++----- kernel/src/ipc/sem_undo.rs | 27 +- kernel/src/ipc/syscall/sys_semctl.rs | 11 +- .../suites/normal/sysv_sem_semantics.cc | 90 ++++ 4 files changed, 464 insertions(+), 157 deletions(-) diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index 8679e7022a..18e502ab20 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -253,7 +253,10 @@ struct SemBlockedOp { #[derive(Debug)] enum SemQueueStatus { Queued(SemBlockedOp), - Completed(Result), + Completed { + result: Result, + next_wake: Option>, + }, } /// An Arc-owned queue entry shared by the set and the blocked caller. @@ -331,7 +334,7 @@ impl SemQueueEntry { fn completed_result(&self) -> Option> { match &*self.status.lock() { SemQueueStatus::Queued(_) => None, - SemQueueStatus::Completed(result) => Some(result.clone()), + SemQueueStatus::Completed { result, .. } => Some(result.clone()), } } @@ -344,10 +347,13 @@ impl SemQueueEntry { fn complete(&self, result: Result) -> bool { let mut status = self.status.lock(); - if matches!(&*status, SemQueueStatus::Completed(_)) { + if matches!(&*status, SemQueueStatus::Completed { .. }) { return false; } - *status = SemQueueStatus::Completed(result); + *status = SemQueueStatus::Completed { + result, + next_wake: None, + }; true } @@ -360,6 +366,50 @@ impl SemQueueEntry { } } +/// Per-operation completion list. The existing entry status lock protects the +/// link; no allocation or scheduler operation is needed while the set is locked. +/// Declare this before manager guards so all exits wake only after unlocking. +#[derive(Default)] +pub(crate) struct SemWakeBatch { + head: Option>, + tail: Option>, +} + +impl SemWakeBatch { + fn complete(&mut self, entry: Arc, result: Result) { + if !entry.complete(result) { + return; + } + if let Some(tail) = self.tail.take() { + if let SemQueueStatus::Completed { next_wake, .. } = &mut *tail.status.lock() { + *next_wake = Some(entry.clone()); + } + } else { + self.head = Some(entry.clone()); + } + self.tail = Some(entry); + } + + pub(crate) fn wake_all(&mut self) { + self.tail = None; + while let Some(entry) = self.head.take() { + self.head = match &mut *entry.status.lock() { + SemQueueStatus::Completed { next_wake, .. } => next_wake.take(), + SemQueueStatus::Queued(_) => unreachable!("wake batch contains queued entry"), + }; + // Release the status lock before entering the scheduler. Detaching + // each link also prevents recursive Arc destruction for large batches. + entry.waker.wake(); + } + } +} + +impl Drop for SemWakeBatch { + fn drop(&mut self) { + self.wake_all(); + } +} + /// Selects one of the set-global pending queues. #[derive(Debug, Clone, Copy)] enum SemPendingQueue { @@ -370,6 +420,8 @@ enum SemPendingQueue { /// Semaphore set #[derive(Debug)] pub struct KernelSemSet { + /// Groups that can carry undo debt for this set, including queued operations. + undo_groups: Vec>, /// Permission information pub kern_ipc_perm: IpcPerm, /// Semaphores in the set @@ -385,6 +437,38 @@ pub struct KernelSemSet { } impl KernelSemSet { + /// Live associations survive SETVAL/SETALL. Only RMID or dead groups + /// remove them, so an existing undo record proves prior association. + /// Insufficient spare capacity requests an unlocked preparation/retry. + /// On success, spare retains any replaced allocation for unlocked disposal. + fn ensure_undo_group_registered_prepared( + &mut self, + group: &Arc, + spare: &mut Vec>, + ) -> Result<(), usize> { + debug_assert!(spare.is_empty()); + let candidate = Arc::downgrade(group); + if self.undo_groups.iter().any(|weak| weak.ptr_eq(&candidate)) { + return Ok(()); + } + if self.undo_groups.len() == self.undo_groups.capacity() { + self.compact_undo_registry(); + } + if self.undo_groups.len() == self.undo_groups.capacity() { + if spare.capacity() <= self.undo_groups.len() { + return Err(self.undo_groups.len().saturating_mul(2).max(4)); + } + spare.append(&mut self.undo_groups); + core::mem::swap(&mut self.undo_groups, spare); + } + self.undo_groups.push(candidate); + Ok(()) + } + + fn compact_undo_registry(&mut self) { + self.undo_groups.retain(|weak| weak.strong_count() != 0); + } + fn try_allocate_sems(nsems: usize) -> Result, SystemError> { let mut sems = Vec::new(); sems.try_reserve_exact(nsems) @@ -395,6 +479,7 @@ impl KernelSemSet { fn new(kern_ipc_perm: IpcPerm, sems: Vec) -> Self { KernelSemSet { + undo_groups: Vec::new(), kern_ipc_perm, sems, sem_otime: 0, @@ -483,15 +568,13 @@ impl KernelSemSet { }; } - /// Complete and wake all entries during IPC_RMID under the manager lock. - fn complete_all_removed(&mut self) { + /// Publish removal under the manager lock; the caller wakes after unlocking. + fn complete_all_removed(&mut self, wakes: &mut SemWakeBatch) { for entry in self.pending_const.drain(..) { - entry.complete(Err(SystemError::EIDRM)); - entry.waker.wake(); + wakes.complete(entry, Err(SystemError::EIDRM)); } for entry in self.pending_alter.drain(..) { - entry.complete(Err(SystemError::EIDRM)); - entry.waker.wake(); + wakes.complete(entry, Err(SystemError::EIDRM)); } } @@ -612,8 +695,6 @@ pub struct SemManager { key2id: HashMap, /// Total semaphores in the namespace (Linux semmns accounting) total_sems: usize, - #[allow(dead_code)] - undo_groups: Vec>, } impl Default for SemManager { @@ -632,7 +713,6 @@ impl SemManager { id2sem: HashMap::new(), key2id: HashMap::new(), total_sems: 0, - undo_groups: Vec::new(), } } @@ -667,45 +747,20 @@ impl SemManager { Ok(self.get_by_semid_checked(semid)?.sems.len()) } - /// Serialize registration with control operations using the manager lock. - /// A live group remains registered even when all of its debt is cleared. - /// Insufficient spare capacity requests an unlocked preparation/retry. - /// On success, spare retains any replaced allocation for unlocked disposal. - fn ensure_undo_group_registered_prepared( - &mut self, - group: &Arc, - spare: &mut Vec>, - ) -> Result<(), usize> { - debug_assert!(spare.is_empty()); - if group.registry_registered() { - return Ok(()); - } - if self.undo_groups.len() == self.undo_groups.capacity() { - self.compact_undo_registry(); - } - if self.undo_groups.len() == self.undo_groups.capacity() { - if spare.capacity() <= self.undo_groups.len() { - return Err(self.undo_groups.len().saturating_mul(2).max(4)); - } - spare.append(&mut self.undo_groups); - core::mem::swap(&mut self.undo_groups, spare); - } - self.undo_groups.push(Arc::downgrade(group)); - group.mark_registry_registered(); - Ok(()) - } - + /// Test-only setup; production reserves registry storage before locking. #[cfg(test)] fn ensure_undo_group_registered( &mut self, group: &Arc, + semid: SemId, ) -> Result<(), SystemError> { + let set = self.get_by_semid_checked_mut(semid)?; let mut spare = Vec::new(); - if let Err(capacity) = self.ensure_undo_group_registered_prepared(group, &mut spare) { + if let Err(capacity) = set.ensure_undo_group_registered_prepared(group, &mut spare) { spare .try_reserve(capacity) .map_err(|_| SystemError::ENOMEM)?; - self.ensure_undo_group_registered_prepared(group, &mut spare) + set.ensure_undo_group_registered_prepared(group, &mut spare) .unwrap(); } Ok(()) @@ -719,13 +774,12 @@ impl SemManager { group.with_record_mut(semid, f) } - fn compact_undo_registry(&mut self) { - self.undo_groups.retain(|weak| weak.strong_count() != 0); - } - pub(crate) fn clear_undo_for_setval(&mut self, semid: SemId, semnum: usize) { + let Ok(set) = self.get_by_semid_checked_mut(semid) else { + return; + }; let mut saw_stale = false; - for weak in self.undo_groups.iter() { + for weak in set.undo_groups.iter() { let Some(group) = weak.upgrade() else { saw_stale = true; continue; @@ -737,13 +791,16 @@ impl SemManager { }); } if saw_stale { - self.compact_undo_registry(); + set.compact_undo_registry(); } } pub(crate) fn clear_undo_for_setall(&mut self, semid: SemId) { + let Ok(set) = self.get_by_semid_checked_mut(semid) else { + return; + }; let mut saw_stale = false; - for weak in self.undo_groups.iter() { + for weak in set.undo_groups.iter() { let Some(group) = weak.upgrade() else { saw_stale = true; continue; @@ -751,13 +808,16 @@ impl SemManager { Self::with_undo_record_mut(&group, semid, |record| record.clear_all_adjustments()); } if saw_stale { - self.compact_undo_registry(); + set.compact_undo_registry(); } } pub(crate) fn discard_undo_for_rmid(&mut self, semid: SemId) { + let Ok(set) = self.get_by_semid_checked_mut(semid) else { + return; + }; let mut saw_stale = false; - for weak in self.undo_groups.iter() { + for weak in set.undo_groups.iter() { let Some(group) = weak.upgrade() else { saw_stale = true; continue; @@ -765,7 +825,7 @@ impl SemManager { group.remove_record(semid); } if saw_stale { - self.compact_undo_registry(); + set.compact_undo_registry(); } } @@ -789,30 +849,37 @@ impl SemManager { let Ok(set) = self.get_by_semid_checked_mut(semid) else { return; }; - Self::update_queue(set, semid); + Self::update_queue(set, semid, &mut SemWakeBatch::default()); } #[cfg(test)] fn live_undo_group_count_for_test(&self) -> usize { - self.undo_groups - .iter() - .filter(|weak| weak.upgrade().is_some()) - .count() + let mut groups: Vec> = Vec::new(); + for weak in self.id2sem.values().flat_map(|set| &set.undo_groups) { + if weak.strong_count() != 0 && !groups.iter().any(|old| old.ptr_eq(weak)) { + groups.push(weak.clone()); + } + } + groups.len() } #[cfg(test)] fn undo_registry_contains_for_test(&self, group: &Arc) -> bool { - self.undo_groups - .iter() + self.id2sem + .values() + .flat_map(|set| &set.undo_groups) .any(|weak| weak.ptr_eq(&Arc::downgrade(group))) } #[cfg(test)] fn namespace_lifecycle_invariant_for_test(&self) -> bool { - self.undo_groups.iter().all(|weak| { - weak.upgrade() - .is_none_or(|group| group.record_count_for_test() == 0) - }) + self.id2sem + .values() + .flat_map(|set| &set.undo_groups) + .all(|weak| { + weak.upgrade() + .is_none_or(|group| group.record_count_for_test() == 0) + }) } #[cfg(test)] @@ -823,7 +890,7 @@ impl SemManager { ) -> Result<(), SystemError> { let nsems = self.validate_semid_nsems(semid)?; let record = group.prepare_record(semid, nsems)?; - self.ensure_undo_group_registered(group)?; + self.ensure_undo_group_registered(group, semid)?; group.commit_prepared_record_noalloc(record) } @@ -1001,7 +1068,11 @@ impl SemManager { } /// Scan one pending queue without head-of-line blocking. - fn scan_pending_queue(set: &mut KernelSemSet, queue: SemPendingQueue) -> bool { + fn scan_pending_queue( + set: &mut KernelSemSet, + queue: SemPendingQueue, + wakes: &mut SemWakeBatch, + ) -> bool { let mut index = 0; while index < set.pending_len(queue) { let entry = set.pending_entry(queue, index); @@ -1011,8 +1082,7 @@ impl SemManager { let mut record_slot = entry.undo_record.lock_irqsave(); let Some(record) = record_slot.take() else { set.remove_pending(queue, index); - entry.complete(Err(SystemError::EINVAL)); - entry.waker.wake(); + wakes.complete(entry.clone(), Err(SystemError::EINVAL)); continue; }; match group.with_prepared_record_noalloc(record, |record| { @@ -1035,8 +1105,7 @@ impl SemManager { match result { Ok(Some(changed)) => { set.remove_pending(queue, index); - entry.complete(Ok(0)); - entry.waker.wake(); + wakes.complete(entry.clone(), Ok(0)); if changed { return true; } @@ -1044,8 +1113,7 @@ impl SemManager { Ok(None) => index += 1, Err(error) => { set.remove_pending(queue, index); - entry.complete(Err(error)); - entry.waker.wake(); + wakes.complete(entry.clone(), Err(error)); } } continue; @@ -1057,16 +1125,14 @@ impl SemManager { let changed = Self::commit_semop(set, simulation, &scratch, entry.pid.clone(), None); set.remove_pending(queue, index); - entry.complete(Ok(0)); - entry.waker.wake(); + wakes.complete(entry.clone(), Ok(0)); if changed { return true; } } Ok(SemopOutcome::Blocked(blocker)) if blocker.nowait => { set.remove_pending(queue, index); - entry.complete(Err(SystemError::EAGAIN_OR_EWOULDBLOCK)); - entry.waker.wake(); + wakes.complete(entry.clone(), Err(SystemError::EAGAIN_OR_EWOULDBLOCK)); } Ok(SemopOutcome::Blocked(blocker)) => { entry.update_blocker(blocker); @@ -1074,8 +1140,7 @@ impl SemManager { } Err(error) => { set.remove_pending(queue, index); - entry.complete(Err(error)); - entry.waker.wake(); + wakes.complete(entry.clone(), Err(error)); } } } @@ -1083,11 +1148,11 @@ impl SemManager { } /// Complete executable const entries before altering entries. - fn update_queue(set: &mut KernelSemSet, _semid: SemId) { + fn update_queue(set: &mut KernelSemSet, _semid: SemId, wakes: &mut SemWakeBatch) { loop { - let const_changed = Self::scan_pending_queue(set, SemPendingQueue::Const); + let const_changed = Self::scan_pending_queue(set, SemPendingQueue::Const, wakes); debug_assert!(!const_changed); - if !Self::scan_pending_queue(set, SemPendingQueue::Alter) { + if !Self::scan_pending_queue(set, SemPendingQueue::Alter, wakes) { return; } } @@ -1127,6 +1192,7 @@ impl SemManager { semid: SemId, adjustments: &[i16], exiting_tgid: Option>, + wakes: &mut SemWakeBatch, ) { let Ok(set) = self.get_by_semid_checked_mut(semid) else { return; @@ -1143,7 +1209,7 @@ impl SemManager { } set.sem_otime = PosixTimeSpec::now().tv_sec; - Self::update_queue(set, semid); + Self::update_queue(set, semid, wakes); } fn cancel_queued_entry( @@ -1262,6 +1328,7 @@ impl SemManager { ) }; + let mut wakes = SemWakeBatch::default(); let mut queue_spare = VecDeque::new(); let mut queue_capacity_needed = 0; let mut registry_spare = Vec::new(); @@ -1327,14 +1394,26 @@ impl SemManager { continue; } if let Some(prepared_entry) = prepared_undo { - if let Err(capacity) = guard.ensure_undo_group_registered_prepared( - undo_group - .as_ref() - .expect("SEM_UNDO operation has a current group"), - &mut registry_spare, - ) { - registry_capacity_needed = capacity; - continue; + // Only a published record can take the fast path. Missing + // (including queued) records must be linked before publication. + // Full semid was rechecked above, so Existing cannot refer to a + // destroyed/reused set. SETVAL/SETALL retain live associations. + let already_associated = prepared_entry + .undo_record + .lock_irqsave() + .as_ref() + .is_some_and(SemUndoRecord::was_existing); + if !already_associated { + let set = guard.get_by_semid_checked_mut(semid)?; + if let Err(capacity) = set.ensure_undo_group_registered_prepared( + undo_group + .as_ref() + .expect("SEM_UNDO operation has a current group"), + &mut registry_spare, + ) { + registry_capacity_needed = capacity; + continue; + } } let mut record_slot = prepared_entry.undo_record.lock_irqsave(); let prepared_record = record_slot @@ -1372,7 +1451,7 @@ impl SemManager { match outcome? { None => { drop(record_slot); - Self::update_queue(set, semid); + Self::update_queue(set, semid, &mut wakes); return Ok(0); } Some(blocker) => { @@ -1398,7 +1477,7 @@ impl SemManager { match Self::simulate_semop(set, sops, None, &mut immediate_scratch)? { SemopOutcome::Ready(simulation) => { Self::commit_semop(set, simulation, &immediate_scratch, pid, None); - Self::update_queue(set, semid); + Self::update_queue(set, semid, &mut wakes); return Ok(0); } SemopOutcome::Blocked(blocker) => { @@ -1426,6 +1505,7 @@ impl SemManager { drop(queue_spare); drop(registry_spare); + wakes.wake_all(); if let Some(timer) = timer.as_ref() { timer.activate(); } @@ -1451,7 +1531,11 @@ impl SemManager { } /// # IPC_RMID: remove the semaphore set and wake all waiters with EIDRM - pub fn ipc_rmid(&mut self, id: SemId) -> Result<(), SystemError> { + pub(crate) fn ipc_rmid( + &mut self, + id: SemId, + wakes: &mut SemWakeBatch, + ) -> Result<(), SystemError> { let decoded = IpcIdAllocator::decode(id.data())?; let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); let key = { @@ -1466,7 +1550,7 @@ impl SemManager { .ok_or(SystemError::EINVAL)?; self.key2id.remove(&SemKey::new(key)); self.total_sems = self.total_sems.saturating_sub(set.sems.len()); - set.complete_all_removed(); + set.complete_all_removed(wakes); self.id_allocator.free_idx(decoded.idx); Ok(()) } @@ -1557,7 +1641,13 @@ impl SemManager { } /// # SETVAL: set a single semaphore value - pub fn setval(&mut self, id: SemId, semnum: usize, val: i32) -> Result<(), SystemError> { + pub(crate) fn setval( + &mut self, + id: SemId, + semnum: usize, + val: i32, + wakes: &mut SemWakeBatch, + ) -> Result<(), SystemError> { // Match Linux: validate the value (ERANGE), then semnum (EINVAL), then permissions // (EACCES). if !(0..=SEMVMX).contains(&val) { @@ -1580,12 +1670,17 @@ impl SemManager { sem.val = val; sem.pid = ProcessManager::current_pcb().task_pid_ptr(PidType::TGID); set.sem_ctime = PosixTimeSpec::now().tv_sec; - Self::update_queue(set, id); + Self::update_queue(set, id, wakes); Ok(()) } /// # SETALL: set values of all semaphores in the set without changes on validation failure - pub fn setall(&mut self, token: SemSetAllToken, vals: &[u16]) -> Result<(), SystemError> { + pub(crate) fn setall( + &mut self, + token: SemSetAllToken, + vals: &[u16], + wakes: &mut SemWakeBatch, + ) -> Result<(), SystemError> { let set_nsems = self .get_by_semid_checked(token.id) .map_err(|_| SystemError::EIDRM)? @@ -1609,7 +1704,7 @@ impl SemManager { sem.pid = pid.clone(); } set.sem_ctime = PosixTimeSpec::now().tv_sec; - Self::update_queue(set, token.id); + Self::update_queue(set, token.id, wakes); Ok(()) } @@ -1885,7 +1980,12 @@ mod tests { }; let exiting_tgid = Pid::new_for_test(RawPid::new(4243), INIT_PID_NAMESPACE.clone()); - manager.replay_sem_undo_adjustments(semid, &[1, 1], Some(exiting_tgid.clone())); + manager.replay_sem_undo_adjustments( + semid, + &[1, 1], + Some(exiting_tgid.clone()), + &mut SemWakeBatch::default(), + ); let set = manager.get_by_semid_checked_mut(semid).unwrap(); let values = [set.sems[0].val, set.sems[1].val]; @@ -1931,7 +2031,12 @@ mod tests { }; let exiting_tgid = Pid::new_for_test(RawPid::new(4244), INIT_PID_NAMESPACE.clone()); - manager.replay_sem_undo_adjustments(semid, &[0, 0], Some(exiting_tgid.clone())); + manager.replay_sem_undo_adjustments( + semid, + &[0, 0], + Some(exiting_tgid.clone()), + &mut SemWakeBatch::default(), + ); let set = manager.get_by_semid_checked_mut(semid).unwrap(); let values = [set.sems[0].val, set.sems[1].val]; @@ -1953,50 +2058,89 @@ mod tests { #[test] fn new_manager_starts_with_empty_undo_registry() { - assert!(SemManager::new().undo_groups.is_empty()); + assert!(SemManager::new().id2sem.is_empty()); } #[test] fn prepared_registry_growth_rechecks_registration_and_capacity() { let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(151), &[1]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); let mut spare = Vec::new(); let capacity = manager + .get_by_semid_checked_mut(semid) + .unwrap() .ensure_undo_group_registered_prepared(&group, &mut spare) .unwrap_err(); - assert!(!group.registry_registered()); - assert!(manager.undo_groups.is_empty()); + assert!(!manager.undo_registry_contains_for_test(&group)); + assert!(manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .is_empty()); spare.try_reserve(capacity).unwrap(); // Simulate other first-time groups consuming the prepared capacity // while this caller has dropped the manager lock. let mut owners = Vec::new(); - while manager.undo_groups.len() < spare.capacity() { + while manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .len() + < spare.capacity() + { let owner = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - manager.ensure_undo_group_registered(&owner).unwrap(); + manager.ensure_undo_group_registered(&owner, semid).unwrap(); owners.push(owner); } let capacity = manager + .get_by_semid_checked_mut(semid) + .unwrap() .ensure_undo_group_registered_prepared(&group, &mut spare) .unwrap_err(); - assert!(!group.registry_registered()); + assert!(!manager.undo_registry_contains_for_test(&group)); spare.try_reserve(capacity).unwrap(); manager + .get_by_semid_checked_mut(semid) + .unwrap() .ensure_undo_group_registered_prepared(&group, &mut spare) .unwrap(); assert!(spare.is_empty()); - assert!(group.registry_registered()); - assert_eq!(manager.undo_groups.len(), owners.len() + 1); - for (weak, owner) in manager.undo_groups.iter().zip(&owners) { + assert!(manager.undo_registry_contains_for_test(&group)); + assert_eq!( + manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .len(), + owners.len() + 1 + ); + for (weak, owner) in manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .iter() + .zip(&owners) + { assert!(weak.ptr_eq(&Arc::downgrade(owner))); } // A concurrent CLONE_SYSVSEM sharer already registered this group. let mut empty_spare = Vec::new(); manager + .get_by_semid_checked_mut(semid) + .unwrap() .ensure_undo_group_registered_prepared(&group, &mut empty_spare) .unwrap(); - assert_eq!(manager.undo_groups.len(), owners.len() + 1); + assert_eq!( + manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .len(), + owners.len() + 1 + ); } #[test] @@ -2072,7 +2216,9 @@ mod tests { assert_eq!(group.adjustment_for_test(signal_semid, 0), 0); let rmid_entry = enqueue_undo_waiter_for_test(&mut manager, rmid_semid, &group); - manager.ipc_rmid(rmid_semid).unwrap(); + manager + .ipc_rmid(rmid_semid, &mut SemWakeBatch::default()) + .unwrap(); assert_eq!(rmid_entry.completed_result(), Some(Err(SystemError::EIDRM))); assert_eq!(group.adjustment_for_test(rmid_semid, 0), 0); } @@ -2104,9 +2250,17 @@ mod tests { let mut manager = ipc_ns.sem.lock(); let semid = insert_test_set(&mut manager, SemKey::new(51), &[1]); - manager.undo_groups.push(stale_weak); - manager.ensure_undo_group_registered(&live).unwrap(); - manager.undo_groups.shrink_to_fit(); + manager + .get_by_semid_checked_mut(semid) + .unwrap() + .undo_groups + .push(stale_weak); + manager.ensure_undo_group_registered(&live, semid).unwrap(); + manager + .get_by_semid_checked_mut(semid) + .unwrap() + .undo_groups + .shrink_to_fit(); manager .prepare_undo_record_and_registry_for_test(&candidate, semid) @@ -2126,20 +2280,45 @@ mod tests { manager .prepare_undo_record_and_registry_for_test(&group, semid) .unwrap(); - let capacity = manager.undo_groups.capacity(); + let capacity = manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .capacity(); manager.clear_undo_for_setall(semid); manager.discard_undo_for_rmid(semid); assert_eq!(group.record_count_for_test(), 0); for _ in 0..32 { - manager.ensure_undo_group_registered(&group).unwrap(); + manager.ensure_undo_group_registered(&group, semid).unwrap(); } - assert!(group.registry_registered()); - assert_eq!(manager.undo_groups.len(), 1); - assert_eq!(manager.undo_groups.capacity(), capacity); + assert!(manager.undo_registry_contains_for_test(&group)); + assert_eq!( + manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .len(), + 1 + ); + assert_eq!( + manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .capacity(), + capacity + ); manager .prepare_undo_record_and_registry_for_test(&group, semid) .unwrap(); - assert_eq!(manager.undo_groups.len(), 1); + assert_eq!( + manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .len(), + 1 + ); } #[test] @@ -2187,12 +2366,19 @@ mod tests { }; let (pcb, group) = test_pcb_with_group(&ipc_ns); group.insert_test_record(semid, &[3]); + ipc_ns + .sem + .lock() + .ensure_undo_group_registered(&group, semid) + .unwrap(); drop(pcb.take_sem_undo_attachment().unwrap()); assert!(group.detach_last_owner_for_test()); { let mut manager = ipc_ns.sem.lock(); - manager.setval(semid, 0, 7).unwrap(); + manager + .setval(semid, 0, 7, &mut SemWakeBatch::default()) + .unwrap(); } group.replay_marked_records_for_test(&pcb); @@ -2212,12 +2398,19 @@ mod tests { }; let (pcb, group) = test_pcb_with_group(&ipc_ns); group.insert_test_record(semid, &[3]); + ipc_ns + .sem + .lock() + .ensure_undo_group_registered(&group, semid) + .unwrap(); drop(pcb.take_sem_undo_attachment().unwrap()); assert!(group.detach_last_owner_for_test()); { let mut manager = ipc_ns.sem.lock(); - manager.ipc_rmid(semid).unwrap(); + manager + .ipc_rmid(semid, &mut SemWakeBatch::default()) + .unwrap(); } group.replay_marked_records_for_test(&pcb); @@ -2255,10 +2448,16 @@ mod tests { group_a.insert_test_record(semid, &[11, 12]); group_b.insert_test_record(semid, &[21, 22]); group_a.insert_test_record(other_semid, &[31, 32]); - manager.ensure_undo_group_registered(&group_a).unwrap(); - manager.ensure_undo_group_registered(&group_b).unwrap(); + manager + .ensure_undo_group_registered(&group_a, semid) + .unwrap(); + manager + .ensure_undo_group_registered(&group_b, semid) + .unwrap(); - manager.setval(semid, 0, 9).unwrap(); + manager + .setval(semid, 0, 9, &mut SemWakeBatch::default()) + .unwrap(); assert_eq!(sem_values(&manager, semid), vec![9, 5]); assert_eq!(group_a.adjustment_for_test(semid, 0), 0); @@ -2278,11 +2477,17 @@ mod tests { group_a.insert_test_record(semid, &[1, 2, 3]); group_b.insert_test_record(semid, &[4, 5, 6]); group_b.insert_test_record(other_semid, &[7, 8, 9]); - manager.ensure_undo_group_registered(&group_a).unwrap(); - manager.ensure_undo_group_registered(&group_b).unwrap(); + manager + .ensure_undo_group_registered(&group_a, semid) + .unwrap(); + manager + .ensure_undo_group_registered(&group_b, semid) + .unwrap(); let token = SemSetAllToken::new(semid, 3); - manager.setall(token, &[10, 11, 12]).unwrap(); + manager + .setall(token, &[10, 11, 12], &mut SemWakeBatch::default()) + .unwrap(); assert_eq!(sem_values(&manager, semid), vec![10, 11, 12]); assert_eq!(group_a.adjustment_for_test(semid, 0), 0); @@ -2300,10 +2505,12 @@ mod tests { let semid = insert_test_set(&mut manager, SemKey::new(58), &[0]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(semid, &[7]); - manager.ensure_undo_group_registered(&group).unwrap(); + manager.ensure_undo_group_registered(&group, semid).unwrap(); let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); - manager.setval(semid, 0, 1).unwrap(); + manager + .setval(semid, 0, 1, &mut SemWakeBatch::default()) + .unwrap(); assert_eq!(entry.completed_result(), Some(Ok(0))); assert_eq!(sem_values(&manager, semid), vec![0]); @@ -2318,8 +2525,12 @@ mod tests { let filler_semid = insert_test_set(&mut manager, SemKey::new(60), &[4]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(old_semid, &[9]); - manager.ensure_undo_group_registered(&group).unwrap(); - manager.ipc_rmid(old_semid).unwrap(); + manager + .ensure_undo_group_registered(&group, old_semid) + .unwrap(); + manager + .ipc_rmid(old_semid, &mut SemWakeBatch::default()) + .unwrap(); let new_semid = insert_test_set(&mut manager, SemKey::new(61), &[5]); @@ -2339,7 +2550,7 @@ mod tests { let semid = insert_test_set(&mut manager, SemKey::new(62), &[4]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(semid, &[7]); - manager.ensure_undo_group_registered(&group).unwrap(); + manager.ensure_undo_group_registered(&group, semid).unwrap(); let record = group.prepare_record_for_test(semid, 1).unwrap(); manager.clear_undo_for_setval(semid, 0); @@ -2366,7 +2577,7 @@ mod tests { let semid = insert_test_set(&mut manager, SemKey::new(63), &[4, 5]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(semid, &[7, -3]); - manager.ensure_undo_group_registered(&group).unwrap(); + manager.ensure_undo_group_registered(&group, semid).unwrap(); let record = group.prepare_record_for_test(semid, 2).unwrap(); manager.clear_undo_for_setall(semid); @@ -2394,7 +2605,7 @@ mod tests { let semid = insert_test_set(&mut manager, SemKey::new(65), &[4]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(semid, &[7]); - manager.ensure_undo_group_registered(&group).unwrap(); + manager.ensure_undo_group_registered(&group, semid).unwrap(); let record = group.prepare_record_for_test(semid, 1).unwrap(); manager.clear_undo_for_setval(semid, 0); @@ -2421,7 +2632,7 @@ mod tests { let semid = insert_test_set(&mut manager, SemKey::new(66), &[0]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(semid, &[7]); - manager.ensure_undo_group_registered(&group).unwrap(); + manager.ensure_undo_group_registered(&group, semid).unwrap(); let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); manager.clear_undo_for_setval(semid, 0); @@ -2655,7 +2866,10 @@ mod tests { remove_test_set(&mut manager, id); - assert_eq!(manager.setall(token, &[7, 8]), Err(SystemError::EIDRM)); + assert_eq!( + manager.setall(token, &[7, 8], &mut SemWakeBatch::default()), + Err(SystemError::EIDRM) + ); } #[test] @@ -2672,7 +2886,10 @@ mod tests { new_id.data() & IpcIdAllocator::IPC_ID_IDX_MASK ); - assert_eq!(manager.setall(token, &[7, 8]), Err(SystemError::EIDRM)); + assert_eq!( + manager.setall(token, &[7, 8], &mut SemWakeBatch::default()), + Err(SystemError::EIDRM) + ); assert_eq!(sem_values(&manager, new_id), vec![3, 4]); } } diff --git a/kernel/src/ipc/sem_undo.rs b/kernel/src/ipc/sem_undo.rs index cf78ceb439..82f1d190a5 100644 --- a/kernel/src/ipc/sem_undo.rs +++ b/kernel/src/ipc/sem_undo.rs @@ -6,7 +6,7 @@ use alloc::{ use system_error::SystemError; use crate::{ - ipc::sem::{SemId, SemManager}, + ipc::sem::{SemId, SemManager, SemWakeBatch}, libs::spinlock::SpinLock, process::{namespace::ipc_namespace::IpcNamespace, pid::PidType, ProcessControlBlock}, }; @@ -27,9 +27,6 @@ pub struct SemUndoGroup { #[derive(Debug)] struct SemUndoGroupState { task_owners: usize, - /// Published once in the bound IPC namespace's manager registry. Clearing - /// records does not unregister a live group; only dead Weak entries expire. - registry_registered: bool, records: Vec, reserved_records: usize, absence_generation: u64, @@ -123,6 +120,15 @@ impl SemUndoGroupState { } impl SemUndoRecord { + /// A production record is published only after its group is associated + /// with the full semaphore-set ID. Clearing debt preserves that link. + pub(crate) fn was_existing(&self) -> bool { + matches!( + self.prepared_state, + PreparedSemUndoRecordState::Existing { .. } + ) + } + #[cfg(test)] fn new_live(semid: SemId, adjustments: Box<[i16]>) -> Self { Self { @@ -238,7 +244,6 @@ impl SemUndoGroup { ipc_ns: Arc::downgrade(ipc_ns), inner: SpinLock::new(SemUndoGroupState { task_owners: 1, - registry_registered: false, records: Vec::new(), reserved_records: 0, absence_generation: 0, @@ -251,16 +256,6 @@ impl SemUndoGroup { .map_err(|_| SystemError::ENOMEM) } - /// The bound namespace's manager lock serializes registration callers. - pub(crate) fn registry_registered(&self) -> bool { - self.inner.lock_irqsave().registry_registered - } - - /// Called only after the manager has successfully inserted its Weak entry. - pub(crate) fn mark_registry_registered(&self) { - self.inner.lock_irqsave().registry_registered = true; - } - pub(crate) fn verify_ipc_ns(&self, ipc_ns: &Arc) -> Result<(), SystemError> { let state = self.inner.lock_irqsave(); if state.task_owners == 0 || state.retired { @@ -620,6 +615,7 @@ fn replay_marked_records(pcb: &Arc, group: &Arc, group: &Arc Result { let ipcns = ProcessManager::current_ipcns(); + // Declared outside all manager-guard scopes: completion notifications are + // delivered after unlocking, including on early returns. + let mut wakes = SemWakeBatch::default(); match cmd { // Retrieve semaphore system information @@ -81,7 +84,7 @@ pub(super) fn do_kernel_semctl( // Remove the semaphore set SemCtlCmd::IpcRmid => { let mut guard = ipcns.sem.lock(); - guard.ipc_rmid(semid)?; + guard.ipc_rmid(semid, &mut wakes)?; Ok(0) } // Query a single semaphore @@ -93,7 +96,7 @@ pub(super) fn do_kernel_semctl( SemCtlCmd::SetVal => { let val = arg as u32 as i32; let mut guard = ipcns.sem.lock(); - guard.setval(semid, semnum, val)?; + guard.setval(semid, semnum, val, &mut wakes)?; Ok(0) } // Get values of all semaphores in the set @@ -131,7 +134,7 @@ pub(super) fn do_kernel_semctl( user_buffer_reader.copy_one_from_user(v, i * core::mem::size_of::())?; } let mut guard = ipcns.sem.lock(); - guard.setall(token, &vals)?; + guard.setall(token, &vals, &mut wakes)?; Ok(0) } // Invalid command diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index 8897048f4e..8311c10d46 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -2193,6 +2193,96 @@ TEST(SysVSem, WaitQueuesGrowForConstAndAlterOperations) { } } +TEST(SysVSem, RemoveWakesBothWaitQueuesInBulk) { + constexpr int kPairs = 16; + SemSet sem(2, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 1)); + std::vector> children; + for (int i = 0; i < kPairs * 2; ++i) { + const bool constant = i % 2 == 0; + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + struct sembuf op = {static_cast(constant ? 0 : 1), + static_cast(constant ? 0 : -1), SEM_UNDO}; + struct timespec timeout = {10, 0}; + int result = SemTimedOp(sem.id(), &op, 1, &timeout); + _exit(result == -1 && errno == EIDRM ? 0 : 115); + } + children.emplace_back(new ChildGuard(pid)); + } + ASSERT_TRUE(WaitForZcnt(sem.id(), 0, kPairs)); + ASSERT_TRUE(WaitForNcnt(sem.id(), 1, kPairs)); + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_RMID, 0)); + sem.release(); + for (auto& child : children) { + WaitChildOk(child.get()); + } +} + +TEST(SysVSem, SharedUndoGroupControlChangesStaySetLocal) { + for (int cmd : {SETVAL, SETALL, IPC_RMID}) { + SemSet target(1, IPC_CREAT | 0600); + SemSet other(1, IPC_CREAT | 0600); + ASSERT_TRUE(target.valid()); + ASSERT_TRUE(other.valid()); + int ready[2], release[2]; + ASSERT_EQ(0, pipe(ready)); + FdGuard ready_read(ready[0]), ready_write(ready[1]); + ASSERT_EQ(0, pipe(release)); + FdGuard release_read(release[0]), release_write(release[1]); + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + ready_read.Close(); + release_write.Close(); + char token = 1; + if (!SemUndoOpMustSucceed(target.id(), 0, 1) || + !SemUndoOpMustSucceed(other.id(), 0, 1) || + !WriteExact(ready_write.get(), &token, 1) || + !ReadExact(release_read.get(), &token, 1)) { + _exit(116); + } + if (cmd != IPC_RMID) { + // Existing records must remain associated after a debt clear. + if (!SemUndoOpMustSucceed(target.id(), 0, 1) || + !WriteExact(ready_write.get(), &token, 1) || + !ReadExact(release_read.get(), &token, 1)) { + _exit(117); + } + } + _exit(0); + } + ChildGuard child(pid); + ready_write.Close(); + release_read.Close(); + char token = 1; + ASSERT_TRUE(ReadExact(ready_read.get(), &token, 1)); + unsigned short value = 9; + const unsigned long arg = cmd == SETALL ? reinterpret_cast(&value) : 9; + ASSERT_EQ(0, SemCtl(target.id(), 0, cmd, arg)); + std::unique_ptr replacement; + if (cmd == IPC_RMID) { + target.release(); + replacement.reset(new SemSet(1, IPC_CREAT | 0600)); + ASSERT_TRUE(replacement->valid()); + ASSERT_EQ(0, SemCtl(replacement->id(), 0, SETVAL, 9)); + } + ASSERT_TRUE(WriteExact(release_write.get(), &token, 1)); + if (cmd != IPC_RMID) { + ASSERT_TRUE(ReadExact(ready_read.get(), &token, 1)); + EXPECT_EQ(10, SemCtl(target.id(), 0, GETVAL, 0)); + ASSERT_EQ(0, SemCtl(target.id(), 0, cmd, arg)); + ASSERT_TRUE(WriteExact(release_write.get(), &token, 1)); + } + WaitChildOk(&child); + EXPECT_EQ(0, SemCtl(other.id(), 0, GETVAL, 0)) << "unrelated debt must replay"; + EXPECT_EQ(9, SemCtl(replacement ? replacement->id() : target.id(), 0, GETVAL, 0)) + << "cleared or removed debt must not replay"; + } +} + TEST(SysVSem, IndependentUndoGroupsSteadyStateAndSetall) { constexpr int kWorkers = 32; std::vector> sets; From d3137229b13c9df7e7789ee66aa5869a01180943 Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 16:24:42 +0000 Subject: [PATCH 18/34] fix(ipc): reclaim unused semaphore undo registry storage Unlink retired groups using their existing records and release failed or canceled missing associations only when no published record or shared waiter remains. Preserve the original whole-group replay critical section. Reclaim empty registries without allocation and shrink sparse registries through unlocked fallible preparation and locked revalidation. Keep allocation failure and competing growth outside syscall success semantics. Add persistent-set undo churn and shrink revalidation coverage. Kernel and workspace builds, host capacity/failure checks, 70 Linux tests, 70 DragonOS tests and 20 full guest repeats passed. Signed-off-by: longjin --- kernel/src/ipc/sem.rs | 141 +++++++++++++++++- kernel/src/ipc/sem_undo.rs | 10 +- .../suites/normal/sysv_sem_semantics.cc | 63 ++++++++ 3 files changed, 212 insertions(+), 2 deletions(-) diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index 18e502ab20..493827c3a8 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -8,7 +8,7 @@ use alloc::{ sync::{Arc, Weak}, vec::Vec, }; -use core::fmt; +use core::{cell::Cell, fmt}; use hashbrown::HashMap; use system_error::SystemError; @@ -469,6 +469,34 @@ impl KernelSemSet { self.undo_groups.retain(|weak| weak.strong_count() != 0); } + /// Request/publish smaller storage without allocating under the manager + /// lock. Empty registries are moved into retired without installing spare. + /// The caller must drop both buffers after unlocking. + fn shrink_undo_registry_prepared( + &mut self, + spare: &mut Vec>, + retired: &mut Vec>, + ) -> usize { + debug_assert!(spare.is_empty()); + debug_assert!(retired.is_empty() && retired.capacity() == 0); + let len = self.undo_groups.len(); + if len == 0 { + core::mem::swap(&mut self.undo_groups, retired); + return 0; + } + if self.undo_groups.capacity() <= 4 || len > self.undo_groups.capacity() / 4 { + return 0; + } + if spare.capacity() < len { + return len.saturating_mul(2).max(4); + } + if spare.capacity() < self.undo_groups.capacity() { + spare.append(&mut self.undo_groups); + core::mem::swap(&mut self.undo_groups, spare); + } + 0 + } + fn try_allocate_sems(nsems: usize) -> Result, SystemError> { let mut sems = Vec::new(); sems.try_reserve_exact(nsems) @@ -774,6 +802,67 @@ impl SemManager { group.with_record_mut(semid, f) } + pub(crate) fn unregister_undo_group(&mut self, semid: SemId, group: &Arc) { + if let Ok(set) = self.get_by_semid_checked_mut(semid) { + let target = Arc::downgrade(group); + set.undo_groups.retain(|weak| !weak.ptr_eq(&target)); + } + } + + /// Reclaim a target registry, never allocating or freeing its buffer under + /// the namespace lock. Concurrent growth can make a shrink unnecessary or + /// consume the spare capacity; in either case leave the live registry intact. + pub(crate) fn shrink_undo_registry(ipcns: &Arc, semid: SemId) { + let mut spare = Vec::new(); + let mut retired = Vec::new(); + let needed = { + let mut manager = ipcns.sem.lock(); + let Ok(set) = manager.get_by_semid_checked_mut(semid) else { + return; + }; + set.shrink_undo_registry_prepared(&mut spare, &mut retired) + }; + if needed == 0 || spare.try_reserve_exact(needed).is_err() { + return; + } + // Allocation is best-effort reclamation, not part of syscall success. + let mut manager = ipcns.sem.lock(); + if let Ok(set) = manager.get_by_semid_checked_mut(semid) { + set.shrink_undo_registry_prepared(&mut spare, &mut retired); + } + } + + fn release_unused_undo_group( + ipcns: &Arc, + semid: SemId, + group: &Arc, + ) { + { + let mut manager = ipcns.sem.lock(); + let Ok(set) = manager.get_by_semid_checked(semid) else { + return; + }; + if group.with_record_mut(semid, |_| ()).is_some() + || set + .pending_const + .iter() + .chain(&set.pending_alter) + .any(|entry| { + entry + .undo_group + .as_ref() + .is_some_and(|owner| Arc::ptr_eq(owner, group)) + }) + { + return; + } + // Other Missing preparations outside the lock re-register before + // publication. Existing records (even zero debt) keep their link. + manager.unregister_undo_group(semid, group); + } + Self::shrink_undo_registry(ipcns, semid); + } + pub(crate) fn clear_undo_for_setval(&mut self, semid: SemId, semnum: usize) { let Ok(set) = self.get_by_semid_checked_mut(semid) else { return; @@ -1306,6 +1395,16 @@ impl SemManager { } else { None }; + let registered_missing = Cell::new(false); + // Declare before all entries and manager guards. Failed/timeout-only + // operations have no published record for final-owner exit to enumerate. + defer::defer!({ + if registered_missing.get() { + if let Some(group) = undo_group.as_ref() { + Self::release_unused_undo_group(ipcns, semid, group); + } + } + }); let mut immediate_scratch = SemopScratch::try_new(sops.len())?; let plain_prepared_entry = if has_undo { None @@ -1414,6 +1513,7 @@ impl SemManager { registry_capacity_needed = capacity; continue; } + registered_missing.set(true); } let mut record_slot = prepared_entry.undo_record.lock_irqsave(); let prepared_record = record_slot @@ -2061,6 +2161,45 @@ mod tests { assert!(SemManager::new().id2sem.is_empty()); } + #[test] + fn undo_registry_shrink_releases_empty_and_rechecks_concurrent_growth() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(152), &[0]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + set.undo_groups.reserve_exact(64); + set.undo_groups.push(Arc::downgrade(&group)); + let mut spare = Vec::new(); + let mut retired = Vec::new(); + let needed = set.shrink_undo_registry_prepared(&mut spare, &mut retired); + assert_eq!(needed, 4); + spare.try_reserve_exact(needed).unwrap(); + // Simulate new associations arriving during unlocked preparation. + for _ in 0..8 { + set.undo_groups.push(Arc::downgrade(&group)); + } + assert!(set.shrink_undo_registry_prepared(&mut spare, &mut retired) > 0); + assert_eq!(set.undo_groups.len(), 9); + assert_eq!(set.undo_groups.capacity(), 64); + set.undo_groups.truncate(1); + assert_eq!( + set.shrink_undo_registry_prepared(&mut spare, &mut retired), + 0 + ); + assert_eq!(set.undo_groups.len(), 1); + assert_eq!(set.undo_groups.capacity(), 4); + assert_eq!(spare.capacity(), 64); + // The spare is already allocated when another owner empties the set. + // It must not be installed back into the empty registry. + set.undo_groups.clear(); + assert_eq!( + set.shrink_undo_registry_prepared(&mut spare, &mut retired), + 0 + ); + assert_eq!(set.undo_groups.capacity(), 0); + assert_eq!(retired.capacity(), 4); + } + #[test] fn prepared_registry_growth_rechecks_registration_and_capacity() { let mut manager = SemManager::new(); diff --git a/kernel/src/ipc/sem_undo.rs b/kernel/src/ipc/sem_undo.rs index 82f1d190a5..a0e6a0c72d 100644 --- a/kernel/src/ipc/sem_undo.rs +++ b/kernel/src/ipc/sem_undo.rs @@ -618,7 +618,7 @@ fn replay_marked_records(pcb: &Arc, group: &Arc, group: &Arc> sets; + for (int i = 0; i < kSets; ++i) { + sets.emplace_back(new SemSet(1, IPC_CREAT | 0600)); + ASSERT_TRUE(sets.back()->valid()); + } + // Keep every set alive across repeated empty -> shared -> empty cycles. + for (int round = 0; round < 3; ++round) { + int ready[2], release[2]; + ASSERT_EQ(0, pipe(ready)); + FdGuard ready_read(ready[0]), ready_write(ready[1]); + ASSERT_EQ(0, pipe(release)); + FdGuard release_read(release[0]), release_write(release[1]); + std::vector> children; + for (int worker = 0; worker < kWorkers; ++worker) { + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + ready_read.Close(); + release_write.Close(); + for (auto& sem : sets) { + // This group has no published record for the set yet. + struct sembuf fail = {0, -32767, SEM_UNDO | IPC_NOWAIT}; + if (SemOp(sem->id(), &fail, 1) != -1 || errno != EAGAIN) { + _exit(118); + } + struct sembuf timed = {0, -32767, SEM_UNDO}; + struct timespec timeout = {0, 1000000}; + if (SemTimedOp(sem->id(), &timed, 1, &timeout) != -1 || errno != EAGAIN || + !SemUndoOpMustSucceed(sem->id(), 0, 1)) { + _exit(119); + } + } + char token = 1; + _exit(WriteExact(ready_write.get(), &token, 1) && + ReadExact(release_read.get(), &token, 1) ? 0 : 120); + } + children.emplace_back(new ChildGuard(pid)); + } + ready_write.Close(); + release_read.Close(); + char token = 1; + for (int i = 0; i < kWorkers; ++i) { + ASSERT_TRUE(ReadExact(ready_read.get(), &token, 1)); + } + for (auto& sem : sets) { + EXPECT_EQ(kWorkers, SemCtl(sem->id(), 0, GETVAL, 0)); + } + // Let all owners exit and replay without removing any set. + for (int i = 0; i < kWorkers; ++i) { + ASSERT_TRUE(WriteExact(release_write.get(), &token, 1)); + } + for (auto& child : children) { + WaitChildOk(child.get()); + } + for (auto& sem : sets) { + EXPECT_EQ(0, SemCtl(sem->id(), 0, GETVAL, 0)); + } + } +} + TEST(SysVSem, ConcurrentWorkers) { constexpr int kWorkers = 8; SemSet sem(1, IPC_CREAT | 0600); From 72b3749d62c36c15bbbdbdc19d8bf7424616af02 Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 16:49:46 +0000 Subject: [PATCH 19/34] fix(ipc): skip empty unshare installs and cache the maximum ID index Return no prepared install when unshare has no replacement state or undo detachment. Preserve validation and required SYSVSEM/NEWIPC work. Track the highest used index in the existing IPC allocator, using its bitmap only when removing the maximum. Make semaphore information queries read the cache without scanning the object table. Add allocator and syscall regressions. Validate kernel/workspace builds, four allocator tests, 50000 randomized allocator steps, installation failure injection, Linux and guest semaphore 72/72, 20 full guest repeats, and SHM 61 passed with four environment skips. Signed-off-by: longjin --- kernel/src/ipc/id.rs | 44 ++++++++++++++ kernel/src/ipc/sem.rs | 2 +- kernel/src/process/namespace/unshare.rs | 33 +++++++++-- .../suites/normal/sysv_sem_semantics.cc | 58 +++++++++++++++++++ 4 files changed, 131 insertions(+), 6 deletions(-) diff --git a/kernel/src/ipc/id.rs b/kernel/src/ipc/id.rs index 077bcc05ab..d89e01c60c 100644 --- a/kernel/src/ipc/id.rs +++ b/kernel/src/ipc/id.rs @@ -13,6 +13,8 @@ pub struct FixedIpcIdAllocator { next_idx: usize, seq: usize, last_idx: Option, + /// Highest currently allocated index, not the cyclic allocation cursor. + max_used_idx: Option, } pub type IpcIdAllocator = FixedIpcIdAllocator<32768, { bitmap::static_bitmap_size::<32768>() }>; @@ -46,6 +48,7 @@ impl FixedIpcIdAllocator FixedIpcIdAllocator FixedIpcIdAllocator Option { + self.max_used_idx + } + pub fn decode(raw: usize) -> Result { if raw > i32::MAX as usize { return Err(SystemError::EINVAL); @@ -148,4 +160,36 @@ mod tests { type InvalidAllocator = FixedIpcIdAllocator<64, 0>; assert_eq!(InvalidAllocator::new(64).unwrap_err(), SystemError::EINVAL); } + + #[test] + fn max_used_index_tracks_holes_wraparound_and_empty() { + let mut allocator = IpcIdAllocator::new(130).unwrap(); + assert_eq!(allocator.max_used_index(), None); + for idx in 0..130 { + assert_eq!(allocator.alloc().unwrap().idx, idx); + assert_eq!(allocator.max_used_index(), Some(idx)); + } + assert_eq!(allocator.alloc().unwrap_err(), SystemError::ENOSPC); + assert_eq!(allocator.max_used_index(), Some(129)); + allocator.free_idx(130); // Out-of-range free cannot alter the cache. + allocator.free_idx(64); + assert_eq!(allocator.max_used_index(), Some(129)); + for idx in (65..130).rev() { + allocator.free_idx(idx); + } + assert_eq!(allocator.max_used_index(), Some(63)); + // The cyclic allocator reuses the hole across a bitmap word boundary. + assert_eq!(allocator.alloc().unwrap().idx, 64); + assert_eq!(allocator.max_used_index(), Some(64)); + for idx in (0..=64).rev() { + allocator.free_idx(idx); + assert_eq!(allocator.max_used_index(), idx.checked_sub(1)); + } + allocator.free_idx(0); // Repeated free is harmless. + assert_eq!(allocator.max_used_index(), None); + let id = allocator.alloc().unwrap(); + assert_eq!(allocator.max_used_index(), Some(id.idx)); + allocator.free_idx(id.idx); // Models rollback of a reserved ID. + assert_eq!(allocator.max_used_index(), None); + } } diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index 493827c3a8..03aaa55ae1 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -984,7 +984,7 @@ impl SemManager { } fn current_max_index(&self) -> usize { - self.id2sem.keys().copied().max().unwrap_or(0) + self.id_allocator.max_used_index().unwrap_or(0) } /// # semget: create or look up a semaphore set diff --git a/kernel/src/process/namespace/unshare.rs b/kernel/src/process/namespace/unshare.rs index 8f9010d5d4..ce1427b0ad 100644 --- a/kernel/src/process/namespace/unshare.rs +++ b/kernel/src/process/namespace/unshare.rs @@ -26,9 +26,11 @@ pub fn ksys_unshare(flags: CloneFlags) -> Result<(), SystemError> { let new_fs = unshare_fs_struct(flags, ¤t_pcb, &fs_refs)?; let new_nsproxy = unshare_nsproxy_namespaces(flags, ¤t_pcb, new_cred.as_deref(), new_fs.as_ref())?; - let prepared = - prepare_unshare_install(¤t_pcb, flags, new_fs, new_nsproxy, new_cred, &fs_refs)?; - prepared.commit(¤t_pcb, &fs_refs)?; + if let Some(prepared) = + prepare_unshare_install(¤t_pcb, flags, new_fs, new_nsproxy, new_cred, &fs_refs)? + { + prepared.commit(¤t_pcb, &fs_refs)?; + } // TODO: 处理其他命名空间的 unshare 操作 // CLONE_FS, CLONE_FILES, CLONE_SIGHAND, CLONE_VM, CLONE_THREAD, CLONE_SYSVSEM, @@ -44,9 +46,14 @@ fn prepare_unshare_install( new_nsproxy: Option>, new_cred: Option>, fs_refs: &FsRefsReadGuard, -) -> Result { - let new_nsproxy = new_nsproxy.unwrap_or_else(|| current.nsproxy()); +) -> Result, SystemError> { let detach_sysvsem = flags.intersects(CloneFlags::CLONE_SYSVSEM | CloneFlags::CLONE_NEWIPC); + // Match Linux's conditional install: a no-op must not allocate RCU retire + // storage or republish the same nsproxy. SYSVSEM detachment is itself work. + if new_fs.is_none() && new_nsproxy.is_none() && new_cred.is_none() && !detach_sysvsem { + return Ok(None); + } + let new_nsproxy = new_nsproxy.unwrap_or_else(|| current.nsproxy()); PreparedNamespaceInstall::prepare_for_unshare( current, new_nsproxy, @@ -55,6 +62,7 @@ fn prepare_unshare_install( detach_sysvsem, fs_refs, ) + .map(Some) } #[inline(always)] @@ -203,6 +211,19 @@ mod tests { ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()) } + #[test] + fn no_state_unshare_does_not_prepare_an_install() { + let pcb = test_pcb(); + let fs_refs = lock_fs_refs_copy(); + for flags in [CloneFlags::empty(), CloneFlags::CLONE_FS] { + assert!( + prepare_unshare_install(&pcb, flags, None, None, None, &fs_refs) + .unwrap() + .is_none() + ); + } + } + #[test] fn unshare_sysvsem_detaches_even_when_ipc_namespace_is_unchanged() { let pcb = test_pcb(); @@ -212,6 +233,7 @@ mod tests { let prepared = prepare_unshare_install(&pcb, CloneFlags::CLONE_SYSVSEM, None, None, None, &fs_refs) + .unwrap() .unwrap(); prepared.commit(&pcb, &fs_refs).unwrap(); @@ -243,6 +265,7 @@ mod tests { None, &fs_refs, ) + .unwrap() .unwrap(); prepared.commit(&pcb, &fs_refs).unwrap(); diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index f57e983d1b..3e585a77d9 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -4,6 +4,8 @@ #include +#include + #include #include #include @@ -554,6 +556,62 @@ TEST(SysVSem, IpcInfoAndSemInfo) { EXPECT_EQ(before.semaem + 5, info.semaem) << "SEM_INFO semaem is the semaphore count"; } +TEST(SysVSem, InfoMaximumIndexTracksCreateAndRemove) { + struct seminfo info = {}; + const int baseline = SemCtl(0, 0, SEM_INFO, reinterpret_cast(&info)); + ASSERT_GE(baseline, 0); + const int baseline_count = info.semusz; + std::vector> sets; + auto expect_max = [&]() { + int expected = baseline; + for (auto& sem : sets) { + // Linux's default SysV IPC index encoding, also used by DragonOS. + expected = std::max(expected, sem->id() & 0x7fff); + } + EXPECT_EQ(expected, SemCtl(0, 0, IPC_INFO, reinterpret_cast(&info))); + EXPECT_EQ(expected, SemCtl(0, 0, SEM_INFO, reinterpret_cast(&info))); + EXPECT_EQ(baseline_count + static_cast(sets.size()), info.semusz); + }; + for (int i = 0; i < 65; ++i) { + sets.emplace_back(new SemSet(1, IPC_CREAT | 0600)); + ASSERT_TRUE(sets.back()->valid()); + expect_max(); + } + std::sort(sets.begin(), sets.end(), [](const auto& a, const auto& b) { + return (a->id() & 0x7fff) < (b->id() & 0x7fff); + }); + sets.erase(sets.begin()); // Nonmaximum removal. + expect_max(); + while (!sets.empty()) { + sets.pop_back(); // Maximum removal, including bitmap word boundaries. + expect_max(); + } +} + +TEST(SysVSem, NoopUnsharePreservesUndoAttachment) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + if (!SemUndoOpMustSucceed(sem.id(), 0, 1)) { + _exit(121); + } + // fork gave this child a private fs_struct. Neither operation should + // detach its undo group, while normal exit must still replay the debt. + for (int i = 0; i < 32; ++i) { + if (syscall(SYS_unshare, 0) != 0 || syscall(SYS_unshare, CLONE_FS) != 0 || + SemCtl(sem.id(), 0, GETVAL, 0) != 1) { + _exit(122); + } + } + _exit(0); + } + ChildGuard child(pid); + WaitChildOk(&child); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); +} + TEST(SysVSem, KeyedCreateRemovalRestoresAccountingAndAllowsReuse) { struct seminfo before = {}; ASSERT_GE(SemCtl(0, 0, SEM_INFO, reinterpret_cast(&before)), 0); From 8d1e231278175290478013e9fa4057101026ae12 Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 17:31:29 +0000 Subject: [PATCH 20/34] fix(namespace): make prepared fs copies fallible Signed-off-by: longjin --- kernel/src/process/namespace/nsproxy.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kernel/src/process/namespace/nsproxy.rs b/kernel/src/process/namespace/nsproxy.rs index 57c4c99753..e4bb27dab4 100644 --- a/kernel/src/process/namespace/nsproxy.rs +++ b/kernel/src/process/namespace/nsproxy.rs @@ -368,7 +368,8 @@ impl PreparedNamespaceInstall { return Err(SystemError::EINVAL); } let current_fs = tsk.fs_struct(); - new_fs = Some(Arc::new((*current_fs).clone())); + new_fs = + Some(Arc::try_new((*current_fs).clone()).map_err(|_| SystemError::ENOMEM)?); } let prepared_fs = new_fs From 2c64d686085a517127fa0dfb8e6ca01352fb5472 Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 17:31:29 +0000 Subject: [PATCH 21/34] fix(ipc): prepare semaphore storage outside the lock and unlink waiters directly Signed-off-by: longjin --- kernel/src/ipc/sem.rs | 584 ++++++++++++++---- kernel/src/ipc/sem_undo.rs | 2 +- kernel/src/ipc/syscall/sys_semget.rs | 5 +- .../suites/normal/sysv_sem_semantics.cc | 87 +++ 4 files changed, 550 insertions(+), 128 deletions(-) diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index 03aaa55ae1..37fb91ef6e 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -4,7 +4,6 @@ // behavior, including SEM_UNDO lifecycle accounting. use alloc::{ - collections::VecDeque, sync::{Arc, Weak}, vec::Vec, }; @@ -252,13 +251,139 @@ struct SemBlockedOp { #[derive(Debug)] enum SemQueueStatus { - Queued(SemBlockedOp), + Queued { + blocker: SemBlockedOp, + links: Option, + }, Completed { result: Result, next_wake: Option>, }, } +#[derive(Debug)] +struct SemWaitLinks { + prev: Option>, + next: Option>, +} + +/// Set-private FIFO. All structural changes require the namespace manager lock. +/// Strong forward / weak backward links avoid cycles; each operation holds at +/// most one entry status lock at a time. +#[derive(Debug, Default)] +struct SemWaitQueue { + head: Option>, + tail: Option>, +} + +impl SemWaitQueue { + fn push_back(&mut self, entry: Arc) { + { + let mut status = entry.status.lock(); + let SemQueueStatus::Queued { links, .. } = &mut *status else { + panic!("cannot enqueue completed semaphore operation"); + }; + assert!(links.is_none(), "semaphore operation already linked"); + *links = Some(SemWaitLinks { + prev: self.tail.as_ref().map(Arc::downgrade), + next: None, + }); + } + if let Some(tail) = self.tail.take() { + let mut status = tail.status.lock(); + let SemQueueStatus::Queued { + links: Some(links), .. + } = &mut *status + else { + panic!("semaphore queue tail is not linked"); + }; + links.next = Some(entry.clone()); + } else { + self.head = Some(entry.clone()); + } + self.tail = Some(entry); + } + + fn remove(&mut self, entry: &Arc) -> bool { + let links = { + let mut status = entry.status.lock(); + match &mut *status { + SemQueueStatus::Queued { links, .. } => links.take(), + SemQueueStatus::Completed { .. } => None, + } + }; + let Some(SemWaitLinks { prev, next }) = links else { + return false; + }; + let prev = prev.map(|weak| weak.upgrade().expect("linked predecessor is live")); + if let Some(prev) = prev.as_ref() { + let mut status = prev.status.lock(); + let SemQueueStatus::Queued { + links: Some(links), .. + } = &mut *status + else { + panic!("semaphore predecessor is not linked"); + }; + links.next = next.clone(); + } else { + debug_assert!(self + .head + .as_ref() + .is_some_and(|head| Arc::ptr_eq(head, entry))); + self.head = next.clone(); + } + if let Some(next) = next { + let mut status = next.status.lock(); + let SemQueueStatus::Queued { + links: Some(links), .. + } = &mut *status + else { + panic!("semaphore successor is not linked"); + }; + links.prev = prev.as_ref().map(Arc::downgrade); + } else { + self.tail = prev; + } + true + } + + fn iter(&self) -> SemWaitIter { + SemWaitIter(self.head.clone()) + } + + #[cfg(test)] + fn is_empty(&self) -> bool { + self.head.is_none() + } +} + +impl Drop for SemWaitQueue { + fn drop(&mut self) { + // Detach iteratively, including namespace teardown, never recursively + // destroy an arbitrarily long chain of Arc-owned entries. + while let Some(entry) = self.head.clone() { + self.remove(&entry); + } + } +} + +struct SemWaitIter(Option>); + +impl Iterator for SemWaitIter { + type Item = Arc; + + fn next(&mut self) -> Option { + let entry = self.0.take()?; + self.0 = match &*entry.status.lock() { + SemQueueStatus::Queued { + links: Some(links), .. + } => links.next.clone(), + _ => None, + }; + Some(entry) + } +} + /// An Arc-owned queue entry shared by the set and the blocked caller. #[derive(Debug)] struct SemQueueEntry { @@ -300,7 +425,10 @@ impl SemQueueEntry { undo_group, undo_record: SpinLock::new(undo_record), waker, - status: SpinLock::new(SemQueueStatus::Queued(blocker)), + status: SpinLock::new(SemQueueStatus::Queued { + blocker, + links: None, + }), } } @@ -333,15 +461,18 @@ impl SemQueueEntry { fn completed_result(&self) -> Option> { match &*self.status.lock() { - SemQueueStatus::Queued(_) => None, + SemQueueStatus::Queued { .. } => None, SemQueueStatus::Completed { result, .. } => Some(result.clone()), } } fn update_blocker(&self, blocker: SemBlockedOp) { let mut status = self.status.lock(); - if matches!(&*status, SemQueueStatus::Queued(_)) { - *status = SemQueueStatus::Queued(blocker); + if let SemQueueStatus::Queued { + blocker: current, .. + } = &mut *status + { + *current = blocker; } } @@ -350,6 +481,10 @@ impl SemQueueEntry { if matches!(&*status, SemQueueStatus::Completed { .. }) { return false; } + assert!( + matches!(&*status, SemQueueStatus::Queued { links: None, .. }), + "semaphore operation must be unlinked before completion" + ); *status = SemQueueStatus::Completed { result, next_wake: None, @@ -360,7 +495,7 @@ impl SemQueueEntry { fn is_waiting_on(&self, semnum: usize, wait_type: SemWaitType) -> bool { matches!( &*self.status.lock(), - SemQueueStatus::Queued(blocker) + SemQueueStatus::Queued { blocker, .. } if blocker.semnum == semnum && blocker.wait_type == wait_type ) } @@ -395,7 +530,7 @@ impl SemWakeBatch { while let Some(entry) = self.head.take() { self.head = match &mut *entry.status.lock() { SemQueueStatus::Completed { next_wake, .. } => next_wake.take(), - SemQueueStatus::Queued(_) => unreachable!("wake batch contains queued entry"), + SemQueueStatus::Queued { .. } => unreachable!("wake batch contains queued entry"), }; // Release the status lock before entering the scheduler. Detaching // each link also prevents recursive Arc destruction for large batches. @@ -431,9 +566,9 @@ pub struct KernelSemSet { /// Time of the last metadata change pub sem_ctime: i64, /// Pending operation groups containing only zero-wait operations - pending_const: VecDeque>, + pending_const: SemWaitQueue, /// Pending operation groups containing at least one altering operation - pending_alter: VecDeque>, + pending_alter: SemWaitQueue, } impl KernelSemSet { @@ -512,8 +647,8 @@ impl KernelSemSet { sems, sem_otime: 0, sem_ctime: PosixTimeSpec::now().tv_sec, - pending_const: VecDeque::new(), - pending_alter: VecDeque::new(), + pending_const: SemWaitQueue::default(), + pending_alter: SemWaitQueue::default(), } } @@ -525,49 +660,17 @@ impl KernelSemSet { } } - /// Return the required capacity without publishing anything if the caller - /// must prepare storage outside the namespace lock. `spare` is empty and - /// retains the old allocation after a swap, to be freed outside the lock. - fn enqueue_waiter_prepared( - &mut self, - waiter: Arc, - spare: &mut VecDeque>, - ) -> Result<(), usize> { - debug_assert!(spare.is_empty()); + /// The prepared entry itself owns the queue links; publication cannot allocate. + fn enqueue_waiter(&mut self, waiter: Arc) { let queue = match Self::pending_queue_for(&waiter.sops) { SemPendingQueue::Const => &mut self.pending_const, SemPendingQueue::Alter => &mut self.pending_alter, }; - if queue.len() == queue.capacity() { - if spare.capacity() <= queue.len() { - return Err(queue.len().saturating_mul(2).max(4)); - } - // Capacity covers the existing entries and the new waiter. Moving - // Arc handles preserves FIFO order and performs no allocation. - spare.extend(queue.drain(..)); - core::mem::swap(queue, spare); - } queue.push_back(waiter); - Ok(()) - } - - #[cfg(test)] - fn enqueue_waiter(&mut self, waiter: Arc) -> Result<(), SystemError> { - let mut spare = VecDeque::new(); - if let Err(capacity) = self.enqueue_waiter_prepared(waiter.clone(), &mut spare) { - spare - .try_reserve(capacity) - .map_err(|_| SystemError::ENOMEM)?; - self.enqueue_waiter_prepared(waiter, &mut spare).unwrap(); - } - Ok(()) } fn remove_waiter(&mut self, target: &Arc) { - self.pending_const - .retain(|entry| !Arc::ptr_eq(entry, target)); - self.pending_alter - .retain(|entry| !Arc::ptr_eq(entry, target)); + self.remove_pending(Self::pending_queue_for(&target.sops), target); } #[cfg(test)] @@ -575,33 +678,28 @@ impl KernelSemSet { self.pending_const.is_empty() && self.pending_alter.is_empty() } - fn pending_len(&self, queue: SemPendingQueue) -> usize { + fn pending_iter(&self, queue: SemPendingQueue) -> SemWaitIter { match queue { - SemPendingQueue::Const => self.pending_const.len(), - SemPendingQueue::Alter => self.pending_alter.len(), + SemPendingQueue::Const => self.pending_const.iter(), + SemPendingQueue::Alter => self.pending_alter.iter(), } } - fn pending_entry(&self, queue: SemPendingQueue, index: usize) -> Arc { + fn remove_pending(&mut self, queue: SemPendingQueue, entry: &Arc) { match queue { - SemPendingQueue::Const => self.pending_const[index].clone(), - SemPendingQueue::Alter => self.pending_alter[index].clone(), - } - } - - fn remove_pending(&mut self, queue: SemPendingQueue, index: usize) { - match queue { - SemPendingQueue::Const => self.pending_const.remove(index), - SemPendingQueue::Alter => self.pending_alter.remove(index), + SemPendingQueue::Const => self.pending_const.remove(entry), + SemPendingQueue::Alter => self.pending_alter.remove(entry), }; } /// Publish removal under the manager lock; the caller wakes after unlocking. fn complete_all_removed(&mut self, wakes: &mut SemWakeBatch) { - for entry in self.pending_const.drain(..) { + for entry in self.pending_const.iter() { + self.pending_const.remove(&entry); wakes.complete(entry, Err(SystemError::EIDRM)); } - for entry in self.pending_alter.drain(..) { + for entry in self.pending_alter.iter() { + self.pending_alter.remove(&entry); wakes.complete(entry, Err(SystemError::EIDRM)); } } @@ -846,7 +944,7 @@ impl SemManager { || set .pending_const .iter() - .chain(&set.pending_alter) + .chain(set.pending_alter.iter()) .any(|entry| { entry .undo_group @@ -987,19 +1085,58 @@ impl SemManager { self.id_allocator.max_used_index().unwrap_or(0) } - /// # semget: create or look up a semaphore set + /// Create or look up a set. Storage preparation and disposal must happen + /// outside the namespace spinlock, including when another creator wins. pub fn semget( - &mut self, + ipcns: &Arc, key: SemKey, nsems: usize, semflg: SemFlags, ) -> Result { + let mut sems = Vec::new(); + let mut id_spare = HashMap::new(); + let mut key_spare = HashMap::new(); + loop { + let mut manager = ipcns.sem.lock(); + if let Some(id) = manager.lookup_semget(key, nsems, semflg)? { + return Ok(id); + } + if sems.is_empty() { + drop(manager); + sems = KernelSemSet::try_allocate_sems(nsems)?; + continue; + } + if let Err((id_capacity, key_capacity)) = + manager.install_create_tables(key, &mut id_spare, &mut key_spare) + { + drop(manager); + id_spare + .try_reserve(id_capacity) + .map_err(|_| SystemError::ENOMEM)?; + key_spare + .try_reserve(key_capacity) + .map_err(|_| SystemError::ENOMEM)?; + continue; + } + return manager.create_prepared(key, semflg, &mut sems); + } + } + + /// None means creation is currently permitted, not a reservation. Call + /// again after unlocked preparation to recheck key races and quotas. + fn lookup_semget( + &self, + key: SemKey, + nsems: usize, + semflg: SemFlags, + ) -> Result, SystemError> { if nsems > SEMMSL { return Err(SystemError::EINVAL); } if key == IPC_PRIVATE { - return self.create(key, nsems, semflg); + self.validate_create(nsems)?; + return Ok(None); } if let Some(&id) = self.key2id.get(&key) { @@ -1016,21 +1153,17 @@ impl SemManager { semflg.bits() & SemFlags::PERM_MASK.bits(), &target_user_ns, )?; - return Ok(id.data()); + return Ok(Some(id.data())); } if !semflg.contains(SemFlags::IPC_CREAT) { return Err(SystemError::ENOENT); } - self.create(key, nsems, semflg) + self.validate_create(nsems)?; + Ok(None) } - fn create( - &mut self, - key: SemKey, - nsems: usize, - semflg: SemFlags, - ) -> Result { + fn validate_create(&self, nsems: usize) -> Result { if nsems == 0 { return Err(SystemError::EINVAL); } @@ -1044,17 +1177,58 @@ impl SemManager { if total_after > SEMMNS { return Err(SystemError::ENOSPC); } + Ok(total_after) + } - self.id2sem - .try_reserve(1) - .map_err(|_| SystemError::ENOMEM)?; - if key != IPC_PRIVATE { - self.key2id - .try_reserve(1) - .map_err(|_| SystemError::ENOMEM)?; + /// Install preallocated tables only when BOTH have enough room. Rehashing + /// still takes O(n) under the lock, but never allocates. The emptied old + /// tables remain in the caller's spares for disposal after unlocking. + fn install_create_tables( + &mut self, + key: SemKey, + id_spare: &mut HashMap, + key_spare: &mut HashMap, + ) -> Result<(), (usize, usize)> { + debug_assert!(id_spare.is_empty() && key_spare.is_empty()); + let grow_ids = self.id2sem.capacity() == self.id2sem.len(); + let grow_keys = key != IPC_PRIVATE && self.key2id.capacity() == self.key2id.len(); + let needed_ids = if grow_ids && id_spare.capacity() <= self.id2sem.len() { + self.id2sem.len().saturating_mul(2).max(4) + } else { + 0 + }; + let needed_keys = if grow_keys && key_spare.capacity() <= self.key2id.len() { + self.key2id.len().saturating_mul(2).max(4) + } else { + 0 + }; + if needed_ids != 0 || needed_keys != 0 { + return Err((needed_ids, needed_keys)); + } + if grow_ids { + core::mem::swap(&mut self.id2sem, id_spare); + for (id, set) in id_spare.drain() { + self.id2sem.insert(id, set); + } + } + if grow_keys { + core::mem::swap(&mut self.key2id, key_spare); + for (key, id) in key_spare.drain() { + self.key2id.insert(key, id); + } } - let sems = KernelSemSet::try_allocate_sems(nsems)?; + Ok(()) + } + fn create_prepared( + &mut self, + key: SemKey, + semflg: SemFlags, + sems: &mut Vec, + ) -> Result { + let total_after = self.validate_create(sems.len())?; + debug_assert!(self.id2sem.capacity() > self.id2sem.len()); + debug_assert!(key == IPC_PRIVATE || self.key2id.capacity() > self.key2id.len()); let ipc_id = self.id_allocator.alloc()?; let sem_id = SemId::new(ipc_id.raw); let current_cred = ProcessManager::current_pcb().cred(); @@ -1065,7 +1239,7 @@ impl SemManager { semflg.bits() & SemFlags::PERM_MASK.bits(), ipc_id.seq, ); - let set = KernelSemSet::new(kern_ipc_perm, sems); + let set = KernelSemSet::new(kern_ipc_perm, core::mem::take(sems)); if key != IPC_PRIVATE { self.key2id.insert(key, sem_id); @@ -1076,6 +1250,32 @@ impl SemManager { Ok(sem_id.data()) } + #[cfg(test)] + pub(crate) fn semget_for_test( + &mut self, + key: SemKey, + nsems: usize, + flags: SemFlags, + ) -> Result { + if let Some(id) = self.lookup_semget(key, nsems, flags)? { + return Ok(id); + } + let mut sems = KernelSemSet::try_allocate_sems(nsems)?; + let mut ids = HashMap::new(); + let mut keys = HashMap::new(); + if let Err((id_capacity, key_capacity)) = + self.install_create_tables(key, &mut ids, &mut keys) + { + ids.try_reserve(id_capacity) + .map_err(|_| SystemError::ENOMEM)?; + keys.try_reserve(key_capacity) + .map_err(|_| SystemError::ENOMEM)?; + self.install_create_tables(key, &mut ids, &mut keys) + .unwrap(); + } + self.create_prepared(key, flags, &mut sems) + } + /// Simulate sops in order without changing shared semaphore values or undo records. fn simulate_semop( set: &KernelSemSet, @@ -1162,15 +1362,13 @@ impl SemManager { queue: SemPendingQueue, wakes: &mut SemWakeBatch, ) -> bool { - let mut index = 0; - while index < set.pending_len(queue) { - let entry = set.pending_entry(queue, index); - + // Iterator captures the successor before the current entry is unlinked. + for entry in set.pending_iter(queue) { if let Some(group) = entry.undo_group.as_ref() { let result = { let mut record_slot = entry.undo_record.lock_irqsave(); let Some(record) = record_slot.take() else { - set.remove_pending(queue, index); + set.remove_pending(queue, &entry); wakes.complete(entry.clone(), Err(SystemError::EINVAL)); continue; }; @@ -1193,15 +1391,15 @@ impl SemManager { match result { Ok(Some(changed)) => { - set.remove_pending(queue, index); + set.remove_pending(queue, &entry); wakes.complete(entry.clone(), Ok(0)); if changed { return true; } } - Ok(None) => index += 1, + Ok(None) => {} Err(error) => { - set.remove_pending(queue, index); + set.remove_pending(queue, &entry); wakes.complete(entry.clone(), Err(error)); } } @@ -1213,22 +1411,21 @@ impl SemManager { Ok(SemopOutcome::Ready(simulation)) => { let changed = Self::commit_semop(set, simulation, &scratch, entry.pid.clone(), None); - set.remove_pending(queue, index); + set.remove_pending(queue, &entry); wakes.complete(entry.clone(), Ok(0)); if changed { return true; } } Ok(SemopOutcome::Blocked(blocker)) if blocker.nowait => { - set.remove_pending(queue, index); + set.remove_pending(queue, &entry); wakes.complete(entry.clone(), Err(SystemError::EAGAIN_OR_EWOULDBLOCK)); } Ok(SemopOutcome::Blocked(blocker)) => { entry.update_blocker(blocker); - index += 1; } Err(error) => { - set.remove_pending(queue, index); + set.remove_pending(queue, &entry); wakes.complete(entry.clone(), Err(error)); } } @@ -1428,20 +1625,10 @@ impl SemManager { }; let mut wakes = SemWakeBatch::default(); - let mut queue_spare = VecDeque::new(); - let mut queue_capacity_needed = 0; let mut registry_spare = Vec::new(); let mut registry_capacity_needed = 0; let entry = loop { - // Only a genuinely blocking operation requests queue storage. - // Revalidate the set and re-simulate after dropping the lock: a - // wakeup, RMID, or another enqueue may have changed the outcome. - if queue_capacity_needed != 0 { - queue_spare - .try_reserve(queue_capacity_needed) - .map_err(|_| SystemError::ENOMEM)?; - queue_capacity_needed = 0; - } + // Revalidate after unlocked undo-registry preparation. if registry_capacity_needed != 0 { registry_spare .try_reserve(registry_capacity_needed) @@ -1563,12 +1750,7 @@ impl SemManager { } drop(record_slot); prepared_entry.update_blocker(blocker); - if let Err(capacity) = - set.enqueue_waiter_prepared(prepared_entry.clone(), &mut queue_spare) - { - queue_capacity_needed = capacity; - continue; - } + set.enqueue_waiter(prepared_entry.clone()); break prepared_entry; } } @@ -1591,19 +1773,13 @@ impl SemManager { .as_ref() .expect("plain queued semop entry is preallocated"); prepared_entry.update_blocker(blocker); - if let Err(capacity) = - set.enqueue_waiter_prepared(prepared_entry.clone(), &mut queue_spare) - { - queue_capacity_needed = capacity; - continue; - } + set.enqueue_waiter(prepared_entry.clone()); break prepared_entry.clone(); } } } }; - drop(queue_spare); drop(registry_spare); wakes.wake_all(); if let Some(timer) = timer.as_ref() { @@ -1910,10 +2086,56 @@ mod tests { ) -> Arc { let (_waiter, waker) = Waiter::new_pair(); let entry = Arc::new(SemQueueEntry::new(sops, None, waker, blocker)); - set.enqueue_waiter(entry.clone()).unwrap(); + set.enqueue_waiter(entry.clone()); entry } + #[test] + fn pending_links_remove_middle_head_tail_and_preserve_fifo() { + let mut manager = SemManager::new(); + let id = insert_test_set(&mut manager, SemKey::new(153), &[0]); + let set = manager.get_by_semid_checked_mut(id).unwrap(); + let ops = [PosixSemBuf { + sem_num: 0, + sem_op: -1, + sem_flg: 0, + }]; + let mut entries = Vec::new(); + for _ in 0..4 { + entries.push(enqueue_test_waiter( + set, + &ops, + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: false, + }, + )); + } + set.remove_waiter(&entries[1]); + set.remove_waiter(&entries[1]); // An already detached node is harmless. + let remaining: Vec<_> = set.pending_alter.iter().collect(); + assert_eq!(remaining.len(), 3); + for (actual, expected) in remaining.iter().zip([0, 2, 3]) { + assert!(Arc::ptr_eq(actual, &entries[expected])); + } + set.remove_waiter(&entries[0]); + set.remove_waiter(&entries[3]); + assert!(Arc::ptr_eq( + set.pending_alter.head.as_ref().unwrap(), + &entries[2] + )); + assert!(Arc::ptr_eq( + set.pending_alter.tail.as_ref().unwrap(), + &entries[2] + )); + set.remove_waiter(&entries[2]); + assert!(set.pending_is_empty()); + for entry in entries { + assert!(entry.complete(Err(SystemError::EINTR))); + } + } + #[test] fn last_owner_replays_adjustment_with_clamp_and_removes_record() { let ipc_ns = test_ipc_ns(); @@ -2013,7 +2235,7 @@ mod tests { waker, blocker, )); - set.enqueue_waiter(entry.clone()).unwrap(); + set.enqueue_waiter(entry.clone()); (semid, entry) }; let (pcb, group) = test_pcb_with_group(&ipc_ns); @@ -2161,6 +2383,121 @@ mod tests { assert!(SemManager::new().id2sem.is_empty()); } + #[test] + fn create_tables_require_both_spares_and_recheck_growth() { + let mut manager = SemManager::new(); + let key = SemKey::new(153); + let mut ids = HashMap::new(); + let mut keys = HashMap::new(); + ids.try_reserve(4).unwrap(); + assert_eq!( + manager.install_create_tables(key, &mut ids, &mut keys), + Err((0, 4)) + ); + assert_eq!(manager.id2sem.capacity(), 0); + assert_eq!(manager.key2id.capacity(), 0); + keys.try_reserve(4).unwrap(); + manager + .install_create_tables(key, &mut ids, &mut keys) + .unwrap(); + while manager.id2sem.len() < manager.id2sem.capacity() { + let next_key = SemKey::new(200 + manager.id2sem.len()); + insert_test_set(&mut manager, next_key, &[3]); + } + let count = manager.id2sem.len(); + assert_eq!(manager.key2id.len(), count); + ids.try_reserve(count + 1).unwrap(); + keys.try_reserve(count + 1).unwrap(); + // Competing creations can consume the prepared headroom before the + // caller reacquires the lock. Neither live table may be moved yet. + while manager.id2sem.len() < ids.capacity() { + let next_key = SemKey::new(200 + manager.id2sem.len()); + insert_test_set(&mut manager, next_key, &[3]); + } + let live_count = manager.id2sem.len(); + let capacities = (manager.id2sem.capacity(), manager.key2id.capacity()); + let (need_ids, need_keys) = manager + .install_create_tables(key, &mut ids, &mut keys) + .unwrap_err(); + assert_eq!( + capacities, + (manager.id2sem.capacity(), manager.key2id.capacity()) + ); + assert_eq!(manager.id2sem.len(), live_count); + ids.try_reserve(need_ids).unwrap(); + keys.try_reserve(need_keys).unwrap(); + manager + .install_create_tables(key, &mut ids, &mut keys) + .unwrap(); + assert_eq!(manager.id2sem.len(), live_count); + assert_eq!(manager.key2id.len(), live_count); + assert!(manager.id2sem.capacity() > live_count); + assert!(manager.key2id.capacity() > live_count); + assert!(ids.is_empty() && keys.is_empty()); + assert_eq!((ids.capacity(), keys.capacity()), capacities); + for id in manager.key2id.values() { + assert_eq!(manager.get_by_semid_checked(*id).unwrap().sems[0].val, 3); + } + } + + #[test] + fn semget_lookup_validates_before_preparing_storage() { + let mut manager = SemManager::new(); + let key = SemKey::new(154); + insert_test_set(&mut manager, key, &[0]); + assert_eq!( + manager.lookup_semget(key, SEMMSL + 1, SemFlags::IPC_CREAT | SemFlags::IPC_EXCL), + Err(SystemError::EINVAL) + ); + assert_eq!( + manager.lookup_semget(key, 2, SemFlags::IPC_CREAT | SemFlags::IPC_EXCL), + Err(SystemError::EEXIST) + ); + assert_eq!( + manager.lookup_semget(key, 2, SemFlags::empty()), + Err(SystemError::EINVAL) + ); + assert_eq!( + manager.lookup_semget(SemKey::new(155), 0, SemFlags::empty()), + Err(SystemError::ENOENT) + ); + assert_eq!( + manager.lookup_semget(SemKey::new(155), 0, SemFlags::IPC_CREAT), + Err(SystemError::EINVAL) + ); + assert_eq!( + manager.lookup_semget(IPC_PRIVATE, 1, SemFlags::IPC_EXCL), + Ok(None) + ); + manager.total_sems = SEMMNS; + assert_eq!( + manager.lookup_semget(IPC_PRIVATE, 1, SemFlags::empty()), + Err(SystemError::ENOSPC) + ); + assert_eq!( + manager.lookup_semget(IPC_PRIVATE, 0, SemFlags::empty()), + Err(SystemError::EINVAL) + ); + } + + #[test] + fn private_create_never_prepares_key_table() { + let mut manager = SemManager::new(); + let mut ids = HashMap::new(); + let mut keys = HashMap::new(); + assert_eq!( + manager.install_create_tables(IPC_PRIVATE, &mut ids, &mut keys), + Err((4, 0)) + ); + ids.try_reserve(4).unwrap(); + manager + .install_create_tables(IPC_PRIVATE, &mut ids, &mut keys) + .unwrap(); + assert!(manager.id2sem.capacity() > 0); + assert_eq!(manager.key2id.capacity(), 0); + assert_eq!(keys.capacity(), 0); + } + #[test] fn undo_registry_shrink_releases_empty_and_rechecks_concurrent_growth() { let mut manager = SemManager::new(); @@ -2311,7 +2648,7 @@ mod tests { }, )); let set = manager.get_by_semid_checked_mut(semid).unwrap(); - set.enqueue_waiter(entry.clone()).unwrap(); + set.enqueue_waiter(entry.clone()); set.sems[0].val = 1; manager.update_queue_for_test(semid); @@ -2825,8 +3162,7 @@ mod tests { manager .get_by_semid_checked_mut(semid) .unwrap() - .enqueue_waiter(entry.clone()) - .unwrap(); + .enqueue_waiter(entry.clone()); entry } diff --git a/kernel/src/ipc/sem_undo.rs b/kernel/src/ipc/sem_undo.rs index a0e6a0c72d..b207ba50f2 100644 --- a/kernel/src/ipc/sem_undo.rs +++ b/kernel/src/ipc/sem_undo.rs @@ -823,7 +823,7 @@ mod tests { let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); let mut manager = ipc_ns.sem.lock(); let semid = manager - .semget( + .semget_for_test( crate::ipc::sem::IPC_PRIVATE, 1, crate::ipc::sem::SemFlags::IPC_CREAT, diff --git a/kernel/src/ipc/syscall/sys_semget.rs b/kernel/src/ipc/syscall/sys_semget.rs index d0cadbeac8..838d668e4f 100644 --- a/kernel/src/ipc/syscall/sys_semget.rs +++ b/kernel/src/ipc/syscall/sys_semget.rs @@ -3,7 +3,7 @@ use crate::arch::interrupt::TrapFrame; use crate::syscall::table::FormattedSyscallParam; use crate::{ arch::syscall::nr::SYS_SEMGET, - ipc::sem::{SemFlags, SemKey}, + ipc::sem::{SemFlags, SemKey, SemManager}, process::ProcessManager, syscall::table::Syscall, }; @@ -37,8 +37,7 @@ impl Syscall for SysSemgetHandle { } let semflg = SemFlags::from_bits_truncate(args[2] as u32); let ipcns = ProcessManager::current_ipcns(); - let mut guard = ipcns.sem.lock(); - guard.semget(key, nsems as usize, semflg) + SemManager::semget(&ipcns, key, nsems as usize, semflg) } fn entry_format(&self, args: &[usize]) -> Vec { diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index 3e585a77d9..e605fa3e51 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -2224,6 +2224,93 @@ TEST(SysVSem, RemoveWhileWaiting) { // ============ concurrency ============ +TEST(SysVSem, ConcurrentSameKeyCreationPublishesOneSet) { + constexpr int kCreators = 12; + for (bool exclusive : {false, true}) { + const key_t key = UniqueKey(); + int gate[2]; + ASSERT_EQ(0, pipe(gate)); + FdGuard gate_read(gate[0]); + FdGuard gate_write(gate[1]); + std::vector> children; + for (int i = 0; i < kCreators; ++i) { + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + gate_write.Close(); + char token; + if (!ReadExact(gate_read.get(), &token, 1)) _exit(120); + int id = SemGet(key, 32000, IPC_CREAT | 0600 | (exclusive ? IPC_EXCL : 0)); + if (id < 0) _exit(exclusive && errno == EEXIST ? 2 : 121); + // A racing creator must find the same published ID, never a duplicate. + _exit(SemGet(key, 0, 0600) == id ? 0 : 122); + } + children.emplace_back(new ChildGuard(pid)); + } + gate_read.Close(); + const char tokens[kCreators] = {}; + ASSERT_TRUE(WriteExact(gate_write.get(), tokens, sizeof(tokens))); + gate_write.Close(); + int successes = 0; + for (auto& child : children) { + int status = 0; + pid_t result; + do { result = waitpid(child->pid(), &status, 0); } while (result < 0 && errno == EINTR); + if (result == child->pid()) child->MarkReaped(); + ASSERT_GT(result, 0); + ASSERT_TRUE(WIFEXITED(status)); + const int code = WEXITSTATUS(status); + EXPECT_TRUE(code == 0 || (exclusive && code == 2)) << code; + successes += code == 0; + } + SemSet published(SemGet(key, 0, 0600), true); + ASSERT_TRUE(published.valid()); + EXPECT_EQ(exclusive ? 1 : kCreators, successes); + EXPECT_EQ(0, SemCtl(published.id(), 31999, GETVAL, 0)); + } +} + +TEST(SysVSem, CancelHeadMiddleTailPreservesBothWaitQueues) { + constexpr int kWaiters = 9; + for (bool constant : {false, true}) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, constant ? 1 : 0)); + std::vector> children; + for (int i = 0; i < kWaiters; ++i) { + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + struct sigaction sa = {}; + sa.sa_handler = [](int) {}; + sigemptyset(&sa.sa_mask); + if (sigaction(SIGUSR1, &sa, nullptr) != 0) _exit(123); + struct sembuf op = {0, static_cast(constant ? 0 : -1), 0}; + int result = SemOp(sem.id(), &op, 1); + const bool cancelled = i == 0 || i == 4 || i == 8; + _exit((cancelled ? result == -1 && errno == EINTR : result == 0) ? 0 : 124); + } + children.emplace_back(new ChildGuard(pid)); + // Establish insertion order using the ABI, not guessed scheduling delays. + ASSERT_TRUE(constant ? WaitForZcnt(sem.id(), 0, i + 1) + : WaitForNcnt(sem.id(), 0, i + 1)); + } + int remaining = kWaiters; + for (int index : {4, 0, 8}) { + ASSERT_EQ(0, kill(children[index]->pid(), SIGUSR1)); + WaitChildOk(children[index].get()); + --remaining; + EXPECT_EQ(remaining, SemCtl(sem.id(), 0, constant ? GETZCNT : GETNCNT, 0)); + } + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, constant ? 0 : remaining)); + for (int i = 0; i < kWaiters; ++i) { + if (i != 0 && i != 4 && i != 8) WaitChildOk(children[i].get()); + } + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); + EXPECT_EQ(0, SemCtl(sem.id(), 0, constant ? GETZCNT : GETNCNT, 0)); + } +} + TEST(SysVSem, WaitQueuesGrowForConstAndAlterOperations) { constexpr int kWaiters = 32; for (bool constant : {true, false}) { From b2717d700ffaede0c70c677f8e7c9966b04972a3 Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 17:31:29 +0000 Subject: [PATCH 22/34] fix(dunitest): use real deadlines when collecting child output Signed-off-by: longjin --- .../normal/cubesandbox_pty_exec_chain.cc | 131 +++++++++++++----- 1 file changed, 95 insertions(+), 36 deletions(-) diff --git a/user/apps/tests/dunitest/suites/normal/cubesandbox_pty_exec_chain.cc b/user/apps/tests/dunitest/suites/normal/cubesandbox_pty_exec_chain.cc index a7619c4eec..15f3df4878 100644 --- a/user/apps/tests/dunitest/suites/normal/cubesandbox_pty_exec_chain.cc +++ b/user/apps/tests/dunitest/suites/normal/cubesandbox_pty_exec_chain.cc @@ -574,15 +574,59 @@ void ExecLsProgram() { std::string CollectFdUntilChildExit(int fd, pid_t child, int timeout_ms, int* status) { std::string output; - for (int elapsed_ms = 0; elapsed_ms < timeout_ms; elapsed_ms += 10) { + auto now_ms = []() -> long { + struct timespec now = {}; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) { + return -1; + } + return static_cast(now.tv_sec) * 1000 + now.tv_nsec / 1000000; + }; + const long start = now_ms(); + bool failed = start < 0; + bool output_closed = false; + auto remaining_ms = [&]() -> long { + const long now = now_ms(); + if (start < 0 || now < 0) { + ADD_FAILURE() << "clock_gettime failed while collecting child output"; + failed = true; + return 0; + } + return timeout_ms - (now - start); + }; + // A drained pipe/PTY can close before its child becomes waitable. Stop + // polling that persistent HUP, but continue bounded waits for process exit. + auto drain_output = [&]() { + while (!output_closed && remaining_ms() > 0) { + std::array buf = {}; + ssize_t n = read(fd, buf.data(), buf.size()); + if (n > 0) { + output.append(buf.data(), static_cast(n)); + continue; + } + if (n == 0 || (n < 0 && errno == EIO)) { + output_closed = true; // Linux PTY masters return EIO after hangup. + } else if (errno == EINTR) { + continue; + } else if (errno != EAGAIN && errno != EWOULDBLOCK) { + ADD_FAILURE() << "read failed while collecting child output: errno=" << errno; + failed = true; + } + break; + } + }; + while (!failed) { + const long remaining = remaining_ms(); + if (remaining <= 0) { + break; + } struct pollfd pfd = { - .fd = fd, + .fd = output_closed ? -1 : fd, .events = POLLIN | POLLERR | POLLHUP, .revents = 0, }; struct timespec ts = { .tv_sec = 0, - .tv_nsec = 10 * 1000 * 1000, + .tv_nsec = (remaining < 10 ? remaining : 10) * 1000 * 1000, }; sigset_t empty; sigemptyset(&empty); @@ -596,38 +640,13 @@ std::string CollectFdUntilChildExit(int fd, pid_t child, int timeout_ms, int* st break; } - if (pret > 0 && (pfd.revents & POLLIN) != 0) { - std::array buf = {}; - for (;;) { - ssize_t n = read(fd, buf.data(), buf.size()); - if (n > 0) { - output.append(buf.data(), static_cast(n)); - continue; - } - if (n < 0 && errno == EINTR) { - continue; - } - if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { - break; - } - break; - } + if (pret > 0 && (pfd.revents & (POLLIN | POLLERR | POLLHUP)) != 0) { + drain_output(); } pid_t wait_ret = waitpid(child, status, WNOHANG); if (wait_ret == child) { - for (;;) { - std::array buf = {}; - ssize_t n = read(fd, buf.data(), buf.size()); - if (n > 0) { - output.append(buf.data(), static_cast(n)); - continue; - } - if (n < 0 && errno == EINTR) { - continue; - } - break; - } + drain_output(); return output; } if (wait_ret < 0 && errno != EINTR) { @@ -635,14 +654,10 @@ std::string CollectFdUntilChildExit(int fd, pid_t child, int timeout_ms, int* st << strerror(errno) << ")"; break; } - - if (pret > 0 && (pfd.revents & (POLLERR | POLLHUP)) != 0) { - continue; - } } kill(child, SIGKILL); - waitpid(child, status, 0); + while (waitpid(child, status, 0) < 0 && errno == EINTR) {} ADD_FAILURE() << "child did not exit within " << timeout_ms << " ms, captured: " << output; return output; } @@ -2027,6 +2042,50 @@ TEST(CubeSandboxPtyExecChain, ZeroLengthPipeIoMatchesLinux) { EXPECT_EQ('x', observed); } +void ExpectOutputHangupBeforeChildExit(bool use_pty) { + UniqueFd read_end, write_end; + if (use_pty) { + PtyPair pair = OpenPty(); + read_end = std::move(pair.master); + write_end = std::move(pair.slave); + } else { + int fds[2]; + ASSERT_EQ(0, pipe(fds)); + read_end.reset(fds[0]); + write_end.reset(fds[1]); + } + ASSERT_GE(read_end.get(), 0); + ASSERT_GE(write_end.get(), 0); + SetNonblock(read_end.get()); + pid_t child = fork(); + ASSERT_GE(child, 0); + if (child == 0) { + read_end.reset(); + constexpr char marker[] = "output-before-hangup"; + bool written = WriteAll(write_end.get(), marker, sizeof(marker) - 1); + write_end.reset(); + // Closing output precedes process exit. Persistent HUP must not spend + // the collector's timeout as though each ready poll had slept 10 ms. + struct timespec delay = {0, 200 * 1000 * 1000}; + while (nanosleep(&delay, &delay) < 0 && errno == EINTR) {} + _exit(written ? 0 : 123); + } + write_end.reset(); + int status = 0; + std::string output = CollectFdUntilChildExit(read_end.get(), child, 5000, &status); + ASSERT_TRUE(WIFEXITED(status)) << "status=" << status; + EXPECT_EQ(0, WEXITSTATUS(status)); + EXPECT_EQ("output-before-hangup", output); +} + +TEST(CubeSandboxPtyExecChain, PipeHangupBeforeChildExitPreservesOutput) { + ExpectOutputHangupBeforeChildExit(false); +} + +TEST(CubeSandboxPtyExecChain, PtyHangupBeforeChildExitPreservesOutput) { + ExpectOutputHangupBeforeChildExit(true); +} + TEST(CubeSandboxPtyExecChain, PtyExecDirectUnameEmitsOutputAndExits) { PtyPair pair = OpenPty(); ASSERT_GE(pair.master.get(), 0); From dabd7307980ebb8dc0a87b852bb3a97a74e40ced Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 19:49:24 +0000 Subject: [PATCH 23/34] fix(ipc): reuse live undo records and replay one set at a time Signed-off-by: longjin --- kernel/src/ipc/sem.rs | 127 +++--- kernel/src/ipc/sem_undo.rs | 418 +++++++++++------- .../suites/normal/sysv_sem_semantics.cc | 138 ++++++ 3 files changed, 480 insertions(+), 203 deletions(-) diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index 37fb91ef6e..493b33e0a5 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -16,7 +16,9 @@ use crate::{ ipc::{ id::IpcIdAllocator, ipc_perm::{self, IpcPerm, IpcPermView, PosixIpcPerm}, - sem_undo::{PreparedSemUndoRecordAction, SemUndoGroup, SemUndoRecord}, + sem_undo::{ + PreparedSemUndoRecord, PreparedSemUndoRecordAction, SemUndoGroup, SemUndoRecord, + }, }, libs::{ spinlock::SpinLock, @@ -390,7 +392,7 @@ struct SemQueueEntry { sops: Vec, pid: Option>, undo_group: Option>, - undo_record: SpinLock>, + undo_record: SpinLock>, waker: Arc, scratch: SpinLock, status: SpinLock, @@ -401,7 +403,7 @@ impl SemQueueEntry { sops: Vec, pid: Option>, undo_group: Option>, - undo_record: Option, + undo_record: Option, waker: Arc, scratch: SemopScratch, blocker: SemBlockedOp, @@ -1688,7 +1690,7 @@ impl SemManager { .undo_record .lock_irqsave() .as_ref() - .is_some_and(SemUndoRecord::was_existing); + .is_some_and(PreparedSemUndoRecord::was_existing); if !already_associated { let set = guard.get_by_semid_checked_mut(semid)?; if let Err(capacity) = set.ensure_undo_group_registered_prepared( @@ -3127,15 +3129,19 @@ mod tests { let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(semid, &[2]); - let mut record = group.prepare_record_for_test(semid, 1).unwrap(); + let record = group.prepare_record_for_test(semid, 1).unwrap(); let mut scratch = SemopScratch::try_new(1).unwrap(); let set = manager.get_by_semid_checked_mut(semid).unwrap(); - let outcome = - SemManager::simulate_semop(set, &[undo_sop(0, -2)], Some(&mut record), &mut scratch) - .unwrap() - .ready_for_test(); - SemManager::commit_semop(set, outcome, &scratch, None, Some(&mut record)); - group.commit_prepared_record_noalloc(record).unwrap(); + group + .with_prepared_record_noalloc(record, |record| { + let outcome = + SemManager::simulate_semop(set, &[undo_sop(0, -2)], Some(record), &mut scratch) + .unwrap() + .ready_for_test(); + SemManager::commit_semop(set, outcome, &scratch, None, Some(record)); + PreparedSemUndoRecordAction::Publish(()) + }) + .unwrap(); assert_eq!(group.adjustment_for_test(semid, 0), 4); } @@ -3232,23 +3238,28 @@ mod tests { let mut manager = SemManager::new(); let semid = insert_test_set(&mut manager, SemKey::new(41), &[4]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - let mut record = group.prepare_record_for_test(semid, 1).unwrap(); + let record = group.prepare_record_for_test(semid, 1).unwrap(); let mut scratch = SemopScratch::try_new(3).unwrap(); let sops = [undo_sop(0, 3), plain_sop(0, -1), undo_sop(0, -2)]; let set = manager.get_by_semid_checked_mut(semid).unwrap(); - let outcome = SemManager::simulate_semop(set, &sops, Some(&mut record), &mut scratch); - assert!(matches!(outcome, Ok(SemopOutcome::Ready(_)))); - SemManager::commit_semop( - set, - outcome.unwrap().ready_for_test(), - &scratch, - None, - Some(&mut record), - ); + group + .with_prepared_record_noalloc(record, |record| { + let outcome = SemManager::simulate_semop(set, &sops, Some(record), &mut scratch); + assert!(matches!(outcome, Ok(SemopOutcome::Ready(_)))); + SemManager::commit_semop( + set, + outcome.unwrap().ready_for_test(), + &scratch, + None, + Some(record), + ); + PreparedSemUndoRecordAction::Publish(()) + }) + .unwrap(); assert_eq!(set.sems[0].val, 4); - assert_eq!(record.adjustment_for_test(0), -1); + assert_eq!(group.adjustment_for_test(semid, 0), -1); } #[test] @@ -3256,19 +3267,24 @@ mod tests { let mut manager = SemManager::new(); let semid = insert_test_set(&mut manager, SemKey::new(42), &[10]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - let mut record = group.prepare_record_for_test(semid, 1).unwrap(); - record.set_adjustment_for_test(0, i16::MAX); + group.insert_test_record(semid, &[i16::MAX]); + let record = group.prepare_record_for_test(semid, 1).unwrap(); let mut scratch = SemopScratch::try_new(2).unwrap(); let sops = [undo_sop(0, -1), undo_sop(0, 1)]; let set = manager.get_by_semid_checked_mut(semid).unwrap(); - assert!(matches!( - SemManager::simulate_semop(set, &sops, Some(&mut record), &mut scratch), - Err(SystemError::ERANGE) - )); + group + .with_prepared_record_noalloc(record, |record| { + assert!(matches!( + SemManager::simulate_semop(set, &sops, Some(record), &mut scratch), + Err(SystemError::ERANGE) + )); + PreparedSemUndoRecordAction::Keep(()) + }) + .unwrap(); assert_eq!(set.sems[0].val, 10); - assert_eq!(record.adjustment_for_test(0), i16::MAX); + assert_eq!(group.adjustment_for_test(semid, 0), i16::MAX); } #[test] @@ -3276,22 +3292,29 @@ mod tests { let mut manager = SemManager::new(); let semid = insert_test_set(&mut manager, SemKey::new(43), &[2]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - let mut record = group.prepare_record_for_test(semid, 1).unwrap(); + // Exercise the live record, not just an unpublished candidate. + group.insert_test_record(semid, &[0]); + let record = group.prepare_record_for_test(semid, 1).unwrap(); let mut scratch = SemopScratch::try_new(2).unwrap(); let sops = [undo_sop(0, -1), nowait_sop(0, -2)]; let set = manager.get_by_semid_checked_mut(semid).unwrap(); - assert!(matches!( - SemManager::simulate_semop(set, &sops, Some(&mut record), &mut scratch), - Ok(SemopOutcome::Blocked(SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Increase, - nowait: true, - })) - )); + group + .with_prepared_record_noalloc(record, |record| { + assert!(matches!( + SemManager::simulate_semop(set, &sops, Some(record), &mut scratch), + Ok(SemopOutcome::Blocked(SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: true, + })) + )); + PreparedSemUndoRecordAction::Keep(()) + }) + .unwrap(); assert_eq!(set.sems[0].val, 2); - assert_eq!(record.adjustment_for_test(0), 0); + assert_eq!(group.adjustment_for_test(semid, 0), 0); } #[test] @@ -3299,21 +3322,25 @@ mod tests { let mut manager = SemManager::new(); let semid = insert_test_set(&mut manager, SemKey::new(44), &[0]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - let mut record = group.prepare_record_for_test(semid, 1).unwrap(); + let record = group.prepare_record_for_test(semid, 1).unwrap(); let mut scratch = SemopScratch::try_new(1).unwrap(); let sops = [undo_sop(0, 0)]; let set = manager.get_by_semid_checked_mut(semid).unwrap(); - let outcome = SemManager::simulate_semop(set, &sops, Some(&mut record), &mut scratch); - assert!(matches!(outcome, Ok(SemopOutcome::Ready(_)))); - SemManager::commit_semop( - set, - outcome.unwrap().ready_for_test(), - &scratch, - None, - Some(&mut record), - ); - group.commit_record(record).unwrap(); + group + .with_prepared_record_noalloc(record, |record| { + let outcome = SemManager::simulate_semop(set, &sops, Some(record), &mut scratch); + assert!(matches!(outcome, Ok(SemopOutcome::Ready(_)))); + SemManager::commit_semop( + set, + outcome.unwrap().ready_for_test(), + &scratch, + None, + Some(record), + ); + PreparedSemUndoRecordAction::Publish(()) + }) + .unwrap(); assert_eq!(set.sems[0].val, 0); assert_eq!(group.record_count_for_test(), 1); diff --git a/kernel/src/ipc/sem_undo.rs b/kernel/src/ipc/sem_undo.rs index b207ba50f2..412f6fafd1 100644 --- a/kernel/src/ipc/sem_undo.rs +++ b/kernel/src/ipc/sem_undo.rs @@ -29,20 +29,12 @@ struct SemUndoGroupState { task_owners: usize, records: Vec, reserved_records: usize, - absence_generation: u64, retired: bool, - records_taken: bool, + replay_started: bool, #[cfg(test)] replay_count: usize, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum PreparedSemUndoRecordState { - Existing { expected_revision: u64 }, - Missing { expected_absence_generation: u64 }, - Live, -} - #[derive(Debug)] struct PendingSemUndoRecordReservation { group: Weak, @@ -53,8 +45,15 @@ struct PendingSemUndoRecordReservation { pub(crate) struct SemUndoRecord { semid: SemId, adjustments: Box<[i16]>, - revision: u64, - prepared_state: PreparedSemUndoRecordState, +} + +/// Existing records need no snapshot: simulation reads the current record +/// while holding the group lock. Only a first-use candidate owns dense storage. +#[derive(Debug)] +pub(crate) struct PreparedSemUndoRecord { + semid: SemId, + nsems: usize, + candidate: Option, reservation: Option, } @@ -113,60 +112,20 @@ fn prepare_records_storage_capacity( Ok(()) } -impl SemUndoGroupState { - fn bump_absence_generation(&mut self) { - self.absence_generation = self.absence_generation.wrapping_add(1); +impl PreparedSemUndoRecord { + pub(crate) fn was_existing(&self) -> bool { + self.candidate.is_none() } -} -impl SemUndoRecord { - /// A production record is published only after its group is associated - /// with the full semaphore-set ID. Clearing debt preserves that link. - pub(crate) fn was_existing(&self) -> bool { - matches!( - self.prepared_state, - PreparedSemUndoRecordState::Existing { .. } - ) + pub(crate) fn adjustment_count(&self) -> usize { + self.nsems } +} +impl SemUndoRecord { #[cfg(test)] fn new_live(semid: SemId, adjustments: Box<[i16]>) -> Self { - Self { - semid, - adjustments, - revision: 0, - prepared_state: PreparedSemUndoRecordState::Live, - reservation: None, - } - } - - fn bump_revision(&mut self) { - self.revision = self.revision.wrapping_add(1); - } - - fn publish_from(&mut self, record: &SemUndoRecord) { - self.adjustments.copy_from_slice(&record.adjustments); - self.bump_revision(); - } - - fn refresh_existing(&mut self, existing: &SemUndoRecord) { - self.adjustments.copy_from_slice(&existing.adjustments); - self.revision = existing.revision; - self.prepared_state = PreparedSemUndoRecordState::Existing { - expected_revision: existing.revision, - }; - } - - fn refresh_missing(&mut self, absence_generation: u64) { - self.adjustments.fill(0); - self.revision = 0; - self.prepared_state = PreparedSemUndoRecordState::Missing { - expected_absence_generation: absence_generation, - }; - } - - fn mark_live(&mut self) { - self.prepared_state = PreparedSemUndoRecordState::Live; + Self { semid, adjustments } } pub(crate) fn adjustment(&self, semnum: usize) -> i16 { @@ -176,7 +135,6 @@ impl SemUndoRecord { pub(crate) fn set_adjustment(&mut self, semnum: usize, adjustment: i16) { if self.adjustments[semnum] != adjustment { self.adjustments[semnum] = adjustment; - self.bump_revision(); } } @@ -187,7 +145,6 @@ impl SemUndoRecord { pub(crate) fn clear_all_adjustments(&mut self) { if self.adjustments.iter().any(|&adjustment| adjustment != 0) { self.adjustments.fill(0); - self.bump_revision(); } } @@ -246,9 +203,8 @@ impl SemUndoGroup { task_owners: 1, records: Vec::new(), reserved_records: 0, - absence_generation: 0, retired: false, - records_taken: false, + replay_started: false, #[cfg(test)] replay_count: 0, }), @@ -287,16 +243,32 @@ impl SemUndoGroup { true } - fn take_retired_records(&self) -> Vec { + /// Claim retirement exactly once without hiding pending debt from semctl. + fn begin_replay(&self) -> bool { let mut state = self.inner.lock_irqsave(); - if !state.retired || state.records_taken { - return Vec::new(); + if !state.retired || state.replay_started { + return false; } - state.records_taken = true; + state.replay_started = true; #[cfg(test)] { state.replay_count += 1; } + true + } + + /// The caller holds the namespace manager lock until this debt is applied. + /// Other records remain visible to SETVAL/SETALL and IPC_RMID between steps. + fn pop_retired_record(&self) -> Option { + let mut state = self.inner.lock_irqsave(); + debug_assert!(state.retired && state.replay_started); + state.records.pop() + } + + /// Only valid after the bound namespace can no longer be upgraded. + fn discard_retired_records(&self) -> Vec { + let mut state = self.inner.lock_irqsave(); + debug_assert!(state.retired && state.replay_started); core::mem::take(&mut state.records) } @@ -322,13 +294,8 @@ impl SemUndoGroup { self: &Arc, semid: SemId, nsems: usize, - ) -> Result { + ) -> Result { let mut adjustments = Vec::new(); - adjustments - .try_reserve_exact(nsems) - .map_err(|_| SystemError::ENOMEM)?; - adjustments.resize(nsems, 0); - let mut reserved_storage = Vec::new(); loop { @@ -341,18 +308,23 @@ impl SemUndoGroup { if existing.adjustments.len() != nsems { return Err(SystemError::EINVAL); } - adjustments.copy_from_slice(&existing.adjustments); - return Ok(SemUndoRecord { + return Ok(PreparedSemUndoRecord { semid, - adjustments: adjustments.into_boxed_slice(), - revision: existing.revision, - prepared_state: PreparedSemUndoRecordState::Existing { - expected_revision: existing.revision, - }, + nsems, + candidate: None, reservation: None, }); } + if adjustments.len() != nsems { + drop(state); + adjustments + .try_reserve_exact(nsems) + .map_err(|_| SystemError::ENOMEM)?; + adjustments.resize(nsems, 0); + continue; + } + let required_capacity = state .records .len() @@ -375,26 +347,30 @@ impl SemUndoGroup { .reserved_records .checked_add(1) .ok_or(SystemError::ENOMEM)?; - return Ok(SemUndoRecord { + // into_boxed_slice may shrink an allocation. Do it after releasing + // the group lock, with an armed reservation already owning the slot. + let reservation = PendingSemUndoRecordReservation::new(self); + drop(state); + return Ok(PreparedSemUndoRecord { semid, - adjustments: adjustments.into_boxed_slice(), - revision: 0, - prepared_state: PreparedSemUndoRecordState::Missing { - expected_absence_generation: state.absence_generation, - }, - reservation: Some(PendingSemUndoRecordReservation::new(self)), + nsems, + candidate: Some(SemUndoRecord { + semid, + adjustments: adjustments.into_boxed_slice(), + }), + reservation: Some(reservation), }); } } #[allow(dead_code)] - pub(crate) fn commit_record(&self, record: SemUndoRecord) -> Result<(), SystemError> { + pub(crate) fn commit_record(&self, record: PreparedSemUndoRecord) -> Result<(), SystemError> { self.commit_prepared_record_noalloc(record) } pub(crate) fn commit_prepared_record_noalloc( &self, - record: SemUndoRecord, + record: PreparedSemUndoRecord, ) -> Result<(), SystemError> { self.with_prepared_record_noalloc( record, @@ -403,11 +379,18 @@ impl SemUndoGroup { .map(|((), _)| ()) } + /// Borrow the current record under the group lock, never a stale snapshot. + /// The callback must simulate without writes and mutate only on Publish; + /// Keep (including errors/blocked operations) must leave the record unchanged. + /// SemopScratch provides that separation without an additional transaction. pub(crate) fn with_prepared_record_noalloc( &self, - mut record: SemUndoRecord, + mut record: PreparedSemUndoRecord, f: impl FnOnce(&mut SemUndoRecord) -> PreparedSemUndoRecordAction, - ) -> Result<(R, Option), SystemError> { + ) -> Result<(R, Option), SystemError> { + // A competing first publisher makes this candidate redundant. Keep + // its disposal outside the group lock and return a lightweight token. + let mut _retired_candidate = None; let mut state = self.inner.lock_irqsave(); if state.task_owners == 0 || state.retired { return Err(SystemError::EINVAL); @@ -419,18 +402,10 @@ impl SemUndoGroup { .position(|item| item.semid == record.semid); if let Some(index) = existing_index { - if state.records[index].adjustments.len() != record.adjustments.len() { + if state.records[index].adjustments.len() != record.nsems { return Err(SystemError::EINVAL); } - let current_revision = state.records[index].revision; - let stale = !matches!( - record.prepared_state, - PreparedSemUndoRecordState::Existing { expected_revision } - if current_revision == expected_revision - ) && !matches!(record.prepared_state, PreparedSemUndoRecordState::Live); - if stale { - record.refresh_existing(&state.records[index]); - } + _retired_candidate = record.candidate.take(); if let Some(mut reservation) = record.reservation.take() { state.reserved_records = state .reserved_records @@ -439,28 +414,20 @@ impl SemUndoGroup { reservation.disarm(); } - return match f(&mut record) { - PreparedSemUndoRecordAction::Publish(result) => { - state.records[index].publish_from(&record); - Ok((result, None)) - } + return match f(&mut state.records[index]) { + PreparedSemUndoRecordAction::Publish(result) => Ok((result, None)), PreparedSemUndoRecordAction::Keep(result) => Ok((result, Some(record))), }; } - match record.prepared_state { - PreparedSemUndoRecordState::Missing { - expected_absence_generation, - } if expected_absence_generation == state.absence_generation => {} - PreparedSemUndoRecordState::Live => {} - _ => record.refresh_missing(state.absence_generation), - } - + // Existing tokens cannot recreate a record removed by RMID. Missing + // candidates stay zero until a successful, locally simulated commit. + let candidate = record.candidate.as_mut().ok_or(SystemError::EINVAL)?; if state.records.len() >= state.records.capacity() { return Err(SystemError::ENOMEM); } - match f(&mut record) { + match f(candidate) { PreparedSemUndoRecordAction::Publish(result) => { if let Some(mut reservation) = record.reservation.take() { state.reserved_records = state @@ -469,9 +436,7 @@ impl SemUndoGroup { .expect("SEM_UNDO record reservation count underflow"); reservation.disarm(); } - record.mark_live(); - state.records.push(record); - state.bump_absence_generation(); + state.records.push(record.candidate.take().unwrap()); Ok((result, None)) } PreparedSemUndoRecordAction::Keep(result) => Ok((result, Some(record))), @@ -494,7 +459,6 @@ impl SemUndoGroup { pub(crate) fn remove_record(&self, semid: SemId) { let mut state = self.inner.lock_irqsave(); state.records.retain(|record| record.semid != semid); - state.bump_absence_generation(); } #[cfg(test)] @@ -521,7 +485,7 @@ impl SemUndoGroup { self: &Arc, semid: SemId, nsems: usize, - ) -> Result { + ) -> Result { self.prepare_record(semid, nsems) } @@ -547,7 +511,6 @@ impl SemUndoGroup { semid, adjustments.to_vec().into_boxed_slice(), )); - state.bump_absence_generation(); } #[cfg(test)] @@ -599,6 +562,9 @@ pub(crate) fn detach_sem_undo(pcb: &Arc) { } fn replay_marked_records(pcb: &Arc, group: &Arc) { + if !group.begin_replay() { + return; + } let exiting_tgid = pcb.try_active_pid_ns().and_then(|pid_ns| { pcb.task_pid_nr_ns(PidType::TGID, Some(pid_ns)) .filter(|tgid| tgid.data() != 0)?; @@ -606,7 +572,7 @@ fn replay_marked_records(pcb: &Arc, group: &Arc, group: &Arc manager.setval(pending, 0, 9, &mut wakes).unwrap(), + 1 => { + let token = manager.prepare_setall(pending).unwrap(); + manager.setall(token, &[9, 8], &mut wakes).unwrap(); + } + _ => manager.ipc_rmid(pending, &mut wakes).unwrap(), + } + let next = group.pop_retired_record(); + if control == 2 { + assert!(next.is_none()); + } else { + let next = next.as_ref().unwrap(); + assert_eq!(next.adjustment(0), 0); + assert_eq!(next.adjustment(1), if control == 0 { -3 } else { 0 }); + manager.replay_sem_undo_adjustments( + next.semid, + &next.adjustments, + None, + &mut wakes, + ); + manager.unregister_undo_group(next.semid, &group); + assert_eq!( + manager.getall(pending).unwrap(), + if control == 0 { vec![9, 0] } else { vec![9, 8] } + ); + } + assert!(group.pop_retired_record().is_none()); + assert!(!group.begin_replay()); + drop(manager); + wakes.wake_all(); + drop(next); + } + } + #[test] fn retired_group_rejects_shared_owner_and_replays_only_once() { let pcb = test_pcb_with_group(); @@ -840,15 +899,21 @@ mod tests { } #[test] - fn prepare_existing_record_copies_current_adjustments() { + fn prepare_existing_record_borrows_current_adjustments() { let group = SemUndoGroup::new_for_test(); let semid = SemId::new(101); group.insert_test_record(semid, &[7, -3]); let record = group.prepare_record_for_test(semid, 2).unwrap(); - assert_eq!(record.adjustment_for_test(0), 7); - assert_eq!(record.adjustment_for_test(1), -3); + assert!(record.was_existing()); + group + .with_prepared_record_noalloc(record, |live| { + assert_eq!(live.adjustment_for_test(0), 7); + assert_eq!(live.adjustment_for_test(1), -3); + PreparedSemUndoRecordAction::Keep(()) + }) + .unwrap(); } #[test] @@ -891,8 +956,7 @@ mod tests { fn missing_record_reservation_loses_to_competing_insert_with_retry() { let group = SemUndoGroup::new_for_test(); let semid = SemId::new(206); - let mut stale = group.prepare_record_for_test(semid, 1).unwrap(); - stale.set_adjustment_for_test(0, 4); + let stale = group.prepare_record_for_test(semid, 1).unwrap(); group.insert_test_record(semid, &[9]); assert_eq!(group.commit_record(stale), Ok(())); @@ -904,8 +968,7 @@ mod tests { fn missing_record_reservation_loses_to_rmid_generation_with_retry() { let group = SemUndoGroup::new_for_test(); let semid = SemId::new(207); - let mut stale = group.prepare_record_for_test(semid, 1).unwrap(); - stale.set_adjustment_for_test(0, 4); + let stale = group.prepare_record_for_test(semid, 1).unwrap(); group.remove_record(semid); assert_eq!(group.commit_record(stale), Ok(())); @@ -918,11 +981,13 @@ mod tests { fn commit_unreserved_missing_record_returns_enomem_without_allocating() { let group = SemUndoGroup::new_for_test(); let before_capacity = group.record_capacity_for_test(); - let record = SemUndoRecord { + let record = PreparedSemUndoRecord { semid: SemId::new(203), - adjustments: alloc::vec![0].into_boxed_slice(), - revision: 0, - prepared_state: PreparedSemUndoRecordState::Live, + nsems: 1, + candidate: Some(SemUndoRecord { + semid: SemId::new(203), + adjustments: alloc::vec![0].into_boxed_slice(), + }), reservation: None, }; @@ -931,6 +996,53 @@ mod tests { assert_eq!(group.record_capacity_for_test(), before_capacity); } + #[test] + fn existing_preparation_reads_current_debt_and_updates_only_one_slot() { + let group = SemUndoGroup::new_for_test(); + let semid = SemId::new(212); + group.insert_test_record(semid, &[3, 7]); + let prepared = group.prepare_record_for_test(semid, 2).unwrap(); + assert!(prepared.was_existing()); + assert!(prepared.candidate.is_none()); + group.with_record_mut(semid, |live| live.set_adjustment(0, 9)); + let (_, kept) = group + .with_prepared_record_noalloc(prepared, |live| { + assert_eq!(live.adjustment(0), 9); + PreparedSemUndoRecordAction::Keep(Err::<(), _>(SystemError::ERANGE)) + }) + .unwrap(); + assert_eq!(group.adjustment_for_test(semid, 0), 9); + assert_eq!(group.adjustment_for_test(semid, 1), 7); + group.with_record_mut(semid, |live| live.clear_all_adjustments()); + group + .with_prepared_record_noalloc(kept.unwrap(), |live| { + assert_eq!(live.adjustment(0), 0); + live.set_adjustment(1, 4); + PreparedSemUndoRecordAction::Publish(()) + }) + .unwrap(); + assert_eq!(group.adjustment_for_test(semid, 0), 0); + assert_eq!(group.adjustment_for_test(semid, 1), 4); + } + + #[test] + fn competing_first_publish_releases_candidate_on_keep() { + let group = SemUndoGroup::new_for_test(); + let semid = SemId::new(213); + let pending = group.prepare_record_for_test(semid, 2).unwrap(); + assert!(!pending.was_existing()); + group.insert_test_record(semid, &[6, 8]); + let (_, kept) = group + .with_prepared_record_noalloc(pending, |live| { + assert_eq!(live.adjustment(0), 6); + PreparedSemUndoRecordAction::Keep(()) + }) + .unwrap(); + assert!(kept.unwrap().was_existing()); + assert_eq!(group.pending_record_reservations_for_test(), 0); + assert_eq!(group.adjustment_for_test(semid, 1), 8); + } + #[test] fn observer_arc_does_not_change_task_owner_count() { let group = SemUndoGroup::new_for_test(); diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index e605fa3e51..944c57f741 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -2428,6 +2428,144 @@ TEST(SysVSem, SharedUndoGroupControlChangesStaySetLocal) { } } +struct LargeSetUndoWorkerArgs { + int semid; + int worker; + const int* fds; +}; + +int LargeSetUndoWorker(void* opaque) { + const auto& args = *static_cast(opaque); + const int ready = args.fds[4 * args.worker + 1]; + const int gate = args.fds[4 * args.worker + 2]; + for (int i = 0; i < 8; ++i) { + if (args.fds[i] != ready && args.fds[i] != gate) close(args.fds[i]); + } + for (int phase = 0; phase < 2; ++phase) { + char token; + if (!ReadExact(gate, &token, 1)) return 125; + for (int i = 0; i < 128; ++i) { + if (!SemUndoOpMustSucceed(args.semid, 31999, 1)) return 126; + } + if (!WriteExact(ready, &token, 1)) return 127; + } + return 0; +} + +int RunLargeSetUndoSupervisor(int semid) { + // A failed worker must return an error through the guards, not terminate + // this supervisor with SIGPIPE and leave its other worker behind. + struct sigaction ignore_pipe = {}; + ignore_pipe.sa_handler = SIG_IGN; + sigemptyset(&ignore_pipe.sa_mask); + if (sigaction(SIGPIPE, &ignore_pipe, nullptr) != 0) return 139; + // Ensure clone inherits one published group, without changing the value. + struct sembuf zero = {31999, 0, SEM_UNDO}; + if (SemOp(semid, &zero, 1) != 0) return 128; + int fds[8]; + std::vector> fd_guards; + for (int i = 0; i < 8; i += 2) { + if (pipe(&fds[i]) != 0) return 129; + fd_guards.emplace_back(new FdGuard(fds[i])); + fd_guards.emplace_back(new FdGuard(fds[i + 1])); + } + alignas(16) char stacks[2][16384]; + LargeSetUndoWorkerArgs args[2] = {{semid, 0, fds}, {semid, 1, fds}}; + std::vector> children; + for (int i = 0; i < 2; ++i) { + pid_t pid = clone(LargeSetUndoWorker, stacks[i] + sizeof(stacks[i]), + CLONE_SYSVSEM | SIGCHLD, &args[i]); + if (pid < 0) return 130; + children.emplace_back(new ChildGuard(pid)); + } + for (int i = 0; i < 2; ++i) { + fd_guards[4 * i + 1]->Close(); + fd_guards[4 * i + 2]->Close(); + } + for (int phase = 0; phase < 2; ++phase) { + char token = 1; + for (int i = 0; i < 2; ++i) { + if (!WriteExact(fds[4 * i + 3], &token, 1)) return 131; + } + for (int i = 0; i < 2; ++i) { + if (!ReadExact(fds[4 * i], &token, 1)) return 132; + } + if (SemCtl(semid, 31999, GETVAL, 0) != 256 + (phase == 0 ? 0 : 7)) return 133; + if (phase == 0 && SemCtl(semid, 31999, SETVAL, 7) != 0) return 134; + } + for (auto& child : children) { + int status; + pid_t ret; + do { ret = waitpid(child->pid(), &status, 0); } while (ret < 0 && errno == EINTR); + if (ret != child->pid()) return 135; + child->MarkReaped(); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) return 136; + } + return 0; +} + +TEST(SysVSem, LargeSetSharedUndoUpdatesSurviveSetvalBarrier) { + SemSet sem(32000, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 13)); + pid_t supervisor = fork(); + ASSERT_GE(supervisor, 0); + if (supervisor == 0) _exit(RunLargeSetUndoSupervisor(sem.id())); + ChildGuard child(supervisor); + WaitChildOk(&child); + EXPECT_EQ(7, SemCtl(sem.id(), 31999, GETVAL, 0)); + EXPECT_EQ(13, SemCtl(sem.id(), 0, GETVAL, 0)); +} + +TEST(SysVSem, MultiSetExitReplayRacesControlWithoutStaleDebt) { + constexpr int kSets = 32; + for (int cmd : {SETVAL, SETALL, IPC_RMID}) { + std::vector> sets; + for (int i = 0; i < kSets; ++i) { + sets.emplace_back(new SemSet(1, IPC_CREAT | 0600)); + ASSERT_TRUE(sets.back()->valid()); + } + int ready[2], release[2]; + ASSERT_EQ(0, pipe(ready)); + FdGuard ready_read(ready[0]), ready_write(ready[1]); + ASSERT_EQ(0, pipe(release)); + FdGuard release_read(release[0]), release_write(release[1]); + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + ready_read.Close(); + release_write.Close(); + for (auto& sem : sets) { + if (!SemUndoOpMustSucceed(sem->id(), 0, 1)) _exit(137); + } + char token = 1; + _exit(WriteExact(ready_write.get(), &token, 1) && + ReadExact(release_read.get(), &token, 1) ? 0 : 138); + } + ChildGuard child(pid); + ready_write.Close(); + release_read.Close(); + char token; + ASSERT_TRUE(ReadExact(ready_read.get(), &token, 1)); + ASSERT_TRUE(WriteExact(release_write.get(), &token, 1)); + // Either ordering is valid. This does not assume that a particular + // control lands between two replay steps in the exiting task. + unsigned short value = 7; + for (auto& sem : sets) { + ASSERT_EQ(0, SemCtl(sem->id(), 0, cmd, + cmd == SETALL ? reinterpret_cast(&value) : 7)); + if (cmd == IPC_RMID) { + sem->release(); + sem.reset(new SemSet(1, IPC_CREAT | 0600)); + ASSERT_TRUE(sem->valid()); + ASSERT_EQ(0, SemCtl(sem->id(), 0, SETVAL, 7)); + } + } + WaitChildOk(&child); + for (auto& sem : sets) EXPECT_EQ(7, SemCtl(sem->id(), 0, GETVAL, 0)); + } +} + TEST(SysVSem, IndependentUndoGroupsSteadyStateAndSetall) { constexpr int kWorkers = 32; std::vector> sets; From 277351ed237d70d703551fd797acfcee82c11690 Mon Sep 17 00:00:00 2001 From: longjin Date: Sat, 5 Sep 2026 20:14:15 +0000 Subject: [PATCH 24/34] fix(ipc): defer semaphore removal reclamation until after unlock Signed-off-by: longjin --- kernel/src/ipc/sem.rs | 129 +++++++++++------- kernel/src/ipc/sem_undo.rs | 17 ++- kernel/src/ipc/syscall/sys_semctl.rs | 8 +- .../suites/normal/sysv_sem_semantics.cc | 33 ++++- 4 files changed, 131 insertions(+), 56 deletions(-) diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index 493b33e0a5..101e410248 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -554,11 +554,31 @@ enum SemPendingQueue { Alter, } +/// A live undo registration, reused to own deferred cleanup after removal. +#[derive(Debug)] +enum SemUndoAssociation { + Group(Weak), + Retired { + _group: Arc, + _record: Option, + }, +} + +impl SemUndoAssociation { + /// Retired slots are only present in a removed, caller-owned set. + fn group(&self) -> &Weak { + match self { + Self::Group(group) => group, + Self::Retired { .. } => unreachable!("retired association in live set"), + } + } +} + /// Semaphore set #[derive(Debug)] pub struct KernelSemSet { /// Groups that can carry undo debt for this set, including queued operations. - undo_groups: Vec>, + undo_groups: Vec, /// Permission information pub kern_ipc_perm: IpcPerm, /// Semaphores in the set @@ -581,11 +601,15 @@ impl KernelSemSet { fn ensure_undo_group_registered_prepared( &mut self, group: &Arc, - spare: &mut Vec>, + spare: &mut Vec, ) -> Result<(), usize> { debug_assert!(spare.is_empty()); let candidate = Arc::downgrade(group); - if self.undo_groups.iter().any(|weak| weak.ptr_eq(&candidate)) { + if self + .undo_groups + .iter() + .any(|entry| entry.group().ptr_eq(&candidate)) + { return Ok(()); } if self.undo_groups.len() == self.undo_groups.capacity() { @@ -598,12 +622,13 @@ impl KernelSemSet { spare.append(&mut self.undo_groups); core::mem::swap(&mut self.undo_groups, spare); } - self.undo_groups.push(candidate); + self.undo_groups.push(SemUndoAssociation::Group(candidate)); Ok(()) } fn compact_undo_registry(&mut self) { - self.undo_groups.retain(|weak| weak.strong_count() != 0); + self.undo_groups + .retain(|entry| entry.group().strong_count() != 0); } /// Request/publish smaller storage without allocating under the manager @@ -611,8 +636,8 @@ impl KernelSemSet { /// The caller must drop both buffers after unlocking. fn shrink_undo_registry_prepared( &mut self, - spare: &mut Vec>, - retired: &mut Vec>, + spare: &mut Vec, + retired: &mut Vec, ) -> usize { debug_assert!(spare.is_empty()); debug_assert!(retired.is_empty() && retired.capacity() == 0); @@ -905,7 +930,8 @@ impl SemManager { pub(crate) fn unregister_undo_group(&mut self, semid: SemId, group: &Arc) { if let Ok(set) = self.get_by_semid_checked_mut(semid) { let target = Arc::downgrade(group); - set.undo_groups.retain(|weak| !weak.ptr_eq(&target)); + set.undo_groups + .retain(|entry| !entry.group().ptr_eq(&target)); } } @@ -968,7 +994,7 @@ impl SemManager { return; }; let mut saw_stale = false; - for weak in set.undo_groups.iter() { + for weak in set.undo_groups.iter().map(SemUndoAssociation::group) { let Some(group) = weak.upgrade() else { saw_stale = true; continue; @@ -989,7 +1015,7 @@ impl SemManager { return; }; let mut saw_stale = false; - for weak in set.undo_groups.iter() { + for weak in set.undo_groups.iter().map(SemUndoAssociation::group) { let Some(group) = weak.upgrade() else { saw_stale = true; continue; @@ -1001,23 +1027,6 @@ impl SemManager { } } - pub(crate) fn discard_undo_for_rmid(&mut self, semid: SemId) { - let Ok(set) = self.get_by_semid_checked_mut(semid) else { - return; - }; - let mut saw_stale = false; - for weak in set.undo_groups.iter() { - let Some(group) = weak.upgrade() else { - saw_stale = true; - continue; - }; - group.remove_record(semid); - } - if saw_stale { - set.compact_undo_registry(); - } - } - #[allow(dead_code)] pub(crate) fn prune_and_apply_setval_undo(&mut self, semid: SemId, semnum: usize) { self.clear_undo_for_setval(semid, semnum); @@ -1028,11 +1037,6 @@ impl SemManager { self.clear_undo_for_setall(semid); } - #[allow(dead_code)] - pub(crate) fn remove_undo_records_for_rmid(&mut self, semid: SemId) { - self.discard_undo_for_rmid(semid); - } - #[cfg(test)] fn update_queue_for_test(&mut self, semid: SemId) { let Ok(set) = self.get_by_semid_checked_mut(semid) else { @@ -1044,7 +1048,12 @@ impl SemManager { #[cfg(test)] fn live_undo_group_count_for_test(&self) -> usize { let mut groups: Vec> = Vec::new(); - for weak in self.id2sem.values().flat_map(|set| &set.undo_groups) { + for weak in self + .id2sem + .values() + .flat_map(|set| &set.undo_groups) + .map(SemUndoAssociation::group) + { if weak.strong_count() != 0 && !groups.iter().any(|old| old.ptr_eq(weak)) { groups.push(weak.clone()); } @@ -1057,6 +1066,7 @@ impl SemManager { self.id2sem .values() .flat_map(|set| &set.undo_groups) + .map(SemUndoAssociation::group) .any(|weak| weak.ptr_eq(&Arc::downgrade(group))) } @@ -1065,6 +1075,7 @@ impl SemManager { self.id2sem .values() .flat_map(|set| &set.undo_groups) + .map(SemUndoAssociation::group) .all(|weak| { weak.upgrade() .is_none_or(|group| group.record_count_for_test() == 0) @@ -1809,11 +1820,13 @@ impl SemManager { } /// # IPC_RMID: remove the semaphore set and wake all waiters with EIDRM + /// The caller must release the manager guard before notifying `wakes` and + /// dropping the returned set, which owns all deferred undo/group disposal. pub(crate) fn ipc_rmid( &mut self, id: SemId, wakes: &mut SemWakeBatch, - ) -> Result<(), SystemError> { + ) -> Result { let decoded = IpcIdAllocator::decode(id.data())?; let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); let key = { @@ -1821,16 +1834,27 @@ impl SemManager { ipc_perm::check_control_permission(&set.kern_ipc_perm, &target_user_ns)?; set.kern_ipc_perm.key }; - self.discard_undo_for_rmid(id); let mut set = self .id2sem .remove(&decoded.idx) .ok_or(SystemError::EINVAL)?; self.key2id.remove(&SemKey::new(key)); self.total_sems = self.total_sems.saturating_sub(set.sems.len()); + // Reuse existing association storage: neither allocation nor undo/group + // destruction is allowed here. Even an empty upgraded group stays alive + // until the caller drops this removed set outside the manager lock. + for association in &mut set.undo_groups { + if let Some(group) = association.group().upgrade() { + let record = group.take_record(id); + *association = SemUndoAssociation::Retired { + _group: group, + _record: record, + }; + } + } set.complete_all_removed(wakes); self.id_allocator.free_idx(decoded.idx); - Ok(()) + Ok(set) } /// # IPC_SET: update permissions (uid/gid/mode) and refresh `sem_ctime` @@ -2507,7 +2531,8 @@ mod tests { let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); let set = manager.get_by_semid_checked_mut(semid).unwrap(); set.undo_groups.reserve_exact(64); - set.undo_groups.push(Arc::downgrade(&group)); + set.undo_groups + .push(SemUndoAssociation::Group(Arc::downgrade(&group))); let mut spare = Vec::new(); let mut retired = Vec::new(); let needed = set.shrink_undo_registry_prepared(&mut spare, &mut retired); @@ -2515,7 +2540,8 @@ mod tests { spare.try_reserve_exact(needed).unwrap(); // Simulate new associations arriving during unlocked preparation. for _ in 0..8 { - set.undo_groups.push(Arc::downgrade(&group)); + set.undo_groups + .push(SemUndoAssociation::Group(Arc::downgrade(&group))); } assert!(set.shrink_undo_registry_prepared(&mut spare, &mut retired) > 0); assert_eq!(set.undo_groups.len(), 9); @@ -2601,7 +2627,7 @@ mod tests { .iter() .zip(&owners) { - assert!(weak.ptr_eq(&Arc::downgrade(owner))); + assert!(weak.group().ptr_eq(&Arc::downgrade(owner))); } // A concurrent CLONE_SYSVSEM sharer already registered this group. @@ -2694,9 +2720,11 @@ mod tests { assert_eq!(group.adjustment_for_test(signal_semid, 0), 0); let rmid_entry = enqueue_undo_waiter_for_test(&mut manager, rmid_semid, &group); - manager - .ipc_rmid(rmid_semid, &mut SemWakeBatch::default()) - .unwrap(); + let mut wakes = SemWakeBatch::default(); + let removed = manager.ipc_rmid(rmid_semid, &mut wakes).unwrap(); + drop(manager); + wakes.wake_all(); + drop(removed); assert_eq!(rmid_entry.completed_result(), Some(Err(SystemError::EIDRM))); assert_eq!(group.adjustment_for_test(rmid_semid, 0), 0); } @@ -2732,7 +2760,7 @@ mod tests { .get_by_semid_checked_mut(semid) .unwrap() .undo_groups - .push(stale_weak); + .push(SemUndoAssociation::Group(stale_weak)); manager.ensure_undo_group_registered(&live, semid).unwrap(); manager .get_by_semid_checked_mut(semid) @@ -2764,7 +2792,9 @@ mod tests { .undo_groups .capacity(); manager.clear_undo_for_setall(semid); - manager.discard_undo_for_rmid(semid); + // Synthetic record removal exercises registration deduplication without + // putting a live set into the RMID-only retired association state. + group.remove_record(semid); assert_eq!(group.record_count_for_test(), 0); for _ in 0..32 { manager.ensure_undo_group_registered(&group, semid).unwrap(); @@ -2884,12 +2914,13 @@ mod tests { drop(pcb.take_sem_undo_attachment().unwrap()); assert!(group.detach_last_owner_for_test()); - { + let mut wakes = SemWakeBatch::default(); + let removed = { let mut manager = ipc_ns.sem.lock(); - manager - .ipc_rmid(semid, &mut SemWakeBatch::default()) - .unwrap(); - } + manager.ipc_rmid(semid, &mut wakes).unwrap() + }; + wakes.wake_all(); + drop(removed); group.replay_marked_records_for_test(&pcb); let manager = ipc_ns.sem.lock(); diff --git a/kernel/src/ipc/sem_undo.rs b/kernel/src/ipc/sem_undo.rs index 412f6fafd1..5fd6f581f5 100644 --- a/kernel/src/ipc/sem_undo.rs +++ b/kernel/src/ipc/sem_undo.rs @@ -456,9 +456,18 @@ impl SemUndoGroup { .map(f) } - pub(crate) fn remove_record(&self, semid: SemId) { + pub(crate) fn take_record(&self, semid: SemId) -> Option { let mut state = self.inner.lock_irqsave(); - state.records.retain(|record| record.semid != semid); + let index = state + .records + .iter() + .position(|record| record.semid == semid)?; + Some(state.records.swap_remove(index)) + } + + #[cfg(test)] + pub(crate) fn remove_record(&self, semid: SemId) { + drop(self.take_record(semid)); } #[cfg(test)] @@ -786,6 +795,7 @@ mod tests { } assert_eq!(group.record_count_for_test(), 1); let mut wakes = SemWakeBatch::default(); + let mut removed = None; let mut manager = ipc_ns.sem.lock(); match control { 0 => manager.setval(pending, 0, 9, &mut wakes).unwrap(), @@ -793,7 +803,7 @@ mod tests { let token = manager.prepare_setall(pending).unwrap(); manager.setall(token, &[9, 8], &mut wakes).unwrap(); } - _ => manager.ipc_rmid(pending, &mut wakes).unwrap(), + _ => removed = Some(manager.ipc_rmid(pending, &mut wakes).unwrap()), } let next = group.pop_retired_record(); if control == 2 { @@ -818,6 +828,7 @@ mod tests { assert!(!group.begin_replay()); drop(manager); wakes.wake_all(); + drop(removed); drop(next); } } diff --git a/kernel/src/ipc/syscall/sys_semctl.rs b/kernel/src/ipc/syscall/sys_semctl.rs index 529cb3f7e9..203b464a59 100644 --- a/kernel/src/ipc/syscall/sys_semctl.rs +++ b/kernel/src/ipc/syscall/sys_semctl.rs @@ -83,8 +83,12 @@ pub(super) fn do_kernel_semctl( } // Remove the semaphore set SemCtlCmd::IpcRmid => { - let mut guard = ipcns.sem.lock(); - guard.ipc_rmid(semid, &mut wakes)?; + let removed = { + let mut guard = ipcns.sem.lock(); + guard.ipc_rmid(semid, &mut wakes)? + }; + wakes.wake_all(); + drop(removed); Ok(0) } // Query a single semaphore diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index 944c57f741..1bce2bfdfc 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -2340,30 +2340,59 @@ TEST(SysVSem, WaitQueuesGrowForConstAndAlterOperations) { TEST(SysVSem, RemoveWakesBothWaitQueuesInBulk) { constexpr int kPairs = 16; - SemSet sem(2, IPC_CREAT | 0600); + SemSet sem(32000, IPC_CREAT | 0600); + SemSet unrelated(1, IPC_CREAT | 0600); ASSERT_TRUE(sem.valid()); + ASSERT_TRUE(unrelated.valid()); ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 1)); + int release[2]; + ASSERT_EQ(0, pipe(release)); + FdGuard release_read(release[0]), release_write(release[1]); std::vector> children; for (int i = 0; i < kPairs * 2; ++i) { const bool constant = i % 2 == 0; pid_t pid = fork(); ASSERT_GE(pid, 0); if (pid == 0) { + release_write.Close(); + // Each independent group owns a dense, published undo record in + // addition to its pending operation. RMID must retire both safely. + if (!SemUndoOpMustSucceed(sem.id(), 31999, 1) || + !SemUndoOpMustSucceed(unrelated.id(), 0, 1)) { + _exit(114); + } struct sembuf op = {static_cast(constant ? 0 : 1), static_cast(constant ? 0 : -1), SEM_UNDO}; struct timespec timeout = {10, 0}; int result = SemTimedOp(sem.id(), &op, 1, &timeout); - _exit(result == -1 && errno == EIDRM ? 0 : 115); + if (result != -1 || errno != EIDRM) _exit(115); + char token; + ssize_t gate_result; + do { + gate_result = read(release_read.get(), &token, 1); + } while (gate_result < 0 && errno == EINTR); + _exit(gate_result == 0 ? 0 : 116); } children.emplace_back(new ChildGuard(pid)); } + release_read.Close(); ASSERT_TRUE(WaitForZcnt(sem.id(), 0, kPairs)); ASSERT_TRUE(WaitForNcnt(sem.id(), 1, kPairs)); + EXPECT_EQ(kPairs * 2, SemCtl(unrelated.id(), 0, GETVAL, 0)); ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_RMID, 0)); sem.release(); + SemSet replacement(32000, IPC_CREAT | 0600); + ASSERT_TRUE(replacement.valid()); + ASSERT_EQ(0, SemCtl(replacement.id(), 31999, SETVAL, 9)); + // Groups cannot exit before replacement initialization. No assumption is + // made about immediate index reuse by the cyclic IPC allocator. + release_write.Close(); // EOF releases all groups without SIGPIPE on failure. for (auto& child : children) { WaitChildOk(child.get()); } + EXPECT_EQ(9, SemCtl(replacement.id(), 31999, GETVAL, 0)); + EXPECT_EQ(0, SemCtl(replacement.id(), 0, GETVAL, 0)); + EXPECT_EQ(0, SemCtl(unrelated.id(), 0, GETVAL, 0)); } TEST(SysVSem, SharedUndoGroupControlChangesStaySetLocal) { From 26adf2c068ac40c933f260a3e0ec6a967c6547fa Mon Sep 17 00:00:00 2001 From: longjin Date: Sun, 6 Sep 2026 03:17:00 +0000 Subject: [PATCH 25/34] fix(ipc): cache wait counts and retain zero undo records Signed-off-by: longjin --- kernel/src/ipc/sem.rs | 231 +++++++++++------- kernel/src/ipc/sem_undo.rs | 60 +++-- .../suites/normal/sysv_sem_semantics.cc | 166 +++++++++++++ 3 files changed, 354 insertions(+), 103 deletions(-) diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs index 101e410248..74de98bd7d 100644 --- a/kernel/src/ipc/sem.rs +++ b/kernel/src/ipc/sem.rs @@ -7,7 +7,7 @@ use alloc::{ sync::{Arc, Weak}, vec::Vec, }; -use core::{cell::Cell, fmt}; +use core::fmt; use hashbrown::HashMap; use system_error::SystemError; @@ -166,6 +166,9 @@ pub struct KernelSem { val: i32, /// sempid: process that last operated on this semaphore pid: Option>, + /// Counts of queued operations currently blocked on this semaphore. + ncnt: usize, + zcnt: usize, } /// Userspace `sembuf` (Linux `struct sembuf`, 6 bytes) @@ -468,16 +471,6 @@ impl SemQueueEntry { } } - fn update_blocker(&self, blocker: SemBlockedOp) { - let mut status = self.status.lock(); - if let SemQueueStatus::Queued { - blocker: current, .. - } = &mut *status - { - *current = blocker; - } - } - fn complete(&self, result: Result) -> bool { let mut status = self.status.lock(); if matches!(&*status, SemQueueStatus::Completed { .. }) { @@ -494,6 +487,7 @@ impl SemQueueEntry { true } + #[cfg(test)] fn is_waiting_on(&self, semnum: usize, wait_type: SemWaitType) -> bool { matches!( &*self.status.lock(), @@ -663,7 +657,15 @@ impl KernelSemSet { let mut sems = Vec::new(); sems.try_reserve_exact(nsems) .map_err(|_| SystemError::ENOMEM)?; - sems.resize(nsems, KernelSem { val: 0, pid: None }); + sems.resize( + nsems, + KernelSem { + val: 0, + pid: None, + ncnt: 0, + zcnt: 0, + }, + ); Ok(sems) } @@ -689,11 +691,53 @@ impl KernelSemSet { /// The prepared entry itself owns the queue links; publication cannot allocate. fn enqueue_waiter(&mut self, waiter: Arc) { + let blocker = match &*waiter.status.lock() { + SemQueueStatus::Queued { + blocker, + links: None, + } => *blocker, + _ => panic!("enqueue requires an unlinked semaphore operation"), + }; let queue = match Self::pending_queue_for(&waiter.sops) { SemPendingQueue::Const => &mut self.pending_const, SemPendingQueue::Alter => &mut self.pending_alter, }; queue.push_back(waiter); + self.change_wait_count(blocker, true); + } + + fn change_wait_count(&mut self, blocker: SemBlockedOp, increase: bool) { + let sem = &mut self.sems[blocker.semnum]; + let count = match blocker.wait_type { + SemWaitType::Increase => &mut sem.ncnt, + SemWaitType::Zero => &mut sem.zcnt, + }; + *count = if increase { + count.checked_add(1).expect("semaphore wait count overflow") + } else { + count + .checked_sub(1) + .expect("semaphore wait count underflow") + }; + } + + /// Only linked entries contribute to counts; preparation uses this same + /// operation without accounting. Manager lock serializes both kinds. + fn update_blocker(&mut self, entry: &SemQueueEntry, blocker: SemBlockedOp) { + let mut status = entry.status.lock(); + if let SemQueueStatus::Queued { + blocker: old, + links, + } = &mut *status + { + if links.is_some() + && (old.semnum != blocker.semnum || old.wait_type != blocker.wait_type) + { + self.change_wait_count(*old, false); + self.change_wait_count(blocker, true); + } + *old = blocker; + } } fn remove_waiter(&mut self, target: &Arc) { @@ -713,38 +757,41 @@ impl KernelSemSet { } fn remove_pending(&mut self, queue: SemPendingQueue, entry: &Arc) { - match queue { + let blocker = match &*entry.status.lock() { + SemQueueStatus::Queued { + blocker, + links: Some(_), + .. + } => *blocker, + _ => return, + }; + let removed = match queue { SemPendingQueue::Const => self.pending_const.remove(entry), SemPendingQueue::Alter => self.pending_alter.remove(entry), }; + if removed { + self.change_wait_count(blocker, false); + } } /// Publish removal under the manager lock; the caller wakes after unlocking. fn complete_all_removed(&mut self, wakes: &mut SemWakeBatch) { for entry in self.pending_const.iter() { - self.pending_const.remove(&entry); + self.remove_pending(SemPendingQueue::Const, &entry); wakes.complete(entry, Err(SystemError::EIDRM)); } for entry in self.pending_alter.iter() { - self.pending_alter.remove(&entry); + self.remove_pending(SemPendingQueue::Alter, &entry); wakes.complete(entry, Err(SystemError::EIDRM)); } } fn ncnt(&self, semnum: usize) -> usize { - self.pending_const - .iter() - .chain(self.pending_alter.iter()) - .filter(|entry| entry.is_waiting_on(semnum, SemWaitType::Increase)) - .count() + self.sems[semnum].ncnt } fn zcnt(&self, semnum: usize) -> usize { - self.pending_const - .iter() - .chain(self.pending_alter.iter()) - .filter(|entry| entry.is_waiting_on(semnum, SemWaitType::Zero)) - .count() + self.sems[semnum].zcnt } } @@ -958,37 +1005,6 @@ impl SemManager { } } - fn release_unused_undo_group( - ipcns: &Arc, - semid: SemId, - group: &Arc, - ) { - { - let mut manager = ipcns.sem.lock(); - let Ok(set) = manager.get_by_semid_checked(semid) else { - return; - }; - if group.with_record_mut(semid, |_| ()).is_some() - || set - .pending_const - .iter() - .chain(set.pending_alter.iter()) - .any(|entry| { - entry - .undo_group - .as_ref() - .is_some_and(|owner| Arc::ptr_eq(owner, group)) - }) - { - return; - } - // Other Missing preparations outside the lock re-register before - // publication. Existing records (even zero debt) keep their link. - manager.unregister_undo_group(semid, group); - } - Self::shrink_undo_registry(ipcns, semid); - } - pub(crate) fn clear_undo_for_setval(&mut self, semid: SemId, semnum: usize) { let Ok(set) = self.get_by_semid_checked_mut(semid) else { return; @@ -1435,7 +1451,7 @@ impl SemManager { wakes.complete(entry.clone(), Err(SystemError::EAGAIN_OR_EWOULDBLOCK)); } Ok(SemopOutcome::Blocked(blocker)) => { - entry.update_blocker(blocker); + set.update_blocker(&entry, blocker); } Err(error) => { set.remove_pending(queue, &entry); @@ -1478,7 +1494,7 @@ impl SemManager { if blocker.nowait { Err(SystemError::EAGAIN_OR_EWOULDBLOCK) } else { - entry.update_blocker(blocker); + set.update_blocker(entry, blocker); Ok(None) } } @@ -1605,16 +1621,6 @@ impl SemManager { } else { None }; - let registered_missing = Cell::new(false); - // Declare before all entries and manager guards. Failed/timeout-only - // operations have no published record for final-owner exit to enumerate. - defer::defer!({ - if registered_missing.get() { - if let Some(group) = undo_group.as_ref() { - Self::release_unused_undo_group(ipcns, semid, group); - } - } - }); let mut immediate_scratch = SemopScratch::try_new(sops.len())?; let plain_prepared_entry = if has_undo { None @@ -1693,15 +1699,18 @@ impl SemManager { continue; } if let Some(prepared_entry) = prepared_undo { - // Only a published record can take the fast path. Missing - // (including queued) records must be linked before publication. + let mut record_slot = prepared_entry.undo_record.lock_irqsave(); + let prepared_record = record_slot + .take() + .expect("prepared SEM_UNDO entry owns its record"); + if prepared_record.adjustment_count() != nsems { + return Err(SystemError::EINVAL); + } + // First-use candidates must be associated before zero-record + // publication. Queued operations retain Existing tokens only. // Full semid was rechecked above, so Existing cannot refer to a // destroyed/reused set. SETVAL/SETALL retain live associations. - let already_associated = prepared_entry - .undo_record - .lock_irqsave() - .as_ref() - .is_some_and(PreparedSemUndoRecord::was_existing); + let already_associated = prepared_record.was_existing(); if !already_associated { let set = guard.get_by_semid_checked_mut(semid)?; if let Err(capacity) = set.ensure_undo_group_registered_prepared( @@ -1713,15 +1722,6 @@ impl SemManager { registry_capacity_needed = capacity; continue; } - registered_missing.set(true); - } - let mut record_slot = prepared_entry.undo_record.lock_irqsave(); - let prepared_record = record_slot - .take() - .expect("prepared SEM_UNDO entry owns its record"); - if prepared_record.adjustment_count() != nsems { - *record_slot = Some(prepared_record); - continue; } let set = guard.get_by_semid_checked_mut(semid)?; let (outcome, kept_record) = undo_group @@ -1762,7 +1762,7 @@ impl SemManager { return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); } drop(record_slot); - prepared_entry.update_blocker(blocker); + set.update_blocker(&prepared_entry, blocker); set.enqueue_waiter(prepared_entry.clone()); break prepared_entry; } @@ -1785,7 +1785,7 @@ impl SemManager { let prepared_entry = plain_prepared_entry .as_ref() .expect("plain queued semop entry is preallocated"); - prepared_entry.update_blocker(blocker); + set.update_blocker(prepared_entry, blocker); set.enqueue_waiter(prepared_entry.clone()); break prepared_entry.clone(); } @@ -2162,6 +2162,65 @@ mod tests { } } + #[test] + fn wait_counts_follow_only_linked_current_blockers() { + let mut manager = SemManager::new(); + let id = insert_test_set(&mut manager, SemKey::new(156), &[0, 1]); + let set = manager.get_by_semid_checked_mut(id).unwrap(); + let (_waiter, waker) = Waiter::new_pair(); + let increase = SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: false, + }; + let zero = SemBlockedOp { + semnum: 1, + wait_type: SemWaitType::Zero, + nowait: false, + }; + let entry = Arc::new(SemQueueEntry::new( + &[plain_sop(0, -1), plain_sop(1, 0)], + None, + waker, + increase, + )); + set.update_blocker(&entry, zero); + assert_eq!((set.ncnt(0), set.zcnt(1)), (0, 0)); + set.enqueue_waiter(entry.clone()); + assert_eq!((set.ncnt(0), set.zcnt(1)), (0, 1)); + set.update_blocker(&entry, increase); + assert_eq!((set.ncnt(0), set.zcnt(1)), (1, 0)); + set.update_blocker(&entry, increase); // No double-accounting. + let same_slot_zero = SemBlockedOp { semnum: 0, ..zero }; + set.update_blocker(&entry, same_slot_zero); + assert_eq!((set.ncnt(0), set.zcnt(0)), (0, 1)); + for semnum in 0..2 { + for (kind, cached) in [ + (SemWaitType::Increase, set.ncnt(semnum)), + (SemWaitType::Zero, set.zcnt(semnum)), + ] { + let scanned = set + .pending_const + .iter() + .chain(set.pending_alter.iter()) + .filter(|entry| entry.is_waiting_on(semnum, kind)) + .count(); + assert_eq!(cached, scanned); + } + } + set.remove_waiter(&entry); + set.remove_waiter(&entry); + assert_eq!((set.ncnt(0), set.zcnt(0)), (0, 0)); + set.enqueue_waiter(entry.clone()); + let mut wakes = SemWakeBatch::default(); + set.complete_all_removed(&mut wakes); + assert_eq!( + (set.ncnt(0), set.zcnt(0), set.ncnt(1), set.zcnt(1)), + (0, 0, 0, 0) + ); + assert_eq!(entry.completed_result(), Some(Err(SystemError::EIDRM))); + } + #[test] fn last_owner_replays_adjustment_with_clamp_and_removes_record() { let ipc_ns = test_ipc_ns(); diff --git a/kernel/src/ipc/sem_undo.rs b/kernel/src/ipc/sem_undo.rs index 5fd6f581f5..72827e55b2 100644 --- a/kernel/src/ipc/sem_undo.rs +++ b/kernel/src/ipc/sem_undo.rs @@ -381,7 +381,9 @@ impl SemUndoGroup { /// Borrow the current record under the group lock, never a stale snapshot. /// The callback must simulate without writes and mutate only on Publish; - /// Keep (including errors/blocked operations) must leave the record unchanged. + /// Keep (including errors/blocked operations) must leave the debt unchanged. + /// First use publishes a zero record before calling the simulation, as in + /// Linux find_alloc_undo; Keep retains only a lightweight existing token. /// SemopScratch provides that separation without an additional transaction. pub(crate) fn with_prepared_record_noalloc( &self, @@ -420,25 +422,25 @@ impl SemUndoGroup { }; } - // Existing tokens cannot recreate a record removed by RMID. Missing - // candidates stay zero until a successful, locally simulated commit. - let candidate = record.candidate.as_mut().ok_or(SystemError::EINVAL)?; + // Like Linux find_alloc_undo, publish a zero record before simulating: + // even a blocked/failed operation retains this group/set association. + // Existing tokens cannot recreate a record removed by RMID. + if record.candidate.is_none() { + return Err(SystemError::EINVAL); + } if state.records.len() >= state.records.capacity() { return Err(SystemError::ENOMEM); } - - match f(candidate) { - PreparedSemUndoRecordAction::Publish(result) => { - if let Some(mut reservation) = record.reservation.take() { - state.reserved_records = state - .reserved_records - .checked_sub(1) - .expect("SEM_UNDO record reservation count underflow"); - reservation.disarm(); - } - state.records.push(record.candidate.take().unwrap()); - Ok((result, None)) - } + if let Some(mut reservation) = record.reservation.take() { + state.reserved_records = state + .reserved_records + .checked_sub(1) + .expect("SEM_UNDO record reservation count underflow"); + reservation.disarm(); + } + state.records.push(record.candidate.take().unwrap()); + match f(state.records.last_mut().unwrap()) { + PreparedSemUndoRecordAction::Publish(result) => Ok((result, None)), PreparedSemUndoRecordAction::Keep(result) => Ok((result, Some(record))), } } @@ -1007,6 +1009,30 @@ mod tests { assert_eq!(group.record_capacity_for_test(), before_capacity); } + #[test] + fn failed_first_operation_keeps_zero_record_without_another_reservation() { + let group = SemUndoGroup::new_for_test(); + let semid = SemId::new(214); + let prepared = group.prepare_record_for_test(semid, 2).unwrap(); + assert_eq!(group.record_count_for_test(), 0); // Preparation alone is not publication. + let (result, token) = group + .with_prepared_record_noalloc(prepared, |record| { + assert_eq!(record.adjustment(0), 0); + PreparedSemUndoRecordAction::Keep(Err::<(), _>(SystemError::EAGAIN_OR_EWOULDBLOCK)) + }) + .unwrap(); + assert_eq!(result, Err(SystemError::EAGAIN_OR_EWOULDBLOCK)); + assert!(token.unwrap().was_existing()); + assert_eq!(group.record_count_for_test(), 1); + assert_eq!(group.pending_record_reservations_for_test(), 0); + assert_eq!(group.adjustment_for_test(semid, 0), 0); + assert_eq!(group.adjustment_for_test(semid, 1), 0); + assert!(group + .prepare_record_for_test(semid, 2) + .unwrap() + .was_existing()); + } + #[test] fn existing_preparation_reads_current_debt_and_updates_only_one_slot() { let group = SemUndoGroup::new_for_test(); diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index 1bce2bfdfc..bde36f1547 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -1941,6 +1941,172 @@ TEST(SysVSem, BlockingWakeupZero) { WaitChildOk(child); } +bool WaitForPipeEof(int fd) { + char token; + ssize_t result; + do { result = read(fd, &token, 1); } while (result < 0 && errno == EINTR); + return result == 0; +} + +TEST(SysVSem, WaitCountsTrackBlockerMigrationAndTerminalRemoval) { + // Success, signal cancellation, a later NOWAIT failure, and same-slot + // zero/increase migration (which cannot complete and is cancelled). + for (int mode = 0; mode < 4; ++mode) { + SemSet sem(3, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 1)); + const int increase_slot = mode == 3 ? 0 : 1; + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + struct sigaction action = {}; + action.sa_handler = [](int) {}; + sigemptyset(&action.sa_mask); + if (sigaction(SIGUSR1, &action, nullptr) != 0) _exit(140); + struct sembuf ops[] = {{0, 0, 0}, + {static_cast(increase_slot), -1, 0}, + {2, -1, IPC_NOWAIT}}; + int result = SemOp(sem.id(), ops, mode == 2 ? 3 : 2); + const int expected_errno = mode == 2 ? EAGAIN : EINTR; + _exit((mode == 0 ? result == 0 : result == -1 && errno == expected_errno) ? 0 : 141); + } + ChildGuard child(pid); + ASSERT_TRUE(WaitForZcnt(sem.id(), 0, 1)); + for (int pass = 0; pass < 2; ++pass) { + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 0)); + ASSERT_TRUE(WaitForNcnt(sem.id(), increase_slot, 1)); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETZCNT, 0)); + EXPECT_EQ(0, SemCtl(sem.id(), 2, GETNCNT, 0)); + if (pass == 0) { + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 1)); + ASSERT_TRUE(WaitForZcnt(sem.id(), 0, 1)); + EXPECT_EQ(0, SemCtl(sem.id(), increase_slot, GETNCNT, 0)); + } + } + if (mode == 1 || mode == 3) { + ASSERT_EQ(0, kill(pid, SIGUSR1)); + } else { + ASSERT_EQ(0, SemCtl(sem.id(), 1, SETVAL, 1)); + } + WaitChildOk(&child); + for (int slot = 0; slot < 3; ++slot) { + EXPECT_EQ(0, SemCtl(sem.id(), slot, GETNCNT, 0)); + EXPECT_EQ(0, SemCtl(sem.id(), slot, GETZCNT, 0)); + } + EXPECT_EQ(mode == 2 ? 1 : 0, SemCtl(sem.id(), 1, GETVAL, 0)) + << "later NOWAIT failure must not commit the earlier decrement"; + } +} + +TEST(SysVSem, FailedOnlyUndoUpdatesTimeOnExit) { + for (bool timed : {false, true}) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + struct semid_ds before = {}; + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_STAT, reinterpret_cast(&before))); + ASSERT_EQ(0, before.sem_otime); + const int original_pid = SemCtl(sem.id(), 0, GETPID, 0); + ASSERT_GE(original_pid, 0); + int ready[2], release[2]; + ASSERT_EQ(0, pipe(ready)); + FdGuard ready_read(ready[0]), ready_write(ready[1]); + ASSERT_EQ(0, pipe(release)); + FdGuard release_read(release[0]), release_write(release[1]); + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + ready_read.Close(); + release_write.Close(); + struct sembuf op = {0, -1, static_cast(SEM_UNDO | (timed ? 0 : IPC_NOWAIT))}; + struct timespec zero = {}; + int result = timed ? SemTimedOp(sem.id(), &op, 1, &zero) : SemOp(sem.id(), &op, 1); + if (result != -1 || errno != EAGAIN) _exit(142); + const char token = 1; + _exit(WriteExact(ready_write.get(), &token, 1) && WaitForPipeEof(release_read.get()) + ? 0 : 143); + } + ChildGuard child(pid); + ready_write.Close(); + release_read.Close(); + char token; + ASSERT_TRUE(ReadExact(ready_read.get(), &token, 1)); + struct semid_ds failed = {}; + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_STAT, reinterpret_cast(&failed))); + EXPECT_EQ(0, failed.sem_otime); + EXPECT_EQ(original_pid, SemCtl(sem.id(), 0, GETPID, 0)); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); + release_write.Close(); + WaitChildOk(&child); + struct semid_ds exited = {}; + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_STAT, reinterpret_cast(&exited))); + EXPECT_NE(0, exited.sem_otime) << "exit replays even a zero-adjustment undo record"; + EXPECT_EQ(original_pid, SemCtl(sem.id(), 0, GETPID, 0)); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETVAL, 0)); + } +} + +struct QueuedSharedUndoArgs { + int semid; + int ready_read; + int ready_write; + int release_read; + int release_write; +}; + +int QueuedSharedUndoChild(void* opaque) { + const auto& args = *static_cast(opaque); + close(args.ready_read); + close(args.release_write); + if (!SemUndoOpMustSucceed(args.semid, 0, -1)) return 144; + const char token = 1; + return WriteExact(args.ready_write, &token, 1) && WaitForPipeEof(args.release_read) ? 0 : 145; +} + +int RunSharedUndoFailureSupervisor(int semid, int cmd) { + int ready[2], release[2]; + if (pipe(ready) != 0) return 146; + FdGuard ready_read(ready[0]), ready_write(ready[1]); + if (pipe(release) != 0) return 147; + FdGuard release_read(release[0]), release_write(release[1]); + alignas(16) char stack[16384]; + QueuedSharedUndoArgs args = {semid, ready[0], ready[1], release[0], release[1]}; + pid_t pid = clone(QueuedSharedUndoChild, stack + sizeof(stack), CLONE_SYSVSEM | SIGCHLD, &args); + if (pid < 0) return 148; + ChildGuard child(pid); + ready_write.Close(); + release_read.Close(); + if (!WaitForNcnt(semid, 0, 1)) return 149; + struct sembuf fail = {0, -1, SEM_UNDO | IPC_NOWAIT}; + if (SemOp(semid, &fail, 1) != -1 || errno != EAGAIN) return 150; + if (SemCtl(semid, 0, SETVAL, 1) != 0) return 151; + char token; + if (!ReadExact(ready_read.get(), &token, 1) || SemCtl(semid, 0, GETVAL, 0) != 0) return 152; + unsigned short value = 7; + if (SemCtl(semid, 0, cmd, cmd == SETALL ? reinterpret_cast(&value) : 7) != 0) + return 153; + release_write.Close(); + int status; + pid_t ret; + do { ret = waitpid(pid, &status, 0); } while (ret < 0 && errno == EINTR); + if (ret != pid) return 154; + child.MarkReaped(); + return WIFEXITED(status) && WEXITSTATUS(status) == 0 ? 0 : 155; +} + +TEST(SysVSem, SharedUndoFailurePreservesQueuedAssociation) { + for (int cmd : {SETVAL, SETALL}) { + SemSet sem(1, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) _exit(RunSharedUndoFailureSupervisor(sem.id(), cmd)); + ChildGuard supervisor(pid); + WaitChildOk(&supervisor); + EXPECT_EQ(7, SemCtl(sem.id(), 0, GETVAL, 0)); + EXPECT_EQ(0, SemCtl(sem.id(), 0, GETNCNT, 0)); + } +} + TEST(SysVSem, GetNcntCountsBlocked) { SemSet sem(1, IPC_CREAT | 0600); ASSERT_TRUE(sem.valid()); From 41ebf5afefe734a46584086d773bf25c1f0aa7db Mon Sep 17 00:00:00 2001 From: longjin Date: Sun, 6 Sep 2026 03:35:02 +0000 Subject: [PATCH 26/34] docs: remove sem undo implementation plan and design spec Signed-off-by: longjin --- .../2026-09-03-sem-undo-implementation.md | 1277 ----------------- .../specs/2026-09-03-sem-undo-design.md | 687 --------- 2 files changed, 1964 deletions(-) delete mode 100644 docs/superpowers/plans/2026-09-03-sem-undo-implementation.md delete mode 100644 docs/superpowers/specs/2026-09-03-sem-undo-design.md diff --git a/docs/superpowers/plans/2026-09-03-sem-undo-implementation.md b/docs/superpowers/plans/2026-09-03-sem-undo-implementation.md deleted file mode 100644 index da1816be5a..0000000000 --- a/docs/superpowers/plans/2026-09-03-sem-undo-implementation.md +++ /dev/null @@ -1,1277 +0,0 @@ -# SEM_UNDO Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** 在 DragonOS 的 System V semaphore 实现中,以 Linux 6.6 生命周期语义实现 `SEM_UNDO`,同时保证立即提交、排队重试、控制操作、fork/clone、namespace 切换和 task exit 的记录与回放均具备失败原子性。 - -**Architecture:** 每个 PCB 持有一个显式的 `SemUndoAttachment` slot;attachment 持有 `Arc`,group 绑定唯一 `Weak`,并以完整、含 generation 的 `SemId` 保存每个 semaphore-set 的 adjustment slice。每个 IPC namespace 的 `SemManager` 用 `Arc>>` registry snapshot 枚举仍活跃的 group;排队操作以不计入 task owner 数的强 `Arc` observer 固定原调用者。操作仍由既有 `IpcNamespace.sem: SpinLock` 串行化;本计划不引入 per-set lock、全局 namespace 重构或新的用户 ABI。 - -**Tech Stack:** Rust `no_std` kernel、`alloc::{Arc, Weak, Vec, Box}`、DragonOS `SpinLock`、System V semaphore syscalls、GoogleTest dunitest、QEMU dunitest runner。 - -## Global Constraints - -- 只在最终提交移除 [`kernel/src/ipc/sem.rs:683-688`](kernel/src/ipc/sem.rs#L683-L688) 的 `SEM_UNDO -> ENOSYS` 门禁;此前所有 `SEM_UNDO` syscall 仍必须返回 `ENOSYS`。 -- PCB `sem_undo` slot lock 不得与其他 SEM_UNDO 锁嵌套;跨对象方向固定为 `IpcNamespace.sem` manager lock → `SemUndoGroup.inner` → queue-entry 的 `undo_record` / `scratch` / `status` 锁。禁止任何 group → manager 反向获取。 -- 禁止在持有 `ipcns.sem` manager spinlock 时动态分配:不得保留 [`simulate_semop()` 的 `HashMap::with_capacity()`,sem.rs:539-580](kernel/src/ipc/sem.rs#L539-L580)、[`SemQueueEntry::new()` 的 `to_vec()`,sem.rs:264-276](kernel/src/ipc/sem.rs#L264-L276) 或 blocked 分支的 `Arc::new()`(sem.rs:702-735)。 -- 所有用户触发的增长都用 `Arc::try_new`、`Vec::try_reserve_exact` 等 fallible API,并映射为 `SystemError::ENOMEM`;任何 allocation error 均不得改变 semval、semadj、queue、record 或 registry。 -- record key 必须是完整 `SemId`,不得使用低 index;`adjustments.len()` 必须严格等于目标 `KernelSemSet.sems.len()`。 -- `SEMUME`、`SEMMNU` 只维持 [`PosixSemInfo::new()`](kernel/src/ipc/sem.rs#L210-L230) 的报告兼容,绝不参与 admission;操作数上限继续使用全局 `SEMOPM = 500`。 -- 维持一个 namespace 一把 manager lock 的既有结构;明确**不实现 per-set lock**。 -- 不能把 namespace/fork 全面 publication 重构作为 SEM_UNDO 主体;仅完成“child owner token 在任何 child publication 前安装、任何后续可失败路径先回滚 publication 再撤销 owner”的最小安全协议。 -- 所有 kernel 内部测试放在相关模块的 `#[cfg(test)] mod tests`;当前 kernel `Cargo.toml` 的 library 是 `staticlib`,根 `kernel/Makefile:test` 只跑 workspace crates 且排除 `dragonos_kernel`,因此新增的 kernel `#[test]` 需使用项目实际可运行的内核测试 target,不能把 `make -C kernel test` 误报为这些测试的执行证据。 -- dunitest 源文件已存在于 [`user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc`](user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc),且 whitelist 已含 `normal/sysv_sem_semantics`(`whitelist.txt:37`);无需新建 CMake 注册项。 - ---- - -## 实际落点与职责 - -| 文件 | 变更职责 | -|---|---| -| [`kernel/src/ipc/sem_undo.rs`](kernel/src/ipc/sem_undo.rs) | 新模块:attachment、group、record、owner 计数、record/registry prepare、SETVAL/SETALL/RMID cleanup、最后 owner replay 的可测试原语。 | -| [`kernel/src/ipc/mod.rs:1-12`](kernel/src/ipc/mod.rs#L1-L12) | 导出 `sem_undo`。 | -| [`kernel/src/ipc/sem.rs:6-1008`](kernel/src/ipc/sem.rs) | registry 字段;无分配 simulation/commit;queue 捕获 group;SETVAL/SETALL/RMID cleanup;semtimedop 最终接入。 | -| [`kernel/src/process/task.rs:59-237, 325-407, 1581-1594`](kernel/src/process/task.rs) | PCB `SpinLock>` 与语义 API。 | -| [`kernel/src/process/fork.rs:575-1100`](kernel/src/process/fork.rs) | `CLONE_NEWIPC|CLONE_SYSVSEM` 早拒绝;普通 fork 不继承;显式 SYSVSEM 共享、child attachment guard、最小 rollback protocol。 | -| [`kernel/src/process/manager/exit.rs:450-488, 662-704`](kernel/src/process/manager/exit.rs) | robust-list 后、`exit_mm` 前调用唯一 detach/replay;reap 只断言 slot 已空。 | -| [`kernel/src/process/namespace/nsproxy.rs:294-343`](kernel/src/process/namespace/nsproxy.rs) | 将 IPC detach 作为 namespace install 的明确输入;prepare 完成后、安装新 nsproxy 前同步 detach。 | -| [`kernel/src/process/namespace/unshare.rs:19-53`](kernel/src/process/namespace/unshare.rs) | `CLONE_SYSVSEM` 和 `CLONE_NEWIPC` 设置 detach 意图,并接入统一 install helper。 | -| [`kernel/src/process/namespace/setns.rs:96-267`](kernel/src/process/namespace/setns.rs) | pidfd / namespace-fd 两条路径均传递 `detach_sysvsem`,same-Arc IPC setns 仍 detach。 | -| [`kernel/src/process/namespace/ipc_namespace.rs:17-64`](kernel/src/process/namespace/ipc_namespace.rs) | 仅增加已审计的不变量断言,或在无法证明时加明确调用上下文的 `destroy_all()`;禁止在 `Drop` 取 manager lock。 | -| [`kernel/src/process/pid.rs:630-681`](kernel/src/process/pid.rs) | 如 replay 需要以 group namespace 可见的 TGID 更新 `sempid`,将现有私有 `task_pid_nr_ns(PidType::TGID, ...)` 以最小方式开放给 process/IPС 内部调用者;不能用 raw PID 或 `task_pid_vnr()` 替代。 | -| [`user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc:41-226, 706-714`](user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc) | 删除 `SemUndoRejected`,改为公开 ABI SEM_UNDO 语义测试。 | -| [`user/apps/tests/dunitest/Makefile:24-105`](user/apps/tests/dunitest/Makefile) | 不应修改;已有 `wildcard suites/*/*.cc` 自动构建。 | -| [`user/apps/tests/dunitest/whitelist.txt:36-38`](user/apps/tests/dunitest/whitelist.txt#L36-L38) | 不应修改;已有 test binary 白名单。 | - ---- - -### Task 1: 固化 SEM_UNDO 所有权模型和 PCB slot,但保持 ABI 关闭 - -**Files:** -- Create: `kernel/src/ipc/sem_undo.rs` -- Modify: `kernel/src/ipc/mod.rs:1-12` -- Modify: `kernel/src/process/task.rs:5-56, 59-237, 325-407` -- Modify: `kernel/src/ipc/sem.rs:388-418` -- Test: `kernel/src/ipc/sem_undo.rs` 的 `#[cfg(test)] mod tests` -- Test: `kernel/src/ipc/sem.rs:930-1008` - -**Interfaces:** -- Produces: - ```rust - pub struct SemUndoAttachment { /* group: Arc */ } - - pub struct SemUndoGroup { - /* ipc_ns: Weak, - inner: SpinLock */ - } - - pub(crate) struct UnpublishedSemUndoAttachmentGuard { /* fork-local */ } - - impl ProcessControlBlock { - pub fn sem_undo_group(&self) -> Option>; - pub fn ensure_sem_undo_group( - &self, - ipc_ns: &Arc, - ) -> Result, SystemError>; - pub fn take_sem_undo_attachment(&self) -> Option; - } - ``` -- Consumes: `IpcNamespace` 的 `Arc`/`Weak`([`ipc_namespace.rs:17-64`](kernel/src/process/namespace/ipc_namespace.rs#L17-L64))、`SpinLock`、`SystemError::ENOMEM`。 -- Does not produce any user-visible SEM_UNDO behavior in this task. - -- [ ] **Step 1: 写失败的 ownership 单测。** - - 在新模块 `sem_undo.rs` 的 `#[cfg(test)]` 中先覆盖以下不可变行为: - - ```rust - #[test] - fn observer_arc_does_not_change_task_owner_count() { - let group = SemUndoGroup::new_for_test(); - let attachment = SemUndoAttachment::new_for_test(group.clone()); - let observer = attachment.group_for_test(); - assert_eq!(group.task_owners_for_test(), 1); - drop(observer); - assert_eq!(group.task_owners_for_test(), 1); - } - - #[test] - fn attachment_is_taken_once_and_drop_never_replays() { - let attachment = SemUndoAttachment::new_for_test(SemUndoGroup::new_for_test()); - let mut slot = Some(attachment); - assert!(slot.take().is_some()); - assert!(slot.take().is_none()); - } - - #[test] - fn group_rejects_different_ipc_namespace() { - let group = SemUndoGroup::new_for_test_bound_to_first_namespace(); - assert_eq!( - group.verify_ipc_ns_for_test(second_test_ipc_ns()), - Err(SystemError::EINVAL) - ); - } - ``` - - 测试 hook 只能在 `cfg(test)` 存在;产品路径不允许公开 fake namespace 构造器或 failpoint ABI。 - -- [ ] **Step 2: 运行并确认测试失败。** - - Run(选择 DragonOS 实际 kernel-test 编译命令,以下为必须执行的 direct target): - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib ipc::sem_undo::tests --target x86_64-unknown-linux-gnu - ``` - - Expected: 编译失败,原因是 `ipc::sem_undo`、`SemUndoGroup` 和测试 helper 尚不存在。 - - 同时记录基线门禁: - - ```bash - grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs - ``` - - Expected: 仍能看到 `return Err(SystemError::ENOSYS);`。 - -- [ ] **Step 3: 实现最小数据模型和 PCB slot。** - - 在 `sem_undo.rs` 实现且只实现以下所有权基础: - - ```rust - pub struct SemUndoAttachment { - group: Arc, - } - - pub struct SemUndoGroup { - ipc_ns: Weak, - inner: SpinLock, - } - - struct SemUndoGroupState { - task_owners: usize, - records: Vec, - } - - struct SemUndoRecord { - semid: SemId, - adjustments: Box<[i16]>, - } - ``` - - 具体规则: - - 1. `SemUndoAttachment` 不派生 `Clone`。 - 2. `SemUndoGroup::new(ipc_ns: &Arc) -> Result, SystemError>` 使用 `Arc::try_new`,初始 `task_owners = 1`,`records = Vec::new()`。 - 3. PCB 新字段为: - ```rust - pub(super) sem_undo: SpinLock>, - ``` - 在 [`ProcessControlBlock::do_create_pcb()` 的 struct literal,task.rs:325-397](kernel/src/process/task.rs#L325-L397) 初始化为 `SpinLock::new(None)`。 - 4. `sem_undo_group()` 只短暂获取 PCB slot lock、clone `Arc` 后释放;不能返回 attachment。 - 5. `ensure_sem_undo_group()`:slot lock 检查为空后释放;锁外 `SemUndoGroup::new()`;再次取得 slot lock,已有 group 胜出时丢弃 candidate,否则安装 `SemUndoAttachment`。不嵌套 slot lock 与 group/manager lock。 - 6. `take_sem_undo_attachment()` 仅 `self.sem_undo.lock_irqsave().take()`;无 replay、无 manager lock。 - 7. `Drop for SemUndoAttachment` 仅 debug assertion / diagnostic,绝不可 replay 或获取 manager lock。 - 8. 给 `SemManager` 增加: - ```rust - undo_groups: Arc>>, - ``` - 并在 [`SemManager::new()`,sem.rs:411-418](kernel/src/ipc/sem.rs#L411-L418) 初始化为空 slice snapshot。此任务不注册任何 group。 - -- [ ] **Step 4: 运行测试并确认通过,且 ABI 仍关闭。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib ipc::sem_undo::tests --target x86_64-unknown-linux-gnu - ``` - - Expected: 新 ownership tests PASS。 - - Run: - - ```bash - grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs - ``` - - Expected: `ENOSYS` gate 仍存在。 - -- [ ] **Step 5: 编译目标内核。** - - Run: - - ```bash - cd . - make kernel - ``` - - Expected: `Kernel Build Done.`,无 SEM_UNDO 相关编译或 clippy error。 - -- [ ] **Step 6: Commit。** - - ```bash - git add kernel/src/ipc/mod.rs kernel/src/ipc/sem.rs kernel/src/ipc/sem_undo.rs kernel/src/process/task.rs - git commit -m "feat(ipc): add private sem undo ownership model" - ``` - ---- - -### Task 2: 接入 clone owner sharing,并建立最小 child rollback 安全协议 - -**Files:** -- Modify: `kernel/src/ipc/sem_undo.rs` -- Modify: `kernel/src/process/task.rs` -- Modify: `kernel/src/process/fork.rs:575-1100` -- Modify: `kernel/src/process/namespace/nsproxy.rs:125-200` -- Test: `kernel/src/ipc/sem_undo.rs` 的 `#[cfg(test)]` -- Test: `kernel/src/process/fork.rs` 的现有或新增 `#[cfg(test)]` 模块 - -**Interfaces:** -- Produces: - ```rust - impl ProcessControlBlock { - pub(crate) fn prepare_shared_sem_undo_attachment( - &self, - ipc_ns: &Arc, - ) -> Result; - } - - impl UnpublishedSemUndoAttachmentGuard { - pub(crate) fn install_into( - &mut self, - child: &ProcessControlBlock, - ); - pub(crate) fn disarm(self); - } - ``` -- Consumes: Task 1 的 attachment/group/PCB API、`CloneFlags::CLONE_SYSVSEM`([`fork.rs:37-93`](kernel/src/process/fork.rs#L37-L93))。 - -- [ ] **Step 1: 先写失败测试。** - - 覆盖以下最小协议,而不是用 `Arc::strong_count()` 推断 owner: - - ```rust - #[test] - fn ordinary_fork_child_starts_without_attachment() { - let parent = test_pcb_with_group(); - let child = test_unpublished_child(); - assert!(child.sem_undo_group().is_none()); - assert!(parent.sem_undo_group().is_some()); - } - - #[test] - fn sysvsem_guard_increments_once_then_install_moves_token() { - let parent = test_pcb_with_group(); - let group = parent.sem_undo_group().unwrap(); - let child = test_unpublished_child(); - - let mut guard = parent.prepare_shared_sem_undo_attachment(test_ipc_ns()).unwrap(); - assert_eq!(group.task_owners_for_test(), 2); - guard.install_into(&child); - assert!(child.sem_undo_group().is_some()); - guard.disarm(); - assert_eq!(group.task_owners_for_test(), 2); - } - - #[test] - fn installed_guard_rollback_takes_child_slot_and_only_drops_owner() { - let parent = test_pcb_with_group(); - let group = parent.sem_undo_group().unwrap(); - let child = test_unpublished_child(); - - let mut guard = parent.prepare_shared_sem_undo_attachment(test_ipc_ns()).unwrap(); - guard.install_into(&child); - drop(guard); - - assert!(child.sem_undo_group().is_none()); - assert_eq!(group.task_owners_for_test(), 1); - assert_eq!(group.replay_count_for_test(), 0); - } - ``` - - 并为 clone flag 前置拒绝加测试: - - ```rust - #[test] - fn newipc_and_sysvsem_are_rejected_before_attachment_prepare() { - let flags = CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_SYSVSEM; - assert_eq!(validate_semundo_clone_flags_for_test(flags), Err(SystemError::EINVAL)); - } - ``` - -- [ ] **Step 2: 确认 RED。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib 'ipc::sem_undo::tests::(ordinary_fork|sysvsem_guard|installed_guard)|process::fork::tests::newipc' --target x86_64-unknown-linux-gnu - ``` - - Expected: 编译失败,因为 guard、owner increment 和 install API 尚不存在。 - -- [ ] **Step 3: 实现 guard 和 clone 接入。** - - 1. `UnpublishedSemUndoAttachmentGuard` 持有“额外 owner token”和待安装 attachment;其 `Drop`: - - 若已安装,调用 child `take_sem_undo_attachment()`; - - 在 group lock 下 `task_owners -= 1`; - - **不回放**; - - debug-assert 减少前 owner 大于 1(parent token 必须仍存在)。 - 2. `prepare_shared_sem_undo_attachment()`: - - 使用 parent 当前 IPC ns,调用 `ensure_sem_undo_group()`;parent 没有 group 时允许创建空 group; - - 验证 group 仍绑定该 IPC namespace; - - 仅 group lock 下 `task_owners += 1`; - - 以已有 group `Arc` 组成 guard; - - 不得从 parent slot `take()`,不得把 `CLONE_THREAD` 推导为 `CLONE_SYSVSEM`。 - 3. 在 [`ProcessManager::copy_process()`](kernel/src/process/fork.rs#L575-L1100) 的既有 early validation 中,在任何 child 状态/namespace 创建/owner 修改前,加入: - ```rust - if (clone_flags & (CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_SYSVSEM)) - == (CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_SYSVSEM) - { - return Err(SystemError::EINVAL); - } - ``` - 不能依赖 [`copy_namespaces()` 中较晚的相同检查,nsproxy.rs:166-177](kernel/src/process/namespace/nsproxy.rs#L166-L177)。 - 4. `copy_namespaces()` 后、PID relation / `thread_group_live` / pid links / `children` / `add_pcb()` / cgroup visibility / scheduler 可观察 child 前,若 flags 含 `CLONE_SYSVSEM`,创建 guard 并立即 `install_into(pcb)`。 - 5. 目前 `copy_process()` 在 PID links 与 `children` publication 后仍可能在 [`install_reserved_fd(...)?`,fork.rs:1061-1066](kernel/src/process/fork.rs#L1061-L1066) 返回错误;所以先将 pidfd install 移进 relation publication 前的 prepare 阶段,或将 install 改为在完整 publication 前、后续不可失败的阶段完成。 - 6. 最小 publication rollback 不可只复用 [`rollback_failed_fork()`,fork.rs:1103-1129](kernel/src/process/fork.rs#L1103-L1129),因为该函数当前仅回收 pidfd/cgroup charge,未撤销 PID links、thread-group membership、children 或 global PCB。 - 7. 本任务的提交目标是:将所有 guard 创建后的 fallible operation 移到 guard 创建前;guard install 后到 `copy_process()` 成功返回之间不得保留 `?` 或显式 `Err` 分支。`guard.disarm()` 在 PID/TGID/PGID/SID attach、thread group / children relation、pidfd、`ProcessManager::add_pcb()`、cgroup visible accounting 完成后执行;[`wake_up_new_task()`,manager/sched.rs:94-121](kernel/src/process/manager/sched.rs#L94-L121) 必须发生在 disarm 后。 - 8. 普通 fork 不碰 child slot,天然保持 `None`。 - -- [ ] **Step 4: 确认 GREEN。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib ipc::sem_undo::tests --target x86_64-unknown-linux-gnu - ``` - - Expected: owner、install、rollback、flag tests PASS;无 replay 发生于 child rollback。 - -- [ ] **Step 5: 验证 ABI 仍保持关闭。** - - Run: - - ```bash - cd . - make kernel - grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs - ``` - - Expected: kernel 成功构建,且 `ENOSYS` 仍存在。 - -- [ ] **Step 6: Commit。** - - ```bash - git add kernel/src/ipc/sem_undo.rs kernel/src/process/task.rs kernel/src/process/fork.rs kernel/src/process/namespace/nsproxy.rs - git commit -m "feat(process): track shared sem undo owners across clone" - ``` - ---- - -### Task 3: 实现 detach/replay 基础并接入 exit,仍不开放 syscall - -**Files:** -- Modify: `kernel/src/ipc/sem_undo.rs` -- Modify: `kernel/src/ipc/sem.rs:311-639, 764-780` -- Modify: `kernel/src/process/manager/exit.rs:450-488, 662-704` -- Modify: `kernel/src/process/pid.rs:649-681`(仅在现有 visibility 不够时) -- Test: `kernel/src/ipc/sem_undo.rs` 的 `#[cfg(test)]` -- Test: `kernel/src/ipc/sem.rs` 的 `#[cfg(test)]` - -**Interfaces:** -- Produces: - ```rust - pub(crate) fn detach_sem_undo( - pcb: &Arc, - ); - - impl SemUndoGroup { - fn detach_owner_and_take_last_records( - &self, - ) -> Option>; - } - ``` -- Consumes: Task 1/2 的 `take_sem_undo_attachment()`;`SemManager` checked full-ID lookup;queue rescan。 - -- [ ] **Step 1: 写失败的 replay 单测。** - - 在 `sem_undo.rs` / `sem.rs` 的内部测试中直接用现有 [`insert_test_set()`,sem.rs:948-959](kernel/src/ipc/sem.rs#L948-L959) 风格构造 manager/set,并覆盖: - - ```rust - #[test] - fn last_owner_replays_adjustment_with_clamp_and_removes_record() { - // semval = 32766, adjustment = 4 -> clamp to SEMVMX. - // last owner replay removes record and leaves semval = 32767. - } - - #[test] - fn non_last_owner_does_not_replay() { - // owner 1 detach: semval and record unchanged. - // owner 2 detach: exactly one replay. - } - - #[test] - fn stale_full_semid_does_not_touch_reused_index() { - // old set record -> RMID -> same low index, new generation. - // replay old record; new set remains unchanged. - } - - #[test] - fn replay_updates_otime_and_rescans_waiter() { - // record adjustment turns a blocked waiter Ready. - // replay must update sem_otime, set exiting visible TGID as sempid, - // then complete the waiter in the same manager critical section. - } - ``` - -- [ ] **Step 2: 确认 RED。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib 'ipc::sem_undo::tests::(last_owner|non_last_owner|stale_full_semid|replay_updates)' --target x86_64-unknown-linux-gnu - ``` - - Expected: 编译或 assertions 失败,因为 detach/replay 尚未实现。 - -- [ ] **Step 3: 实现 explicit detach 与 replay。** - - 1. 新增唯一入口 `detach_sem_undo(pcb)`: - - 首先 `pcb.take_sem_undo_attachment()`,立即释放 PCB slot lock; - - 仅 group lock 下递减 `task_owners`; - - 非最后 owner 直接返回; - - 最后 owner 成为唯一 drainer,取走 records;禁止依赖 `Arc::strong_count()`、thread group、leader 或 TGID 判断。 - 2. 将调用放到 [`RobustListHead::cleanup_robust_list(&pcb)` 后、`exit_mm` 前,exit.rs:450-463](kernel/src/process/manager/exit.rs#L450-L463)。 - 3. 对每条 record: - - 先仅持 group lock 取 full `SemId`; - - upgrade group 的 namespace Weak; - - 取得 `ipcns.sem` manager lock,再取得 group lock; - - full-ID `get_by_semid_checked_mut(record.semid)` 成功才回放;generation mismatch / RMID 仅删除 record。 - 4. 回放每个非零 adjustment: - ```rust - let next = (sem.val as i64 + adjustment as i64).clamp(0, SEMVMX as i64); - sem.val = next as i32; - sem.pid = exiting_tgid.clone(); - ``` - `exiting_tgid` 必须由 group 的 IPC namespace 对应 task 视角解析;当前 [`task_tgid_vnr()`,pid.rs:679-681](kernel/src/process/pid.rs#L679-L681) 使用 current task active pid ns,不可在跨 namespace replay 中直接使用。最小改动是将 [`task_pid_nr_ns()`,pid.rs:649-655](kernel/src/process/pid.rs#L649-L655) 提升为 `pub(crate)`,让 replay 使用 group namespace 关联的 PID namespace 获取 `PidType::TGID`。 - 5. replay 发生值变动时更新 set 的 `sem_otime`,并在同一 manager 临界区复用 queue rescan。 - 6. `ProcessManager::release()`([`exit.rs:662-704`](kernel/src/process/manager/exit.rs#L662-L704))不应首次 detach;仅加 debug assertion 确保 slot 已空。 - 7. namespace Weak upgrade 失败只能按“无 live namespace state、不再回放”删除 record 并 debug-log;不可访问失效 namespace。 - -- [ ] **Step 4: 确认 GREEN。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib 'ipc::sem_undo::tests|ipc::sem::tests' --target x86_64-unknown-linux-gnu - ``` - - Expected: clamp、single final drainer、generation guard、`sem_otime` / queue rescan tests PASS。 - -- [ ] **Step 5: 编译并确认 user ABI 未发布。** - - Run: - - ```bash - cd . - make kernel - grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs - ``` - - Expected: build PASS,`ENOSYS` gate 仍在。 - -- [ ] **Step 6: Commit。** - - ```bash - git add kernel/src/ipc/sem_undo.rs kernel/src/ipc/sem.rs kernel/src/process/manager/exit.rs kernel/src/process/pid.rs - git commit -m "feat(ipc): replay sem undo on final task exit" - ``` - ---- - -### Task 4: 实现 namespace detach transaction,并保持 SEM_UNDO syscall 关闭 - -**Files:** -- Modify: `kernel/src/process/namespace/nsproxy.rs:294-371` -- Modify: `kernel/src/process/namespace/unshare.rs:19-53, 140-190` -- Modify: `kernel/src/process/namespace/setns.rs:96-267` -- Modify: `kernel/src/ipc/sem_undo.rs` -- Test: `kernel/src/process/namespace/nsproxy.rs` 或同目录对应 `#[cfg(test)]` -- Test: `kernel/src/ipc/sem_undo.rs` 的 owner/replay tests - -**Interfaces:** -- Produces: - ```rust - pub(crate) struct PreparedNamespaceInstall { - new_nsproxy: Arc, - new_fs: Option>, - new_cred: Option>, - detach_sysvsem: bool, - } - - impl PreparedNamespaceInstall { - pub(crate) fn commit( - self, - tsk: &Arc, - fs_refs: &FsRefsReadGuard, - ) -> Result<(), SystemError>; - } - ``` - -**Scope boundary:** 此 task 不重写 namespace subsystem;只将已有 `setns` / `unshare` 已完成的 fallible prepare 与 publication 组合为无失败 commit,并把 SEM_UNDO detach 定义为显式 bool。 - -- [ ] **Step 1: 写失败测试。** - - ```rust - #[test] - fn unshare_sysvsem_detaches_even_when_ipc_namespace_is_unchanged() { - // old attachment is drained before syscall returns; - // subsequent SEM_UNDO ensure produces a different empty group. - } - - #[test] - fn unshare_newipc_detaches_once_when_sysvsem_is_also_present() { - // no double decrement/replay. - } - - #[test] - fn setns_newipc_detaches_even_for_same_arc_target() { - // force installation request with CLONE_NEWIPC and same Arc target; - // old attachment is detached. - } - - #[test] - fn namespace_prepare_error_preserves_attachment_and_old_state() { - // induced prepare failure: no fs/nsproxy/cred/attachment mutation. - } - ``` - -- [ ] **Step 2: 确认 RED。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib 'process::namespace::.*::tests|ipc::sem_undo::tests' --target x86_64-unknown-linux-gnu - ``` - - Expected: tests fail because `detach_sysvsem` is not represented or committed atomically. - -- [ ] **Step 3: 实现 prepare/commit 边界。** - - 1. 现状中 [`ksys_unshare()`](kernel/src/process/namespace/unshare.rs#L19-L53) 在 nsproxy/fs publication 后还可能在 `set_cred()?` 失败;[`switch_task_namespaces_inner()`](kernel/src/process/namespace/nsproxy.rs#L318-L343) 又在 `tsk.set_nsproxy()` 前修改 fs。将所有 permission、target lookup、mount path projection、new `FsStruct`、new `NsProxy`、new `Cred` 的失败工作放进 prepare。 - 2. `PreparedNamespaceInstall` 显式保留 `detach_sysvsem`;不得使用 old/new IPC `Arc::ptr_eq()` 推导: - - `unshare(CLONE_SYSVSEM)`:true; - - `unshare(CLONE_NEWIPC)`:true; - - 两者同时:true,且 commit 只 detach 一次; - - pidfd 或 namespace-fd `setns`:若已验证的 installation flags 含 `CLONE_NEWIPC`,即使 target IPC Arc 与 old Arc 相同也 true; - - 其他 UTS/NET/MNT/CGROUP/PID-for-children switch:false。 - 3. commit 开始后按以下顺序执行,无 allocation、无 user-memory access、无 permission check、无新的 fallible work: - 1. 若 `detach_sysvsem`,调用 Task 3 的 synchronous `detach_sem_undo(tsk)`; - 2. 安装已准备好的 fs root/pwd 或 `FsStruct`; - 3. `tsk.set_nsproxy(new_nsproxy)`; - 4. 若有,`tsk.set_cred(new_cred)`;虽然现有 [`set_cred()`,task.rs:862-866](kernel/src/process/task.rs#L862-L866) 类型为 `Result`,实现恒为 `Ok(())`,在该 commit 中应改为或包裹为无法失败的内部 install API,避免 PONR 后错误返回。 - 4. pidfd-setns 与 namespace-fd setns 在 [`ksys_setns()`](kernel/src/process/namespace/setns.rs#L96-L267) 的两条分支都改为创建同一 prepared install 后 commit。 - 5. `exec_task_namespaces()`([`nsproxy.rs:283-291`](kernel/src/process/namespace/nsproxy.rs#L283-L291))传 `detach_sysvsem = false`,确保 exec 保留 attachment。 - 6. 保持 `CLONE_NEWIPC | CLONE_SYSVSEM` 在 fork early check 拒绝;不要尝试把 unshare 的两个标志误判为非法组合。 - -- [ ] **Step 4: 确认 GREEN。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib 'process::namespace::.*::tests|ipc::sem_undo::tests' --target x86_64-unknown-linux-gnu - ``` - - Expected: four namespace detach tests PASS;prepare failure 不改变 old attachment。 - -- [ ] **Step 5: 编译并确认 ENOSYS 仍在。** - - Run: - - ```bash - cd . - make kernel - grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs - ``` - - Expected: kernel PASS;门禁仍未移除。 - -- [ ] **Step 6: Commit。** - - ```bash - git add kernel/src/process/namespace/nsproxy.rs kernel/src/process/namespace/unshare.rs kernel/src/process/namespace/setns.rs kernel/src/process/task.rs kernel/src/ipc/sem_undo.rs - git commit -m "fix(process): detach sem undo during namespace transitions" - ``` - ---- - -### Task 5: 重构 semop 为 lock 外 prepare + 无分配 simulation/commit - -**Files:** -- Modify: `kernel/src/ipc/sem.rs:6-10, 249-309, 376-639, 663-761` -- Modify: `kernel/src/ipc/sem_undo.rs` -- Test: `kernel/src/ipc/sem.rs:930-1008` -- Test: `kernel/src/ipc/sem_undo.rs` - -**Interfaces:** -- Produces: - ```rust - struct SemopScratch { - // fixed-capacity, pre-reserved ordered virtual state - } - - enum SemopOutcome { - Ready(SemopSimulation), - Blocked(SemBlockedOp), - } - - impl SemManager { - fn simulate_semop( - set: &KernelSemSet, - sops: &[PosixSemBuf], - undo: Option<&mut SemUndoRecord>, - scratch: &mut SemopScratch, - ) -> Result; - } - ``` -- Changes: - ```rust - struct SemQueueEntry { - sops: Vec, - pid: Option>, - undo_group: Option>, - waker: Arc, - status: SpinLock, - } - ``` - -- [ ] **Step 1: 写 RED tests:逐项 transactional simulation。** - - 先在 `sem.rs` 的测试模块中扩展已有 `insert_test_set()`,覆盖: - - ```rust - #[test] - fn ordered_mixed_undo_ops_apply_each_adjustment_step() { - // Same semnum, e.g. +3|UNDO, -1 without UNDO, -2|UNDO. - // Verify virtual semval and semadj after every element, not only net sum. - } - - #[test] - fn intermediate_adjustment_overflow_is_erange_even_if_later_op_cancels_it() { - // Begin adjustment at 32767; operation sequence +1|UNDO, -1|UNDO. - // Must return ERANGE and mutate neither semval nor semadj. - } - - #[test] - fn blocked_or_nowait_failure_does_not_commit_semval_or_semadj_prefix() { - // First op is successful UNDO; second op blocks/NOWAIT. - // Both semval and record adjustment remain original. - } - - #[test] - fn zero_undo_op_can_prepare_zero_record_without_adjustment() { - // sem_op = 0 | SEM_UNDO has group/record preparation but adjustment remains 0. - } - ``` - -- [ ] **Step 2: 确认 RED。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib 'ipc::sem::tests::(ordered_mixed|intermediate_adjustment|blocked_or_nowait|zero_undo)' --target x86_64-unknown-linux-gnu - ``` - - Expected: tests fail because old simulation neither receives undo record nor validates `semadj` incrementally. - -- [ ] **Step 3: 实现 scratch、record prepare 和立即路径 commit。** - - 1. 删除 `hashbrown::HashMap` 对 `SemopSimulation` 的依赖;现有 [`SemopSimulation { values: HashMap }`,sem.rs:376-380](kernel/src/ipc/sem.rs#L376-L380) 改为 lock 外 `try_reserve_exact(sops.len())` 的有界 ordered scratch。每个 unique `sem_num` 记录: - ```rust - struct SemopScratchEntry { - semnum: usize, - initial_val: i32, - virtual_val: i32, - initial_adj: i16, - virtual_adj: i16, - } - ``` - 使用线性查找即可,最多 `SEMOPM = 500`,不要在 manager lock 里分配 HashMap。 - 2. 只要任意 op 带 `SEM_UNDO`(含 `sem_op == 0`),在 manager lock 外: - - 读取 PCB group;没有时 `ensure_sem_undo_group(&ipcns)`; - - group 必须绑定本 syscall IPC namespace; - - manager lock 下先验证 full semid、读取 `nsems`,随后释放锁; - - group lock 下检查 record;缺失时锁外以 `Vec::try_reserve_exact(nsems)` + `resize` 制造 `Box<[i16]>`; - - 仅 group lock 下预留 records 一项容量;释放; - - 采用 Task 6 定义的 registry snapshot replacement; - - 重新 `manager -> group` 验证 full semid / generation / nsems 并无失败插入。 - 3. simulation 按原始 `sops` 顺序: - - 先每项验证 `sem_num`; - - `sem_op == 0`:virtual semval 非零返回 `Blocked(Zero)`; - - nonzero:每步 `next_val = current + sem_op`,大于 `SEMVMX` 返回 `ERANGE`,小于 0 返回 `Blocked(Increase)`; - - 若该项有 `SEM_UNDO` 且 nonzero,计算宽类型: - ```rust - let next_adj = current_adj as i32 - op.sem_op as i32; - ``` - 每步要求 `-32768..=32767`,否则 `ERANGE`; - - 任一 Blocked/Error 不写共享 semval/record。 - 4. `Ready` 时唯一 commit: - - 按 scratch 写入 semval; - - 写最终 adjustment; - - 写受影响 `KernelSem.pid`; - - 更新 `KernelSemSet.sem_otime`; - - 随后 queue rescan。 - 5. 仅 Task 5 结束时,直接 ready `SEM_UNDO` 仍不向用户开放,因为 syscall gate 必须保留;调用路径可以受 `cfg(test)` / internal entry 驱动验证。 - -- [ ] **Step 4: 确认 GREEN。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib 'ipc::sem::tests|ipc::sem_undo::tests' --target x86_64-unknown-linux-gnu - ``` - - Expected: all immediate transaction tests PASS。 - -- [ ] **Step 5: 审计临界区 allocation。** - - Run: - - ```bash - cd . - grep -nE 'HashMap::with_capacity|\\.to_vec\\(\\)|Arc::new\\(' kernel/src/ipc/sem.rs - ``` - - Expected: 在 semtimedop manager-locked section、`simulate_semop()` 和 queue creation path 中不再出现这些分配;如还有其他合理非临界区分配,提交说明必须逐个给出其 lock context。 - -- [ ] **Step 6: 保持门禁并编译。** - - Run: - - ```bash - make kernel - grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs - ``` - - Expected: build PASS;`ENOSYS` 仍存在。 - -- [ ] **Step 7: Commit。** - - ```bash - git add kernel/src/ipc/sem.rs kernel/src/ipc/sem_undo.rs - git commit -m "feat(ipc): prepare atomic sem undo accounting" - ``` - ---- - -### Task 6: 实现 namespace registry、queue-origin attribution 与 queued commit - -**Files:** -- Modify: `kernel/src/ipc/sem.rs:249-309, 311-639, 663-761` -- Modify: `kernel/src/ipc/sem_undo.rs` -- Modify: `kernel/src/process/namespace/ipc_namespace.rs:17-64`(仅为 lifecycle invariant/debug assertion 或明确 `destroy_all()`) -- Test: `kernel/src/ipc/sem.rs` 的 `#[cfg(test)]` -- Test: `kernel/src/ipc/sem_undo.rs` 的 `#[cfg(test)]` - -**Interfaces:** -- Produces: - ```rust - impl SemManager { - fn prepare_undo_record_and_registry( - &mut self, - group: &Arc, - semid: SemId, - ) -> Result<(), SystemError>; - - fn prune_and_apply_setval_undo( - &mut self, - semid: SemId, - semnum: usize, - ); - - fn prune_and_apply_setall_undo( - &mut self, - semid: SemId, - ); - - fn remove_undo_records_for_rmid( - &mut self, - semid: SemId, - ); - } - ``` - -- [ ] **Step 1: 写 RED tests。** - - ```rust - #[test] - fn queued_undo_commits_to_captured_group_not_waker_current_task() { - // Queue A with SEM_UNDO; change a test-current PCB before rescan. - // Ready retry updates A's captured group only. - } - - #[test] - fn queued_timeout_signal_and_rmid_never_commit_adjustment() { - // Timeout -> EAGAIN; signal -> EINTR; RMID -> EIDRM. - // Every result preserves record adjustment. - } - - #[test] - fn first_record_is_registry_visible_before_setval_cleanup() { - // Create first record, run SETVAL cleanup, then assert its target slot == 0. - } - - #[test] - fn stale_weak_entries_are_compacted_without_losing_live_group() { - // Snapshot contains stale + live + candidate. - // Replacement preserves live/candidate exactly once. - } - - #[test] - fn namespace_lifecycle_invariant_has_no_live_record_at_final_drop() { - // Test-only invariant: last namespace state cannot coexist with live owner, - // waiter, or registered group record. - } - ``` - -- [ ] **Step 2: 确认 RED。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib 'ipc::sem::tests::(queued_|first_record|stale_weak)|ipc::sem_undo::tests::namespace_lifecycle' --target x86_64-unknown-linux-gnu - ``` - - Expected: failures because `SemQueueEntry` has no `undo_group` and registry manipulation does not exist. - -- [ ] **Step 3: 实现 registry replacement protocol。** - - 1. `SemManager.undo_groups` 是 `Arc>>` snapshot。 - 2. record 第一次插入前,manager lock 内只 clone old snapshot Arc 并记录 identity;锁外: - - upgrade each weak; - - 去掉 stale; - - 保留 live; - - 若当前 group 不在 live set,append downgrade(group); - - 用 `Vec::try_reserve_exact` 及 `Arc::try_new` 创建 replacement slice; - 3. 回到 manager lock,只有 `Arc::ptr_eq(&self.undo_groups, &old_snapshot)` 时,按 `manager -> group` 二次检查 full semid、nsems、record presence,然后无失败 swap snapshot + insert record;snapshot 已变化时释放锁、重新 snapshot/rebuild,绝不能在不一致 snapshot 下只插 record。 - 4. registry scans 始终在 manager lock 中逐个 upgrade,一个 group 一次,处理后释放 group lock;不得在 group lock 内取 manager lock。 - 5. stale weak 压缩也采用 snapshot replacement;不得原地 `Vec` mutate / allocate。 - -- [ ] **Step 4: 实现 queue ownership 和 retry commit。** - - 1. `SemQueueEntry` 新增: - ```rust - undo_group: Option>, - ``` - 2. 将 [`SemQueueEntry::new()`,sem.rs:264-276](kernel/src/ipc/sem.rs#L264-L276) 改为接收预先复制的 `Vec`、预先 `Arc::try_new(Waker)` 结果和 captured strong observer;它本身不调用 `to_vec()`。 - 3. `semtimedop()` 在进 manager lock 前: - - prepare queue `sops` copy; - - `Arc::try_new(SemQueueEntry { ... })`; - - timer/waker allocation; - - 准备失败立即 `ENOMEM` 且无 queue/semadj 变化。 - 4. [`update_queue()`,sem.rs:600-639](kernel/src/ipc/sem.rs#L600-L639) 改为 manager lock 内使用 Task 5 同一 simulation/commit: - - entry 带 `Arc` observer 时,在 `manager -> captured group` 方向锁 record; - - `Weak::upgrade()` 失败是 internal invariant violation:完成 entry 的内部错误路径,绝不能给 current/waker task 记账; - - 只有 `Ready` 同时提交 semval+semadj,随后 remove/complete/wake; - - `Blocked`、NOWAIT、timeout、signal、RMID 都只 remove/complete,不记 adjustment。 - 5. 确认 waiter syscall 存续时持有强 group(来自 PCB attachment / semtimedop local observer);last owner detach 加 debug assertion,不能存在仍可能对该 group commit 的 queue entry。 - 6. namespace lifecycle: - - 先证明 `IpcNamespace` 的 `Arc` 被 task nsproxy / blocked syscall 保持; - - group 只持 `Weak`; - - namespace switch 在发布前完成 detach/replay; - - 若该证明无法在 test/debug assertion 中建立,才新增明确调用者的 `SemManager::destroy_all()`,在 manager lock 下 delete record、不回放、完成 waiter `EIDRM`、移除 sets,最后 free index; - - 不在 `IpcNamespace::Drop` 取 manager lock。 - -- [ ] **Step 5: 确认 GREEN。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib 'ipc::sem::tests|ipc::sem_undo::tests' --target x86_64-unknown-linux-gnu - ``` - - Expected: queue owner, cancellation, registry, lifecycle tests PASS。 - -- [ ] **Step 6: 审计 manager lock 下无新增 allocation。** - - Run: - - ```bash - cd . - grep -nE 'HashMap::with_capacity|\\.to_vec\\(\\)|Arc::new\\(|try_reserve|Vec::new' kernel/src/ipc/sem.rs kernel/src/ipc/sem_undo.rs - ``` - - Expected: 所有可能分配操作都位于 manager lock 外的 prepare/replacement 阶段;检查每个命中行的调用上下文。 - -- [ ] **Step 7: 保持 ABI 关闭并构建。** - - Run: - - ```bash - make kernel - grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs - ``` - - Expected: `ENOSYS` gate remains。 - -- [ ] **Step 8: Commit。** - - ```bash - git add kernel/src/ipc/sem.rs kernel/src/ipc/sem_undo.rs kernel/src/process/namespace/ipc_namespace.rs - git commit -m "feat(ipc): commit queued sem undo to originating group" - ``` - ---- - -### Task 7: 接入 SETVAL、SETALL、IPC_RMID 的 undo cleanup - -**Files:** -- Modify: `kernel/src/ipc/sem.rs:763-780, 868-927` -- Modify: `kernel/src/ipc/sem_undo.rs` -- Test: `kernel/src/ipc/sem.rs:930-1008` - -**Interfaces:** -- Produces: - ```rust - impl SemManager { - fn clear_undo_for_setval(&mut self, id: SemId, semnum: usize); - fn clear_undo_for_setall(&mut self, id: SemId); - fn discard_undo_for_rmid(&mut self, id: SemId); - } - ``` - -- [ ] **Step 1: 写 RED tests。** - - ```rust - #[test] - fn setval_clears_only_target_sem_adjustment_across_all_groups() { - // Two groups, one multi-sem record. - // SETVAL semnum 0 clears slot 0 but preserves slot 1 in both groups. - } - - #[test] - fn setall_clears_entire_full_semid_record_across_all_groups() { - // SETALL zeros every adjustment in matching records only. - } - - #[test] - fn setval_cleanup_precedes_value_write_and_queue_rescan() { - // SETVAL wakes queued undo operation. - // The newly-ready operation creates new debt that survives this cleanup. - } - - #[test] - fn rmid_discards_record_before_index_can_be_reused() { - // Old full ID record is removed; reuse low index with new generation; - // old adjustment cannot affect replacement. - } - ``` - -- [ ] **Step 2: 确认 RED。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib 'ipc::sem::tests::(setval_clears|setall_clears|setval_cleanup|rmid_discards)' --target x86_64-unknown-linux-gnu - ``` - - Expected: tests fail because existing `setval()` / `setall()` only write values, and [`ipc_rmid()`,sem.rs:764-780](kernel/src/ipc/sem.rs#L764-L780) frees index before undo cleanup. - -- [ ] **Step 3: 实现 cleanup order。** - - 1. `setval(id, semnum, val)` 保持已有 errno order:value range → semnum → permission([`sem.rs:869-880`](kernel/src/ipc/sem.rs#L869-L880))。 - 2. 完整 semid/permission 验证后,在同一 manager lock 内: - 1. 扫描 registry 中 live groups; - 2. 对 matching full `SemId` record 执行 `adjustments[semnum] = 0`; - 3. 写 semval、sempid、ctime; - 4. queue rescan。 - 3. `setall(token, vals)` 保持既有 [`prepare_setall()` 两阶段 token 语义,sem.rs:921-927](kernel/src/ipc/sem.rs#L921-L927):最终 full-ID token 复核成功后,先清 matching record 的整个 slice,再写全部 sem value/pid/ctime,最后 rescan。 - 4. `ipc_rmid(id)`: - 1. full-ID 与 permission 验证; - 2. manager lock 下扫描 live group,删除 matching full-ID record,不回放; - 3. `complete_all_removed()` 给每个 waiter `EIDRM`; - 4. remove key/id table、更新 total; - 5. 最后 `id_allocator.free_idx(decoded.idx)`。 - 5. 所有这些 scan 都遵循 `manager -> group`;不能借助 record 的 low index 判断。 - -- [ ] **Step 4: 确认 GREEN。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib ipc::sem::tests --target x86_64-unknown-linux-gnu - ``` - - Expected: SETVAL/SETALL/RMID cleanup + 既有 stale setall token tests PASS。 - -- [ ] **Step 5: 构建并确认门禁仍存在。** - - Run: - - ```bash - cd . - make kernel - grep -n -A6 'SEM_UNDO' kernel/src/ipc/sem.rs - ``` - - Expected: build PASS;`ENOSYS` 未删除。 - -- [ ] **Step 6: Commit。** - - ```bash - git add kernel/src/ipc/sem.rs kernel/src/ipc/sem_undo.rs - git commit -m "feat(ipc): clear sem undo state on semaphore controls" - ``` - ---- - -### Task 8: 增加公开 dunitest ABI 覆盖,但暂时保留 ENOSYS - -**Files:** -- Modify: `user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc:41-226, 706-714` -- Test: `user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc` -- Do not modify: `user/apps/tests/dunitest/whitelist.txt` -- Do not modify: `user/apps/tests/dunitest/Makefile` - -**Interfaces:** -- Replaces: - ```cpp - TEST(SysVSem, SemUndoRejected) - ``` -- Adds helpers using only existing public syscalls: `semget`, `semop`, `semtimedop`, `semctl`, `fork`, `clone`, `exec`, `unshare`, `setns`, pipe/eventfd/futex-style synchronization as already available to the environment. - -- [ ] **Step 1: 先将当前 ENOSYS test 改为失败的 future ABI tests。** - - 删除 [`SemUndoRejected`,sysv_sem_semantics.cc:706-714](user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc#L706-L714),增加下列 12 组(每组可包含多个 gtest case): - - 1. **`SemUndoBasicAccumulationAndSign`** - child 对初值 0 做 `+3 | SEM_UNDO` 与 `-1 | SEM_UNDO`;parent 在 child exit 前读到 2,exit 后读回 0;初值 3 的 `-2 | SEM_UNDO` exit 后回到 3。 - 2. **`SemUndoArrayOrderAndRollback`** - 同 semnum 重复、混合 flags;前项 UNDO 成功但后项 NOWAIT blocked 或 ERANGE 时,调用后和 child exit 后均不出现 prefix effect。 - 3. **`SemUndoAdjustmentBounds`** - 到达 `32767` / `-32768` 成功;下一项超界 `ERANGE`;同一数组中先超界而后抵消仍必须 `ERANGE`。 - 4. **`SemUndoQueueCapturedOwner`** - A `-1|SEM_UNDO` 排队;用 `GETNCNT` 确认;B 普通 `+1` 唤醒;A 成功后退出才回放,不是 B 的 exit。 - 5. **`SemUndoUncommittedPaths`** - NOWAIT、超时、signal、wait 中 RMID 分别验证 `EAGAIN/EINTR/EIDRM` 与无 undo 回放。原有 `WaitForWaiters()` 使用轮询;新增 case 必须改为 `GETNCNT/GETZCNT` 明确握手而不是 sleep 猜测。 - 6. **`SemUndoSetvalSetallClearDebt`** - 两 group、多 sem:SETVAL 只清目标 sem;SETALL 清该 set 全 slice;SETVAL 唤醒后由 queue 新提交的 debt 仍在 exit replay。 - 7. **`SemUndoRmidDiscardsDebt`** - 成功 UNDO 后 RMID、owner exit,确认不会回放,且无 crash。 - 8. **`SemUndoForkAndCloneOwners`** - 普通 fork 不继承;`clone(CLONE_SYSVSEM | SIGCHLD)` share;先 exit 不回放、最后 owner 仅回放一次;`CLONE_THREAD` 不含 SYSVSEM 时不共享;`CLONE_NEWIPC | CLONE_SYSVSEM` 返回 `EINVAL`。 - 9. **`SemUndoExecPreservesAttachment`** - child 做 UNDO 后 `exec` 当前 binary 的 helper mode,helper exit 后观察 replay;失败 exec 后仍保留 attachment 并在 exit 回放。 - 10. **`SemUndoExitClampAndWake`** - 正向/负向 clamp;用 `GETNCNT`/`GETZCNT` 确认 waiter 入队;final-owner replay 变值触发 waiter completion。 - 11. **`SemUndoUnshareSysvsem`** - share owners 中一方 `unshare(CLONE_SYSVSEM)` 后,新 UNDO 进入新 group;剩余 owner drain 旧 group;唯一 owner unshare 在 syscall return 前 drain。 - 12. **`SemUndoIpcNamespaceAndErrnos`** - `unshare(CLONE_NEWIPC)`、namespace-fd `setns`、pidfd `setns` 全 detach;same-Arc IPC setns 也 detach;权限/invalid target 的 prepare failure 保留 old attachment;回归 `EINVAL/E2BIG/EFAULT/EFBIG/EACCES`。 - - 注:用户 ABI 测试不得通过循环创建数万 semaphore 强迫 index reuse,也不得尝试 `SEMMNU + 1` admission;generation reuse 与 allocation failure 留给 kernel-internal tests。 - -- [ ] **Step 2: 确认 dunitest 编译通过而运行仍 RED。** - - Run: - - ```bash - cd user/apps/tests/dunitest - make build-suites - ``` - - Expected: `bin/normal/sysv_sem_semantics_test` 编译成功。 - - Run: - - ```bash - ./bin/normal/sysv_sem_semantics_test --gtest_filter='SysVSem.SemUndo*' - ``` - - Expected: FAIL;当前 kernel 在 `SEM_UNDO` 首次操作返回 `ENOSYS`,证明 tests 不是误绿。 - -- [ ] **Step 3: 不修改内核门禁,只整理 ABI tests。** - - 确保: - - 使用已有 `SemSet` RAII cleanup; - - 多进程测试使用 `ChildGuard` / `WaitChildOk`; - - 所有 blocked rendezvous 用 `GETNCNT` / `GETZCNT`、pipe 或 eventfd,不依赖固化 sleep; - - fork/exec helper 通过 argv/环境变量选择 helper mode,保持单一 test binary; - - QEMU 平台不支持某一 capability 时,不得无声 skip;设计应使用现有 DragonOS 支持路径。 - -- [ ] **Step 4: 再确认 RED。** - - Run: - - ```bash - ./bin/normal/sysv_sem_semantics_test --gtest_filter='SysVSem.SemUndo*' - ``` - - Expected: failing cases 的共同直接原因是 `ENOSYS`,而不是 test synchronization 超时或不确定 race。 - -- [ ] **Step 5: Commit。** - - ```bash - git add user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc - git commit -m "test(ipc): add sem undo ABI conformance cases" - ``` - ---- - -### Task 9: 最终开放 SEM_UNDO ABI,并执行全量门禁验证 - -**Files:** -- Modify: `kernel/src/ipc/sem.rs:671-761` -- Modify: `user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc`(仅修正因真实 ABI 暴露而发现的确定性测试问题) -- No new APIs. -- No per-set lock. -- No namespace tunable. -- No global publication refactor beyond Task 2 的最小安全协议。 - -**Hard prerequisites:** -- Task 1–8 所有 internal tests 已通过。 -- immediate simulation/commit、queued retry、exit replay、SETVAL/SETALL、RMID、full-ID reuse protection、fork/clone owner、exec、unshare、same-Arc setns、namespace lifecycle、prepare/rollback 都有真实实现和测试。 -- 审计确认没有 `group -> manager` lock acquisition、没有 PCB slot 与 manager/group lock nesting。 -- `SEM_UNDO` 仍只在唯一 gate 被拦截。 - -- [ ] **Step 1: 最后写 RED publication assertion。** - - 在 `sysv_sem_semantics.cc` 保留至少一个明确断言: - - ```cpp - TEST(SysVSem, SemUndoBasicAccumulationAndSign) { - // ... - ASSERT_EQ(0, SemOp(sem.id(), &undo_inc, 1)) - << "SEM_UNDO must be published only after all lifecycle prerequisites"; - } - ``` - - 这不是新测试;它确保删除 gate 是 ABI publication 而非仅 internals 完成。 - -- [ ] **Step 2: 运行全部 kernel-internal gates。** - - Run: - - ```bash - cd kernel - cargo +nightly-2026-02-24 test --lib 'ipc::sem::tests|ipc::sem_undo::tests|process::namespace::.*::tests|process::fork::tests' --target x86_64-unknown-linux-gnu - ``` - - Expected: PASS。若项目的实际 kernel test runner 不支持 host target,改用已验证的 DragonOS CI kernel-test target;不能以 `make -C kernel test` 代替,因为它明确 `--exclude dragonos_kernel`。 - -- [ ] **Step 3: 删除唯一 ENOSYS gate。** - - 在 [`SemManager::semtimedop()`,sem.rs:671-688](kernel/src/ipc/sem.rs#L671-L688) 删除: - - ```rust - if sops - .iter() - .any(|op| (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() != 0) - { - return Err(SystemError::ENOSYS); - } - ``` - - 其余 input validation (`sops.is_empty()`、`SEMOPM`) 保持原位。随后让 Task 5/6 的 lock-out prepare、record prepare、queue preallocation 和 manager-locked no-fail commit 成为正常路径。 - -- [ ] **Step 4: 确认 GREEN:局部 ABI test。** - - 先本地编译: - - ```bash - cd user/apps/tests/dunitest - make build-suites - ``` - - 再在 DragonOS QEMU dunitest 环境执行目标 binary: - - ```bash - /opt/tests/dunitest/bin/normal/sysv_sem_semantics_test --gtest_filter='SysVSem.SemUndo*' - ``` - - Expected: `PASSED`,所有 `SemUndo*` cases 无 skipped、无 timeout。 - -- [ ] **Step 5: 构建与 QEMU dunitest 回归。** - - Run: - - ```bash - cd . - make kernel - make test-dunit - ``` - - Expected: - - `make kernel` 成功; - - dunitest runner 中 `normal/sysv_sem_semantics` PASS; - - 既有 `SysVSem` 非-UNDO tests 无回归。 - - 如开发环境适合走 Nix,等价完整命令为: - - ```bash - nix develop --command make test-dunit - ``` - -- [ ] **Step 6: 静态锁/分配审计。** - - Run: - - ```bash - cd . - grep -RIn --include='*.rs' 'sem_undo' kernel/src/ipc kernel/src/process - grep -nE 'HashMap::with_capacity|\\.to_vec\\(\\)|Arc::new\\(' kernel/src/ipc/sem.rs - ``` - - Expected: - - `sem_undo` 仅通过明确语义 API 修改 PCB slot/owner; - - manager-locked semop/queue paths 无 allocation; - - 没有 per-set lock 字段或 lock acquisition; - - 没有以 `Arc::strong_count()` 判定 SEM_UNDO 最后 owner。 - -- [ ] **Step 7: Commit final ABI publication。** - - ```bash - git add kernel/src/ipc/sem.rs user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc - git commit -m "feat(ipc): enable Linux-compatible sem undo" - ``` - ---- - -## Planned Commit Boundaries - -1. `feat(ipc): add private sem undo ownership model` -2. `feat(process): track shared sem undo owners across clone` -3. `feat(ipc): replay sem undo on final task exit` -4. `fix(process): detach sem undo during namespace transitions` -5. `feat(ipc): prepare atomic sem undo accounting` -6. `feat(ipc): commit queued sem undo to originating group` -7. `feat(ipc): clear sem undo state on semaphore controls` -8. `test(ipc): add sem undo ABI conformance cases` -9. `feat(ipc): enable Linux-compatible sem undo` - -`ENOSYS` 必须贯穿提交 1–8;第 9 个提交是唯一 publication point。 - -## Plan Self-Review - -- **规格覆盖:** 覆盖 `SemUndoGroup/Attachment`、PCB slot、`CLONE_SYSVSEM`、普通 fork、child rollback、task exit、exec 保留、unshare/setns detach、immediate transaction、queued transaction、full generation semid、SETVAL、SETALL、RMID、namespace lifetime、ENOMEM failure atomicity、dunitest。 -- **明确排除:** 不实现 per-set lock;不新增 namespace-local semaphore tunable;不以 `SEMUME/SEMMNU` admission;不扩大全局 namespace/fork 重构。 -- **现有事实锚点:** 当前 ENOSYS gate 位于 `sem.rs:683-688`;manager lock 覆盖当前 semop `sem.rs:702-735`;当前 queue copy 在 `sem.rs:264-276`;exit hook 位于 `exit.rs:450-463`;fork publication 后 PIDFD installation 仍可能失败于 `fork.rs:1061-1066`;setns/unshare 当前确有 prepare/publication 分离缺口。 -- **测试可执行性注意:** dunitest 有明确构建/运行路径:`make build-suites`、`make test-dunit`。内核 `#[cfg(test)]` 必须使用能编译 `dragonos_kernel` 本体的实际 test target;不可声称 `kernel/Makefile:test` 覆盖它,因为该命令明示排除 `dragonos_kernel`。 - -### Critical Files for Implementation -- `kernel/src/ipc/sem.rs` -- `kernel/src/ipc/sem_undo.rs` -- `kernel/src/process/task.rs` -- `kernel/src/process/fork.rs` -- `kernel/src/process/manager/exit.rs` diff --git a/docs/superpowers/specs/2026-09-03-sem-undo-design.md b/docs/superpowers/specs/2026-09-03-sem-undo-design.md deleted file mode 100644 index 95d12a6580..0000000000 --- a/docs/superpowers/specs/2026-09-03-sem-undo-design.md +++ /dev/null @@ -1,687 +0,0 @@ -# DragonOS `SEM_UNDO` 实现规格(Linux 6.6 兼容) - -## 1. 结论与范围 - -采用当前实现的最小可发布模型:**PCB 显式 attachment → `Arc`;每个 group 绑定一个 IPC namespace;`SemManager` 维护 namespace 级 `Arc>>` registry;record 只以完整 generation-bearing `semid` 为键。** - -```puml -@startuml SemUndoOwnership -skinparam classAttributeIconSize 0 -skinparam linetype ortho -left to right direction - -class ProcessControlBlock { - sem_undo: SpinLock> -} - -class SemUndoAttachment { - group: Arc -} - -class SemUndoGroup { - ipc_ns: Weak - inner: SpinLock -} - -class SemUndoGroupState { - task_owners: usize - records: Vec - reserved_records: usize - absence_generation: u64 - retired: bool - records_taken: bool -} - -class SemUndoRecord { - semid: SemId - adjustments: Box<[i16]> - revision: u64 - prepared_state: PreparedSemUndoRecordState - reservation: Option -} - -class IpcNamespace { - sem: SpinLock -} - -class SemManager { - undo_groups: Arc>> -} - -ProcessControlBlock "1" *-- "0..1" SemUndoAttachment : PCB 显式 attachment -SemUndoAttachment *-- "1" SemUndoGroup : group -SemUndoGroup --> "1" IpcNamespace : ipc_ns,每组唯一 -SemUndoGroup *-- "1" SemUndoGroupState : inner -SemUndoGroupState *-- "0..*" SemUndoRecord : records -IpcNamespace *-- "1" SemManager : sem -SemManager o-- "0..*" SemUndoGroup : undo_groups 弱引用注册 - -@enduml -``` - -不引入 per-set lock、RCU 式 namespace teardown 或额外用户 ABI。为实现无分配 record publication 与 namespace/fork 事务,当前实现使用局部 prepared-record state、record reservation、final-owner `retired` 状态和预分配 RCU retirement;这些不是第二套 group/namespace 身份模型。`NsCommon.nsid` 仍只提供诊断身份。 - -本规格覆盖 Linux 6.6 的 `semop`/`semtimedop`、queue、`SETVAL`、`SETALL`、`IPC_RMID`、fork/clone、exec、task exit、`unshare(CLONE_SYSVSEM)`、IPC namespace unshare/setns 语义。namespace 与 fork publication 的事务修复是开放 ABI 前置条件;当前实现以 prepared namespace install、预分配 RCU retirement 和 child attachment guard 达成该前提。 - -在执行、等待重试、退出回放、控制操作、RMID、fork/clone、unshare/setns 和 namespace 生命周期路径实现完成后,已删除 `SEM_UNDO -> ENOSYS` 门禁;这是本实现的用户可见 publication point。后续变更不得重新引入仅部分路径可用的 ABI。 - -## 2. 当前实现基线 - -- 实现前的基线在 `kernel/src/ipc/sem.rs` 对 `SEM_UNDO` 返回 `ENOSYS`;当前最终实现已删除该门禁并接入 lifecycle accounting。 -- `SemQueueEntry::new()` 的 `to_vec()` 已改为锁外 fallible 的预准备 sops copy。 -- `simulate_semop()` 的临时 `HashMap` 已改为锁外预留、临界区内固定容量的 scratch。 -- blocked 路径的 queue `Arc`、sops、waker/timer 和 scratch 均在 manager lock 外预准备。 -- 一个 namespace 的全部 semaphore set、queue 和 ID 表仍由 `IpcNamespace.sem: SpinLock` 串行化。 -- `SEMOPM` 是 `kernel/src/ipc/sem.rs` 的全局常量 500;不存在 namespace-local `sc_semopm`。 -- namespace switch/unshare 已汇入 `PreparedNamespaceInstall`,并在 commit 前准备 RCU retirement。 -- fork 已将 pidfd 安装置于 child relation publication 之前,并用 child attachment guard 覆盖 publication 区间。 - -## 3. Linux 6.6 行为基准 - -权威参考: - -- [`ipc/sem.c` 数据结构](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L143-L166) -- [原子执行与 adjustment](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L630-L784) -- [queued retry](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L934-L1067) -- [`IPC_RMID`](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L1139-L1193) -- [`SETVAL`/`SETALL`](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L1343-L1527) -- [undo 分配与 semop 主路径](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L1840-L2220) -- [`copy_semundo()` / `exit_sem()`](https://github.com/torvalds/linux/blob/v6.6/ipc/sem.c#L2302-L2446) -- [UAPI 常量](https://github.com/torvalds/linux/blob/v6.6/include/uapi/linux/sem.h#L68-L91) -- [namespace clone 约束与切换](https://github.com/torvalds/linux/blob/v6.6/kernel/nsproxy.c#L151-L187) -- [`unshare()`](https://github.com/torvalds/linux/blob/v6.6/kernel/fork.c#L3378-L3460) - -核心不变量: - -```text -adjustment(group, semid, sem_num) - = -Σ(该 group 成功提交且带 SEM_UNDO 的非零 sem_op) -``` - -对成功的 `sem_op = x`: - -```text -semval' = semval + x -semadj' = semadj - x -``` - -必须保持: - -1. 用户数组按原顺序逐项模拟;同一 `sem_num` 可重复,带/不带 `SEM_UNDO` 可混合。 -2. `sem_op == 0 | SEM_UNDO` 可触发空 group/record 的惰性创建,但 adjustment 不变。 -3. 每一步 `semval` 必须在 `0..=32767`;每一步 `semadj` 必须在 `-32768..=32767`,不能只检查最终净值。 -4. 任一项阻塞或失败时,整个数组不修改 semval 或 semadj。 -5. NOWAIT 阻塞和超时返回 `EAGAIN`,信号取消返回 `EINTR`,等待期间 RMID 返回 `EIDRM`;均不记账。 -6. 普通 fork 不继承;显式 `CLONE_SYSVSEM` 共享;`CLONE_THREAD` 不隐含 `CLONE_SYSVSEM`;exec 保留。 -7. 最后 task owner detach 时执行 `semval = clamp(semval + semadj, 0, 32767)`,不等待,并按 Linux `exit_sem()` 经 `do_smart_update(..., otime=1, ...)` 更新 `sem_otime`。 -8. `SETVAL` 清所有 groups 对目标 semaphore 的旧 adjustment;`SETALL` 清该 set 的全部旧 adjustment。 -9. `IPC_RMID` 删除 record 而不回放,waiter 得到 `EIDRM`,且清理完成后才允许低 index 复用。 -10. `SEMUME`、`SEMMNU` 仅作为兼容报告字段,不作为 admission limit。 - -## 4. 数据模型与所有权 - -新增 `kernel/src/ipc/sem_undo.rs`: - -```rust -pub struct SemUndoAttachment { - group: Arc, -} - -pub struct SemUndoGroup { - ipc_ns: Weak, - inner: SpinLock, -} - -struct SemUndoGroupState { - task_owners: usize, - records: Vec, - reserved_records: usize, - absence_generation: u64, - retired: bool, - records_taken: bool, -} - -struct SemUndoRecord { - semid: SemId, - adjustments: Box<[i16]>, - revision: u64, - prepared_state: PreparedSemUndoRecordState, - reservation: Option, -} -``` - -`PreparedSemUndoRecordState` 区分已存在 record 的 revision、缺失 record 的 absence generation 和已发布 record;`PendingSemUndoRecordReservation` 仅为锁外准备保留 `records` 容量,不能成为 owner。 - -规则: - -- queue 保存 captured `Arc`、预准备 `SemUndoRecord` 和 scratch;该 strong reference 只保障阻塞操作的归属与完成,不参与 `task_owners`,也不替代 PCB attachment。 -- group 创建时绑定当前 `Weak`,之后不得换绑;每条 record 因此无需重复保存 namespace 或 namespace ID。 -- record 的 `semid` 必须是完整 ID,不得只保存低 index。 -- adjustment buffer 长度创建后严格等于对应 set 的 `nsems`。 -- `task_owners` 只由显式 share、rollback、detach 修改;禁止使用 `Arc::strong_count()`。 -- attachment 的 `Drop` 不回放、不取 manager lock。所有正常路径必须显式 take/detach;debug assertion 用于发现遗漏。 -- group 首次获得 record 时在所属 namespace 的 `SemManager.undo_groups` 注册一次;registry 以 snapshot replacement 为主,并可在 `Arc::get_mut()` 成功时无分配压缩 stale weak。 -- 最后 owner 将 group 标记为 `retired`,一次性取走 records;retired group 拒绝新的 owner、record prepare 和 publication,`records_taken` 保证回放仅发生一次。 - -- `sem_undo_group() -> Option>`:observer。 -- `ensure_sem_undo_group(ipc_ns) -> Result, ENOMEM>`:二次检查并安装 `task_owners=1` 的空 group。 -- `take_sem_undo_attachment() -> Option`:原子清空 slot。 -- child guard 的 install/rollback API;调用者不得直接改 owner 数。 - -普通 fork 的 child slot 初始化为 `None`。group 首次获得 record 时在所属 namespace 的 `SemManager.undo_groups` 注册一次;stale weak 通过 snapshot replacement 或独占 registry 的 `Arc::get_mut()` 路径压缩。 - -## 5. 锁与分配契约 - -联合锁约束如下: - -```puml -@startuml SemUndoLockOrder -skinparam linetype ortho -left to right direction - -rectangle "PCB sem_undo slot 锁" as PcbSlot -rectangle "ipcns.sem 管理器锁" as Manager -rectangle "SemUndoGroup.inner" as Group -rectangle "队列局部锁\nundo_record / scratch / status" as QueueLocks - -PcbSlot -[hidden]-> Manager -Manager --> Group : 获取顺序 -Group --> QueueLocks : 获取顺序 -Group -[#red,dashed]-> Manager : 禁止反向获取 - -note bottom of PcbSlot - 不与其余锁嵌套 -end note - -note bottom of Manager - 每次最多锁一个 group -end note -@enduml -``` - -禁止持 group lock 获取 manager lock。manager lock 下每次只锁一个 group;queue entry 的 `undo_record`、`scratch` 与 `status` 均为 entry 局部锁,必须避免反向取得 manager/group 锁。 - -所有用户可触发增长必须 fallible,失败映射 `ENOMEM`。共享 semval、semadj、registry 或 queue 状态修改前,所有内存与容量必须准备好。不得在 manager spinlock 内调用 `HashMap::with_capacity`、`to_vec()`、`Arc::new()` 或其他可能分配的 API。 - -具体准备策略: - -- 使用 `Arc::try_new` 创建 group、queue entry、waker-owned 对象。 -- adjustment 使用 `Vec::try_reserve_exact(nsems)`、`resize(0)`、`into_boxed_slice()`。 -- simulation scratch 与 queue-owned sops 在 manager lock 外完成 fallible reserve/copy。 -- group record storage 必须分阶段准备:先在 `manager -> group` 下验证并快照 `required_capacity`、record revision/absence generation,随后释放全部锁;在锁外对 replacement storage 执行 fallible reserve;再按 `manager -> group` 重新获取锁并复核 full semid、`nsems`、容量需求及准备 token。仅当复核仍成立时,才通过无分配的 `append`/`swap` 安装已准备 storage 和 record;不得在 group lock 下 reserve。 -- manager registry 使用 immutable `Arc>>` 快照。manager lock 内只 clone 快照 `Arc` 并记录其身份;锁外 fallibly 构造“压缩 stale weak + 保留 live group + 必要时追加当前 group”的 replacement;重新获取 manager lock,只有旧快照仍 `Arc::ptr_eq` 时才无失败 swap,否则重试。当 registry 无其他 snapshot 持有者时,允许用 `Arc::get_mut()` 在 manager lock 内无分配压缩 stale weak。 -- 当前 `simulate_semop()` 的 `HashMap` 已替换为锁外预留的有界 scratch;queue 的 sops copy、queue `Arc`、waker/timer 和 queue-owned scratch 也在 manager 临界区外完成。 - -不新增 per-set lock 或额外的通用生命周期 guard 契约;本文已定义的 child attachment guard、prepared-record reservation 和 RCU retirement 是实现所需的局部机制。性能优化必须另行设计。 - -## 6. group 与 record prepare - -只要 sops 任一项带 `SEM_UNDO`,包括零操作,就执行 prepare。 - -### 6.1 group prepare - -1. 读取 PCB slot;已有 group 时验证它绑定当前 IPC namespace并返回 observer。 -2. 为空时,锁外 fallibly 构造 `task_owners=1` 的 candidate group。 -3. 再锁 PCB slot二次检查;已有者胜出,candidate直接丢弃;否则安装 candidate attachment。 -4. semop 后续失败时允许保留空 group,匹配 Linux 惰性分配行为。 - -### 6.2 record 与 registry prepare - -1. manager lock 下以完整 semid 验证 set并读取 `nsems`,同时取得 registry snapshot;再获取 group lock,检查 record 状态并快照 `required_capacity`、revision/absence generation,然后释放全部锁。 -2. 锁外构造全零 adjustment buffer、simulation/queue 所需对象以及 registry replacement;缺失 record 时同时 fallibly reserve 足以容纳快照容量的 replacement record storage。 -3. 获取 manager lock,再获取 group lock;重新验证完整 semid、`nsems`、registry snapshot 和 prepared record 的 revision/absence generation。 -4. 已有 record 在 prepare 后发生 `SETVAL`/`SETALL`/竞争更新时,以当前 record 内容刷新 prepared record;缺失 record 仅在 absence generation 与容量需求均一致时,才可使用锁外准备的 storage 发布。 -5. registry snapshot 已变化、RMID/reuse 或 record preparation 已失效时,释放锁并重试或返回既有 checked lookup errno;不得留下半个 record/registry。 -6. 所有验证通过后,无失败地 swap registry;缺失 record 通过 allocation-free `append`/`swap` 安装 prepared storage 并发布 prepared record,已有 record 则保留或刷新。该路径不得在 manager/group 临界区分配。 - -```puml -@startuml SemUndoRecordPrepare -skinparam sequenceArrowThickness 1.5 -autonumber - -participant "SemManager" as Manager -participant "SemUndoGroup.inner" as Group -participant "锁外预备存储" as Prepared - -Manager -> Manager : 验证 full semid,读取 nsems\n取得 registry snapshot -Manager -> Group : 按 SemManager -> SemUndoGroup.inner 获取锁 -Group -> Group : 快照 required_capacity\nrevision / absence_generation -Group --> Manager : 释放 group lock - -Manager -> Prepared : 释放 manager lock 后开始准备 -Prepared -> Prepared : 可失败地 reserve 替换存储\n构造 adjustment / scratch / queue / registry replacement - -Manager -> Group : 重新按 SemManager -> SemUndoGroup.inner 获取锁 -Group -> Group : 复核 full semid / nsems / registry snapshot\nrevision / absence_generation / required_capacity -alt 快照失效或 RMID/reuse - Group --> Manager : 不安装,释放锁 - Manager -> Manager : 重试或返回既有查找 errno -else 复核通过 - Prepared -> Group : 以无分配 append/swap\n安装预备存储与 record - Group -> Manager : 无失败 swap registry - Group --> Manager : 释放 group lock -end - -note over Group, Prepared - reserve 只能发生在全部锁释放之后; - group lock 内不得 reserve -end note -@enduml -``` - -允许留下空 group或全零 record;不允许 record 存在而 registry 缺失,也不允许 allocation error 发生在 semval/semadj 修改后。 - -## 7. semop 事务与 queue - -### 7.1 锁外准备 - -`SemManager::semtimedop()` 在获取 manager lock 前完成: - -1. 现有 nsops、timeout、用户复制和 flag 校验;上限使用全局 `SEMOPM`。 -2. 若含 `SEM_UNDO`,准备当前 namespace-bound group和目标 record。 -3. fallibly准备容量至多为 sops 唯一 semnum数的 simulation scratch。 -4. 若调用可能阻塞,fallibly准备 queue-owned sops、queue entry、waker/timer所需对象;尚不入队。 - -### 7.2 无分配 simulate/commit - -获取 manager lock后: - -1. 重新验证 full semid、semnum边界和权限;semnum越界保持先于权限检查。 -2. 若需要 undo,按 `manager -> group` 查找 record。 -3. 统一执行器按用户数组顺序在 scratch中模拟 semval和semadj。 -4. `Ready` 时一次不可失败 commit:写最终 semval、最终 adjustment、受影响 semaphore 的 `sempid`,并更新正常成功 semop 的 `sem_otime`。 -5. `Blocked` 或错误时不写任何共享值。 -6. commit 后调用 queue rescan;blocked 时依据 NOWAIT/timeout立即返回或把已准备 entry无失败入队。 - -每个带 `SEM_UNDO` 的非零 op 使用宽类型计算: - -```text -next_adjustment = current_virtual_adjustment - sem_op -``` - -每一步检查 `-32768..=32767`。semval每一步检查 `0..=32767`;负结果是阻塞,不是部分提交。 - -### 7.3 queued operation - -`SemQueueEntry` 增加: - -```rust -undo_group: Option> -undo_record: SpinLock> -scratch: SpinLock -``` - -```puml -@startuml SemUndoQueuedTransaction -skinparam sequenceArrowThickness 1.5 -autonumber - -participant "原调用任务" as Caller -participant SemQueueEntry as Entry -participant "SemManager\nupdate_queue()" as Manager -participant "SemUndoGroup.inner" as Group -participant "信号量集合" as Set -participant "等待任务" as Sleeper - -Caller -> Entry : 锁外准备并捕获\nundo_group / undo_record / scratch -Caller -> Manager : 无失败入队 -Manager -> Entry : 状态设为 Queued - -Manager -> Group : 按 SemManager -> SemUndoGroup.inner 获取锁 -activate Group -Manager -> Entry : 锁定局部 undo_record / scratch -Manager -> Manager : 刷新 prepared record 并模拟 -alt Ready - Manager -> Set : 原子提交 semval / semadj - Manager -> Entry : status = Completed(result) -else 仍然 Blocked - Manager -> Entry : 保持 Queued,不写共享值 -else 超时 / 信号 / RMID - Manager -> Entry : 移除并完成,不提交 adjustment -end -deactivate Group -Manager -> Manager : 释放 group 与 manager lock -Manager -> Sleeper : 锁外实际唤醒 - -Sleeper -> Entry : 只消费 Completed(result) - -note over Caller, Entry - NOWAIT 阻塞不进入 queue; - 队列捕获的 Arc 不增加 task_owners -end note -@enduml -``` - -规则: - -- 捕获原调用者 group,不读取 waker 的 `current_pcb()`;queue 的 `Arc` 仅保持 captured group 可用,不增加 `task_owners`。 -- queue entry 保存预准备 record;retry 在 manager lock 内通过 captured group 的 prepared-record protocol 刷新、保留或发布该 record。 -- `update_queue()` 复用同一无分配执行器;只有 retry 真正 `Ready` 时才同时提交 semval/semadj;sleeper 醒来只消费 `Completed(result)`,不再执行。 -- NOWAIT、timeout、signal、RMID均只移除/完成 entry,不修改 adjustment。 -- task exit 前阻塞 syscall必须已正常完成或取消;debug assertion验证最后 owner drain时没有该 group可提交的 queued entry。 - -## 8. 控制操作与 RMID - -`SemManager.undo_groups` 的扫描均在 manager lock内进行;逐个 upgrade Weak、锁一个 group、处理后释放,并压缩 stale weak。registry replacement必须按第5节在锁外准备,manager临界区只做无失败 swap。 - -### 8.1 `SETVAL` - -1. 验证 full semid、权限和值。 -2. 扫描所有 live groups;若存在该 full semid record,将目标 `adjustments[semnum]` 清零。 -3. 写管理员值并更新现有 `sempid`/`sem_ctime`。 -4. rescan queue。 - -顺序必须是先清旧债、再写值、最后 rescan。被新值唤醒后提交的 `SEM_UNDO` 是新债务,不得被本次清理删除。 - -### 8.2 `SETALL` - -用户数组复制和验证保持现有两阶段 token协议。最终 full semid token复核成功后,在同一 manager临界区清所有 groups 对该 set record的整个 adjustment slice,再写全部值、更新时间/sempid并 rescan queue。 - -### 8.3 `IPC_RMID` - -在 manager lock内: - -1. 验证 full semid/generation和权限。 -2. 扫描 live groups并删除匹配 full semid的 record;不实施 adjustment。 -3. 将全部 waiter完成为 `EIDRM`。 -4. 从 key/ID表删除 set、更新计数。 -5. 最后释放 index供复用。 - -旧 record不得作用于相同低 index的新 generation。 - -## 9. fork、clone、exec 与 exit - -### 9.1 clone flags 与共享规则 - -- `CLONE_NEWIPC | CLONE_SYSVSEM` 在 `copy_process()` 的早期纯 flag检查阶段返回 `EINVAL`,早于 group创建或 owner变化。 -- 无 `CLONE_SYSVSEM`:child slot保持 `None`。 -- 有 `CLONE_SYSVSEM`:parent无 group时可惰性创建空 group并保留;group必须绑定parent当前 IPC namespace。随后为 child增加一个 owner token。 -- `CLONE_THREAD` 不自动推导 `CLONE_SYSVSEM`。 - -### 9.2 child attachment install、rollback 与 publication - -`UnpublishedSemUndoAttachmentGuard` 只表示一个尚未发布 child 的 owner token,不是 attachment状态或未来生命周期 guard;它是 fork局部回滚对象。 - -精确协议: - -1. 完成 `CLONE_NEWIPC | CLONE_SYSVSEM` 拒绝和 child namespace创建后,在 parent group lock下增加一次 `task_owners`并创建 guard;此时 attachment尚未进入 child PCB。 -2. 在任何使其他任务能通过 PID表、TGID/PGID/SID关系、线程组、parent children list、全局PCB表、pidfd、cgroup accounting或scheduler观察/运行 child 的操作之前,调用 `guard.install_into(child_pcb)`。该操作只把已准备 attachment移动到确认为空的 child slot,不分配、不失败、不获取 group lock。 -3. install 后 guard仍保留 rollback authority,不得立即 disarm。 -4. fork publication prerequisite 必须先独立修复:pidfd安装移到关系 publication之前,或为 PID links、thread-group live、group task、children、全局PCB、cgroup accounting等所有 side effect提供完整逆序 rollback;逐项枚举 guard创建后的每个 `?` 和显式错误返回。 -5. publication commit结束点是 PID/TGID/PGID/SID attach、线程组/children插入、pidfd安装、`ProcessManager::add_pcb()`、cgroup task/accounting全部完成,且到 `copy_process()` 成功返回间不再存在可失败操作。只有此处调用 `guard.disarm()`。 -6. `wake_up_new_task()` 必须晚于 disarm;可运行 child始终已经安装 attachment。 -7. 任一错误先完整撤销 child publication,再由 guard从 child slot取回attachment(若已安装)并在 group lock下只减少该 unpublished owner。不得回放;parent owner仍存活,计数降到零是内核 bug。 - -### 9.3 exec/de-thread - -- exec成功和可恢复失败都保留当前 task attachment。 -- non-leader exec survivor保留自己的 attachment,不从旧 leader搬运。 -- siblings和旧 leader通过各自真实 exit路径逐个detach。 -- PID/TID身份交换不改变owner关系。 - -### 9.4 task detach 与回放 - -exit在 robust-list后、`exit_mm`前调用统一 detach: - -1. PCB slot短锁内 `take()` attachment,释放slot锁。 -2. 仅持 group lock执行 `task_owners -= 1`;非最后 owner立即返回。 -3. 减到零的调用者将 group 标记为 `retired`,因此不再接受 share、prepare 或 record publication;它是唯一 drainer。 -4. 解析退出者在正确 PID namespace可见的 TGID,复用 canonical `task_tgid_vnr()` 等价 helper;禁止 raw PID/TID。 -5. 一次性从 retired group 取走全部 records,并以 `records_taken` 保证仅取一次;释放 group lock 后 upgrade group唯一的 namespace Weak。 -6. namespace Weak 已失效时,仅 debug 记录并丢弃取出的 records,不得访问失效 namespace。 -7. namespace 仍存活时,在 manager lock 下逐条回放:set 仍存在则对每个非零 adjustment 执行 `semval = clamp(semval + adjustment, 0, 32767)`,更新该 semaphore的`sempid`为退出者可见TGID、更新 `sem_otime` 并 rescan queue;set 已被 RMID 或 generation 不匹配时只丢弃 record。 -8. `release()`/reap只断言slot已空,不能首次detach。 - -```puml -@startuml SemUndoFinalOwnerReplay -skinparam activityDiamondBackgroundColor white - -start -:从 PCB slot take attachment; -:释放 PCB slot lock; -:在 SemUndoGroup.inner 下执行 task_owners -= 1; -if (最后 owner?) then (否) - :释放 SemUndoGroup.inner; - stop -else (是) - :retired = true; - :解析命名空间可见 TGID\n使用 task_tgid_vnr() 等价 helper; - :records_taken = true\n一次性 take 全部 records; - :释放 SemUndoGroup.inner; -endif - -if (ipc_ns.upgrade() 成功?) then (否) - :debug 记录并丢弃 records; - stop -else (是) - :获取 ipcns.sem manager lock; -endif - -while (仍有 SemUndoRecord?) is (是) - if (完整 semid / generation 仍有效?) then (是) - :按 Linux exit_sem() 语义回放; - :更新 sempid / sem_otime\n并 rescan queue; - else (否) - :丢弃该 record; - endif -endwhile (否) -:释放 manager lock; -stop -@enduml -``` - -每个真实task只detach一次;不得用thread group live、leader、TGID或`Arc::strong_count()`判断最后owner。 - -## 10. namespace transition 的强制前置事务 - -当前 namespace切换不满足“全部可失败prepare → 不可失败commit”。开放 `SEM_UNDO` 前必须先建立统一 `PreparedNamespaceInstall`: - -- prepare只计算并持有新 `NsProxy`、新cred、目标mount root/pwd、新或复用的`FsStruct`、安装flags及所有fd/权限/用户内存结果;不得修改PCB、fs、cred或undo attachment。 -- prepare结果显式携带`detach_sysvsem`,不得由old/new `Arc::ptr_eq()`推导。 -- commit在现有namespace/fs publication串行化边界内按固定顺序执行:detach并同步完成旧group回放;无失败安装fs root/pwd与FsStruct;无失败安装nsproxy;无失败安装已构造cred。 -- commit开始后不得分配、访问用户内存、做权限检查或返回错误。 -- pidfd setns、namespace-fd setns和unshare必须汇入同一prepare/commit helper。 - -具体 detach 判定: - -- `unshare(CLONE_SYSVSEM)`:置位,哪怕IPC namespace不变。成功后slot为空,后续UNDO创建新group。 -- `unshare(CLONE_NEWIPC)`:置位;若同时含`CLONE_SYSVSEM`仍只detach一次。 -- setns:已验证的installation set包含`CLONE_NEWIPC`就置位,即使目标IPC namespace与当前是同一个`Arc`。 -- 只切换UTS、NET、MNT、CGROUP或PID-for-children不detach。 - -任何prepare错误发生在detach之前,必须保持旧namespace、fs、cred、attachment及债务完全不变。 - -```puml -@startuml PreparedNamespaceInstallLifecycle -skinparam activityDiamondBackgroundColor white - -start -:unshare() / setns(); - -partition "prepare(允许失败)" { - :验证 fd / 权限 / 用户内存; - :构造新的 NsProxy / cred / FsStruct\n以及 root / pwd / 安装 flags; - :显式计算 detach_sysvsem; - if (prepare 失败?) then (是) - :返回 errno;旧命名空间 / fs / cred /\nattachment / 债务保持不变; - stop - else (否) - endif -} - -partition "commit(不可失败)" { - :进入命名空间 / fs 发布串行化边界; - if (detach_sysvsem?) then (是) - :同步 detach 旧 SemUndoGroup\n并完成 final-owner replay; - endif - :无失败安装 FsStruct 与 root / pwd; - :无失败安装 nsproxy; - :无失败安装 cred; - note right - commit 开始后不得分配、访问用户内存、 - 做权限检查或返回错误 - end note -} - -:新的 IpcNamespace 对任务可见; -note right - 任务 / nsproxy 与阻塞 syscall 持有 Arc; - SemUndoGroup 只持 Weak -end note -:任务离开且阻塞 syscall / RMID 完成后\n释放各自强引用; -if (最后 Arc?) then (是) - :在 IpcNamespace::Drop 前验证\n无存活 owner / waiter / registered record; -endif -stop -@enduml -``` - -## 11. namespace 生命周期 - -当前实现不在 `IpcNamespace::Drop` 中取 manager lock、RMID set或唤醒 waiter,也不把 Drop 上下文作为 teardown 实现。依赖并验证以下生命周期约束: - -1. task/nsproxy和blocked syscall持有`Arc`; -2. group只持namespace Weak,不能延长namespace生命周期; -3. task离开IPC namespace前同步detach,最后owner回放完成后才发布新namespace; -4. blocked syscall结束或RMID完成前,其namespace强引用仍在; -5. 因此最后`Arc`析构时不存在live task、blocked waiter、live group owner或有record的registered group。 - -若该不变量被后续引用路径变更打破,必须在引入明确调用者和安全上下文后增加 `destroy_all()`:在 manager lock 内对每个 set 执行“删 group records、不回放、waiter 完成 `EIDRM`、删除 ID、最后释放 index”。不得把未知 Drop 上下文当作 teardown 实现。当前 group 的 `retired`/`records_taken` 只保证 final-owner records 单次消费,不构成 namespace teardown。 - -## 12. errno 与失败原子性 - -| 条件 | 结果 | 共享状态 | -|---|---:|---| -| 空sops、负semid、非法timeout | `EINVAL` | 不变 | -| `nsops > SEMOPM` | `E2BIG` | 不变 | -| 用户复制失败 | `EFAULT` | 不变 | -| semnum越界 | `EFBIG` | 不变 | -| 权限/安全检查失败 | `EACCES`或原errno | 不变 | -| semval或semadj逐步越界 | `ERANGE` | 整组不变 | -| NOWAIT阻塞、超时 | `EAGAIN` | 不变 | -| signal取消 | `EINTR` | 不变 | -| 等待期间RMID | `EIDRM` | 不变 | -| group/record/adjustment/registry/scratch/queue分配失败 | `ENOMEM` | 不变 | -| 初始无效semid | 现有checked lookup errno,通常`EINVAL` | 不变 | -| prepare后RMID或generation变化 | `EIDRM` | 不变 | - -commit必须无分配、无失败;blocked/cancelled queue不得commit;控制清账、写值和rescan处于同一manager临界区;RMID在index复用前清完record。 - -## 13. 历史实施分期与当前状态 - -以下分期记录实现时的提交边界,不再描述当前 tree 的提交状态;最终实现已经合并 ownership、namespace transaction、record accounting、ABI 开放和测试。 - -### 13.1 历史前置提交:process publication事务化 - -文件: - -- `kernel/src/process/namespace/nsproxy.rs` -- `kernel/src/process/namespace/unshare.rs` -- `kernel/src/process/namespace/setns.rs` -- `kernel/src/process/fork.rs` -- 必要的 `kernel/src/process/task.rs`、cred/fs helper文件 - -内容: - -- 引入namespace纯prepare/不可失败commit;两条setns和unshare汇流。 -- 修复fork中pidfd与PID/线程组/children/global PCB/cgroup publication顺序或完整rollback。 - -验收:每个commit前可失败点都有测试;失败不留下半发布namespace、fs、cred、PID关系、pidfd或child。 - -### 13.2 历史提交1:最小undo所有权 - -文件: - -- 新增 `kernel/src/ipc/sem_undo.rs` -- `kernel/src/ipc/mod.rs` -- `kernel/src/ipc/sem.rs` -- `kernel/src/process/task.rs` -- `kernel/src/process/fork.rs` -- `kernel/src/process/manager/exit.rs` -- `kernel/src/process/namespace/nsproxy.rs` -- `kernel/src/process/namespace/unshare.rs` -- `kernel/src/process/namespace/setns.rs` - -内容:group、non-Clone attachment、PCB slot、owner计数、namespace weak registry骨架、精确child guard、fork/exec/exit/unshare/setns attach/detach;records为空。用户态仍返回`ENOSYS`。 - -验收:owner/observer分离,普通fork不继承,SYSVSEM共享,guard安装/回滚/publication点和namespace detach内部测试通过;独立构建通过。 - -### 13.3 历史提交2:完整但未发布的记账路径 - -文件: - -- `kernel/src/ipc/sem_undo.rs` -- `kernel/src/ipc/sem.rs` -- `kernel/src/process/manager/exit.rs` -- `kernel/src/process/namespace/ipc_namespace.rs`(仅生命周期不变量或经证明必需的显式teardown) - -内容:full-semid records、锁外fallible prepare、`Arc>>` registry replacement、无分配simulate/commit、queue原group归属、SETVAL/SETALL/RMID清账、最后owner replay/clamp/sempid/rescan,以及namespace lifecycle不变量或经明确调用上下文实现的`destroy_all()`。该历史阶段的用户态仍返回`ENOSYS`。 - -验收:所有执行、replay、控制、RMID、queue、namespace lifecycle内部测试通过;manager lock内无新增分配;独立构建通过。 - -### 13.4 历史提交3:一次性开放ABI与测试 - -文件: - -- `kernel/src/ipc/sem.rs` -- `user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc` -- 同目录必要的最小syscall/helper代码 -- kernel现有测试模块 - -内容:仅在全部 prerequisite通过后删除`SEM_UNDO -> ENOSYS`;替换`SemUndoRejected`;加入下述两层测试。禁止夹带per-set lock、namespace tunable或无关重构。 - -验收:格式、目标内核构建、kernel-internal目标及QEMU dunitest全部通过。 - -删除门禁的硬 prerequisite:立即执行、queued retry、exit replay、SETVAL/SETALL、RMID、full-ID复用保护、fork/clone owner、exec、unshare/setns(含same-Arc IPC install)、namespace lifecycle、所有prepare/rollback路径均已实现并通过内部测试。缺一项就不得开放。 - -## 14. 测试矩阵 - -测试明确分层: - -- **QEMU dunitest ABI层**只使用公开syscall和pipe/eventfd/futex及`GETNCNT`/`GETZCNT`握手,不依赖sleep猜时序,不依赖内核私有failpoint。 -- **kernel-internal层**可用测试allocator、指定generation ID和lock gate;注入只存在于`cfg(test)`或等价非产品构建,不新增用户调试ABI。 - -### 14.1 QEMU ABI(12组) - -1. **基本累计与符号**:child依次`+3|UNDO`、`-1|UNDO`,退出前净+2、退出后恢复;另测初值3上的`-2|UNDO`。 -2. **数组顺序与回滚**:重复semnum、混合flag;前项UNDO成功但后项NOWAIT阻塞或ERANGE时,调用后和退出后均无前缀效果。 -3. **semadj边界**:分别到达32767和-32768成功,下一步越界ERANGE;单数组中间越界即使后续可抵消仍失败。 -4. **queue归属与提交点**:A排队UNDO,`GETNCNT`确认;B普通操作使Ready;债务只由A退出回放。 -5. **未提交路径**:timeout、signal、NOWAIT、等待期间RMID分别验证EAGAIN/EINTR/EAGAIN/EIDRM且无回放。 -6. **SETVAL/SETALL**:两个独立groups、多sem清账;SETVAL仅清目标,SETALL清全set;SETVAL唤醒后形成的新债务仍回放。 -7. **RMID**:成功UNDO后RMID再退出,不回放、不崩溃。generation定向复用放内部层。 -8. **fork/clone owner**:普通fork不继承;`clone(CLONE_SYSVSEM|SIGCHLD)`双方操作,首个退出不回放,最后owner回放一次;不带SYSVSEM的thread flags不共享;NEWIPC|SYSVSEM返回EINVAL。 -9. **exec与退出**:成功exec到同binary helper mode后退出仍回放;可恢复exec失败后债务仍在。non-leader exec/group-exit作为基础设施稳定后的同组回归,不阻塞首版独立门禁。 -10. **exit clamp与唤醒**:上下界clamp;用GETNCNT/GETZCNT确认负/零waiter入队,最后owner退出改变值后waiter提交。 -11. **unshare SYSVSEM**:共享owners之一unshare后,新UNDO进入新group,旧group由剩余owner结算;唯一owner unshare在syscall返回前完成旧债结算。 -12. **IPC namespace与errno**:已覆盖 namespace-fd setns、pidfd setns(含 same-Arc IPC install)及 prepare 失败保持旧 attachment;回归 `EINVAL`/`E2BIG`/`EFAULT`/`EFBIG`/`EACCES`。**follow-up:补充 `unshare(CLONE_NEWIPC)` 的 QEMU ABI 用例;当前该路径仅有 kernel-internal 覆盖。** - -### 14.2 kernel-internal(3组) - -1. **owner/publication**:attachment不可Clone;observer不改变owner;share只加一次;每task take/detach至多一次;最后owner唯一drain;child guard在安装前、安装后、最终disarm前失败时先publication rollback再撤owner。 -2. **fallible事务与竞态**:已覆盖 record reservation/capacity、stale prepared record、registry snapshot、queue retry 和 RCU retirement prepare 的失败原子性,以及 SETVAL/RMID/exit 的合法串行结果。**follow-up:为 group、adjustment、registry replacement、simulation scratch、queue Arc/sops 的 allocator failure 增加可控 failpoint,逐类验证 `ENOMEM` 与零部分状态。** -3. **ID与算法**:直接构造旧set RMID后同index新generation,旧group不污染新set;覆盖逐项semadj边界、重复semnum、混合flag、SETVAL局部清账、SETALL/RMID全record清账、stale Weak压缩;代码路径断言不读取SEMUME/SEMMNU作admission判断。 - -所有case独立创建并RMID自己的set。用户态不得通过不确定循环强迫ID复用,也不得尝试创建`SEMMNU + 1`对象。 - -## 15. 最终验收 - -1. ABI 已在前置事务、ownership、记账与测试路径合入后开放;后续变更不得倒退为部分 lifecycle 支持。 -2. 上述已实现的 QEMU 与 kernel-internal 覆盖必须由明确构建目标通过;`unshare(CLONE_NEWIPC)` ABI 与逐类 allocator failpoint 保持为第 14 节标注的 follow-up,不得误报为已覆盖。 -3. `semadj = -Σ(successful nonzero SEM_UNDO sem_op)`在立即、queued和退出路径一致;逐项边界准确。 -4. blocked、errno、timeout、signal、RMID和已覆盖的 `ENOMEM` 路径均不留下semval/semadj前缀或半个registry/record/queue变更。 -5. 所有用户可触发增长保持 fallible;manager lock内无动态分配。 -6. 普通fork不继承,显式SYSVSEM共享,最后显式owner唯一回放;observer和 queue capture 的强引用都不影响 owner 结算。 -7. child attachment在任何publication前安装,guard持续到完整不可失败publication结束;失败先撤publication再撤owner。 -8. exec保留;每个真实task在robust-list后、exit_mm前只detach一次。 -9. replay clamp到`[0,32767]`、使用namespace-visible TGID更新sempid、更新`sem_otime`并rescan queue。 -10. SETVAL/SETALL先清旧债、再写值、后rescan;RMID删债不回放且先于index复用。 -11. record只用full generation-bearing semid;定向复用测试证明旧债不污染新set。 -12. queued operation归属原调用者group,只在Ready时记账;queue 保存的 `Arc` 与 prepared record 仅保障该归属,不是 task owner。 -13. `unshare(CLONE_SYSVSEM)`及两条setns路径通过;setns安装same-Arc IPC namespace仍detach;prepare失败不改变旧状态。`unshare(CLONE_NEWIPC)` 的 ABI 测试为 follow-up。 -14. namespace最后引用不变量经审计和debug测试维持;若将来无法满足,必须实现具备明确调用上下文的`destroy_all()`。 -15. 锁依赖符合第5节:不存在`group -> manager`,PCB slot不嵌套;queue 的 `undo_record`、`scratch` 与 `status` 遵循 entry 局部锁约束。 -16. `SEMUME`、`SEMMNU`不参与admission,操作数限制继续使用全局`SEMOPM`。 -17. 非`SEM_UNDO`行为无回归;kernel格式、目标架构构建、kernel-internal测试和QEMU dunitest必须持续通过。 - -本规格已同步当前最终实现:不保留待选数据模型;局部 prepared record/reservation/revision/retired、queue strong capture 与预分配 RCU retirement 均为既定实现机制。第14节明确标注的 `unshare(CLONE_NEWIPC)` ABI 和 allocator failpoint 覆盖是 follow-up,不得作为已完成验收宣称。 From 303064c73873cb9815715ff566b21ea6a740792b Mon Sep 17 00:00:00 2001 From: longjin Date: Sun, 6 Sep 2026 04:11:08 +0000 Subject: [PATCH 27/34] refactor(ipc): separate semaphore state, operations and undo lifecycle Split semaphore ABI, namespace management, atomic execution and wait queues into focused modules. Centralize terminal publication without rescanning known queues, and represent undo retirement with an explicit phase. Preserve syscall paths, locking and deferred reclamation. Move existing tests with their owning modules and add atomic-attempt regression coverage. Signed-off-by: longjin --- kernel/src/ipc/sem.rs | 3487 -------------------- kernel/src/ipc/sem/abi.rs | 193 ++ kernel/src/ipc/sem/manager/control.rs | 178 + kernel/src/ipc/sem/manager/mod.rs | 326 ++ kernel/src/ipc/sem/manager/test_support.rs | 110 + kernel/src/ipc/sem/manager/tests.rs | 165 + kernel/src/ipc/sem/mod.rs | 22 + kernel/src/ipc/sem/operation.rs | 302 ++ kernel/src/ipc/sem/set/mod.rs | 326 ++ kernel/src/ipc/sem/set/operation.rs | 214 ++ kernel/src/ipc/sem/set/operation/tests.rs | 281 ++ kernel/src/ipc/sem/set/queue.rs | 570 ++++ kernel/src/ipc/sem/set/queue/tests.rs | 109 + kernel/src/ipc/sem/set/test_support.rs | 135 + kernel/src/ipc/sem/set/tests.rs | 837 +++++ kernel/src/ipc/sem_undo.rs | 1151 ------- kernel/src/ipc/sem_undo/lifecycle.rs | 157 + kernel/src/ipc/sem_undo/mod.rs | 339 ++ kernel/src/ipc/sem_undo/record.rs | 115 + kernel/src/ipc/sem_undo/test_support.rs | 113 + kernel/src/ipc/sem_undo/tests.rs | 455 +++ 21 files changed, 4947 insertions(+), 4638 deletions(-) delete mode 100644 kernel/src/ipc/sem.rs create mode 100644 kernel/src/ipc/sem/abi.rs create mode 100644 kernel/src/ipc/sem/manager/control.rs create mode 100644 kernel/src/ipc/sem/manager/mod.rs create mode 100644 kernel/src/ipc/sem/manager/test_support.rs create mode 100644 kernel/src/ipc/sem/manager/tests.rs create mode 100644 kernel/src/ipc/sem/mod.rs create mode 100644 kernel/src/ipc/sem/operation.rs create mode 100644 kernel/src/ipc/sem/set/mod.rs create mode 100644 kernel/src/ipc/sem/set/operation.rs create mode 100644 kernel/src/ipc/sem/set/operation/tests.rs create mode 100644 kernel/src/ipc/sem/set/queue.rs create mode 100644 kernel/src/ipc/sem/set/queue/tests.rs create mode 100644 kernel/src/ipc/sem/set/test_support.rs create mode 100644 kernel/src/ipc/sem/set/tests.rs delete mode 100644 kernel/src/ipc/sem_undo.rs create mode 100644 kernel/src/ipc/sem_undo/lifecycle.rs create mode 100644 kernel/src/ipc/sem_undo/mod.rs create mode 100644 kernel/src/ipc/sem_undo/record.rs create mode 100644 kernel/src/ipc/sem_undo/test_support.rs create mode 100644 kernel/src/ipc/sem_undo/tests.rs diff --git a/kernel/src/ipc/sem.rs b/kernel/src/ipc/sem.rs deleted file mode 100644 index 74de98bd7d..0000000000 --- a/kernel/src/ipc/sem.rs +++ /dev/null @@ -1,3487 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// -// System V semaphore subsystem, tracking Linux 6.6 `ipc/sem.c` observable -// behavior, including SEM_UNDO lifecycle accounting. - -use alloc::{ - sync::{Arc, Weak}, - vec::Vec, -}; -use core::fmt; - -use hashbrown::HashMap; -use system_error::SystemError; - -use crate::{ - ipc::{ - id::IpcIdAllocator, - ipc_perm::{self, IpcPerm, IpcPermView, PosixIpcPerm}, - sem_undo::{ - PreparedSemUndoRecord, PreparedSemUndoRecordAction, SemUndoGroup, SemUndoRecord, - }, - }, - libs::{ - spinlock::SpinLock, - wait_queue::{TimeoutWaker, Waiter, Waker}, - }, - process::{ - namespace::ipc_namespace::IpcNamespace, - pid::{Pid, PidType}, - ProcessManager, - }, - time::{ - timer::{clock, next_n_us_timer_jiffies, Timer}, - Duration, PosixTimeSpec, - }, -}; - -/// Used to create a new private semaphore set -pub const IPC_PRIVATE: SemKey = SemKey::new(0); - -int_like!(SemId, usize); -int_like!(SemKey, usize); - -#[derive(Debug, Clone, Copy)] -pub struct SemSetAllToken { - id: SemId, - nsems: usize, -} - -impl SemSetAllToken { - fn new(id: SemId, nsems: usize) -> Self { - Self { id, nsems } - } - - pub fn nsems(&self) -> usize { - self.nsems - } -} - -// Limit constants from Linux include/uapi/linux/sem.h -pub const SEMMNI: usize = 32000; -pub const SEMMSL: usize = 32000; -pub const SEMMNS: usize = SEMMNI * SEMMSL; -pub const SEMOPM: usize = 500; -pub const SEMVMX: i32 = 32767; - -bitflags! { - pub struct SemFlags: u32 { - const PERM_MASK = 0o777; - const IPC_CREAT = 0o1000; - const IPC_EXCL = 0o2000; - const IPC_NOWAIT = 0x800; - const SEM_UNDO = 0x1000; - } -} - -/// Semaphore-set control commands (Linux x86_64 UAPI include/uapi/linux/sem.h) -#[derive(Eq, Clone, Copy)] -pub enum SemCtlCmd { - /// Remove the semaphore set - IpcRmid = 0, - /// Set permissions - IpcSet = 1, - /// Retrieve `SemIdDs` - IpcStat = 2, - /// Retrieve `SemInfo` - IpcInfo = 3, - /// Get the PID of the last process to operate on the specified semaphore - GetPid = 11, - /// Get the specified semaphore value - GetVal = 12, - /// Get values of all semaphores in the set - GetAll = 13, - /// Get the number of processes waiting for the specified semaphore to increase - GetNcnt = 14, - /// Get the number of processes waiting for the specified semaphore to reach zero - GetZcnt = 15, - /// Set the specified semaphore value - SetVal = 16, - /// Set values of all semaphores in the set - SetAll = 17, - /// Retrieve `SemIdDs` by index - SemStat = 18, - /// Retrieve `SemInfo` - SemInfo = 19, - /// Retrieve `SemIdDs` by index without permission checks - SemStatAny = 20, - - Default, -} - -impl From for SemCtlCmd { - fn from(cmd: usize) -> SemCtlCmd { - match cmd { - 0 => Self::IpcRmid, - 1 => Self::IpcSet, - 2 => Self::IpcStat, - 3 => Self::IpcInfo, - 11 => Self::GetPid, - 12 => Self::GetVal, - 13 => Self::GetAll, - 14 => Self::GetNcnt, - 15 => Self::GetZcnt, - 16 => Self::SetVal, - 17 => Self::SetAll, - 18 => Self::SemStat, - 19 => Self::SemInfo, - 20 => Self::SemStatAny, - _ => Self::Default, - } - } -} - -impl fmt::Display for SemCtlCmd { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SemCtlCmd::IpcRmid => write!(f, "IPC_RMID"), - SemCtlCmd::IpcSet => write!(f, "IPC_SET"), - SemCtlCmd::IpcStat => write!(f, "IPC_STAT"), - SemCtlCmd::IpcInfo => write!(f, "IPC_INFO"), - SemCtlCmd::GetPid => write!(f, "GETPID"), - SemCtlCmd::GetVal => write!(f, "GETVAL"), - SemCtlCmd::GetAll => write!(f, "GETALL"), - SemCtlCmd::GetNcnt => write!(f, "GETNCNT"), - SemCtlCmd::GetZcnt => write!(f, "GETZCNT"), - SemCtlCmd::SetVal => write!(f, "SETVAL"), - SemCtlCmd::SetAll => write!(f, "SETALL"), - SemCtlCmd::SemStat => write!(f, "SEM_STAT"), - SemCtlCmd::SemInfo => write!(f, "SEM_INFO"), - SemCtlCmd::SemStatAny => write!(f, "SEM_STAT_ANY"), - SemCtlCmd::Default => write!(f, "DEFAULT (Invalid Cmd)"), - } - } -} - -impl PartialEq for SemCtlCmd { - fn eq(&self, other: &SemCtlCmd) -> bool { - *self as usize == *other as usize - } -} - -/// A single semaphore (fields of Linux `struct sem`) -#[derive(Debug, Clone)] -pub struct KernelSem { - /// semval - val: i32, - /// sempid: process that last operated on this semaphore - pid: Option>, - /// Counts of queued operations currently blocked on this semaphore. - ncnt: usize, - zcnt: usize, -} - -/// Userspace `sembuf` (Linux `struct sembuf`, 6 bytes) -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub struct PosixSemBuf { - pub sem_num: u16, - pub sem_op: i16, - pub sem_flg: i16, -} - -/// Semaphore-set information matching Linux x86_64 `struct semid64_ds` (104 bytes, -/// including the high halves of 32-bit timestamp fields) -#[repr(C)] -#[derive(Debug, Clone, Copy, Default)] -pub struct PosixSemIdDs { - /// Permission information - pub sem_perm: PosixIpcPerm, - /// Time of the last `semop` (64-bit; upper 32 bits are in `__sem_otime_high`) - pub sem_otime: i64, - _sem_otime_high: i64, - /// Time of the last metadata change - pub sem_ctime: i64, - _sem_ctime_high: i64, - /// Number of semaphores in the set - pub sem_nsems: usize, - _unused1: usize, - _unused2: usize, -} - -/// Semaphore system information matching Linux `struct seminfo` (40 bytes) -#[repr(C)] -#[derive(Debug, Clone, Copy, Default)] -pub struct PosixSemInfo { - pub semmap: i32, - pub semmni: i32, - pub semmns: i32, - pub semmnu: i32, - pub semmsl: i32, - pub semopm: i32, - pub semume: i32, - pub semusz: i32, - pub semvmx: i32, - pub semaem: i32, -} - -impl PosixSemInfo { - fn new(cmd: SemCtlCmd, set_count: usize, total_sems: usize) -> Self { - let (semusz, semaem) = if cmd == SemCtlCmd::SemInfo { - (set_count as i32, total_sems as i32) - } else { - (20, SEMVMX) - }; - PosixSemInfo { - semmap: SEMMNS as i32, - semmni: SEMMNI as i32, - semmns: SEMMNS as i32, - semmnu: SEMMNS as i32, - semmsl: SEMMSL as i32, - semopm: SEMOPM as i32, - semume: SEMOPM as i32, - semusz, - semvmx: SEMVMX, - semaem, - } - } -} - -/// Why a waiter is blocked, determining wakeup timing and GETNCNT/GETZCNT accounting -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum SemWaitType { - /// `sem_op < 0`: wait for semval to increase (GETNCNT) - Increase, - /// `sem_op == 0`: wait for semval to reach zero (GETZCNT) - Zero, -} - -/// The precise operation currently blocking an operation group. -#[derive(Debug, Clone, Copy)] -struct SemBlockedOp { - semnum: usize, - wait_type: SemWaitType, - nowait: bool, -} - -#[derive(Debug)] -enum SemQueueStatus { - Queued { - blocker: SemBlockedOp, - links: Option, - }, - Completed { - result: Result, - next_wake: Option>, - }, -} - -#[derive(Debug)] -struct SemWaitLinks { - prev: Option>, - next: Option>, -} - -/// Set-private FIFO. All structural changes require the namespace manager lock. -/// Strong forward / weak backward links avoid cycles; each operation holds at -/// most one entry status lock at a time. -#[derive(Debug, Default)] -struct SemWaitQueue { - head: Option>, - tail: Option>, -} - -impl SemWaitQueue { - fn push_back(&mut self, entry: Arc) { - { - let mut status = entry.status.lock(); - let SemQueueStatus::Queued { links, .. } = &mut *status else { - panic!("cannot enqueue completed semaphore operation"); - }; - assert!(links.is_none(), "semaphore operation already linked"); - *links = Some(SemWaitLinks { - prev: self.tail.as_ref().map(Arc::downgrade), - next: None, - }); - } - if let Some(tail) = self.tail.take() { - let mut status = tail.status.lock(); - let SemQueueStatus::Queued { - links: Some(links), .. - } = &mut *status - else { - panic!("semaphore queue tail is not linked"); - }; - links.next = Some(entry.clone()); - } else { - self.head = Some(entry.clone()); - } - self.tail = Some(entry); - } - - fn remove(&mut self, entry: &Arc) -> bool { - let links = { - let mut status = entry.status.lock(); - match &mut *status { - SemQueueStatus::Queued { links, .. } => links.take(), - SemQueueStatus::Completed { .. } => None, - } - }; - let Some(SemWaitLinks { prev, next }) = links else { - return false; - }; - let prev = prev.map(|weak| weak.upgrade().expect("linked predecessor is live")); - if let Some(prev) = prev.as_ref() { - let mut status = prev.status.lock(); - let SemQueueStatus::Queued { - links: Some(links), .. - } = &mut *status - else { - panic!("semaphore predecessor is not linked"); - }; - links.next = next.clone(); - } else { - debug_assert!(self - .head - .as_ref() - .is_some_and(|head| Arc::ptr_eq(head, entry))); - self.head = next.clone(); - } - if let Some(next) = next { - let mut status = next.status.lock(); - let SemQueueStatus::Queued { - links: Some(links), .. - } = &mut *status - else { - panic!("semaphore successor is not linked"); - }; - links.prev = prev.as_ref().map(Arc::downgrade); - } else { - self.tail = prev; - } - true - } - - fn iter(&self) -> SemWaitIter { - SemWaitIter(self.head.clone()) - } - - #[cfg(test)] - fn is_empty(&self) -> bool { - self.head.is_none() - } -} - -impl Drop for SemWaitQueue { - fn drop(&mut self) { - // Detach iteratively, including namespace teardown, never recursively - // destroy an arbitrarily long chain of Arc-owned entries. - while let Some(entry) = self.head.clone() { - self.remove(&entry); - } - } -} - -struct SemWaitIter(Option>); - -impl Iterator for SemWaitIter { - type Item = Arc; - - fn next(&mut self) -> Option { - let entry = self.0.take()?; - self.0 = match &*entry.status.lock() { - SemQueueStatus::Queued { - links: Some(links), .. - } => links.next.clone(), - _ => None, - }; - Some(entry) - } -} - -/// An Arc-owned queue entry shared by the set and the blocked caller. -#[derive(Debug)] -struct SemQueueEntry { - sops: Vec, - pid: Option>, - undo_group: Option>, - undo_record: SpinLock>, - waker: Arc, - scratch: SpinLock, - status: SpinLock, -} - -impl SemQueueEntry { - fn new_prepared( - sops: Vec, - pid: Option>, - undo_group: Option>, - undo_record: Option, - waker: Arc, - scratch: SemopScratch, - blocker: SemBlockedOp, - ) -> Self { - debug_assert_eq!( - undo_group.is_some(), - undo_record.is_some(), - "queued SEM_UNDO group and prepared record must be captured together" - ); - debug_assert!( - undo_group.is_some() - || sops - .iter() - .all(|op| (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() == 0), - "queued SEM_UNDO entry requires a captured undo group" - ); - Self { - scratch: SpinLock::new(scratch), - sops, - pid, - undo_group, - undo_record: SpinLock::new(undo_record), - waker, - status: SpinLock::new(SemQueueStatus::Queued { - blocker, - links: None, - }), - } - } - - fn prepare_sops(sops: &[PosixSemBuf]) -> Result, SystemError> { - let mut owned_sops = Vec::new(); - owned_sops - .try_reserve_exact(sops.len()) - .map_err(|_| SystemError::ENOMEM)?; - owned_sops.extend_from_slice(sops); - Ok(owned_sops) - } - - #[cfg(test)] - fn new( - sops: &[PosixSemBuf], - pid: Option>, - waker: Arc, - blocker: SemBlockedOp, - ) -> Self { - Self::new_prepared( - Self::prepare_sops(sops).unwrap(), - pid, - None, - None, - waker, - SemopScratch::try_new(sops.len()).unwrap(), - blocker, - ) - } - - fn completed_result(&self) -> Option> { - match &*self.status.lock() { - SemQueueStatus::Queued { .. } => None, - SemQueueStatus::Completed { result, .. } => Some(result.clone()), - } - } - - fn complete(&self, result: Result) -> bool { - let mut status = self.status.lock(); - if matches!(&*status, SemQueueStatus::Completed { .. }) { - return false; - } - assert!( - matches!(&*status, SemQueueStatus::Queued { links: None, .. }), - "semaphore operation must be unlinked before completion" - ); - *status = SemQueueStatus::Completed { - result, - next_wake: None, - }; - true - } - - #[cfg(test)] - fn is_waiting_on(&self, semnum: usize, wait_type: SemWaitType) -> bool { - matches!( - &*self.status.lock(), - SemQueueStatus::Queued { blocker, .. } - if blocker.semnum == semnum && blocker.wait_type == wait_type - ) - } -} - -/// Per-operation completion list. The existing entry status lock protects the -/// link; no allocation or scheduler operation is needed while the set is locked. -/// Declare this before manager guards so all exits wake only after unlocking. -#[derive(Default)] -pub(crate) struct SemWakeBatch { - head: Option>, - tail: Option>, -} - -impl SemWakeBatch { - fn complete(&mut self, entry: Arc, result: Result) { - if !entry.complete(result) { - return; - } - if let Some(tail) = self.tail.take() { - if let SemQueueStatus::Completed { next_wake, .. } = &mut *tail.status.lock() { - *next_wake = Some(entry.clone()); - } - } else { - self.head = Some(entry.clone()); - } - self.tail = Some(entry); - } - - pub(crate) fn wake_all(&mut self) { - self.tail = None; - while let Some(entry) = self.head.take() { - self.head = match &mut *entry.status.lock() { - SemQueueStatus::Completed { next_wake, .. } => next_wake.take(), - SemQueueStatus::Queued { .. } => unreachable!("wake batch contains queued entry"), - }; - // Release the status lock before entering the scheduler. Detaching - // each link also prevents recursive Arc destruction for large batches. - entry.waker.wake(); - } - } -} - -impl Drop for SemWakeBatch { - fn drop(&mut self) { - self.wake_all(); - } -} - -/// Selects one of the set-global pending queues. -#[derive(Debug, Clone, Copy)] -enum SemPendingQueue { - Const, - Alter, -} - -/// A live undo registration, reused to own deferred cleanup after removal. -#[derive(Debug)] -enum SemUndoAssociation { - Group(Weak), - Retired { - _group: Arc, - _record: Option, - }, -} - -impl SemUndoAssociation { - /// Retired slots are only present in a removed, caller-owned set. - fn group(&self) -> &Weak { - match self { - Self::Group(group) => group, - Self::Retired { .. } => unreachable!("retired association in live set"), - } - } -} - -/// Semaphore set -#[derive(Debug)] -pub struct KernelSemSet { - /// Groups that can carry undo debt for this set, including queued operations. - undo_groups: Vec, - /// Permission information - pub kern_ipc_perm: IpcPerm, - /// Semaphores in the set - pub sems: Vec, - /// Time of the last `semop` - pub sem_otime: i64, - /// Time of the last metadata change - pub sem_ctime: i64, - /// Pending operation groups containing only zero-wait operations - pending_const: SemWaitQueue, - /// Pending operation groups containing at least one altering operation - pending_alter: SemWaitQueue, -} - -impl KernelSemSet { - /// Live associations survive SETVAL/SETALL. Only RMID or dead groups - /// remove them, so an existing undo record proves prior association. - /// Insufficient spare capacity requests an unlocked preparation/retry. - /// On success, spare retains any replaced allocation for unlocked disposal. - fn ensure_undo_group_registered_prepared( - &mut self, - group: &Arc, - spare: &mut Vec, - ) -> Result<(), usize> { - debug_assert!(spare.is_empty()); - let candidate = Arc::downgrade(group); - if self - .undo_groups - .iter() - .any(|entry| entry.group().ptr_eq(&candidate)) - { - return Ok(()); - } - if self.undo_groups.len() == self.undo_groups.capacity() { - self.compact_undo_registry(); - } - if self.undo_groups.len() == self.undo_groups.capacity() { - if spare.capacity() <= self.undo_groups.len() { - return Err(self.undo_groups.len().saturating_mul(2).max(4)); - } - spare.append(&mut self.undo_groups); - core::mem::swap(&mut self.undo_groups, spare); - } - self.undo_groups.push(SemUndoAssociation::Group(candidate)); - Ok(()) - } - - fn compact_undo_registry(&mut self) { - self.undo_groups - .retain(|entry| entry.group().strong_count() != 0); - } - - /// Request/publish smaller storage without allocating under the manager - /// lock. Empty registries are moved into retired without installing spare. - /// The caller must drop both buffers after unlocking. - fn shrink_undo_registry_prepared( - &mut self, - spare: &mut Vec, - retired: &mut Vec, - ) -> usize { - debug_assert!(spare.is_empty()); - debug_assert!(retired.is_empty() && retired.capacity() == 0); - let len = self.undo_groups.len(); - if len == 0 { - core::mem::swap(&mut self.undo_groups, retired); - return 0; - } - if self.undo_groups.capacity() <= 4 || len > self.undo_groups.capacity() / 4 { - return 0; - } - if spare.capacity() < len { - return len.saturating_mul(2).max(4); - } - if spare.capacity() < self.undo_groups.capacity() { - spare.append(&mut self.undo_groups); - core::mem::swap(&mut self.undo_groups, spare); - } - 0 - } - - fn try_allocate_sems(nsems: usize) -> Result, SystemError> { - let mut sems = Vec::new(); - sems.try_reserve_exact(nsems) - .map_err(|_| SystemError::ENOMEM)?; - sems.resize( - nsems, - KernelSem { - val: 0, - pid: None, - ncnt: 0, - zcnt: 0, - }, - ); - Ok(sems) - } - - fn new(kern_ipc_perm: IpcPerm, sems: Vec) -> Self { - KernelSemSet { - undo_groups: Vec::new(), - kern_ipc_perm, - sems, - sem_otime: 0, - sem_ctime: PosixTimeSpec::now().tv_sec, - pending_const: SemWaitQueue::default(), - pending_alter: SemWaitQueue::default(), - } - } - - fn pending_queue_for(sops: &[PosixSemBuf]) -> SemPendingQueue { - if sops.iter().any(|op| op.sem_op != 0) { - SemPendingQueue::Alter - } else { - SemPendingQueue::Const - } - } - - /// The prepared entry itself owns the queue links; publication cannot allocate. - fn enqueue_waiter(&mut self, waiter: Arc) { - let blocker = match &*waiter.status.lock() { - SemQueueStatus::Queued { - blocker, - links: None, - } => *blocker, - _ => panic!("enqueue requires an unlinked semaphore operation"), - }; - let queue = match Self::pending_queue_for(&waiter.sops) { - SemPendingQueue::Const => &mut self.pending_const, - SemPendingQueue::Alter => &mut self.pending_alter, - }; - queue.push_back(waiter); - self.change_wait_count(blocker, true); - } - - fn change_wait_count(&mut self, blocker: SemBlockedOp, increase: bool) { - let sem = &mut self.sems[blocker.semnum]; - let count = match blocker.wait_type { - SemWaitType::Increase => &mut sem.ncnt, - SemWaitType::Zero => &mut sem.zcnt, - }; - *count = if increase { - count.checked_add(1).expect("semaphore wait count overflow") - } else { - count - .checked_sub(1) - .expect("semaphore wait count underflow") - }; - } - - /// Only linked entries contribute to counts; preparation uses this same - /// operation without accounting. Manager lock serializes both kinds. - fn update_blocker(&mut self, entry: &SemQueueEntry, blocker: SemBlockedOp) { - let mut status = entry.status.lock(); - if let SemQueueStatus::Queued { - blocker: old, - links, - } = &mut *status - { - if links.is_some() - && (old.semnum != blocker.semnum || old.wait_type != blocker.wait_type) - { - self.change_wait_count(*old, false); - self.change_wait_count(blocker, true); - } - *old = blocker; - } - } - - fn remove_waiter(&mut self, target: &Arc) { - self.remove_pending(Self::pending_queue_for(&target.sops), target); - } - - #[cfg(test)] - fn pending_is_empty(&self) -> bool { - self.pending_const.is_empty() && self.pending_alter.is_empty() - } - - fn pending_iter(&self, queue: SemPendingQueue) -> SemWaitIter { - match queue { - SemPendingQueue::Const => self.pending_const.iter(), - SemPendingQueue::Alter => self.pending_alter.iter(), - } - } - - fn remove_pending(&mut self, queue: SemPendingQueue, entry: &Arc) { - let blocker = match &*entry.status.lock() { - SemQueueStatus::Queued { - blocker, - links: Some(_), - .. - } => *blocker, - _ => return, - }; - let removed = match queue { - SemPendingQueue::Const => self.pending_const.remove(entry), - SemPendingQueue::Alter => self.pending_alter.remove(entry), - }; - if removed { - self.change_wait_count(blocker, false); - } - } - - /// Publish removal under the manager lock; the caller wakes after unlocking. - fn complete_all_removed(&mut self, wakes: &mut SemWakeBatch) { - for entry in self.pending_const.iter() { - self.remove_pending(SemPendingQueue::Const, &entry); - wakes.complete(entry, Err(SystemError::EIDRM)); - } - for entry in self.pending_alter.iter() { - self.remove_pending(SemPendingQueue::Alter, &entry); - wakes.complete(entry, Err(SystemError::EIDRM)); - } - } - - fn ncnt(&self, semnum: usize) -> usize { - self.sems[semnum].ncnt - } - - fn zcnt(&self, semnum: usize) -> usize { - self.sems[semnum].zcnt - } -} - -#[derive(Debug)] -struct SemopScratchEntry { - semnum: usize, - initial_val: i32, - virtual_val: i32, - initial_adj: i16, - virtual_adj: i16, -} - -#[derive(Debug)] -struct SemopScratch { - entries: Vec, -} - -impl SemopScratch { - fn try_new(capacity: usize) -> Result { - let mut entries = Vec::new(); - entries - .try_reserve_exact(capacity) - .map_err(|_| SystemError::ENOMEM)?; - Ok(Self { entries }) - } - - fn clear(&mut self) { - self.entries.clear(); - } - - fn entry_for( - &mut self, - set: &KernelSemSet, - semnum: usize, - undo: Option<&SemUndoRecord>, - ) -> Result<&mut SemopScratchEntry, SystemError> { - if let Some(index) = self.entries.iter().position(|entry| entry.semnum == semnum) { - return Ok(&mut self.entries[index]); - } - - if self.entries.len() == self.entries.capacity() { - return Err(SystemError::ENOMEM); - } - - let initial_val = set.sems[semnum].val; - let initial_adj = undo - .map(|record| record.adjustment(semnum)) - .unwrap_or_default(); - self.entries.push(SemopScratchEntry { - semnum, - initial_val, - virtual_val: initial_val, - initial_adj, - virtual_adj: initial_adj, - }); - Ok(self - .entries - .last_mut() - .expect("SEM_UNDO scratch entry was just inserted")) - } -} - -/// Fixed-capacity virtual semaphore state produced by `SemopScratch`. -#[derive(Debug)] -struct SemopSimulation { - entry_count: usize, -} - -impl SemopSimulation { - #[cfg(test)] - fn empty_for_test() -> Self { - Self { entry_count: 0 } - } -} - -/// Result of an attempted `semop` execution -#[derive(Debug)] -enum SemopOutcome { - Ready(SemopSimulation), - Blocked(SemBlockedOp), -} - -impl SemopOutcome { - #[cfg(test)] - fn ready_for_test(self) -> SemopSimulation { - match self { - Self::Ready(simulation) => simulation, - Self::Blocked(_) => panic!("expected ready semop outcome"), - } - } -} - -/// Semaphore manager -#[derive(Debug)] -pub struct SemManager { - /// SemId allocator - id_allocator: IpcIdAllocator, - /// Semaphore set table keyed by low IPC index - id2sem: HashMap, - /// SemId table keyed by SemKey - key2id: HashMap, - /// Total semaphores in the namespace (Linux semmns accounting) - total_sems: usize, -} - -impl Default for SemManager { - fn default() -> Self { - Self::new() - } -} - -impl SemManager { - const IPC_READ: u32 = 0o4; - const IPC_WRITE: u32 = 0o2; - - pub fn new() -> Self { - SemManager { - id_allocator: IpcIdAllocator::new(SEMMNI).unwrap(), - id2sem: HashMap::new(), - key2id: HashMap::new(), - total_sems: 0, - } - } - - fn get_by_semid_checked(&self, id: SemId) -> Result<&KernelSemSet, SystemError> { - let decoded = IpcIdAllocator::decode(id.data())?; - let set = self.id2sem.get(&decoded.idx).ok_or(SystemError::EINVAL)?; - if set.kern_ipc_perm.id != id.data() || set.kern_ipc_perm.seq != decoded.seq { - return Err(SystemError::EINVAL); - } - Ok(set) - } - - fn get_by_semid_checked_mut(&mut self, id: SemId) -> Result<&mut KernelSemSet, SystemError> { - let decoded = IpcIdAllocator::decode(id.data())?; - let set = self - .id2sem - .get_mut(&decoded.idx) - .ok_or(SystemError::EINVAL)?; - if set.kern_ipc_perm.id != id.data() || set.kern_ipc_perm.seq != decoded.seq { - return Err(SystemError::EINVAL); - } - Ok(set) - } - - fn get_by_index(&self, id: usize) -> Result<&KernelSemSet, SystemError> { - let idx = id & IpcIdAllocator::IPC_ID_IDX_MASK; - self.id2sem.get(&idx).ok_or(SystemError::EINVAL) - } - - #[allow(dead_code)] - fn validate_semid_nsems(&self, semid: SemId) -> Result { - Ok(self.get_by_semid_checked(semid)?.sems.len()) - } - - /// Test-only setup; production reserves registry storage before locking. - #[cfg(test)] - fn ensure_undo_group_registered( - &mut self, - group: &Arc, - semid: SemId, - ) -> Result<(), SystemError> { - let set = self.get_by_semid_checked_mut(semid)?; - let mut spare = Vec::new(); - if let Err(capacity) = set.ensure_undo_group_registered_prepared(group, &mut spare) { - spare - .try_reserve(capacity) - .map_err(|_| SystemError::ENOMEM)?; - set.ensure_undo_group_registered_prepared(group, &mut spare) - .unwrap(); - } - Ok(()) - } - - fn with_undo_record_mut( - group: &Arc, - semid: SemId, - f: impl FnOnce(&mut SemUndoRecord) -> R, - ) -> Option { - group.with_record_mut(semid, f) - } - - pub(crate) fn unregister_undo_group(&mut self, semid: SemId, group: &Arc) { - if let Ok(set) = self.get_by_semid_checked_mut(semid) { - let target = Arc::downgrade(group); - set.undo_groups - .retain(|entry| !entry.group().ptr_eq(&target)); - } - } - - /// Reclaim a target registry, never allocating or freeing its buffer under - /// the namespace lock. Concurrent growth can make a shrink unnecessary or - /// consume the spare capacity; in either case leave the live registry intact. - pub(crate) fn shrink_undo_registry(ipcns: &Arc, semid: SemId) { - let mut spare = Vec::new(); - let mut retired = Vec::new(); - let needed = { - let mut manager = ipcns.sem.lock(); - let Ok(set) = manager.get_by_semid_checked_mut(semid) else { - return; - }; - set.shrink_undo_registry_prepared(&mut spare, &mut retired) - }; - if needed == 0 || spare.try_reserve_exact(needed).is_err() { - return; - } - // Allocation is best-effort reclamation, not part of syscall success. - let mut manager = ipcns.sem.lock(); - if let Ok(set) = manager.get_by_semid_checked_mut(semid) { - set.shrink_undo_registry_prepared(&mut spare, &mut retired); - } - } - - pub(crate) fn clear_undo_for_setval(&mut self, semid: SemId, semnum: usize) { - let Ok(set) = self.get_by_semid_checked_mut(semid) else { - return; - }; - let mut saw_stale = false; - for weak in set.undo_groups.iter().map(SemUndoAssociation::group) { - let Some(group) = weak.upgrade() else { - saw_stale = true; - continue; - }; - Self::with_undo_record_mut(&group, semid, |record| { - if semnum < record.adjustment_count() { - record.clear_adjustment(semnum); - } - }); - } - if saw_stale { - set.compact_undo_registry(); - } - } - - pub(crate) fn clear_undo_for_setall(&mut self, semid: SemId) { - let Ok(set) = self.get_by_semid_checked_mut(semid) else { - return; - }; - let mut saw_stale = false; - for weak in set.undo_groups.iter().map(SemUndoAssociation::group) { - let Some(group) = weak.upgrade() else { - saw_stale = true; - continue; - }; - Self::with_undo_record_mut(&group, semid, |record| record.clear_all_adjustments()); - } - if saw_stale { - set.compact_undo_registry(); - } - } - - #[allow(dead_code)] - pub(crate) fn prune_and_apply_setval_undo(&mut self, semid: SemId, semnum: usize) { - self.clear_undo_for_setval(semid, semnum); - } - - #[allow(dead_code)] - pub(crate) fn prune_and_apply_setall_undo(&mut self, semid: SemId) { - self.clear_undo_for_setall(semid); - } - - #[cfg(test)] - fn update_queue_for_test(&mut self, semid: SemId) { - let Ok(set) = self.get_by_semid_checked_mut(semid) else { - return; - }; - Self::update_queue(set, semid, &mut SemWakeBatch::default()); - } - - #[cfg(test)] - fn live_undo_group_count_for_test(&self) -> usize { - let mut groups: Vec> = Vec::new(); - for weak in self - .id2sem - .values() - .flat_map(|set| &set.undo_groups) - .map(SemUndoAssociation::group) - { - if weak.strong_count() != 0 && !groups.iter().any(|old| old.ptr_eq(weak)) { - groups.push(weak.clone()); - } - } - groups.len() - } - - #[cfg(test)] - fn undo_registry_contains_for_test(&self, group: &Arc) -> bool { - self.id2sem - .values() - .flat_map(|set| &set.undo_groups) - .map(SemUndoAssociation::group) - .any(|weak| weak.ptr_eq(&Arc::downgrade(group))) - } - - #[cfg(test)] - fn namespace_lifecycle_invariant_for_test(&self) -> bool { - self.id2sem - .values() - .flat_map(|set| &set.undo_groups) - .map(SemUndoAssociation::group) - .all(|weak| { - weak.upgrade() - .is_none_or(|group| group.record_count_for_test() == 0) - }) - } - - #[cfg(test)] - pub(crate) fn prepare_undo_record_and_registry_for_test( - &mut self, - group: &Arc, - semid: SemId, - ) -> Result<(), SystemError> { - let nsems = self.validate_semid_nsems(semid)?; - let record = group.prepare_record(semid, nsems)?; - self.ensure_undo_group_registered(group, semid)?; - group.commit_prepared_record_noalloc(record) - } - - fn current_max_index(&self) -> usize { - self.id_allocator.max_used_index().unwrap_or(0) - } - - /// Create or look up a set. Storage preparation and disposal must happen - /// outside the namespace spinlock, including when another creator wins. - pub fn semget( - ipcns: &Arc, - key: SemKey, - nsems: usize, - semflg: SemFlags, - ) -> Result { - let mut sems = Vec::new(); - let mut id_spare = HashMap::new(); - let mut key_spare = HashMap::new(); - loop { - let mut manager = ipcns.sem.lock(); - if let Some(id) = manager.lookup_semget(key, nsems, semflg)? { - return Ok(id); - } - if sems.is_empty() { - drop(manager); - sems = KernelSemSet::try_allocate_sems(nsems)?; - continue; - } - if let Err((id_capacity, key_capacity)) = - manager.install_create_tables(key, &mut id_spare, &mut key_spare) - { - drop(manager); - id_spare - .try_reserve(id_capacity) - .map_err(|_| SystemError::ENOMEM)?; - key_spare - .try_reserve(key_capacity) - .map_err(|_| SystemError::ENOMEM)?; - continue; - } - return manager.create_prepared(key, semflg, &mut sems); - } - } - - /// None means creation is currently permitted, not a reservation. Call - /// again after unlocked preparation to recheck key races and quotas. - fn lookup_semget( - &self, - key: SemKey, - nsems: usize, - semflg: SemFlags, - ) -> Result, SystemError> { - if nsems > SEMMSL { - return Err(SystemError::EINVAL); - } - - if key == IPC_PRIVATE { - self.validate_create(nsems)?; - return Ok(None); - } - - if let Some(&id) = self.key2id.get(&key) { - if semflg.contains(SemFlags::IPC_CREAT | SemFlags::IPC_EXCL) { - return Err(SystemError::EEXIST); - } - let set = self.get_by_semid_checked(id)?; - if nsems > set.sems.len() { - return Err(SystemError::EINVAL); - } - let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - ipc_perm::ipc_permission( - &set.kern_ipc_perm, - semflg.bits() & SemFlags::PERM_MASK.bits(), - &target_user_ns, - )?; - return Ok(Some(id.data())); - } - - if !semflg.contains(SemFlags::IPC_CREAT) { - return Err(SystemError::ENOENT); - } - self.validate_create(nsems)?; - Ok(None) - } - - fn validate_create(&self, nsems: usize) -> Result { - if nsems == 0 { - return Err(SystemError::EINVAL); - } - if self.id2sem.len() >= SEMMNI { - return Err(SystemError::ENOSPC); - } - let total_after = self - .total_sems - .checked_add(nsems) - .ok_or(SystemError::ENOSPC)?; - if total_after > SEMMNS { - return Err(SystemError::ENOSPC); - } - Ok(total_after) - } - - /// Install preallocated tables only when BOTH have enough room. Rehashing - /// still takes O(n) under the lock, but never allocates. The emptied old - /// tables remain in the caller's spares for disposal after unlocking. - fn install_create_tables( - &mut self, - key: SemKey, - id_spare: &mut HashMap, - key_spare: &mut HashMap, - ) -> Result<(), (usize, usize)> { - debug_assert!(id_spare.is_empty() && key_spare.is_empty()); - let grow_ids = self.id2sem.capacity() == self.id2sem.len(); - let grow_keys = key != IPC_PRIVATE && self.key2id.capacity() == self.key2id.len(); - let needed_ids = if grow_ids && id_spare.capacity() <= self.id2sem.len() { - self.id2sem.len().saturating_mul(2).max(4) - } else { - 0 - }; - let needed_keys = if grow_keys && key_spare.capacity() <= self.key2id.len() { - self.key2id.len().saturating_mul(2).max(4) - } else { - 0 - }; - if needed_ids != 0 || needed_keys != 0 { - return Err((needed_ids, needed_keys)); - } - if grow_ids { - core::mem::swap(&mut self.id2sem, id_spare); - for (id, set) in id_spare.drain() { - self.id2sem.insert(id, set); - } - } - if grow_keys { - core::mem::swap(&mut self.key2id, key_spare); - for (key, id) in key_spare.drain() { - self.key2id.insert(key, id); - } - } - Ok(()) - } - - fn create_prepared( - &mut self, - key: SemKey, - semflg: SemFlags, - sems: &mut Vec, - ) -> Result { - let total_after = self.validate_create(sems.len())?; - debug_assert!(self.id2sem.capacity() > self.id2sem.len()); - debug_assert!(key == IPC_PRIVATE || self.key2id.capacity() > self.key2id.len()); - let ipc_id = self.id_allocator.alloc()?; - let sem_id = SemId::new(ipc_id.raw); - let current_cred = ProcessManager::current_pcb().cred(); - let kern_ipc_perm = IpcPerm::new_with_cred( - sem_id.data(), - key.data(), - current_cred, - semflg.bits() & SemFlags::PERM_MASK.bits(), - ipc_id.seq, - ); - let set = KernelSemSet::new(kern_ipc_perm, core::mem::take(sems)); - - if key != IPC_PRIVATE { - self.key2id.insert(key, sem_id); - } - self.id2sem.insert(ipc_id.idx, set); - self.total_sems = total_after; - - Ok(sem_id.data()) - } - - #[cfg(test)] - pub(crate) fn semget_for_test( - &mut self, - key: SemKey, - nsems: usize, - flags: SemFlags, - ) -> Result { - if let Some(id) = self.lookup_semget(key, nsems, flags)? { - return Ok(id); - } - let mut sems = KernelSemSet::try_allocate_sems(nsems)?; - let mut ids = HashMap::new(); - let mut keys = HashMap::new(); - if let Err((id_capacity, key_capacity)) = - self.install_create_tables(key, &mut ids, &mut keys) - { - ids.try_reserve(id_capacity) - .map_err(|_| SystemError::ENOMEM)?; - keys.try_reserve(key_capacity) - .map_err(|_| SystemError::ENOMEM)?; - self.install_create_tables(key, &mut ids, &mut keys) - .unwrap(); - } - self.create_prepared(key, flags, &mut sems) - } - - /// Simulate sops in order without changing shared semaphore values or undo records. - fn simulate_semop( - set: &KernelSemSet, - sops: &[PosixSemBuf], - undo: Option<&mut SemUndoRecord>, - scratch: &mut SemopScratch, - ) -> Result { - scratch.clear(); - - for op in sops { - let idx = op.sem_num as usize; - if idx >= set.sems.len() { - return Err(SystemError::EFBIG); - } - - let has_undo = (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() != 0; - let entry = scratch.entry_for(set, idx, undo.as_deref())?; - let current = entry.virtual_val; - if op.sem_op == 0 { - if current != 0 { - return Ok(SemopOutcome::Blocked(SemBlockedOp { - semnum: idx, - wait_type: SemWaitType::Zero, - nowait: (op.sem_flg as u32) & SemFlags::IPC_NOWAIT.bits() != 0, - })); - } - continue; - } - - let result = current as i64 + op.sem_op as i64; - if result > SEMVMX as i64 { - return Err(SystemError::ERANGE); - } - if result < 0 { - return Ok(SemopOutcome::Blocked(SemBlockedOp { - semnum: idx, - wait_type: SemWaitType::Increase, - nowait: (op.sem_flg as u32) & SemFlags::IPC_NOWAIT.bits() != 0, - })); - } - - if has_undo { - let next_adj = entry.virtual_adj as i32 - op.sem_op as i32; - if !(i16::MIN as i32..=i16::MAX as i32).contains(&next_adj) { - return Err(SystemError::ERANGE); - } - entry.virtual_adj = next_adj as i16; - } - entry.virtual_val = result as i32; - } - - Ok(SemopOutcome::Ready(SemopSimulation { - entry_count: scratch.entries.len(), - })) - } - - /// Commit a successful simulation while the manager lock is held. - fn commit_semop( - set: &mut KernelSemSet, - simulation: SemopSimulation, - scratch: &SemopScratch, - pid: Option>, - mut undo: Option<&mut SemUndoRecord>, - ) -> bool { - let mut values_changed = false; - for entry in scratch.entries.iter().take(simulation.entry_count) { - let sem = &mut set.sems[entry.semnum]; - values_changed |= entry.initial_val != entry.virtual_val; - sem.val = entry.virtual_val; - sem.pid = pid.clone(); - if entry.virtual_adj != entry.initial_adj { - if let Some(record) = undo.as_deref_mut() { - record.set_adjustment(entry.semnum, entry.virtual_adj); - } - } - } - set.sem_otime = PosixTimeSpec::now().tv_sec; - values_changed - } - - /// Scan one pending queue without head-of-line blocking. - fn scan_pending_queue( - set: &mut KernelSemSet, - queue: SemPendingQueue, - wakes: &mut SemWakeBatch, - ) -> bool { - // Iterator captures the successor before the current entry is unlinked. - for entry in set.pending_iter(queue) { - if let Some(group) = entry.undo_group.as_ref() { - let result = { - let mut record_slot = entry.undo_record.lock_irqsave(); - let Some(record) = record_slot.take() else { - set.remove_pending(queue, &entry); - wakes.complete(entry.clone(), Err(SystemError::EINVAL)); - continue; - }; - match group.with_prepared_record_noalloc(record, |record| { - match Self::retry_queued_undo_entry(set, &entry, record) { - Ok(Some(changed)) => { - PreparedSemUndoRecordAction::Publish(Ok(Some(changed))) - } - Ok(None) => PreparedSemUndoRecordAction::Keep(Ok(None)), - Err(error) => PreparedSemUndoRecordAction::Keep(Err(error)), - } - }) { - Ok((result, kept_record)) => { - *record_slot = kept_record; - result - } - Err(error) => Err(error), - } - }; - - match result { - Ok(Some(changed)) => { - set.remove_pending(queue, &entry); - wakes.complete(entry.clone(), Ok(0)); - if changed { - return true; - } - } - Ok(None) => {} - Err(error) => { - set.remove_pending(queue, &entry); - wakes.complete(entry.clone(), Err(error)); - } - } - continue; - } - - let mut scratch = entry.scratch.lock(); - match Self::simulate_semop(set, &entry.sops, None, &mut scratch) { - Ok(SemopOutcome::Ready(simulation)) => { - let changed = - Self::commit_semop(set, simulation, &scratch, entry.pid.clone(), None); - set.remove_pending(queue, &entry); - wakes.complete(entry.clone(), Ok(0)); - if changed { - return true; - } - } - Ok(SemopOutcome::Blocked(blocker)) if blocker.nowait => { - set.remove_pending(queue, &entry); - wakes.complete(entry.clone(), Err(SystemError::EAGAIN_OR_EWOULDBLOCK)); - } - Ok(SemopOutcome::Blocked(blocker)) => { - set.update_blocker(&entry, blocker); - } - Err(error) => { - set.remove_pending(queue, &entry); - wakes.complete(entry.clone(), Err(error)); - } - } - } - false - } - - /// Complete executable const entries before altering entries. - fn update_queue(set: &mut KernelSemSet, _semid: SemId, wakes: &mut SemWakeBatch) { - loop { - let const_changed = Self::scan_pending_queue(set, SemPendingQueue::Const, wakes); - debug_assert!(!const_changed); - if !Self::scan_pending_queue(set, SemPendingQueue::Alter, wakes) { - return; - } - } - } - - fn retry_queued_undo_entry( - set: &mut KernelSemSet, - entry: &Arc, - record: &mut SemUndoRecord, - ) -> Result, SystemError> { - if record.adjustment_count() != set.sems.len() { - return Err(SystemError::EINVAL); - } - let mut scratch = entry.scratch.lock(); - match Self::simulate_semop(set, &entry.sops, Some(record), &mut scratch) { - Ok(SemopOutcome::Ready(simulation)) => Ok(Some(Self::commit_semop( - set, - simulation, - &scratch, - entry.pid.clone(), - Some(record), - ))), - Ok(SemopOutcome::Blocked(blocker)) => { - if blocker.nowait { - Err(SystemError::EAGAIN_OR_EWOULDBLOCK) - } else { - set.update_blocker(entry, blocker); - Ok(None) - } - } - Err(error) => Err(error), - } - } - - pub(crate) fn replay_sem_undo_adjustments( - &mut self, - semid: SemId, - adjustments: &[i16], - exiting_tgid: Option>, - wakes: &mut SemWakeBatch, - ) { - let Ok(set) = self.get_by_semid_checked_mut(semid) else { - return; - }; - - for (sem, adjustment) in set.sems.iter_mut().zip(adjustments.iter().copied()) { - if adjustment == 0 { - continue; - } - - let next = (sem.val as i64 + adjustment as i64).clamp(0, SEMVMX as i64) as i32; - sem.val = next; - sem.pid = exiting_tgid.clone(); - } - - set.sem_otime = PosixTimeSpec::now().tv_sec; - Self::update_queue(set, semid, wakes); - } - - fn cancel_queued_entry( - &mut self, - semid: SemId, - entry: &Arc, - error: SystemError, - ) -> Result { - if let Some(result) = entry.completed_result() { - return result; - } - - if let Ok(set) = self.get_by_semid_checked_mut(semid) { - if let Some(result) = entry.completed_result() { - return result; - } - set.remove_waiter(entry); - if entry.complete(Err(error.clone())) { - return Err(error); - } - return entry - .completed_result() - .expect("completed semaphore queue entry lost its terminal result"); - } - - if let Some(result) = entry.completed_result() { - return result; - } - if entry.complete(Err(SystemError::EIDRM)) { - return Err(SystemError::EIDRM); - } - entry - .completed_result() - .expect("completed semaphore queue entry lost its terminal result") - } - - /// # semtimedop: execute `sops` atomically, blocking if necessary - /// - /// This function manages the lock internally (it must release it while waiting); - /// callers must not hold the `ipcns.sem` lock in advance. - /// - /// - `timeout == None`: wait indefinitely (equivalent to `semop`) - /// - `timeout == Some(Duration::ZERO)`: do not block - /// - Otherwise: block until timeout and return EAGAIN - pub fn semtimedop( - ipcns: &Arc, - semid: SemId, - sops: &[PosixSemBuf], - timeout: Option, - ) -> Result { - if sops.is_empty() { - return Err(SystemError::EINVAL); - } - if sops.len() > SEMOPM { - return Err(SystemError::E2BIG); - } - - let non_blocking = timeout == Some(Duration::ZERO); - let has_undo = sops - .iter() - .any(|op| (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() != 0); - // Check read permission only for all-zero waits; otherwise check write permission - // to match Linux semantics. - let alter = sops.iter().any(|op| op.sem_op != 0); - - let target_user_ns = ipcns.user_ns.clone(); - { - let guard = ipcns.sem.lock(); - let set = guard.get_by_semid_checked(semid)?; - // Match Linux: check semnum bounds (EFBIG) before permissions (EACCES). - if sops.iter().any(|op| op.sem_num as usize >= set.sems.len()) { - return Err(SystemError::EFBIG); - } - ipc_perm::ipc_permission( - &set.kern_ipc_perm, - if alter { - Self::IPC_WRITE - } else { - Self::IPC_READ - }, - &target_user_ns, - )?; - } - - let deadline_ticks = timeout.map(|d| next_n_us_timer_jiffies(d.total_micros())); - let (waiter, waker) = Waiter::new_pair(); - let timer = - deadline_ticks.map(|deadline| Timer::new(TimeoutWaker::new(waker.clone()), deadline)); - - let current = ProcessManager::current_pcb(); - let pid = current.task_pid_ptr(PidType::TGID); - let undo_group = if has_undo { - Some(current.ensure_sem_undo_group(ipcns)?) - } else { - None - }; - let mut immediate_scratch = SemopScratch::try_new(sops.len())?; - let plain_prepared_entry = if has_undo { - None - } else { - Some( - Arc::try_new(SemQueueEntry::new_prepared( - SemQueueEntry::prepare_sops(sops)?, - pid.clone(), - None, - None, - waker.clone(), - SemopScratch::try_new(sops.len())?, - SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Zero, - nowait: false, - }, - )) - .map_err(|_| SystemError::ENOMEM)?, - ) - }; - - let mut wakes = SemWakeBatch::default(); - let mut registry_spare = Vec::new(); - let mut registry_capacity_needed = 0; - let entry = loop { - // Revalidate after unlocked undo-registry preparation. - if registry_capacity_needed != 0 { - registry_spare - .try_reserve(registry_capacity_needed) - .map_err(|_| SystemError::ENOMEM)?; - registry_capacity_needed = 0; - } - let nsems = { - let guard = ipcns.sem.lock(); - let set = guard.get_by_semid_checked(semid)?; - // Match Linux: check semnum bounds (EFBIG) before permissions (EACCES). - if sops.iter().any(|op| op.sem_num as usize >= set.sems.len()) { - return Err(SystemError::EFBIG); - } - ipc_perm::ipc_permission( - &set.kern_ipc_perm, - if alter { - Self::IPC_WRITE - } else { - Self::IPC_READ - }, - &target_user_ns, - )?; - set.sems.len() - }; - - let prepared_undo = if let Some(group) = undo_group.as_ref() { - let record = group.prepare_record(semid, nsems)?; - let entry = Arc::try_new(SemQueueEntry::new_prepared( - SemQueueEntry::prepare_sops(sops)?, - pid.clone(), - Some(group.clone()), - Some(record), - waker.clone(), - SemopScratch::try_new(sops.len())?, - SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Zero, - nowait: false, - }, - )) - .map_err(|_| SystemError::ENOMEM)?; - Some(entry) - } else { - None - }; - - let mut guard = ipcns.sem.lock(); - if guard.validate_semid_nsems(semid)? != nsems { - continue; - } - if let Some(prepared_entry) = prepared_undo { - let mut record_slot = prepared_entry.undo_record.lock_irqsave(); - let prepared_record = record_slot - .take() - .expect("prepared SEM_UNDO entry owns its record"); - if prepared_record.adjustment_count() != nsems { - return Err(SystemError::EINVAL); - } - // First-use candidates must be associated before zero-record - // publication. Queued operations retain Existing tokens only. - // Full semid was rechecked above, so Existing cannot refer to a - // destroyed/reused set. SETVAL/SETALL retain live associations. - let already_associated = prepared_record.was_existing(); - if !already_associated { - let set = guard.get_by_semid_checked_mut(semid)?; - if let Err(capacity) = set.ensure_undo_group_registered_prepared( - undo_group - .as_ref() - .expect("SEM_UNDO operation has a current group"), - &mut registry_spare, - ) { - registry_capacity_needed = capacity; - continue; - } - } - let set = guard.get_by_semid_checked_mut(semid)?; - let (outcome, kept_record) = undo_group - .as_ref() - .expect("SEM_UNDO operation has a current group") - .with_prepared_record_noalloc(prepared_record, |record| { - let outcome = - Self::simulate_semop(set, sops, Some(record), &mut immediate_scratch); - match outcome { - Ok(SemopOutcome::Ready(simulation)) => { - Self::commit_semop( - set, - simulation, - &immediate_scratch, - pid.clone(), - Some(record), - ); - PreparedSemUndoRecordAction::Publish(Ok(None)) - } - Ok(SemopOutcome::Blocked(blocker)) => { - PreparedSemUndoRecordAction::Keep(Ok(Some(blocker))) - } - Err(error) => PreparedSemUndoRecordAction::Keep(Err(error)), - } - })?; - *record_slot = kept_record; - match outcome? { - None => { - drop(record_slot); - Self::update_queue(set, semid, &mut wakes); - return Ok(0); - } - Some(blocker) => { - if blocker.nowait || non_blocking { - return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); - } - if deadline_ticks.is_some_and(|deadline| clock() >= deadline) { - return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); - } - drop(record_slot); - set.update_blocker(&prepared_entry, blocker); - set.enqueue_waiter(prepared_entry.clone()); - break prepared_entry; - } - } - } else { - let set = guard.get_by_semid_checked_mut(semid)?; - match Self::simulate_semop(set, sops, None, &mut immediate_scratch)? { - SemopOutcome::Ready(simulation) => { - Self::commit_semop(set, simulation, &immediate_scratch, pid, None); - Self::update_queue(set, semid, &mut wakes); - return Ok(0); - } - SemopOutcome::Blocked(blocker) => { - if blocker.nowait || non_blocking { - return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); - } - if deadline_ticks.is_some_and(|deadline| clock() >= deadline) { - return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); - } - let prepared_entry = plain_prepared_entry - .as_ref() - .expect("plain queued semop entry is preallocated"); - set.update_blocker(prepared_entry, blocker); - set.enqueue_waiter(prepared_entry.clone()); - break prepared_entry.clone(); - } - } - } - }; - - drop(registry_spare); - wakes.wake_all(); - if let Some(timer) = timer.as_ref() { - timer.activate(); - } - let _wait_result = waiter.wait(true); - let completed = entry.completed_result(); - let was_timeout = timer.as_ref().is_some_and(|timer| timer.timeout()); - if !was_timeout { - if let Some(timer) = timer.as_ref() { - timer.cancel(); - } - } - if let Some(result) = completed { - return result; - } - - let error = if was_timeout { - SystemError::EAGAIN_OR_EWOULDBLOCK - } else { - SystemError::EINTR - }; - let mut guard = ipcns.sem.lock(); - guard.cancel_queued_entry(semid, &entry, error) - } - - /// # IPC_RMID: remove the semaphore set and wake all waiters with EIDRM - /// The caller must release the manager guard before notifying `wakes` and - /// dropping the returned set, which owns all deferred undo/group disposal. - pub(crate) fn ipc_rmid( - &mut self, - id: SemId, - wakes: &mut SemWakeBatch, - ) -> Result { - let decoded = IpcIdAllocator::decode(id.data())?; - let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - let key = { - let set = self.get_by_semid_checked(id)?; - ipc_perm::check_control_permission(&set.kern_ipc_perm, &target_user_ns)?; - set.kern_ipc_perm.key - }; - let mut set = self - .id2sem - .remove(&decoded.idx) - .ok_or(SystemError::EINVAL)?; - self.key2id.remove(&SemKey::new(key)); - self.total_sems = self.total_sems.saturating_sub(set.sems.len()); - // Reuse existing association storage: neither allocation nor undo/group - // destruction is allowed here. Even an empty upgraded group stays alive - // until the caller drops this removed set outside the manager lock. - for association in &mut set.undo_groups { - if let Some(group) = association.group().upgrade() { - let record = group.take_record(id); - *association = SemUndoAssociation::Retired { - _group: group, - _record: record, - }; - } - } - set.complete_all_removed(wakes); - self.id_allocator.free_idx(decoded.idx); - Ok(set) - } - - /// # IPC_SET: update permissions (uid/gid/mode) and refresh `sem_ctime` - pub fn ipc_set(&mut self, id: SemId, semid_ds: PosixSemIdDs) -> Result<(), SystemError> { - let set = self.get_by_semid_checked_mut(id)?; - let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - ipc_perm::check_control_permission(&set.kern_ipc_perm, &target_user_ns)?; - let current_user_ns = ProcessManager::current_user_ns(); - set.kern_ipc_perm.copy_from_posix( - semid_ds.sem_perm.uid(), - semid_ds.sem_perm.gid(), - semid_ds.sem_perm.mode(), - ¤t_user_ns, - )?; - set.sem_ctime = PosixTimeSpec::now().tv_sec; - Ok(()) - } - - /// IPC_STAT/SEM_STAT/SEM_STAT_ANY: return `semid_ds` - pub fn sem_stat_data( - &self, - id_or_index: SemId, - cmd: SemCtlCmd, - ) -> Result<(usize, PosixSemIdDs), SystemError> { - let set = match cmd { - SemCtlCmd::IpcStat => self.get_by_semid_checked(id_or_index)?, - SemCtlCmd::SemStat | SemCtlCmd::SemStatAny => self.get_by_index(id_or_index.data())?, - _ => return Err(SystemError::EINVAL), - }; - if cmd != SemCtlCmd::SemStatAny { - let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_READ, &target_user_ns)?; - } - let current_user_ns = ProcessManager::current_user_ns(); - let sem_perm = set.kern_ipc_perm.to_posix(¤t_user_ns)?; - let semid_ds = PosixSemIdDs { - sem_perm, - sem_otime: set.sem_otime, - _sem_otime_high: 0, - sem_ctime: set.sem_ctime, - _sem_ctime_high: 0, - sem_nsems: set.sems.len(), - _unused1: 0, - _unused2: 0, - }; - let ret = if cmd == SemCtlCmd::IpcStat { - 0 - } else { - set.kern_ipc_perm.id - }; - Ok((ret, semid_ds)) - } - - /// IPC_INFO/SEM_INFO: return system information - pub fn sem_info_data(&self, cmd: SemCtlCmd) -> (usize, PosixSemInfo) { - ( - self.current_max_index(), - PosixSemInfo::new(cmd, self.id2sem.len(), self.total_sems), - ) - } - - /// GETVAL/GETPID/GETNCNT/GETZCNT: query a single semaphore - pub fn sem_get_value( - &self, - id: SemId, - semnum: usize, - cmd: SemCtlCmd, - ) -> Result { - let set = self.get_by_semid_checked(id)?; - let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_READ, &target_user_ns)?; - if semnum >= set.sems.len() { - return Err(SystemError::EINVAL); - } - match cmd { - SemCtlCmd::GetVal => Ok(set.sems[semnum].val as usize), - SemCtlCmd::GetPid => Ok(set.sems[semnum] - .pid - .as_ref() - .map(|pid| pid.pid_vnr().data()) - .unwrap_or(0)), - SemCtlCmd::GetNcnt => Ok(set.ncnt(semnum)), - SemCtlCmd::GetZcnt => Ok(set.zcnt(semnum)), - _ => Err(SystemError::EINVAL), - } - } - - /// # SETVAL: set a single semaphore value - pub(crate) fn setval( - &mut self, - id: SemId, - semnum: usize, - val: i32, - wakes: &mut SemWakeBatch, - ) -> Result<(), SystemError> { - // Match Linux: validate the value (ERANGE), then semnum (EINVAL), then permissions - // (EACCES). - if !(0..=SEMVMX).contains(&val) { - return Err(SystemError::ERANGE); - } - let nsems = { - let set = self.get_by_semid_checked(id)?; - if semnum >= set.sems.len() { - return Err(SystemError::EINVAL); - } - let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_WRITE, &target_user_ns)?; - set.sems.len() - }; - debug_assert!(semnum < nsems); - - self.clear_undo_for_setval(id, semnum); - let set = self.get_by_semid_checked_mut(id)?; - let sem = &mut set.sems[semnum]; - sem.val = val; - sem.pid = ProcessManager::current_pcb().task_pid_ptr(PidType::TGID); - set.sem_ctime = PosixTimeSpec::now().tv_sec; - Self::update_queue(set, id, wakes); - Ok(()) - } - - /// # SETALL: set values of all semaphores in the set without changes on validation failure - pub(crate) fn setall( - &mut self, - token: SemSetAllToken, - vals: &[u16], - wakes: &mut SemWakeBatch, - ) -> Result<(), SystemError> { - let set_nsems = self - .get_by_semid_checked(token.id) - .map_err(|_| SystemError::EIDRM)? - .sems - .len(); - if vals.len() != token.nsems || vals.len() != set_nsems { - return Err(SystemError::EINVAL); - } - if vals.iter().any(|&v| v as i32 > SEMVMX) { - return Err(SystemError::ERANGE); - } - - self.clear_undo_for_setall(token.id); - let set = self - .get_by_semid_checked_mut(token.id) - .map_err(|_| SystemError::EIDRM)?; - let pid = ProcessManager::current_pcb().task_pid_ptr(PidType::TGID); - for (i, &v) in vals.iter().enumerate() { - let sem = &mut set.sems[i]; - sem.val = v as i32; - sem.pid = pid.clone(); - } - set.sem_ctime = PosixTimeSpec::now().tv_sec; - Self::update_queue(set, token.id, wakes); - Ok(()) - } - - /// # GETALL: get values of all semaphores in the set - pub fn getall(&self, id: SemId) -> Result, SystemError> { - let set = self.get_by_semid_checked(id)?; - let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_READ, &target_user_ns)?; - let mut vals = Vec::new(); - vals.try_reserve_exact(set.sems.len()) - .map_err(|_| SystemError::ENOMEM)?; - vals.extend(set.sems.iter().map(|s| s.val as u16)); - Ok(vals) - } - - /// Validate SETALL before the caller accesses the userspace array. - pub fn prepare_setall(&self, id: SemId) -> Result { - let set = self.get_by_semid_checked(id)?; - let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); - ipc_perm::ipc_permission(&set.kern_ipc_perm, Self::IPC_WRITE, &target_user_ns)?; - Ok(SemSetAllToken::new(id, set.sems.len())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - ipc::sem_undo::detach_sem_undo, - process::{ - cred::{Kgid, Kuid}, - fork::CloneFlags, - namespace::ipc_namespace::INIT_IPC_NAMESPACE, - namespace::pid_namespace::INIT_PID_NAMESPACE, - KernelStack, ProcessControlBlock, RawPid, - }, - }; - - fn test_perm(id: SemId, key: SemKey, seq: usize) -> IpcPerm { - IpcPerm { - id: id.data(), - key: key.data(), - uid: Kuid::new(0), - gid: Kgid::new(0), - cuid: Kuid::new(0), - cgid: Kgid::new(0), - mode: 0o600, - seq, - } - } - - fn insert_test_set(manager: &mut SemManager, key: SemKey, vals: &[i32]) -> SemId { - let ipc_id = manager.id_allocator.alloc().unwrap(); - let id = SemId::new(ipc_id.raw); - let sems = KernelSemSet::try_allocate_sems(vals.len()).unwrap(); - let mut set = KernelSemSet::new(test_perm(id, key, ipc_id.seq), sems); - for (sem, val) in set.sems.iter_mut().zip(vals.iter().copied()) { - sem.val = val; - } - manager.key2id.insert(key, id); - manager.id2sem.insert(ipc_id.idx, set); - manager.total_sems += vals.len(); - id - } - - fn remove_test_set(manager: &mut SemManager, id: SemId) { - let decoded = IpcIdAllocator::decode(id.data()).unwrap(); - let set = manager.id2sem.remove(&decoded.idx).unwrap(); - manager.key2id.remove(&SemKey::new(set.kern_ipc_perm.key)); - manager.id_allocator.free_idx(decoded.idx); - manager.total_sems = manager.total_sems.saturating_sub(set.sems.len()); - } - - fn sem_values(manager: &SemManager, id: SemId) -> Vec { - manager - .get_by_semid_checked(id) - .unwrap() - .sems - .iter() - .map(|sem| sem.val) - .collect() - } - - fn test_ipc_ns() -> Arc { - INIT_IPC_NAMESPACE.copy_ipc_ns( - &CloneFlags::CLONE_NEWIPC, - INIT_IPC_NAMESPACE.user_ns.clone(), - ) - } - - fn test_pcb_with_group( - ipc_ns: &Arc, - ) -> (Arc, Arc) { - let pcb = ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()); - let group = pcb.ensure_sem_undo_group(ipc_ns).unwrap(); - (pcb, group) - } - - fn enqueue_test_waiter( - set: &mut KernelSemSet, - sops: &[PosixSemBuf], - blocker: SemBlockedOp, - ) -> Arc { - let (_waiter, waker) = Waiter::new_pair(); - let entry = Arc::new(SemQueueEntry::new(sops, None, waker, blocker)); - set.enqueue_waiter(entry.clone()); - entry - } - - #[test] - fn pending_links_remove_middle_head_tail_and_preserve_fifo() { - let mut manager = SemManager::new(); - let id = insert_test_set(&mut manager, SemKey::new(153), &[0]); - let set = manager.get_by_semid_checked_mut(id).unwrap(); - let ops = [PosixSemBuf { - sem_num: 0, - sem_op: -1, - sem_flg: 0, - }]; - let mut entries = Vec::new(); - for _ in 0..4 { - entries.push(enqueue_test_waiter( - set, - &ops, - SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Increase, - nowait: false, - }, - )); - } - set.remove_waiter(&entries[1]); - set.remove_waiter(&entries[1]); // An already detached node is harmless. - let remaining: Vec<_> = set.pending_alter.iter().collect(); - assert_eq!(remaining.len(), 3); - for (actual, expected) in remaining.iter().zip([0, 2, 3]) { - assert!(Arc::ptr_eq(actual, &entries[expected])); - } - set.remove_waiter(&entries[0]); - set.remove_waiter(&entries[3]); - assert!(Arc::ptr_eq( - set.pending_alter.head.as_ref().unwrap(), - &entries[2] - )); - assert!(Arc::ptr_eq( - set.pending_alter.tail.as_ref().unwrap(), - &entries[2] - )); - set.remove_waiter(&entries[2]); - assert!(set.pending_is_empty()); - for entry in entries { - assert!(entry.complete(Err(SystemError::EINTR))); - } - } - - #[test] - fn wait_counts_follow_only_linked_current_blockers() { - let mut manager = SemManager::new(); - let id = insert_test_set(&mut manager, SemKey::new(156), &[0, 1]); - let set = manager.get_by_semid_checked_mut(id).unwrap(); - let (_waiter, waker) = Waiter::new_pair(); - let increase = SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Increase, - nowait: false, - }; - let zero = SemBlockedOp { - semnum: 1, - wait_type: SemWaitType::Zero, - nowait: false, - }; - let entry = Arc::new(SemQueueEntry::new( - &[plain_sop(0, -1), plain_sop(1, 0)], - None, - waker, - increase, - )); - set.update_blocker(&entry, zero); - assert_eq!((set.ncnt(0), set.zcnt(1)), (0, 0)); - set.enqueue_waiter(entry.clone()); - assert_eq!((set.ncnt(0), set.zcnt(1)), (0, 1)); - set.update_blocker(&entry, increase); - assert_eq!((set.ncnt(0), set.zcnt(1)), (1, 0)); - set.update_blocker(&entry, increase); // No double-accounting. - let same_slot_zero = SemBlockedOp { semnum: 0, ..zero }; - set.update_blocker(&entry, same_slot_zero); - assert_eq!((set.ncnt(0), set.zcnt(0)), (0, 1)); - for semnum in 0..2 { - for (kind, cached) in [ - (SemWaitType::Increase, set.ncnt(semnum)), - (SemWaitType::Zero, set.zcnt(semnum)), - ] { - let scanned = set - .pending_const - .iter() - .chain(set.pending_alter.iter()) - .filter(|entry| entry.is_waiting_on(semnum, kind)) - .count(); - assert_eq!(cached, scanned); - } - } - set.remove_waiter(&entry); - set.remove_waiter(&entry); - assert_eq!((set.ncnt(0), set.zcnt(0)), (0, 0)); - set.enqueue_waiter(entry.clone()); - let mut wakes = SemWakeBatch::default(); - set.complete_all_removed(&mut wakes); - assert_eq!( - (set.ncnt(0), set.zcnt(0), set.ncnt(1), set.zcnt(1)), - (0, 0, 0, 0) - ); - assert_eq!(entry.completed_result(), Some(Err(SystemError::EIDRM))); - } - - #[test] - fn last_owner_replays_adjustment_with_clamp_and_removes_record() { - let ipc_ns = test_ipc_ns(); - let semid = { - let mut manager = ipc_ns.sem.lock(); - insert_test_set(&mut manager, SemKey::new(31), &[32766]) - }; - let (pcb, group) = test_pcb_with_group(&ipc_ns); - group.insert_test_record(semid, &[4]); - - detach_sem_undo(&pcb); - - let manager = ipc_ns.sem.lock(); - assert_eq!(sem_values(&manager, semid), vec![SEMVMX]); - assert_eq!(group.record_count_for_test(), 0); - assert!(pcb.sem_undo_group().is_none()); - } - - #[test] - fn non_last_owner_does_not_replay() { - let ipc_ns = test_ipc_ns(); - let semid = { - let mut manager = ipc_ns.sem.lock(); - insert_test_set(&mut manager, SemKey::new(32), &[10]) - }; - let (owner_one, group) = test_pcb_with_group(&ipc_ns); - let owner_two = ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()); - let mut guard = owner_one - .prepare_shared_sem_undo_attachment(&ipc_ns) - .unwrap(); - guard.install_into(&owner_two); - guard.disarm(); - group.insert_test_record(semid, &[4]); - - detach_sem_undo(&owner_one); - - assert_eq!(sem_values(&ipc_ns.sem.lock(), semid), vec![10]); - assert_eq!(group.record_count_for_test(), 1); - - detach_sem_undo(&owner_two); - - assert_eq!(sem_values(&ipc_ns.sem.lock(), semid), vec![14]); - assert_eq!(group.record_count_for_test(), 0); - } - - #[test] - fn stale_full_semid_does_not_touch_reused_index() { - let ipc_ns = test_ipc_ns(); - let old_semid = { - let mut manager = ipc_ns.sem.lock(); - manager.id_allocator = IpcIdAllocator::new(2).unwrap(); - insert_test_set(&mut manager, SemKey::new(33), &[7]) - }; - let (pcb, group) = test_pcb_with_group(&ipc_ns); - group.insert_test_record(old_semid, &[9]); - - let new_semid = { - let mut manager = ipc_ns.sem.lock(); - insert_test_set(&mut manager, SemKey::new(34), &[5]); - remove_test_set(&mut manager, old_semid); - insert_test_set(&mut manager, SemKey::new(35), &[21]) - }; - assert_ne!(old_semid, new_semid); - assert_eq!( - old_semid.data() & IpcIdAllocator::IPC_ID_IDX_MASK, - new_semid.data() & IpcIdAllocator::IPC_ID_IDX_MASK - ); - - detach_sem_undo(&pcb); - - assert_eq!(sem_values(&ipc_ns.sem.lock(), new_semid), vec![21]); - assert_eq!(group.record_count_for_test(), 0); - } - - #[test] - fn replay_updates_otime_and_rescans_waiter() { - let ipc_ns = test_ipc_ns(); - let (semid, entry) = { - let mut manager = ipc_ns.sem.lock(); - let semid = insert_test_set(&mut manager, SemKey::new(35), &[1, 2]); - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - set.sem_otime = -1; - - let (_waiter, waker) = Waiter::new_pair(); - let blocker = SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Zero, - nowait: false, - }; - let entry = Arc::new(SemQueueEntry::new( - &[PosixSemBuf { - sem_num: 0, - sem_op: 0, - sem_flg: 0, - }], - None, - waker, - blocker, - )); - set.enqueue_waiter(entry.clone()); - (semid, entry) - }; - let (pcb, group) = test_pcb_with_group(&ipc_ns); - let exiting_tgid = Pid::new_for_test(RawPid::new(4242), INIT_PID_NAMESPACE.clone()); - pcb.install_pid_identity_for_test(exiting_tgid.clone()); - group.insert_test_record(semid, &[-1, 1]); - - detach_sem_undo(&pcb); - - let mut manager = ipc_ns.sem.lock(); - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - let values = [set.sems[0].val, set.sems[1].val]; - let waiter_pid_was_applied = set.sems[0].pid.is_none(); - let replay_pid_was_applied = set.sems[1] - .pid - .as_ref() - .is_some_and(|pid| Arc::ptr_eq(pid, &exiting_tgid)); - let replay_pid_vnr = set.sems[1] - .pid - .as_ref() - .map(|pid| pid.pid_nr_ns(&INIT_PID_NAMESPACE)); - let sem_otime = set.sem_otime; - let waiters_are_empty = set.pending_is_empty(); - let completed_result = entry.completed_result(); - let record_count = group.record_count_for_test(); - - for sem in &mut set.sems { - sem.pid = None; - } - drop(manager); - pcb.clear_pid_identity_for_test(); - exiting_tgid.clear_numbers_for_test(); - - assert_eq!(values, [0, 3]); - assert!(waiter_pid_was_applied); - assert!(replay_pid_was_applied); - assert_eq!(replay_pid_vnr, Some(RawPid::new(4242))); - assert_ne!(sem_otime, -1); - assert!(waiters_are_empty); - assert_eq!(completed_result, Some(Ok(0))); - assert_eq!(record_count, 0); - } - - #[test] - fn replay_rescans_and_updates_otime_when_clamp_does_not_change_value() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(36), &[SEMVMX, SEMVMX]); - let entry = { - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - set.sem_otime = -1; - enqueue_test_waiter( - set, - &[PosixSemBuf { - sem_num: 0, - sem_op: -1, - sem_flg: 0, - }], - SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Increase, - nowait: false, - }, - ) - }; - let exiting_tgid = Pid::new_for_test(RawPid::new(4243), INIT_PID_NAMESPACE.clone()); - - manager.replay_sem_undo_adjustments( - semid, - &[1, 1], - Some(exiting_tgid.clone()), - &mut SemWakeBatch::default(), - ); - - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - let values = [set.sems[0].val, set.sems[1].val]; - let replay_pid_was_applied = set.sems[1] - .pid - .as_ref() - .is_some_and(|pid| Arc::ptr_eq(pid, &exiting_tgid)); - let sem_otime = set.sem_otime; - let waiters_are_empty = set.pending_is_empty(); - let completed_result = entry.completed_result(); - for sem in &mut set.sems { - sem.pid = None; - } - exiting_tgid.clear_numbers_for_test(); - - assert_eq!(values, [SEMVMX - 1, SEMVMX]); - assert!(replay_pid_was_applied); - assert_ne!(sem_otime, -1); - assert!(waiters_are_empty); - assert_eq!(completed_result, Some(Ok(0))); - } - - #[test] - fn valid_all_zero_record_still_updates_otime_and_rescans_queue() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(37), &[0, 5]); - let entry = { - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - set.sem_otime = -1; - enqueue_test_waiter( - set, - &[PosixSemBuf { - sem_num: 0, - sem_op: 0, - sem_flg: 0, - }], - SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Zero, - nowait: false, - }, - ) - }; - let exiting_tgid = Pid::new_for_test(RawPid::new(4244), INIT_PID_NAMESPACE.clone()); - - manager.replay_sem_undo_adjustments( - semid, - &[0, 0], - Some(exiting_tgid.clone()), - &mut SemWakeBatch::default(), - ); - - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - let values = [set.sems[0].val, set.sems[1].val]; - let untouched_pid = set.sems[1].pid.is_none(); - let sem_otime = set.sem_otime; - let waiters_are_empty = set.pending_is_empty(); - let completed_result = entry.completed_result(); - for sem in &mut set.sems { - sem.pid = None; - } - exiting_tgid.clear_numbers_for_test(); - - assert_eq!(values, [0, 5]); - assert!(untouched_pid); - assert_ne!(sem_otime, -1); - assert!(waiters_are_empty); - assert_eq!(completed_result, Some(Ok(0))); - } - - #[test] - fn new_manager_starts_with_empty_undo_registry() { - assert!(SemManager::new().id2sem.is_empty()); - } - - #[test] - fn create_tables_require_both_spares_and_recheck_growth() { - let mut manager = SemManager::new(); - let key = SemKey::new(153); - let mut ids = HashMap::new(); - let mut keys = HashMap::new(); - ids.try_reserve(4).unwrap(); - assert_eq!( - manager.install_create_tables(key, &mut ids, &mut keys), - Err((0, 4)) - ); - assert_eq!(manager.id2sem.capacity(), 0); - assert_eq!(manager.key2id.capacity(), 0); - keys.try_reserve(4).unwrap(); - manager - .install_create_tables(key, &mut ids, &mut keys) - .unwrap(); - while manager.id2sem.len() < manager.id2sem.capacity() { - let next_key = SemKey::new(200 + manager.id2sem.len()); - insert_test_set(&mut manager, next_key, &[3]); - } - let count = manager.id2sem.len(); - assert_eq!(manager.key2id.len(), count); - ids.try_reserve(count + 1).unwrap(); - keys.try_reserve(count + 1).unwrap(); - // Competing creations can consume the prepared headroom before the - // caller reacquires the lock. Neither live table may be moved yet. - while manager.id2sem.len() < ids.capacity() { - let next_key = SemKey::new(200 + manager.id2sem.len()); - insert_test_set(&mut manager, next_key, &[3]); - } - let live_count = manager.id2sem.len(); - let capacities = (manager.id2sem.capacity(), manager.key2id.capacity()); - let (need_ids, need_keys) = manager - .install_create_tables(key, &mut ids, &mut keys) - .unwrap_err(); - assert_eq!( - capacities, - (manager.id2sem.capacity(), manager.key2id.capacity()) - ); - assert_eq!(manager.id2sem.len(), live_count); - ids.try_reserve(need_ids).unwrap(); - keys.try_reserve(need_keys).unwrap(); - manager - .install_create_tables(key, &mut ids, &mut keys) - .unwrap(); - assert_eq!(manager.id2sem.len(), live_count); - assert_eq!(manager.key2id.len(), live_count); - assert!(manager.id2sem.capacity() > live_count); - assert!(manager.key2id.capacity() > live_count); - assert!(ids.is_empty() && keys.is_empty()); - assert_eq!((ids.capacity(), keys.capacity()), capacities); - for id in manager.key2id.values() { - assert_eq!(manager.get_by_semid_checked(*id).unwrap().sems[0].val, 3); - } - } - - #[test] - fn semget_lookup_validates_before_preparing_storage() { - let mut manager = SemManager::new(); - let key = SemKey::new(154); - insert_test_set(&mut manager, key, &[0]); - assert_eq!( - manager.lookup_semget(key, SEMMSL + 1, SemFlags::IPC_CREAT | SemFlags::IPC_EXCL), - Err(SystemError::EINVAL) - ); - assert_eq!( - manager.lookup_semget(key, 2, SemFlags::IPC_CREAT | SemFlags::IPC_EXCL), - Err(SystemError::EEXIST) - ); - assert_eq!( - manager.lookup_semget(key, 2, SemFlags::empty()), - Err(SystemError::EINVAL) - ); - assert_eq!( - manager.lookup_semget(SemKey::new(155), 0, SemFlags::empty()), - Err(SystemError::ENOENT) - ); - assert_eq!( - manager.lookup_semget(SemKey::new(155), 0, SemFlags::IPC_CREAT), - Err(SystemError::EINVAL) - ); - assert_eq!( - manager.lookup_semget(IPC_PRIVATE, 1, SemFlags::IPC_EXCL), - Ok(None) - ); - manager.total_sems = SEMMNS; - assert_eq!( - manager.lookup_semget(IPC_PRIVATE, 1, SemFlags::empty()), - Err(SystemError::ENOSPC) - ); - assert_eq!( - manager.lookup_semget(IPC_PRIVATE, 0, SemFlags::empty()), - Err(SystemError::EINVAL) - ); - } - - #[test] - fn private_create_never_prepares_key_table() { - let mut manager = SemManager::new(); - let mut ids = HashMap::new(); - let mut keys = HashMap::new(); - assert_eq!( - manager.install_create_tables(IPC_PRIVATE, &mut ids, &mut keys), - Err((4, 0)) - ); - ids.try_reserve(4).unwrap(); - manager - .install_create_tables(IPC_PRIVATE, &mut ids, &mut keys) - .unwrap(); - assert!(manager.id2sem.capacity() > 0); - assert_eq!(manager.key2id.capacity(), 0); - assert_eq!(keys.capacity(), 0); - } - - #[test] - fn undo_registry_shrink_releases_empty_and_rechecks_concurrent_growth() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(152), &[0]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - set.undo_groups.reserve_exact(64); - set.undo_groups - .push(SemUndoAssociation::Group(Arc::downgrade(&group))); - let mut spare = Vec::new(); - let mut retired = Vec::new(); - let needed = set.shrink_undo_registry_prepared(&mut spare, &mut retired); - assert_eq!(needed, 4); - spare.try_reserve_exact(needed).unwrap(); - // Simulate new associations arriving during unlocked preparation. - for _ in 0..8 { - set.undo_groups - .push(SemUndoAssociation::Group(Arc::downgrade(&group))); - } - assert!(set.shrink_undo_registry_prepared(&mut spare, &mut retired) > 0); - assert_eq!(set.undo_groups.len(), 9); - assert_eq!(set.undo_groups.capacity(), 64); - set.undo_groups.truncate(1); - assert_eq!( - set.shrink_undo_registry_prepared(&mut spare, &mut retired), - 0 - ); - assert_eq!(set.undo_groups.len(), 1); - assert_eq!(set.undo_groups.capacity(), 4); - assert_eq!(spare.capacity(), 64); - // The spare is already allocated when another owner empties the set. - // It must not be installed back into the empty registry. - set.undo_groups.clear(); - assert_eq!( - set.shrink_undo_registry_prepared(&mut spare, &mut retired), - 0 - ); - assert_eq!(set.undo_groups.capacity(), 0); - assert_eq!(retired.capacity(), 4); - } - - #[test] - fn prepared_registry_growth_rechecks_registration_and_capacity() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(151), &[1]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - let mut spare = Vec::new(); - let capacity = manager - .get_by_semid_checked_mut(semid) - .unwrap() - .ensure_undo_group_registered_prepared(&group, &mut spare) - .unwrap_err(); - assert!(!manager.undo_registry_contains_for_test(&group)); - assert!(manager - .get_by_semid_checked(semid) - .unwrap() - .undo_groups - .is_empty()); - spare.try_reserve(capacity).unwrap(); - - // Simulate other first-time groups consuming the prepared capacity - // while this caller has dropped the manager lock. - let mut owners = Vec::new(); - while manager - .get_by_semid_checked(semid) - .unwrap() - .undo_groups - .len() - < spare.capacity() - { - let owner = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - manager.ensure_undo_group_registered(&owner, semid).unwrap(); - owners.push(owner); - } - let capacity = manager - .get_by_semid_checked_mut(semid) - .unwrap() - .ensure_undo_group_registered_prepared(&group, &mut spare) - .unwrap_err(); - assert!(!manager.undo_registry_contains_for_test(&group)); - spare.try_reserve(capacity).unwrap(); - manager - .get_by_semid_checked_mut(semid) - .unwrap() - .ensure_undo_group_registered_prepared(&group, &mut spare) - .unwrap(); - assert!(spare.is_empty()); - assert!(manager.undo_registry_contains_for_test(&group)); - assert_eq!( - manager - .get_by_semid_checked(semid) - .unwrap() - .undo_groups - .len(), - owners.len() + 1 - ); - for (weak, owner) in manager - .get_by_semid_checked(semid) - .unwrap() - .undo_groups - .iter() - .zip(&owners) - { - assert!(weak.group().ptr_eq(&Arc::downgrade(owner))); - } - - // A concurrent CLONE_SYSVSEM sharer already registered this group. - let mut empty_spare = Vec::new(); - manager - .get_by_semid_checked_mut(semid) - .unwrap() - .ensure_undo_group_registered_prepared(&group, &mut empty_spare) - .unwrap(); - assert_eq!( - manager - .get_by_semid_checked(semid) - .unwrap() - .undo_groups - .len(), - owners.len() + 1 - ); - } - - #[test] - fn queued_undo_commits_to_captured_group_not_waker_current_task() { - let ipc_ns = test_ipc_ns(); - let group_a = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); - let group_b = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); - let mut manager = ipc_ns.sem.lock(); - let semid = insert_test_set(&mut manager, SemKey::new(46), &[0]); - manager - .prepare_undo_record_and_registry_for_test(&group_a, semid) - .unwrap(); - manager - .prepare_undo_record_and_registry_for_test(&group_b, semid) - .unwrap(); - - let (_waiter, waker) = Waiter::new_pair(); - let entry = Arc::new(SemQueueEntry::new_prepared( - SemQueueEntry::prepare_sops(&[undo_sop(0, -1)]).unwrap(), - None, - Some(group_a.clone()), - Some(group_a.prepare_record_for_test(semid, 1).unwrap()), - waker, - SemopScratch::try_new(1).unwrap(), - SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Increase, - nowait: false, - }, - )); - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - set.enqueue_waiter(entry.clone()); - set.sems[0].val = 1; - - manager.update_queue_for_test(semid); - - assert_eq!(entry.completed_result(), Some(Ok(0))); - assert_eq!(sem_values(&manager, semid), vec![0]); - assert_eq!(group_a.adjustment_for_test(semid, 0), 1); - assert_eq!(group_b.adjustment_for_test(semid, 0), 0); - } - - #[test] - fn queued_timeout_signal_and_rmid_never_commit_adjustment() { - let ipc_ns = test_ipc_ns(); - let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); - let mut manager = ipc_ns.sem.lock(); - let timeout_semid = insert_test_set(&mut manager, SemKey::new(47), &[0]); - let signal_semid = insert_test_set(&mut manager, SemKey::new(48), &[0]); - let rmid_semid = insert_test_set(&mut manager, SemKey::new(49), &[0]); - for semid in [timeout_semid, signal_semid, rmid_semid] { - manager - .prepare_undo_record_and_registry_for_test(&group, semid) - .unwrap(); - } - - let timeout_entry = enqueue_undo_waiter_for_test(&mut manager, timeout_semid, &group); - assert_eq!( - manager.cancel_queued_entry( - timeout_semid, - &timeout_entry, - SystemError::EAGAIN_OR_EWOULDBLOCK, - ), - Err(SystemError::EAGAIN_OR_EWOULDBLOCK) - ); - assert_eq!(group.adjustment_for_test(timeout_semid, 0), 0); - - let signal_entry = enqueue_undo_waiter_for_test(&mut manager, signal_semid, &group); - assert_eq!( - manager.cancel_queued_entry(signal_semid, &signal_entry, SystemError::EINTR), - Err(SystemError::EINTR) - ); - assert_eq!(group.adjustment_for_test(signal_semid, 0), 0); - - let rmid_entry = enqueue_undo_waiter_for_test(&mut manager, rmid_semid, &group); - let mut wakes = SemWakeBatch::default(); - let removed = manager.ipc_rmid(rmid_semid, &mut wakes).unwrap(); - drop(manager); - wakes.wake_all(); - drop(removed); - assert_eq!(rmid_entry.completed_result(), Some(Err(SystemError::EIDRM))); - assert_eq!(group.adjustment_for_test(rmid_semid, 0), 0); - } - - #[test] - fn first_record_is_registry_visible_before_future_cleanup() { - let ipc_ns = test_ipc_ns(); - let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); - let mut manager = ipc_ns.sem.lock(); - let semid = insert_test_set(&mut manager, SemKey::new(50), &[3]); - - manager - .prepare_undo_record_and_registry_for_test(&group, semid) - .unwrap(); - - assert_eq!(manager.live_undo_group_count_for_test(), 1); - assert!(manager.undo_registry_contains_for_test(&group)); - assert_eq!(group.adjustment_for_test(semid, 0), 0); - } - - #[test] - fn stale_weak_entries_are_compacted_without_losing_live_group() { - let ipc_ns = test_ipc_ns(); - let live = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); - let candidate = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); - let stale = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); - let stale_weak = Arc::downgrade(&stale); - drop(stale); - - let mut manager = ipc_ns.sem.lock(); - let semid = insert_test_set(&mut manager, SemKey::new(51), &[1]); - manager - .get_by_semid_checked_mut(semid) - .unwrap() - .undo_groups - .push(SemUndoAssociation::Group(stale_weak)); - manager.ensure_undo_group_registered(&live, semid).unwrap(); - manager - .get_by_semid_checked_mut(semid) - .unwrap() - .undo_groups - .shrink_to_fit(); - - manager - .prepare_undo_record_and_registry_for_test(&candidate, semid) - .unwrap(); - - assert_eq!(manager.live_undo_group_count_for_test(), 2); - assert!(manager.undo_registry_contains_for_test(&live)); - assert!(manager.undo_registry_contains_for_test(&candidate)); - } - - #[test] - fn live_group_registration_survives_debt_removal_without_duplicates() { - let ipc_ns = test_ipc_ns(); - let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); - let mut manager = ipc_ns.sem.lock(); - let semid = insert_test_set(&mut manager, SemKey::new(150), &[1]); - manager - .prepare_undo_record_and_registry_for_test(&group, semid) - .unwrap(); - let capacity = manager - .get_by_semid_checked(semid) - .unwrap() - .undo_groups - .capacity(); - manager.clear_undo_for_setall(semid); - // Synthetic record removal exercises registration deduplication without - // putting a live set into the RMID-only retired association state. - group.remove_record(semid); - assert_eq!(group.record_count_for_test(), 0); - for _ in 0..32 { - manager.ensure_undo_group_registered(&group, semid).unwrap(); - } - assert!(manager.undo_registry_contains_for_test(&group)); - assert_eq!( - manager - .get_by_semid_checked(semid) - .unwrap() - .undo_groups - .len(), - 1 - ); - assert_eq!( - manager - .get_by_semid_checked(semid) - .unwrap() - .undo_groups - .capacity(), - capacity - ); - manager - .prepare_undo_record_and_registry_for_test(&group, semid) - .unwrap(); - assert_eq!( - manager - .get_by_semid_checked(semid) - .unwrap() - .undo_groups - .len(), - 1 - ); - } - - #[test] - fn queued_undo_entry_retains_group_after_external_owner_drops() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(52), &[1]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - manager - .prepare_undo_record_and_registry_for_test(&group, semid) - .unwrap(); - let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); - let group_weak = Arc::downgrade(&group); - drop(group); - assert!(group_weak.upgrade().is_some()); - manager.get_by_semid_checked_mut(semid).unwrap().sems[0].val = 1; - - manager.update_queue_for_test(semid); - - assert_eq!(entry.completed_result(), Some(Ok(0))); - assert_eq!(sem_values(&manager, semid), vec![0]); - } - - #[test] - fn queued_undo_record_length_mismatch_completes_with_internal_error() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(53), &[1, 1]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - group.insert_test_record(semid, &[0]); - let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); - manager.get_by_semid_checked_mut(semid).unwrap().sems[0].val = 1; - - manager.update_queue_for_test(semid); - - assert_eq!(entry.completed_result(), Some(Err(SystemError::EINVAL))); - assert_eq!(sem_values(&manager, semid), vec![1, 1]); - assert_eq!(group.adjustment_for_test(semid, 0), 0); - } - - #[test] - fn final_owner_detach_replays_against_setval_as_a_single_serial_order() { - let ipc_ns = test_ipc_ns(); - let semid = { - let mut manager = ipc_ns.sem.lock(); - insert_test_set(&mut manager, SemKey::new(62), &[2]) - }; - let (pcb, group) = test_pcb_with_group(&ipc_ns); - group.insert_test_record(semid, &[3]); - ipc_ns - .sem - .lock() - .ensure_undo_group_registered(&group, semid) - .unwrap(); - drop(pcb.take_sem_undo_attachment().unwrap()); - assert!(group.detach_last_owner_for_test()); - - { - let mut manager = ipc_ns.sem.lock(); - manager - .setval(semid, 0, 7, &mut SemWakeBatch::default()) - .unwrap(); - } - group.replay_marked_records_for_test(&pcb); - - let manager = ipc_ns.sem.lock(); - assert_eq!(sem_values(&manager, semid), vec![7]); - assert_eq!(group.record_count_for_test(), 0); - assert_eq!(group.replay_count_for_test(), 1); - assert!(pcb.sem_undo_group().is_none()); - } - - #[test] - fn rmid_before_final_owner_replay_skips_detached_record_once() { - let ipc_ns = test_ipc_ns(); - let semid = { - let mut manager = ipc_ns.sem.lock(); - insert_test_set(&mut manager, SemKey::new(63), &[2]) - }; - let (pcb, group) = test_pcb_with_group(&ipc_ns); - group.insert_test_record(semid, &[3]); - ipc_ns - .sem - .lock() - .ensure_undo_group_registered(&group, semid) - .unwrap(); - drop(pcb.take_sem_undo_attachment().unwrap()); - assert!(group.detach_last_owner_for_test()); - - let mut wakes = SemWakeBatch::default(); - let removed = { - let mut manager = ipc_ns.sem.lock(); - manager.ipc_rmid(semid, &mut wakes).unwrap() - }; - wakes.wake_all(); - drop(removed); - group.replay_marked_records_for_test(&pcb); - - let manager = ipc_ns.sem.lock(); - assert_eq!( - manager.get_by_semid_checked(semid), - Err(SystemError::EINVAL) - ); - assert_eq!(group.record_count_for_test(), 0); - assert_eq!(group.replay_count_for_test(), 1); - assert!(pcb.sem_undo_group().is_none()); - } - - #[test] - fn prepare_existing_undo_record_length_mismatch_returns_einval_without_mutation() { - let semid = SemId::new(64); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - group.insert_test_record(semid, &[5]); - - let err = group.prepare_record_for_test(semid, 2).unwrap_err(); - - assert_eq!(err, SystemError::EINVAL); - assert_eq!(group.adjustment_for_test(semid, 0), 5); - assert_eq!(group.record_count_for_test(), 1); - assert_eq!(group.pending_record_reservations_for_test(), 0); - } - - #[test] - fn setval_clears_only_target_sem_adjustment_across_all_groups() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(54), &[4, 5]); - let other_semid = insert_test_set(&mut manager, SemKey::new(55), &[6, 7]); - let group_a = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - let group_b = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - group_a.insert_test_record(semid, &[11, 12]); - group_b.insert_test_record(semid, &[21, 22]); - group_a.insert_test_record(other_semid, &[31, 32]); - manager - .ensure_undo_group_registered(&group_a, semid) - .unwrap(); - manager - .ensure_undo_group_registered(&group_b, semid) - .unwrap(); - - manager - .setval(semid, 0, 9, &mut SemWakeBatch::default()) - .unwrap(); - - assert_eq!(sem_values(&manager, semid), vec![9, 5]); - assert_eq!(group_a.adjustment_for_test(semid, 0), 0); - assert_eq!(group_a.adjustment_for_test(semid, 1), 12); - assert_eq!(group_b.adjustment_for_test(semid, 0), 0); - assert_eq!(group_b.adjustment_for_test(semid, 1), 22); - assert_eq!(group_a.adjustment_for_test(other_semid, 0), 31); - } - - #[test] - fn setall_clears_entire_full_semid_record_across_all_groups() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(56), &[1, 2, 3]); - let other_semid = insert_test_set(&mut manager, SemKey::new(57), &[4, 5, 6]); - let group_a = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - let group_b = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - group_a.insert_test_record(semid, &[1, 2, 3]); - group_b.insert_test_record(semid, &[4, 5, 6]); - group_b.insert_test_record(other_semid, &[7, 8, 9]); - manager - .ensure_undo_group_registered(&group_a, semid) - .unwrap(); - manager - .ensure_undo_group_registered(&group_b, semid) - .unwrap(); - let token = SemSetAllToken::new(semid, 3); - - manager - .setall(token, &[10, 11, 12], &mut SemWakeBatch::default()) - .unwrap(); - - assert_eq!(sem_values(&manager, semid), vec![10, 11, 12]); - assert_eq!(group_a.adjustment_for_test(semid, 0), 0); - assert_eq!(group_a.adjustment_for_test(semid, 1), 0); - assert_eq!(group_a.adjustment_for_test(semid, 2), 0); - assert_eq!(group_b.adjustment_for_test(semid, 0), 0); - assert_eq!(group_b.adjustment_for_test(semid, 1), 0); - assert_eq!(group_b.adjustment_for_test(semid, 2), 0); - assert_eq!(group_b.adjustment_for_test(other_semid, 1), 8); - } - - #[test] - fn setval_cleanup_precedes_value_write_and_queue_rescan() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(58), &[0]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - group.insert_test_record(semid, &[7]); - manager.ensure_undo_group_registered(&group, semid).unwrap(); - let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); - - manager - .setval(semid, 0, 1, &mut SemWakeBatch::default()) - .unwrap(); - - assert_eq!(entry.completed_result(), Some(Ok(0))); - assert_eq!(sem_values(&manager, semid), vec![0]); - assert_eq!(group.adjustment_for_test(semid, 0), 1); - } - - #[test] - fn rmid_discards_record_before_index_can_be_reused() { - let mut manager = SemManager::new(); - manager.id_allocator = IpcIdAllocator::new(2).unwrap(); - let old_semid = insert_test_set(&mut manager, SemKey::new(59), &[3]); - let filler_semid = insert_test_set(&mut manager, SemKey::new(60), &[4]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - group.insert_test_record(old_semid, &[9]); - manager - .ensure_undo_group_registered(&group, old_semid) - .unwrap(); - manager - .ipc_rmid(old_semid, &mut SemWakeBatch::default()) - .unwrap(); - - let new_semid = insert_test_set(&mut manager, SemKey::new(61), &[5]); - - assert_ne!(old_semid, new_semid); - assert_eq!( - old_semid.data() & IpcIdAllocator::IPC_ID_IDX_MASK, - new_semid.data() & IpcIdAllocator::IPC_ID_IDX_MASK - ); - assert_eq!(sem_values(&manager, new_semid), vec![5]); - assert_eq!(group.record_count_for_test(), 0); - assert_eq!(sem_values(&manager, filler_semid), vec![4]); - } - - #[test] - fn setval_clear_between_prepare_and_commit_refreshes_stale_existing_record() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(62), &[4]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - group.insert_test_record(semid, &[7]); - manager.ensure_undo_group_registered(&group, semid).unwrap(); - - let record = group.prepare_record_for_test(semid, 1).unwrap(); - manager.clear_undo_for_setval(semid, 0); - let mut scratch = SemopScratch::try_new(1).unwrap(); - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - - let result = group.with_prepared_record_noalloc(record, |record| { - let outcome = - SemManager::simulate_semop(set, &[undo_sop(0, -1)], Some(record), &mut scratch) - .unwrap() - .ready_for_test(); - SemManager::commit_semop(set, outcome, &scratch, None, Some(record)); - PreparedSemUndoRecordAction::Publish(()) - }); - - assert!(result.is_ok()); - assert_eq!(sem_values(&manager, semid), vec![3]); - assert_eq!(group.adjustment_for_test(semid, 0), 1); - } - - #[test] - fn setall_clear_between_prepare_and_commit_refreshes_stale_existing_record() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(63), &[4, 5]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - group.insert_test_record(semid, &[7, -3]); - manager.ensure_undo_group_registered(&group, semid).unwrap(); - - let record = group.prepare_record_for_test(semid, 2).unwrap(); - manager.clear_undo_for_setall(semid); - let mut scratch = SemopScratch::try_new(1).unwrap(); - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - - let result = group.with_prepared_record_noalloc(record, |record| { - let outcome = - SemManager::simulate_semop(set, &[undo_sop(0, -1)], Some(record), &mut scratch) - .unwrap() - .ready_for_test(); - SemManager::commit_semop(set, outcome, &scratch, None, Some(record)); - PreparedSemUndoRecordAction::Publish(()) - }); - - assert!(result.is_ok()); - assert_eq!(sem_values(&manager, semid), vec![3, 5]); - assert_eq!(group.adjustment_for_test(semid, 0), 1); - assert_eq!(group.adjustment_for_test(semid, 1), 0); - } - - #[test] - fn stale_existing_prepared_record_refreshes_before_immediate_commit() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(65), &[4]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - group.insert_test_record(semid, &[7]); - manager.ensure_undo_group_registered(&group, semid).unwrap(); - - let record = group.prepare_record_for_test(semid, 1).unwrap(); - manager.clear_undo_for_setval(semid, 0); - let mut scratch = SemopScratch::try_new(1).unwrap(); - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - - let result = group.with_prepared_record_noalloc(record, |record| { - let outcome = - SemManager::simulate_semop(set, &[undo_sop(0, -1)], Some(record), &mut scratch) - .unwrap() - .ready_for_test(); - SemManager::commit_semop(set, outcome, &scratch, None, Some(record)); - PreparedSemUndoRecordAction::Publish(()) - }); - - assert!(result.is_ok()); - assert_eq!(sem_values(&manager, semid), vec![3]); - assert_eq!(group.adjustment_for_test(semid, 0), 1); - } - - #[test] - fn queued_stale_existing_record_retries_and_completes_without_error() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(66), &[0]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - group.insert_test_record(semid, &[7]); - manager.ensure_undo_group_registered(&group, semid).unwrap(); - let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); - - manager.clear_undo_for_setval(semid, 0); - manager.get_by_semid_checked_mut(semid).unwrap().sems[0].val = 2; - manager.update_queue_for_test(semid); - - assert_eq!(entry.completed_result(), Some(Ok(0))); - assert_eq!(sem_values(&manager, semid), vec![1]); - assert_eq!(group.adjustment_for_test(semid, 0), 1); - } - - #[test] - fn consecutive_sem_undo_on_unchanged_existing_record_still_accumulates() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(64), &[5]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - group.insert_test_record(semid, &[2]); - - let record = group.prepare_record_for_test(semid, 1).unwrap(); - let mut scratch = SemopScratch::try_new(1).unwrap(); - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - group - .with_prepared_record_noalloc(record, |record| { - let outcome = - SemManager::simulate_semop(set, &[undo_sop(0, -2)], Some(record), &mut scratch) - .unwrap() - .ready_for_test(); - SemManager::commit_semop(set, outcome, &scratch, None, Some(record)); - PreparedSemUndoRecordAction::Publish(()) - }) - .unwrap(); - - assert_eq!(group.adjustment_for_test(semid, 0), 4); - } - - fn enqueue_undo_waiter_for_test( - manager: &mut SemManager, - semid: SemId, - group: &Arc, - ) -> Arc { - let (_waiter, waker) = Waiter::new_pair(); - let entry = Arc::new(SemQueueEntry::new_prepared( - SemQueueEntry::prepare_sops(&[undo_sop(0, -1)]).unwrap(), - None, - Some(Arc::clone(group)), - Some(group.prepare_record_for_test(semid, 1).unwrap()), - waker, - SemopScratch::try_new(1).unwrap(), - SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Increase, - nowait: false, - }, - )); - manager - .get_by_semid_checked_mut(semid) - .unwrap() - .enqueue_waiter(entry.clone()); - entry - } - - fn undo_sop(sem_num: u16, sem_op: i16) -> PosixSemBuf { - PosixSemBuf { - sem_num, - sem_op, - sem_flg: SemFlags::SEM_UNDO.bits() as i16, - } - } - - fn plain_sop(sem_num: u16, sem_op: i16) -> PosixSemBuf { - PosixSemBuf { - sem_num, - sem_op, - sem_flg: 0, - } - } - - fn nowait_sop(sem_num: u16, sem_op: i16) -> PosixSemBuf { - PosixSemBuf { - sem_num, - sem_op, - sem_flg: SemFlags::IPC_NOWAIT.bits() as i16, - } - } - - #[test] - fn const_waiters_complete_before_altering_waiters() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(67), &[1]); - let (altering, constant) = { - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - let altering = enqueue_test_waiter( - set, - &[plain_sop(0, 0), plain_sop(0, 1)], - SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Zero, - nowait: false, - }, - ); - let constant = enqueue_test_waiter( - set, - &[plain_sop(0, 0)], - SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Zero, - nowait: false, - }, - ); - set.sems[0].val = 0; - (altering, constant) - }; - - manager.update_queue_for_test(semid); - - let set = manager.get_by_semid_checked(semid).unwrap(); - assert_eq!(constant.completed_result(), Some(Ok(0))); - assert_eq!(altering.completed_result(), Some(Ok(0))); - assert!(set.pending_is_empty()); - assert_eq!(set.sems[0].val, 1); - } - - #[test] - fn ordered_mixed_undo_ops_apply_each_adjustment_step() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(41), &[4]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - let record = group.prepare_record_for_test(semid, 1).unwrap(); - let mut scratch = SemopScratch::try_new(3).unwrap(); - let sops = [undo_sop(0, 3), plain_sop(0, -1), undo_sop(0, -2)]; - - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - group - .with_prepared_record_noalloc(record, |record| { - let outcome = SemManager::simulate_semop(set, &sops, Some(record), &mut scratch); - assert!(matches!(outcome, Ok(SemopOutcome::Ready(_)))); - SemManager::commit_semop( - set, - outcome.unwrap().ready_for_test(), - &scratch, - None, - Some(record), - ); - PreparedSemUndoRecordAction::Publish(()) - }) - .unwrap(); - - assert_eq!(set.sems[0].val, 4); - assert_eq!(group.adjustment_for_test(semid, 0), -1); - } - - #[test] - fn intermediate_adjustment_overflow_is_erange_even_if_later_op_cancels_it() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(42), &[10]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - group.insert_test_record(semid, &[i16::MAX]); - let record = group.prepare_record_for_test(semid, 1).unwrap(); - let mut scratch = SemopScratch::try_new(2).unwrap(); - let sops = [undo_sop(0, -1), undo_sop(0, 1)]; - - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - group - .with_prepared_record_noalloc(record, |record| { - assert!(matches!( - SemManager::simulate_semop(set, &sops, Some(record), &mut scratch), - Err(SystemError::ERANGE) - )); - PreparedSemUndoRecordAction::Keep(()) - }) - .unwrap(); - - assert_eq!(set.sems[0].val, 10); - assert_eq!(group.adjustment_for_test(semid, 0), i16::MAX); - } - - #[test] - fn blocked_or_nowait_failure_does_not_commit_semval_or_semadj_prefix() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(43), &[2]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - // Exercise the live record, not just an unpublished candidate. - group.insert_test_record(semid, &[0]); - let record = group.prepare_record_for_test(semid, 1).unwrap(); - let mut scratch = SemopScratch::try_new(2).unwrap(); - let sops = [undo_sop(0, -1), nowait_sop(0, -2)]; - - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - group - .with_prepared_record_noalloc(record, |record| { - assert!(matches!( - SemManager::simulate_semop(set, &sops, Some(record), &mut scratch), - Ok(SemopOutcome::Blocked(SemBlockedOp { - semnum: 0, - wait_type: SemWaitType::Increase, - nowait: true, - })) - )); - PreparedSemUndoRecordAction::Keep(()) - }) - .unwrap(); - - assert_eq!(set.sems[0].val, 2); - assert_eq!(group.adjustment_for_test(semid, 0), 0); - } - - #[test] - fn zero_undo_op_can_prepare_zero_record_without_adjustment() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(44), &[0]); - let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - let record = group.prepare_record_for_test(semid, 1).unwrap(); - let mut scratch = SemopScratch::try_new(1).unwrap(); - let sops = [undo_sop(0, 0)]; - - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - group - .with_prepared_record_noalloc(record, |record| { - let outcome = SemManager::simulate_semop(set, &sops, Some(record), &mut scratch); - assert!(matches!(outcome, Ok(SemopOutcome::Ready(_)))); - SemManager::commit_semop( - set, - outcome.unwrap().ready_for_test(), - &scratch, - None, - Some(record), - ); - PreparedSemUndoRecordAction::Publish(()) - }) - .unwrap(); - - assert_eq!(set.sems[0].val, 0); - assert_eq!(group.record_count_for_test(), 1); - } - - #[test] - fn scratch_entry_for_returns_enomem_instead_of_extending_past_capacity() { - let mut manager = SemManager::new(); - let semid = insert_test_set(&mut manager, SemKey::new(45), &[1, 1]); - let set = manager.get_by_semid_checked_mut(semid).unwrap(); - let mut scratch = SemopScratch::try_new(1).unwrap(); - let sops = [plain_sop(0, -1), plain_sop(1, -1)]; - - assert!(matches!( - SemManager::simulate_semop(set, &sops, None, &mut scratch), - Err(SystemError::ENOMEM) - )); - } - - #[test] - fn prepared_setall_token_returns_eidrm_after_rmid() { - let mut manager = SemManager::new(); - let id = insert_test_set(&mut manager, SemKey::new(11), &[1, 2]); - let token = SemSetAllToken::new(id, 2); - - remove_test_set(&mut manager, id); - - assert_eq!( - manager.setall(token, &[7, 8], &mut SemWakeBatch::default()), - Err(SystemError::EIDRM) - ); - } - - #[test] - fn stale_prepared_setall_token_does_not_modify_reused_index() { - let mut manager = SemManager::new(); - let old_id = insert_test_set(&mut manager, SemKey::new(21), &[1, 2]); - let token = SemSetAllToken::new(old_id, 2); - - remove_test_set(&mut manager, old_id); - let new_id = insert_test_set(&mut manager, SemKey::new(22), &[3, 4]); - assert_ne!(old_id, new_id); - assert_eq!( - old_id.data() & IpcIdAllocator::IPC_ID_IDX_MASK, - new_id.data() & IpcIdAllocator::IPC_ID_IDX_MASK - ); - - assert_eq!( - manager.setall(token, &[7, 8], &mut SemWakeBatch::default()), - Err(SystemError::EIDRM) - ); - assert_eq!(sem_values(&manager, new_id), vec![3, 4]); - } -} diff --git a/kernel/src/ipc/sem/abi.rs b/kernel/src/ipc/sem/abi.rs new file mode 100644 index 0000000000..d64464b6f6 --- /dev/null +++ b/kernel/src/ipc/sem/abi.rs @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +//! Linux semaphore IDs, flags and userspace layouts. +use crate::ipc::ipc_perm::PosixIpcPerm; +use core::fmt; + +pub const IPC_PRIVATE: SemKey = SemKey::new(0); + +int_like!(SemId, usize); +int_like!(SemKey, usize); + +pub const SEMMNI: usize = 32000; +pub const SEMMSL: usize = 32000; +pub const SEMMNS: usize = SEMMNI * SEMMSL; +pub const SEMOPM: usize = 500; +pub const SEMVMX: i32 = 32767; + +bitflags! { + pub struct SemFlags: u32 { + const PERM_MASK = 0o777; + const IPC_CREAT = 0o1000; + const IPC_EXCL = 0o2000; + const IPC_NOWAIT = 0x800; + const SEM_UNDO = 0x1000; + } +} + +/// Semaphore-set control commands (Linux x86_64 UAPI include/uapi/linux/sem.h) +#[derive(Eq, Clone, Copy)] +pub enum SemCtlCmd { + /// Remove the semaphore set + IpcRmid = 0, + /// Set permissions + IpcSet = 1, + /// Retrieve `SemIdDs` + IpcStat = 2, + /// Retrieve `SemInfo` + IpcInfo = 3, + /// Get the PID of the last process to operate on the specified semaphore + GetPid = 11, + /// Get the specified semaphore value + GetVal = 12, + /// Get values of all semaphores in the set + GetAll = 13, + /// Get the number of processes waiting for the specified semaphore to increase + GetNcnt = 14, + /// Get the number of processes waiting for the specified semaphore to reach zero + GetZcnt = 15, + /// Set the specified semaphore value + SetVal = 16, + /// Set values of all semaphores in the set + SetAll = 17, + /// Retrieve `SemIdDs` by index + SemStat = 18, + /// Retrieve `SemInfo` + SemInfo = 19, + /// Retrieve `SemIdDs` by index without permission checks + SemStatAny = 20, + + Default, +} + +impl From for SemCtlCmd { + fn from(cmd: usize) -> SemCtlCmd { + match cmd { + 0 => Self::IpcRmid, + 1 => Self::IpcSet, + 2 => Self::IpcStat, + 3 => Self::IpcInfo, + 11 => Self::GetPid, + 12 => Self::GetVal, + 13 => Self::GetAll, + 14 => Self::GetNcnt, + 15 => Self::GetZcnt, + 16 => Self::SetVal, + 17 => Self::SetAll, + 18 => Self::SemStat, + 19 => Self::SemInfo, + 20 => Self::SemStatAny, + _ => Self::Default, + } + } +} + +impl fmt::Display for SemCtlCmd { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SemCtlCmd::IpcRmid => write!(f, "IPC_RMID"), + SemCtlCmd::IpcSet => write!(f, "IPC_SET"), + SemCtlCmd::IpcStat => write!(f, "IPC_STAT"), + SemCtlCmd::IpcInfo => write!(f, "IPC_INFO"), + SemCtlCmd::GetPid => write!(f, "GETPID"), + SemCtlCmd::GetVal => write!(f, "GETVAL"), + SemCtlCmd::GetAll => write!(f, "GETALL"), + SemCtlCmd::GetNcnt => write!(f, "GETNCNT"), + SemCtlCmd::GetZcnt => write!(f, "GETZCNT"), + SemCtlCmd::SetVal => write!(f, "SETVAL"), + SemCtlCmd::SetAll => write!(f, "SETALL"), + SemCtlCmd::SemStat => write!(f, "SEM_STAT"), + SemCtlCmd::SemInfo => write!(f, "SEM_INFO"), + SemCtlCmd::SemStatAny => write!(f, "SEM_STAT_ANY"), + SemCtlCmd::Default => write!(f, "DEFAULT (Invalid Cmd)"), + } + } +} + +impl PartialEq for SemCtlCmd { + fn eq(&self, other: &SemCtlCmd) -> bool { + *self as usize == *other as usize + } +} + +/// Userspace `sembuf` (Linux `struct sembuf`, 6 bytes) +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct PosixSemBuf { + pub sem_num: u16, + pub sem_op: i16, + pub sem_flg: i16, +} + +/// Semaphore-set information matching Linux x86_64 `struct semid64_ds` (104 bytes, +/// including the high halves of 32-bit timestamp fields) +#[repr(C)] +#[derive(Debug, Clone, Copy, Default)] +pub struct PosixSemIdDs { + /// Permission information + pub sem_perm: PosixIpcPerm, + /// Time of the last `semop` (64-bit; upper 32 bits are in `__sem_otime_high`) + pub sem_otime: i64, + _sem_otime_high: i64, + /// Time of the last metadata change + pub sem_ctime: i64, + _sem_ctime_high: i64, + /// Number of semaphores in the set + pub sem_nsems: usize, + _unused1: usize, + _unused2: usize, +} + +/// Semaphore system information matching Linux `struct seminfo` (40 bytes) +#[repr(C)] +#[derive(Debug, Clone, Copy, Default)] +pub struct PosixSemInfo { + pub semmap: i32, + pub semmni: i32, + pub semmns: i32, + pub semmnu: i32, + pub semmsl: i32, + pub semopm: i32, + pub semume: i32, + pub semusz: i32, + pub semvmx: i32, + pub semaem: i32, +} + +impl PosixSemInfo { + pub(super) fn new(cmd: SemCtlCmd, set_count: usize, total_sems: usize) -> Self { + let (semusz, semaem) = if cmd == SemCtlCmd::SemInfo { + (set_count as i32, total_sems as i32) + } else { + (20, SEMVMX) + }; + PosixSemInfo { + semmap: SEMMNS as i32, + semmni: SEMMNI as i32, + semmns: SEMMNS as i32, + semmnu: SEMMNS as i32, + semmsl: SEMMSL as i32, + semopm: SEMOPM as i32, + semume: SEMOPM as i32, + semusz, + semvmx: SEMVMX, + semaem, + } + } +} + +impl PosixSemIdDs { + pub(super) fn new( + sem_perm: PosixIpcPerm, + sem_otime: i64, + sem_ctime: i64, + sem_nsems: usize, + ) -> Self { + Self { + sem_perm, + sem_otime, + sem_ctime, + sem_nsems, + ..Self::default() + } + } +} diff --git a/kernel/src/ipc/sem/manager/control.rs b/kernel/src/ipc/sem/manager/control.rs new file mode 100644 index 0000000000..92efc7a554 --- /dev/null +++ b/kernel/src/ipc/sem/manager/control.rs @@ -0,0 +1,178 @@ +//! Command-specific validation and dispatch; user copies remain in syscall wrappers. +use super::*; +use crate::ipc::ipc_perm::IpcPermView; + +pub struct SemSetAllToken { + id: SemId, + nsems: usize, +} + +impl SemSetAllToken { + pub(super) fn new(id: SemId, nsems: usize) -> Self { + Self { id, nsems } + } + + pub fn nsems(&self) -> usize { + self.nsems + } +} + +impl SemManager { + /// # IPC_RMID: remove the semaphore set and wake all waiters with EIDRM + /// The caller must release the manager guard before notifying `wakes` and + /// dropping the returned set, which owns all deferred undo/group disposal. + pub(crate) fn ipc_rmid( + &mut self, + id: SemId, + wakes: &mut SemWakeBatch, + ) -> Result { + let decoded = IpcIdAllocator::decode(id.data())?; + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + let key = { + let set = self.get_by_semid_checked(id)?; + ipc_perm::check_control_permission(set.permissions(), &target_user_ns)?; + set.permissions().key + }; + let mut set = self + .id2sem + .remove(&decoded.idx) + .ok_or(SystemError::EINVAL)?; + self.key2id.remove(&SemKey::new(key)); + self.total_sems = self.total_sems.saturating_sub(set.nsems()); + // Reuse existing association storage: neither allocation nor undo/group + // destruction is allowed here. Even an empty upgraded group stays alive + // until the caller drops this removed set outside the manager lock. + set.retire_undo_records(id); + set.complete_all_removed(wakes); + self.id_allocator.free_idx(decoded.idx); + Ok(set) + } + + /// # IPC_SET: update permissions (uid/gid/mode) and refresh `sem_ctime` + pub fn ipc_set(&mut self, id: SemId, semid_ds: PosixSemIdDs) -> Result<(), SystemError> { + let set = self.get_by_semid_checked_mut(id)?; + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::check_control_permission(set.permissions(), &target_user_ns)?; + set.set_permissions(semid_ds) + } + + /// IPC_STAT/SEM_STAT/SEM_STAT_ANY: return `semid_ds` + pub fn sem_stat_data( + &self, + id_or_index: SemId, + cmd: SemCtlCmd, + ) -> Result<(usize, PosixSemIdDs), SystemError> { + let set = match cmd { + SemCtlCmd::IpcStat => self.get_by_semid_checked(id_or_index)?, + SemCtlCmd::SemStat | SemCtlCmd::SemStatAny => self.get_by_index(id_or_index.data())?, + _ => return Err(SystemError::EINVAL), + }; + if cmd != SemCtlCmd::SemStatAny { + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission(set.permissions(), Self::IPC_READ, &target_user_ns)?; + } + let current_user_ns = ProcessManager::current_user_ns(); + let sem_perm = set.permissions().to_posix(¤t_user_ns)?; + let semid_ds = set.stat(sem_perm); + let ret = if cmd == SemCtlCmd::IpcStat { + 0 + } else { + set.permissions().id + }; + Ok((ret, semid_ds)) + } + + /// IPC_INFO/SEM_INFO: return system information + pub fn sem_info_data(&self, cmd: SemCtlCmd) -> (usize, PosixSemInfo) { + ( + self.current_max_index(), + PosixSemInfo::new(cmd, self.id2sem.len(), self.total_sems), + ) + } + + /// GETVAL/GETPID/GETNCNT/GETZCNT: query a single semaphore + pub fn sem_get_value( + &self, + id: SemId, + semnum: usize, + cmd: SemCtlCmd, + ) -> Result { + let set = self.get_by_semid_checked(id)?; + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission(set.permissions(), Self::IPC_READ, &target_user_ns)?; + if semnum >= set.nsems() { + return Err(SystemError::EINVAL); + } + set.get_value(semnum, cmd) + } + + /// # SETVAL: set a single semaphore value + pub(crate) fn setval( + &mut self, + id: SemId, + semnum: usize, + val: i32, + wakes: &mut SemWakeBatch, + ) -> Result<(), SystemError> { + // Match Linux: validate the value (ERANGE), then semnum (EINVAL), then permissions + // (EACCES). + if !(0..=SEMVMX).contains(&val) { + return Err(SystemError::ERANGE); + } + let nsems = { + let set = self.get_by_semid_checked(id)?; + if semnum >= set.nsems() { + return Err(SystemError::EINVAL); + } + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission(set.permissions(), Self::IPC_WRITE, &target_user_ns)?; + set.nsems() + }; + debug_assert!(semnum < nsems); + + let set = self.get_by_semid_checked_mut(id)?; + set.setval(id, semnum, val, wakes); + Ok(()) + } + + /// # SETALL: set values of all semaphores in the set without changes on validation failure + pub(crate) fn setall( + &mut self, + token: SemSetAllToken, + vals: &[u16], + wakes: &mut SemWakeBatch, + ) -> Result<(), SystemError> { + let set_nsems = self + .get_by_semid_checked(token.id) + .map_err(|_| SystemError::EIDRM)? + .nsems(); + if vals.len() != token.nsems || vals.len() != set_nsems { + return Err(SystemError::EINVAL); + } + if vals.iter().any(|&v| v as i32 > SEMVMX) { + return Err(SystemError::ERANGE); + } + + let set = self + .get_by_semid_checked_mut(token.id) + .map_err(|_| SystemError::EIDRM)?; + set.setall(token.id, vals, wakes); + Ok(()) + } + + /// # GETALL: get values of all semaphores in the set + pub fn getall(&self, id: SemId) -> Result, SystemError> { + let set = self.get_by_semid_checked(id)?; + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission(set.permissions(), Self::IPC_READ, &target_user_ns)?; + set.values() + } + + /// Validate SETALL before the caller accesses the userspace array. + pub fn prepare_setall(&self, id: SemId) -> Result { + let set = self.get_by_semid_checked(id)?; + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission(set.permissions(), Self::IPC_WRITE, &target_user_ns)?; + Ok(SemSetAllToken::new(id, set.nsems())) + } +} diff --git a/kernel/src/ipc/sem/manager/mod.rs b/kernel/src/ipc/sem/manager/mod.rs new file mode 100644 index 0000000000..45280b0d20 --- /dev/null +++ b/kernel/src/ipc/sem/manager/mod.rs @@ -0,0 +1,326 @@ +//! Namespace identity, lookup, quota and semaphore creation. +use super::{ + abi::*, + set::{KernelSem, KernelSemSet, SemWakeBatch}, +}; +use crate::{ + ipc::{ + id::IpcIdAllocator, + ipc_perm::{self, IpcPerm}, + sem_undo::SemUndoGroup, + }, + process::{namespace::ipc_namespace::IpcNamespace, pid::Pid, ProcessManager}, +}; +use alloc::{sync::Arc, vec::Vec}; +use hashbrown::HashMap; +use system_error::SystemError; +mod control; +pub use control::SemSetAllToken; + +/// Semaphore manager +#[derive(Debug)] +pub struct SemManager { + /// SemId allocator + id_allocator: IpcIdAllocator, + /// Semaphore set table keyed by low IPC index + id2sem: HashMap, + /// SemId table keyed by SemKey + key2id: HashMap, + /// Total semaphores in the namespace (Linux semmns accounting) + total_sems: usize, +} + +impl Default for SemManager { + fn default() -> Self { + Self::new() + } +} + +impl SemManager { + pub(super) const IPC_READ: u32 = 0o4; + pub(super) const IPC_WRITE: u32 = 0o2; + + pub fn new() -> Self { + SemManager { + id_allocator: IpcIdAllocator::new(SEMMNI).unwrap(), + id2sem: HashMap::new(), + key2id: HashMap::new(), + total_sems: 0, + } + } + + pub(super) fn get_by_semid_checked(&self, id: SemId) -> Result<&KernelSemSet, SystemError> { + let decoded = IpcIdAllocator::decode(id.data())?; + let set = self.id2sem.get(&decoded.idx).ok_or(SystemError::EINVAL)?; + if set.permissions().id != id.data() || set.permissions().seq != decoded.seq { + return Err(SystemError::EINVAL); + } + Ok(set) + } + + pub(super) fn get_by_semid_checked_mut( + &mut self, + id: SemId, + ) -> Result<&mut KernelSemSet, SystemError> { + let decoded = IpcIdAllocator::decode(id.data())?; + let set = self + .id2sem + .get_mut(&decoded.idx) + .ok_or(SystemError::EINVAL)?; + if set.permissions().id != id.data() || set.permissions().seq != decoded.seq { + return Err(SystemError::EINVAL); + } + Ok(set) + } + + fn get_by_index(&self, id: usize) -> Result<&KernelSemSet, SystemError> { + let idx = id & IpcIdAllocator::IPC_ID_IDX_MASK; + self.id2sem.get(&idx).ok_or(SystemError::EINVAL) + } + + pub(super) fn validate_semid_nsems(&self, semid: SemId) -> Result { + Ok(self.get_by_semid_checked(semid)?.nsems()) + } + + /// Reclaim a target registry, never allocating or freeing its buffer under + /// the namespace lock. Concurrent growth can make a shrink unnecessary or + /// consume the spare capacity; in either case leave the live registry intact. + pub(crate) fn shrink_undo_registry(ipcns: &Arc, semid: SemId) { + let mut spare = Vec::new(); + let mut retired = Vec::new(); + let needed = { + let mut manager = ipcns.sem.lock(); + let Ok(set) = manager.get_by_semid_checked_mut(semid) else { + return; + }; + set.shrink_undo_registry_prepared(&mut spare, &mut retired) + }; + if needed == 0 || spare.try_reserve_exact(needed).is_err() { + return; + } + // Allocation is best-effort reclamation, not part of syscall success. + let mut manager = ipcns.sem.lock(); + if let Ok(set) = manager.get_by_semid_checked_mut(semid) { + set.shrink_undo_registry_prepared(&mut spare, &mut retired); + } + } + + fn current_max_index(&self) -> usize { + self.id_allocator.max_used_index().unwrap_or(0) + } + + /// Create or look up a set. Storage preparation and disposal must happen + /// outside the namespace spinlock, including when another creator wins. + pub fn semget( + ipcns: &Arc, + key: SemKey, + nsems: usize, + semflg: SemFlags, + ) -> Result { + let mut sems = Vec::new(); + let mut id_spare = HashMap::new(); + let mut key_spare = HashMap::new(); + loop { + let mut manager = ipcns.sem.lock(); + if let Some(id) = manager.lookup_semget(key, nsems, semflg)? { + return Ok(id); + } + if sems.is_empty() { + drop(manager); + sems = KernelSemSet::try_allocate_sems(nsems)?; + continue; + } + if let Err((id_capacity, key_capacity)) = + manager.install_create_tables(key, &mut id_spare, &mut key_spare) + { + drop(manager); + id_spare + .try_reserve(id_capacity) + .map_err(|_| SystemError::ENOMEM)?; + key_spare + .try_reserve(key_capacity) + .map_err(|_| SystemError::ENOMEM)?; + continue; + } + return manager.create_prepared(key, semflg, &mut sems); + } + } + + /// None means creation is currently permitted, not a reservation. Call + /// again after unlocked preparation to recheck key races and quotas. + fn lookup_semget( + &self, + key: SemKey, + nsems: usize, + semflg: SemFlags, + ) -> Result, SystemError> { + if nsems > SEMMSL { + return Err(SystemError::EINVAL); + } + + if key == IPC_PRIVATE { + self.validate_create(nsems)?; + return Ok(None); + } + + if let Some(&id) = self.key2id.get(&key) { + if semflg.contains(SemFlags::IPC_CREAT | SemFlags::IPC_EXCL) { + return Err(SystemError::EEXIST); + } + let set = self.get_by_semid_checked(id)?; + if nsems > set.nsems() { + return Err(SystemError::EINVAL); + } + let target_user_ns = ProcessManager::current_ipcns().user_ns.clone(); + ipc_perm::ipc_permission( + set.permissions(), + semflg.bits() & SemFlags::PERM_MASK.bits(), + &target_user_ns, + )?; + return Ok(Some(id.data())); + } + + if !semflg.contains(SemFlags::IPC_CREAT) { + return Err(SystemError::ENOENT); + } + self.validate_create(nsems)?; + Ok(None) + } + + fn validate_create(&self, nsems: usize) -> Result { + if nsems == 0 { + return Err(SystemError::EINVAL); + } + if self.id2sem.len() >= SEMMNI { + return Err(SystemError::ENOSPC); + } + let total_after = self + .total_sems + .checked_add(nsems) + .ok_or(SystemError::ENOSPC)?; + if total_after > SEMMNS { + return Err(SystemError::ENOSPC); + } + Ok(total_after) + } + + /// Install preallocated tables only when BOTH have enough room. Rehashing + /// still takes O(n) under the lock, but never allocates. The emptied old + /// tables remain in the caller's spares for disposal after unlocking. + fn install_create_tables( + &mut self, + key: SemKey, + id_spare: &mut HashMap, + key_spare: &mut HashMap, + ) -> Result<(), (usize, usize)> { + debug_assert!(id_spare.is_empty() && key_spare.is_empty()); + let grow_ids = self.id2sem.capacity() == self.id2sem.len(); + let grow_keys = key != IPC_PRIVATE && self.key2id.capacity() == self.key2id.len(); + let needed_ids = if grow_ids && id_spare.capacity() <= self.id2sem.len() { + self.id2sem.len().saturating_mul(2).max(4) + } else { + 0 + }; + let needed_keys = if grow_keys && key_spare.capacity() <= self.key2id.len() { + self.key2id.len().saturating_mul(2).max(4) + } else { + 0 + }; + if needed_ids != 0 || needed_keys != 0 { + return Err((needed_ids, needed_keys)); + } + if grow_ids { + core::mem::swap(&mut self.id2sem, id_spare); + for (id, set) in id_spare.drain() { + self.id2sem.insert(id, set); + } + } + if grow_keys { + core::mem::swap(&mut self.key2id, key_spare); + for (key, id) in key_spare.drain() { + self.key2id.insert(key, id); + } + } + Ok(()) + } + + fn create_prepared( + &mut self, + key: SemKey, + semflg: SemFlags, + sems: &mut Vec, + ) -> Result { + let total_after = self.validate_create(sems.len())?; + debug_assert!(self.id2sem.capacity() > self.id2sem.len()); + debug_assert!(key == IPC_PRIVATE || self.key2id.capacity() > self.key2id.len()); + let ipc_id = self.id_allocator.alloc()?; + let sem_id = SemId::new(ipc_id.raw); + let current_cred = ProcessManager::current_pcb().cred(); + let kern_ipc_perm = IpcPerm::new_with_cred( + sem_id.data(), + key.data(), + current_cred, + semflg.bits() & SemFlags::PERM_MASK.bits(), + ipc_id.seq, + ); + let set = KernelSemSet::new(kern_ipc_perm, core::mem::take(sems)); + + if key != IPC_PRIVATE { + self.key2id.insert(key, sem_id); + } + self.id2sem.insert(ipc_id.idx, set); + self.total_sems = total_after; + + Ok(sem_id.data()) + } + + #[cfg(test)] + pub(crate) fn semget_for_test( + &mut self, + key: SemKey, + nsems: usize, + flags: SemFlags, + ) -> Result { + if let Some(id) = self.lookup_semget(key, nsems, flags)? { + return Ok(id); + } + let mut sems = KernelSemSet::try_allocate_sems(nsems)?; + let mut ids = HashMap::new(); + let mut keys = HashMap::new(); + if let Err((id_capacity, key_capacity)) = + self.install_create_tables(key, &mut ids, &mut keys) + { + ids.try_reserve(id_capacity) + .map_err(|_| SystemError::ENOMEM)?; + keys.try_reserve(key_capacity) + .map_err(|_| SystemError::ENOMEM)?; + self.install_create_tables(key, &mut ids, &mut keys) + .unwrap(); + } + self.create_prepared(key, flags, &mut sems) + } +} + +impl SemManager { + pub(crate) fn unregister_undo_group(&mut self, semid: SemId, group: &Arc) { + if let Ok(set) = self.get_by_semid_checked_mut(semid) { + set.unregister_undo_group(group); + } + } + pub(crate) fn replay_sem_undo_adjustments( + &mut self, + semid: SemId, + adjustments: &[i16], + exiting_tgid: Option>, + wakes: &mut SemWakeBatch, + ) { + if let Ok(set) = self.get_by_semid_checked_mut(semid) { + set.replay_undo(adjustments, exiting_tgid, wakes); + } + } +} + +#[cfg(test)] +pub(in crate::ipc::sem) mod test_support; +#[cfg(test)] +mod tests; diff --git a/kernel/src/ipc/sem/manager/test_support.rs b/kernel/src/ipc/sem/manager/test_support.rs new file mode 100644 index 0000000000..d3eb83efb8 --- /dev/null +++ b/kernel/src/ipc/sem/manager/test_support.rs @@ -0,0 +1,110 @@ +use super::*; +use crate::ipc::sem::set::test_support::test_perm; +use alloc::sync::Weak; + +impl SemManager { + /// Test-only setup; production reserves registry storage before locking. + #[cfg(test)] + pub(in crate::ipc::sem) fn ensure_undo_group_registered( + &mut self, + group: &Arc, + semid: SemId, + ) -> Result<(), SystemError> { + let set = self.get_by_semid_checked_mut(semid)?; + let mut spare = Vec::new(); + if let Err(capacity) = set.ensure_undo_group_registered_prepared(group, &mut spare) { + spare + .try_reserve(capacity) + .map_err(|_| SystemError::ENOMEM)?; + set.ensure_undo_group_registered_prepared(group, &mut spare) + .unwrap(); + } + Ok(()) + } + + pub(in crate::ipc::sem) fn update_queue_for_test(&mut self, semid: SemId) { + let Ok(set) = self.get_by_semid_checked_mut(semid) else { + return; + }; + set.update_queue(&mut SemWakeBatch::default()); + } + + #[cfg(test)] + pub(in crate::ipc::sem) fn live_undo_group_count_for_test(&self) -> usize { + let mut groups: Vec> = Vec::new(); + for weak in self + .id2sem + .values() + .flat_map(|set| set.undo_groups_for_test()) + { + if weak.strong_count() != 0 && !groups.iter().any(|old| old.ptr_eq(weak)) { + groups.push(weak.clone()); + } + } + groups.len() + } + + #[cfg(test)] + pub(in crate::ipc::sem) fn undo_registry_contains_for_test( + &self, + group: &Arc, + ) -> bool { + self.id2sem + .values() + .flat_map(|set| set.undo_groups_for_test()) + .any(|weak| weak.ptr_eq(&Arc::downgrade(group))) + } + + #[cfg(test)] + pub(crate) fn namespace_lifecycle_invariant_for_test(&self) -> bool { + self.id2sem + .values() + .flat_map(|set| set.undo_groups_for_test()) + .all(|weak| { + weak.upgrade() + .is_none_or(|group| group.record_count_for_test() == 0) + }) + } + + #[cfg(test)] + pub(crate) fn prepare_undo_record_and_registry_for_test( + &mut self, + group: &Arc, + semid: SemId, + ) -> Result<(), SystemError> { + let nsems = self.validate_semid_nsems(semid)?; + let record = group.prepare_record(semid, nsems)?; + self.ensure_undo_group_registered(group, semid)?; + group.commit_prepared_record_noalloc(record) + } + + pub(in crate::ipc::sem) fn insert_test_set(&mut self, key: SemKey, vals: &[i32]) -> SemId { + let ipc_id = self.id_allocator.alloc().unwrap(); + let id = SemId::new(ipc_id.raw); + let set = KernelSemSet::new_for_test(test_perm(id, key, ipc_id.seq), vals); + self.key2id.insert(key, id); + self.id2sem.insert(ipc_id.idx, set); + self.total_sems += vals.len(); + id + } + pub(in crate::ipc::sem) fn remove_test_set(&mut self, id: SemId) { + let decoded = IpcIdAllocator::decode(id.data()).unwrap(); + let set = self.id2sem.remove(&decoded.idx).unwrap(); + self.key2id.remove(&SemKey::new(set.permissions().key)); + self.id_allocator.free_idx(decoded.idx); + self.total_sems = self.total_sems.saturating_sub(set.nsems()); + } + pub(in crate::ipc::sem) fn reset_allocator_for_test(&mut self, capacity: usize) { + self.id_allocator = IpcIdAllocator::new(capacity).unwrap(); + } + pub(in crate::ipc::sem) fn clear_undo_for_setval(&mut self, id: SemId, semnum: usize) { + if let Ok(set) = self.get_by_semid_checked_mut(id) { + set.clear_undo_for_setval(id, semnum); + } + } + pub(in crate::ipc::sem) fn clear_undo_for_setall(&mut self, id: SemId) { + if let Ok(set) = self.get_by_semid_checked_mut(id) { + set.clear_undo_for_setall(id); + } + } +} diff --git a/kernel/src/ipc/sem/manager/tests.rs b/kernel/src/ipc/sem/manager/tests.rs new file mode 100644 index 0000000000..168b79c415 --- /dev/null +++ b/kernel/src/ipc/sem/manager/tests.rs @@ -0,0 +1,165 @@ +use super::*; +use crate::ipc::sem::manager::SemManager; +use crate::ipc::sem::set::test_support::*; + +#[test] +fn new_manager_starts_with_empty_undo_registry() { + assert!(SemManager::new().id2sem.is_empty()); +} + +#[test] +fn create_tables_require_both_spares_and_recheck_growth() { + let mut manager = SemManager::new(); + let key = SemKey::new(153); + let mut ids = HashMap::new(); + let mut keys = HashMap::new(); + ids.try_reserve(4).unwrap(); + assert_eq!( + manager.install_create_tables(key, &mut ids, &mut keys), + Err((0, 4)) + ); + assert_eq!(manager.id2sem.capacity(), 0); + assert_eq!(manager.key2id.capacity(), 0); + keys.try_reserve(4).unwrap(); + manager + .install_create_tables(key, &mut ids, &mut keys) + .unwrap(); + while manager.id2sem.len() < manager.id2sem.capacity() { + let next_key = SemKey::new(200 + manager.id2sem.len()); + insert_test_set(&mut manager, next_key, &[3]); + } + let count = manager.id2sem.len(); + assert_eq!(manager.key2id.len(), count); + ids.try_reserve(count + 1).unwrap(); + keys.try_reserve(count + 1).unwrap(); + // Competing creations can consume the prepared headroom before the + // caller reacquires the lock. Neither live table may be moved yet. + while manager.id2sem.len() < ids.capacity() { + let next_key = SemKey::new(200 + manager.id2sem.len()); + insert_test_set(&mut manager, next_key, &[3]); + } + let live_count = manager.id2sem.len(); + let capacities = (manager.id2sem.capacity(), manager.key2id.capacity()); + let (need_ids, need_keys) = manager + .install_create_tables(key, &mut ids, &mut keys) + .unwrap_err(); + assert_eq!( + capacities, + (manager.id2sem.capacity(), manager.key2id.capacity()) + ); + assert_eq!(manager.id2sem.len(), live_count); + ids.try_reserve(need_ids).unwrap(); + keys.try_reserve(need_keys).unwrap(); + manager + .install_create_tables(key, &mut ids, &mut keys) + .unwrap(); + assert_eq!(manager.id2sem.len(), live_count); + assert_eq!(manager.key2id.len(), live_count); + assert!(manager.id2sem.capacity() > live_count); + assert!(manager.key2id.capacity() > live_count); + assert!(ids.is_empty() && keys.is_empty()); + assert_eq!((ids.capacity(), keys.capacity()), capacities); + for id in manager.key2id.values() { + assert_eq!( + manager + .get_by_semid_checked(*id) + .unwrap() + .get_value(0, SemCtlCmd::GetVal) + .unwrap(), + 3 + ); + } +} + +#[test] +fn semget_lookup_validates_before_preparing_storage() { + let mut manager = SemManager::new(); + let key = SemKey::new(154); + insert_test_set(&mut manager, key, &[0]); + assert_eq!( + manager.lookup_semget(key, SEMMSL + 1, SemFlags::IPC_CREAT | SemFlags::IPC_EXCL), + Err(SystemError::EINVAL) + ); + assert_eq!( + manager.lookup_semget(key, 2, SemFlags::IPC_CREAT | SemFlags::IPC_EXCL), + Err(SystemError::EEXIST) + ); + assert_eq!( + manager.lookup_semget(key, 2, SemFlags::empty()), + Err(SystemError::EINVAL) + ); + assert_eq!( + manager.lookup_semget(SemKey::new(155), 0, SemFlags::empty()), + Err(SystemError::ENOENT) + ); + assert_eq!( + manager.lookup_semget(SemKey::new(155), 0, SemFlags::IPC_CREAT), + Err(SystemError::EINVAL) + ); + assert_eq!( + manager.lookup_semget(IPC_PRIVATE, 1, SemFlags::IPC_EXCL), + Ok(None) + ); + manager.total_sems = SEMMNS; + assert_eq!( + manager.lookup_semget(IPC_PRIVATE, 1, SemFlags::empty()), + Err(SystemError::ENOSPC) + ); + assert_eq!( + manager.lookup_semget(IPC_PRIVATE, 0, SemFlags::empty()), + Err(SystemError::EINVAL) + ); +} + +#[test] +fn private_create_never_prepares_key_table() { + let mut manager = SemManager::new(); + let mut ids = HashMap::new(); + let mut keys = HashMap::new(); + assert_eq!( + manager.install_create_tables(IPC_PRIVATE, &mut ids, &mut keys), + Err((4, 0)) + ); + ids.try_reserve(4).unwrap(); + manager + .install_create_tables(IPC_PRIVATE, &mut ids, &mut keys) + .unwrap(); + assert!(manager.id2sem.capacity() > 0); + assert_eq!(manager.key2id.capacity(), 0); + assert_eq!(keys.capacity(), 0); +} + +#[test] +fn prepared_setall_token_returns_eidrm_after_rmid() { + let mut manager = SemManager::new(); + let id = insert_test_set(&mut manager, SemKey::new(11), &[1, 2]); + let token = SemSetAllToken::new(id, 2); + + remove_test_set(&mut manager, id); + + assert_eq!( + manager.setall(token, &[7, 8], &mut SemWakeBatch::default()), + Err(SystemError::EIDRM) + ); +} + +#[test] +fn stale_prepared_setall_token_does_not_modify_reused_index() { + let mut manager = SemManager::new(); + let old_id = insert_test_set(&mut manager, SemKey::new(21), &[1, 2]); + let token = SemSetAllToken::new(old_id, 2); + + remove_test_set(&mut manager, old_id); + let new_id = insert_test_set(&mut manager, SemKey::new(22), &[3, 4]); + assert_ne!(old_id, new_id); + assert_eq!( + old_id.data() & IpcIdAllocator::IPC_ID_IDX_MASK, + new_id.data() & IpcIdAllocator::IPC_ID_IDX_MASK + ); + + assert_eq!( + manager.setall(token, &[7, 8], &mut SemWakeBatch::default()), + Err(SystemError::EIDRM) + ); + assert_eq!(sem_values(&manager, new_id), vec![3, 4]); +} diff --git a/kernel/src/ipc/sem/mod.rs b/kernel/src/ipc/sem/mod.rs new file mode 100644 index 0000000000..2ae90c8c7f --- /dev/null +++ b/kernel/src/ipc/sem/mod.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +//! Linux-compatible System V semaphores. +//! +//! The namespace manager serializes set values, queues and undo associations. +//! Set execution is allocation-free; syscall workflow owns preparation and waits. +//! User copies, scheduler wakeups and retired storage disposal stay outside the +//! manager lock. With SEM_UNDO, acquire manager -> entry record -> group, never +//! acquire the manager while holding the group lock. +mod abi; +mod manager; +mod operation; +mod set; + +#[allow(unused_imports)] // Preserve the existing ipc::sem API across the module split. +pub use abi::{ + PosixSemBuf, PosixSemIdDs, PosixSemInfo, SemCtlCmd, SemFlags, SemId, SemKey, IPC_PRIVATE, + SEMMNI, SEMMNS, SEMMSL, SEMOPM, SEMVMX, +}; +pub use manager::SemManager; +#[allow(unused_imports)] // Named token remains available to callers of prepare_setall. +pub use manager::SemSetAllToken; +pub(crate) use set::SemWakeBatch; diff --git a/kernel/src/ipc/sem/operation.rs b/kernel/src/ipc/sem/operation.rs new file mode 100644 index 0000000000..5a301ff248 --- /dev/null +++ b/kernel/src/ipc/sem/operation.rs @@ -0,0 +1,302 @@ +//! semtimedop preparation, identity rechecks, waiting and cancellation. +use crate::{ + ipc::{ipc_perm, sem_undo::PreparedSemUndoRecordAction}, + libs::wait_queue::{TimeoutWaker, Waiter}, + process::{namespace::ipc_namespace::IpcNamespace, pid::PidType, ProcessManager}, + time::{ + timer::{clock, next_n_us_timer_jiffies, Timer}, + Duration, + }, +}; +use alloc::{sync::Arc, vec::Vec}; +use system_error::SystemError; + +use super::{ + abi::*, + manager::SemManager, + set::{SemAttempt, SemBlockedOp, SemQueueEntry, SemWaitType, SemWakeBatch, SemopScratch}, +}; +impl SemManager { + pub(super) fn cancel_queued_entry( + &mut self, + semid: SemId, + entry: &Arc, + error: SystemError, + ) -> Result { + if let Some(result) = entry.completed_result() { + return result; + } + + if let Ok(set) = self.get_by_semid_checked_mut(semid) { + if let Some(result) = entry.completed_result() { + return result; + } + if set.finish_waiter(entry, Err(error.clone())) { + return Err(error); + } + return entry + .completed_result() + .expect("completed semaphore queue entry lost its terminal result"); + } + + if let Some(result) = entry.completed_result() { + return result; + } + if entry.complete(Err(SystemError::EIDRM)) { + return Err(SystemError::EIDRM); + } + entry + .completed_result() + .expect("completed semaphore queue entry lost its terminal result") + } + + /// # semtimedop: execute `sops` atomically, blocking if necessary + /// + /// This function manages the lock internally (it must release it while waiting); + /// callers must not hold the `ipcns.sem` lock in advance. + /// + /// - `timeout == None`: wait indefinitely (equivalent to `semop`) + /// - `timeout == Some(Duration::ZERO)`: do not block + /// - Otherwise: block until timeout and return EAGAIN + pub fn semtimedop( + ipcns: &Arc, + semid: SemId, + sops: &[PosixSemBuf], + timeout: Option, + ) -> Result { + if sops.is_empty() { + return Err(SystemError::EINVAL); + } + if sops.len() > SEMOPM { + return Err(SystemError::E2BIG); + } + + let non_blocking = timeout == Some(Duration::ZERO); + let has_undo = sops + .iter() + .any(|op| (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() != 0); + // Check read permission only for all-zero waits; otherwise check write permission + // to match Linux semantics. + let alter = sops.iter().any(|op| op.sem_op != 0); + + let target_user_ns = ipcns.user_ns.clone(); + { + let guard = ipcns.sem.lock(); + let set = guard.get_by_semid_checked(semid)?; + // Match Linux: check semnum bounds (EFBIG) before permissions (EACCES). + if sops.iter().any(|op| op.sem_num as usize >= set.nsems()) { + return Err(SystemError::EFBIG); + } + ipc_perm::ipc_permission( + set.permissions(), + if alter { + Self::IPC_WRITE + } else { + Self::IPC_READ + }, + &target_user_ns, + )?; + } + + let deadline_ticks = timeout.map(|d| next_n_us_timer_jiffies(d.total_micros())); + let (waiter, waker) = Waiter::new_pair(); + let timer = + deadline_ticks.map(|deadline| Timer::new(TimeoutWaker::new(waker.clone()), deadline)); + + let current = ProcessManager::current_pcb(); + let pid = current.task_pid_ptr(PidType::TGID); + let undo_group = if has_undo { + Some(current.ensure_sem_undo_group(ipcns)?) + } else { + None + }; + let mut immediate_scratch = SemopScratch::try_new(sops.len())?; + let plain_prepared_entry = if has_undo { + None + } else { + Some( + Arc::try_new(SemQueueEntry::new_prepared( + SemQueueEntry::prepare_sops(sops)?, + pid.clone(), + None, + None, + waker.clone(), + SemopScratch::try_new(sops.len())?, + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }, + )) + .map_err(|_| SystemError::ENOMEM)?, + ) + }; + + let mut wakes = SemWakeBatch::default(); + let mut registry_spare = Vec::new(); + let mut registry_capacity_needed = 0; + let entry = loop { + // Revalidate after unlocked undo-registry preparation. + if registry_capacity_needed != 0 { + registry_spare + .try_reserve(registry_capacity_needed) + .map_err(|_| SystemError::ENOMEM)?; + registry_capacity_needed = 0; + } + let nsems = { + let guard = ipcns.sem.lock(); + let set = guard.get_by_semid_checked(semid)?; + // Match Linux: check semnum bounds (EFBIG) before permissions (EACCES). + if sops.iter().any(|op| op.sem_num as usize >= set.nsems()) { + return Err(SystemError::EFBIG); + } + ipc_perm::ipc_permission( + set.permissions(), + if alter { + Self::IPC_WRITE + } else { + Self::IPC_READ + }, + &target_user_ns, + )?; + set.nsems() + }; + + let prepared_undo = if let Some(group) = undo_group.as_ref() { + let record = group.prepare_record(semid, nsems)?; + let entry = Arc::try_new(SemQueueEntry::new_prepared( + SemQueueEntry::prepare_sops(sops)?, + pid.clone(), + Some(group.clone()), + Some(record), + waker.clone(), + SemopScratch::try_new(sops.len())?, + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }, + )) + .map_err(|_| SystemError::ENOMEM)?; + Some(entry) + } else { + None + }; + + let mut guard = ipcns.sem.lock(); + if guard.validate_semid_nsems(semid)? != nsems { + continue; + } + if let Some(prepared_entry) = prepared_undo { + let mut record_slot = prepared_entry.undo_record.lock_irqsave(); + let prepared_record = record_slot + .take() + .expect("prepared SEM_UNDO entry owns its record"); + if prepared_record.adjustment_count() != nsems { + return Err(SystemError::EINVAL); + } + // First-use candidates must be associated before zero-record + // publication. Queued operations retain Existing tokens only. + // Full semid was rechecked above, so Existing cannot refer to a + // destroyed/reused set. SETVAL/SETALL retain live associations. + let already_associated = prepared_record.was_existing(); + if !already_associated { + let set = guard.get_by_semid_checked_mut(semid)?; + if let Err(capacity) = set.ensure_undo_group_registered_prepared( + undo_group + .as_ref() + .expect("SEM_UNDO operation has a current group"), + &mut registry_spare, + ) { + registry_capacity_needed = capacity; + continue; + } + } + let set = guard.get_by_semid_checked_mut(semid)?; + let (outcome, kept_record) = undo_group + .as_ref() + .expect("SEM_UNDO operation has a current group") + .with_prepared_record_noalloc(prepared_record, |record| { + let outcome = + set.try_apply(sops, pid.clone(), Some(record), &mut immediate_scratch); + match outcome { + Ok(SemAttempt::Completed { .. }) => { + PreparedSemUndoRecordAction::Complete(Ok(None)) + } + Ok(SemAttempt::Blocked(blocker)) => { + PreparedSemUndoRecordAction::Keep(Ok(Some(blocker))) + } + Err(error) => PreparedSemUndoRecordAction::Keep(Err(error)), + } + })?; + *record_slot = kept_record; + match outcome? { + None => { + drop(record_slot); + set.update_queue(&mut wakes); + return Ok(0); + } + Some(blocker) => { + if blocker.nowait || non_blocking { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + if deadline_ticks.is_some_and(|deadline| clock() >= deadline) { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + drop(record_slot); + set.update_blocker(&prepared_entry, blocker); + set.enqueue_waiter(prepared_entry.clone()); + break prepared_entry; + } + } + } else { + let set = guard.get_by_semid_checked_mut(semid)?; + match set.try_apply(sops, pid.clone(), None, &mut immediate_scratch)? { + SemAttempt::Completed { .. } => { + set.update_queue(&mut wakes); + return Ok(0); + } + SemAttempt::Blocked(blocker) => { + if blocker.nowait || non_blocking { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + if deadline_ticks.is_some_and(|deadline| clock() >= deadline) { + return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); + } + let prepared_entry = plain_prepared_entry + .as_ref() + .expect("plain queued semop entry is preallocated"); + set.update_blocker(prepared_entry, blocker); + set.enqueue_waiter(prepared_entry.clone()); + break prepared_entry.clone(); + } + } + } + }; + + drop(registry_spare); + wakes.wake_all(); + if let Some(timer) = timer.as_ref() { + timer.activate(); + } + let _wait_result = waiter.wait(true); + let completed = entry.completed_result(); + let was_timeout = timer.as_ref().is_some_and(|timer| timer.timeout()); + if !was_timeout { + if let Some(timer) = timer.as_ref() { + timer.cancel(); + } + } + if let Some(result) = completed { + return result; + } + + let error = if was_timeout { + SystemError::EAGAIN_OR_EWOULDBLOCK + } else { + SystemError::EINTR + }; + let mut guard = ipcns.sem.lock(); + guard.cancel_queued_entry(semid, &entry, error) + } +} diff --git a/kernel/src/ipc/sem/set/mod.rs b/kernel/src/ipc/sem/set/mod.rs new file mode 100644 index 0000000000..7c4accefa3 --- /dev/null +++ b/kernel/src/ipc/sem/set/mod.rs @@ -0,0 +1,326 @@ +//! Set-owned values, metadata and undo associations. All mutation requires the manager lock. +use crate::{ + ipc::{ + ipc_perm::IpcPerm, + sem_undo::{ + PreparedSemUndoRecord, PreparedSemUndoRecordAction, SemUndoGroup, SemUndoRecord, + }, + }, + libs::{spinlock::SpinLock, wait_queue::Waker}, + process::{ + pid::{Pid, PidType}, + ProcessManager, + }, + time::PosixTimeSpec, +}; +use alloc::{ + sync::{Arc, Weak}, + vec::Vec, +}; +use system_error::SystemError; + +use super::abi::*; +mod operation; +mod queue; +pub(in crate::ipc::sem) use operation::{SemAttempt, SemBlockedOp, SemWaitType, SemopScratch}; +pub(in crate::ipc::sem) use queue::SemQueueEntry; +use queue::SemWaitQueue; +pub(crate) use queue::SemWakeBatch; + +/// A single semaphore (fields of Linux `struct sem`) +#[derive(Debug, Clone)] +pub struct KernelSem { + /// semval + val: i32, + /// sempid: process that last operated on this semaphore + pid: Option>, + /// Counts of queued operations currently blocked on this semaphore. + ncnt: usize, + zcnt: usize, +} + +/// A live undo registration, reused to own deferred cleanup after removal. +#[derive(Debug)] +pub(in crate::ipc::sem) enum SemUndoAssociation { + Group(Weak), + Retired { + _group: Arc, + _record: Option, + }, +} + +impl SemUndoAssociation { + /// Retired slots are only present in a removed, caller-owned set. + fn group(&self) -> &Weak { + match self { + Self::Group(group) => group, + Self::Retired { .. } => unreachable!("retired association in live set"), + } + } +} + +/// Semaphore set +#[derive(Debug)] +pub struct KernelSemSet { + /// Groups that can carry undo debt for this set, including queued operations. + undo_groups: Vec, + /// Permission information + kern_ipc_perm: IpcPerm, + /// Semaphores in the set + sems: Vec, + /// Time of the last `semop` + sem_otime: i64, + /// Time of the last metadata change + sem_ctime: i64, + /// Pending operation groups containing only zero-wait operations + pending_const: SemWaitQueue, + /// Pending operation groups containing at least one altering operation + pending_alter: SemWaitQueue, +} + +impl KernelSemSet { + /// Live associations survive SETVAL/SETALL. Only RMID or dead groups + /// remove them, so an existing undo record proves prior association. + /// Insufficient spare capacity requests an unlocked preparation/retry. + /// On success, spare retains any replaced allocation for unlocked disposal. + pub(in crate::ipc::sem) fn ensure_undo_group_registered_prepared( + &mut self, + group: &Arc, + spare: &mut Vec, + ) -> Result<(), usize> { + debug_assert!(spare.is_empty()); + let candidate = Arc::downgrade(group); + if self + .undo_groups + .iter() + .any(|entry| entry.group().ptr_eq(&candidate)) + { + return Ok(()); + } + if self.undo_groups.len() == self.undo_groups.capacity() { + self.compact_undo_registry(); + } + if self.undo_groups.len() == self.undo_groups.capacity() { + if spare.capacity() <= self.undo_groups.len() { + return Err(self.undo_groups.len().saturating_mul(2).max(4)); + } + spare.append(&mut self.undo_groups); + core::mem::swap(&mut self.undo_groups, spare); + } + self.undo_groups.push(SemUndoAssociation::Group(candidate)); + Ok(()) + } + + fn compact_undo_registry(&mut self) { + self.undo_groups + .retain(|entry| entry.group().strong_count() != 0); + } + + /// Request/publish smaller storage without allocating under the manager + /// lock. Empty registries are moved into retired without installing spare. + /// The caller must drop both buffers after unlocking. + pub(in crate::ipc::sem) fn shrink_undo_registry_prepared( + &mut self, + spare: &mut Vec, + retired: &mut Vec, + ) -> usize { + debug_assert!(spare.is_empty()); + debug_assert!(retired.is_empty() && retired.capacity() == 0); + let len = self.undo_groups.len(); + if len == 0 { + core::mem::swap(&mut self.undo_groups, retired); + return 0; + } + if self.undo_groups.capacity() <= 4 || len > self.undo_groups.capacity() / 4 { + return 0; + } + if spare.capacity() < len { + return len.saturating_mul(2).max(4); + } + if spare.capacity() < self.undo_groups.capacity() { + spare.append(&mut self.undo_groups); + core::mem::swap(&mut self.undo_groups, spare); + } + 0 + } + + pub(in crate::ipc::sem) fn try_allocate_sems( + nsems: usize, + ) -> Result, SystemError> { + let mut sems = Vec::new(); + sems.try_reserve_exact(nsems) + .map_err(|_| SystemError::ENOMEM)?; + sems.resize( + nsems, + KernelSem { + val: 0, + pid: None, + ncnt: 0, + zcnt: 0, + }, + ); + Ok(sems) + } + + pub(in crate::ipc::sem) fn new(kern_ipc_perm: IpcPerm, sems: Vec) -> Self { + KernelSemSet { + undo_groups: Vec::new(), + kern_ipc_perm, + sems, + sem_otime: 0, + sem_ctime: PosixTimeSpec::now().tv_sec, + pending_const: SemWaitQueue::default(), + pending_alter: SemWaitQueue::default(), + } + } +} + +impl KernelSemSet { + pub(in crate::ipc::sem) fn permissions(&self) -> &IpcPerm { + &self.kern_ipc_perm + } + pub(in crate::ipc::sem) fn nsems(&self) -> usize { + self.sems.len() + } + pub(in crate::ipc::sem) fn stat( + &self, + sem_perm: crate::ipc::ipc_perm::PosixIpcPerm, + ) -> PosixSemIdDs { + PosixSemIdDs::new(sem_perm, self.sem_otime, self.sem_ctime, self.nsems()) + } + pub(in crate::ipc::sem) fn set_permissions( + &mut self, + data: PosixSemIdDs, + ) -> Result<(), SystemError> { + self.kern_ipc_perm.copy_from_posix( + data.sem_perm.uid(), + data.sem_perm.gid(), + data.sem_perm.mode(), + &ProcessManager::current_user_ns(), + )?; + self.sem_ctime = PosixTimeSpec::now().tv_sec; + Ok(()) + } + pub(in crate::ipc::sem) fn get_value( + &self, + semnum: usize, + cmd: SemCtlCmd, + ) -> Result { + match cmd { + SemCtlCmd::GetVal => Ok(self.sems[semnum].val as usize), + SemCtlCmd::GetPid => Ok(self.sems[semnum] + .pid + .as_ref() + .map(|pid| pid.pid_vnr().data()) + .unwrap_or(0)), + SemCtlCmd::GetNcnt => Ok(self.sems[semnum].ncnt), + SemCtlCmd::GetZcnt => Ok(self.sems[semnum].zcnt), + _ => Err(SystemError::EINVAL), + } + } + pub(in crate::ipc::sem) fn values(&self) -> Result, SystemError> { + let mut vals = Vec::new(); + vals.try_reserve_exact(self.nsems()) + .map_err(|_| SystemError::ENOMEM)?; + vals.extend(self.sems.iter().map(|s| s.val as u16)); + Ok(vals) + } + pub(in crate::ipc::sem) fn unregister_undo_group(&mut self, group: &Arc) { + let target = Arc::downgrade(group); + self.undo_groups + .retain(|entry| !entry.group().ptr_eq(&target)); + } + pub(in crate::ipc::sem) fn retire_undo_records(&mut self, id: SemId) { + // Removed set owns all disposal until the caller releases the manager lock. + for association in &mut self.undo_groups { + if let Some(group) = association.group().upgrade() { + let record = group.take_record(id); + *association = SemUndoAssociation::Retired { + _group: group, + _record: record, + }; + } + } + } + pub(in crate::ipc::sem) fn clear_undo_for_setval(&mut self, semid: SemId, semnum: usize) { + let mut saw_stale = false; + for weak in self.undo_groups.iter().map(SemUndoAssociation::group) { + let Some(group) = weak.upgrade() else { + saw_stale = true; + continue; + }; + group.with_record_mut(semid, |record| { + if semnum < record.adjustment_count() { + record.clear_adjustment(semnum); + } + }); + } + if saw_stale { + self.compact_undo_registry(); + } + } + pub(in crate::ipc::sem) fn clear_undo_for_setall(&mut self, semid: SemId) { + let mut saw_stale = false; + for weak in self.undo_groups.iter().map(SemUndoAssociation::group) { + let Some(group) = weak.upgrade() else { + saw_stale = true; + continue; + }; + group.with_record_mut(semid, |record| record.clear_all_adjustments()); + } + if saw_stale { + self.compact_undo_registry(); + } + } + pub(in crate::ipc::sem) fn setval( + &mut self, + id: SemId, + semnum: usize, + val: i32, + wakes: &mut SemWakeBatch, + ) { + self.clear_undo_for_setval(id, semnum); + let sem = &mut self.sems[semnum]; + sem.val = val; + sem.pid = ProcessManager::current_pcb().task_pid_ptr(PidType::TGID); + self.sem_ctime = PosixTimeSpec::now().tv_sec; + self.update_queue(wakes); + } + pub(in crate::ipc::sem) fn setall( + &mut self, + id: SemId, + vals: &[u16], + wakes: &mut SemWakeBatch, + ) { + self.clear_undo_for_setall(id); + let pid = ProcessManager::current_pcb().task_pid_ptr(PidType::TGID); + for (i, &v) in vals.iter().enumerate() { + let sem = &mut self.sems[i]; + sem.val = v as i32; + sem.pid = pid.clone(); + } + self.sem_ctime = PosixTimeSpec::now().tv_sec; + self.update_queue(wakes); + } + pub(in crate::ipc::sem) fn replay_undo( + &mut self, + adjustments: &[i16], + exiting_tgid: Option>, + wakes: &mut SemWakeBatch, + ) { + for (sem, adjustment) in self.sems.iter_mut().zip(adjustments.iter().copied()) { + if adjustment == 0 { + continue; + } + sem.val = (sem.val as i64 + adjustment as i64).clamp(0, SEMVMX as i64) as i32; + sem.pid = exiting_tgid.clone(); + } + self.sem_otime = PosixTimeSpec::now().tv_sec; + self.update_queue(wakes); + } +} + +#[cfg(test)] +pub(in crate::ipc::sem) mod test_support; +#[cfg(test)] +mod tests; diff --git a/kernel/src/ipc/sem/set/operation.rs b/kernel/src/ipc/sem/set/operation.rs new file mode 100644 index 0000000000..f1a4951887 --- /dev/null +++ b/kernel/src/ipc/sem/set/operation.rs @@ -0,0 +1,214 @@ +//! Allocation-free atomic attempt: simulation and commit cannot be separated by callers. +use super::*; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::ipc::sem) enum SemWaitType { + /// `sem_op < 0`: wait for semval to increase (GETNCNT) + Increase, + /// `sem_op == 0`: wait for semval to reach zero (GETZCNT) + Zero, +} + +/// The precise operation currently blocking an operation group. +#[derive(Debug, Clone, Copy)] +pub(in crate::ipc::sem) struct SemBlockedOp { + pub(in crate::ipc::sem) semnum: usize, + pub(in crate::ipc::sem) wait_type: SemWaitType, + pub(in crate::ipc::sem) nowait: bool, +} +#[derive(Debug)] +struct SemopScratchEntry { + semnum: usize, + initial_val: i32, + virtual_val: i32, + initial_adj: i16, + virtual_adj: i16, +} + +#[derive(Debug)] +pub(in crate::ipc::sem) struct SemopScratch { + entries: Vec, +} + +impl SemopScratch { + pub(in crate::ipc::sem) fn try_new(capacity: usize) -> Result { + let mut entries = Vec::new(); + entries + .try_reserve_exact(capacity) + .map_err(|_| SystemError::ENOMEM)?; + Ok(Self { entries }) + } + + fn clear(&mut self) { + self.entries.clear(); + } + + fn entry_for( + &mut self, + set: &KernelSemSet, + semnum: usize, + undo: Option<&SemUndoRecord>, + ) -> Result<&mut SemopScratchEntry, SystemError> { + if let Some(index) = self.entries.iter().position(|entry| entry.semnum == semnum) { + return Ok(&mut self.entries[index]); + } + + if self.entries.len() == self.entries.capacity() { + return Err(SystemError::ENOMEM); + } + + let initial_val = set.sems[semnum].val; + let initial_adj = undo + .map(|record| record.adjustment(semnum)) + .unwrap_or_default(); + self.entries.push(SemopScratchEntry { + semnum, + initial_val, + virtual_val: initial_val, + initial_adj, + virtual_adj: initial_adj, + }); + Ok(self + .entries + .last_mut() + .expect("SEM_UNDO scratch entry was just inserted")) + } +} + +/// Fixed-capacity virtual semaphore state produced by `SemopScratch`. +#[derive(Debug)] +struct SemopSimulation { + entry_count: usize, +} + +impl SemopSimulation { + #[cfg(test)] + fn empty_for_test() -> Self { + Self { entry_count: 0 } + } +} + +/// Result of an attempted `semop` execution +#[derive(Debug)] +enum SemopOutcome { + Ready(SemopSimulation), + Blocked(SemBlockedOp), +} + +impl SemopOutcome { + #[cfg(test)] + fn ready_for_test(self) -> SemopSimulation { + match self { + Self::Ready(simulation) => simulation, + Self::Blocked(_) => panic!("expected ready semop outcome"), + } + } +} + +impl KernelSemSet { + /// Simulate sops in order without changing shared semaphore values or undo records. + fn simulate_semop( + set: &KernelSemSet, + sops: &[PosixSemBuf], + undo: Option<&mut SemUndoRecord>, + scratch: &mut SemopScratch, + ) -> Result { + scratch.clear(); + + for op in sops { + let idx = op.sem_num as usize; + if idx >= set.sems.len() { + return Err(SystemError::EFBIG); + } + + let has_undo = (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() != 0; + let entry = scratch.entry_for(set, idx, undo.as_deref())?; + let current = entry.virtual_val; + if op.sem_op == 0 { + if current != 0 { + return Ok(SemopOutcome::Blocked(SemBlockedOp { + semnum: idx, + wait_type: SemWaitType::Zero, + nowait: (op.sem_flg as u32) & SemFlags::IPC_NOWAIT.bits() != 0, + })); + } + continue; + } + + let result = current as i64 + op.sem_op as i64; + if result > SEMVMX as i64 { + return Err(SystemError::ERANGE); + } + if result < 0 { + return Ok(SemopOutcome::Blocked(SemBlockedOp { + semnum: idx, + wait_type: SemWaitType::Increase, + nowait: (op.sem_flg as u32) & SemFlags::IPC_NOWAIT.bits() != 0, + })); + } + + if has_undo { + let next_adj = entry.virtual_adj as i32 - op.sem_op as i32; + if !(i16::MIN as i32..=i16::MAX as i32).contains(&next_adj) { + return Err(SystemError::ERANGE); + } + entry.virtual_adj = next_adj as i16; + } + entry.virtual_val = result as i32; + } + + Ok(SemopOutcome::Ready(SemopSimulation { + entry_count: scratch.entries.len(), + })) + } + + /// Commit a successful simulation while the manager lock is held. + fn commit_semop( + set: &mut KernelSemSet, + simulation: SemopSimulation, + scratch: &SemopScratch, + pid: Option>, + mut undo: Option<&mut SemUndoRecord>, + ) -> bool { + let mut values_changed = false; + for entry in scratch.entries.iter().take(simulation.entry_count) { + let sem = &mut set.sems[entry.semnum]; + values_changed |= entry.initial_val != entry.virtual_val; + sem.val = entry.virtual_val; + sem.pid = pid.clone(); + if entry.virtual_adj != entry.initial_adj { + if let Some(record) = undo.as_deref_mut() { + record.set_adjustment(entry.semnum, entry.virtual_adj); + } + } + } + set.sem_otime = PosixTimeSpec::now().tv_sec; + values_changed + } +} + +#[derive(Debug)] +pub(in crate::ipc::sem) enum SemAttempt { + Completed { values_changed: bool }, + Blocked(SemBlockedOp), +} +impl KernelSemSet { + /// The caller holds the manager lock and (for SEM_UNDO) the current group record. + /// Does not allocate, queue, wake or authorize. Failed/blocked attempts never commit. + pub(in crate::ipc::sem) fn try_apply( + &mut self, + sops: &[PosixSemBuf], + pid: Option>, + mut undo: Option<&mut SemUndoRecord>, + scratch: &mut SemopScratch, + ) -> Result { + match Self::simulate_semop(self, sops, undo.as_deref_mut(), scratch)? { + SemopOutcome::Ready(simulation) => Ok(SemAttempt::Completed { + values_changed: Self::commit_semop(self, simulation, scratch, pid, undo), + }), + SemopOutcome::Blocked(blocker) => Ok(SemAttempt::Blocked(blocker)), + } + } +} + +#[cfg(test)] +mod tests; diff --git a/kernel/src/ipc/sem/set/operation/tests.rs b/kernel/src/ipc/sem/set/operation/tests.rs new file mode 100644 index 0000000000..f883de6a03 --- /dev/null +++ b/kernel/src/ipc/sem/set/operation/tests.rs @@ -0,0 +1,281 @@ +use super::*; + +#[test] +fn try_apply_commits_metadata_even_when_values_are_unchanged() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(301), &[4]); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + set.sem_otime = -1; + let mut scratch = SemopScratch::try_new(2).unwrap(); + assert!(matches!( + set.try_apply( + &[plain_sop(0, -1), plain_sop(0, 1)], + None, + None, + &mut scratch + ), + Ok(SemAttempt::Completed { + values_changed: false + }) + )); + assert_eq!(set.sems[0].val, 4); + assert_ne!(set.sem_otime, -1); +} + +#[test] +fn try_apply_rebuilds_scratch_after_a_blocked_attempt() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(302), &[0]); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + set.sem_otime = -1; + let mut scratch = SemopScratch::try_new(1).unwrap(); + assert!(matches!( + set.try_apply(&[plain_sop(0, -1)], None, None, &mut scratch), + Ok(SemAttempt::Blocked(_)) + )); + assert_eq!(set.sem_otime, -1); + set.sems[0].val = 1; + assert!(matches!( + set.try_apply(&[plain_sop(0, -1)], None, None, &mut scratch), + Ok(SemAttempt::Completed { + values_changed: true + }) + )); + assert_eq!(set.sems[0].val, 0); +} +use crate::ipc::sem::manager::SemManager; +use crate::ipc::sem::set::test_support::*; +use crate::process::namespace::ipc_namespace::INIT_IPC_NAMESPACE; + +#[test] +fn setval_clear_between_prepare_and_commit_refreshes_stale_existing_record() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(62), &[4]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[7]); + manager.ensure_undo_group_registered(&group, semid).unwrap(); + + let record = group.prepare_record_for_test(semid, 1).unwrap(); + manager.clear_undo_for_setval(semid, 0); + let mut scratch = SemopScratch::try_new(1).unwrap(); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + + let result = group.with_prepared_record_noalloc(record, |record| { + let outcome = + KernelSemSet::simulate_semop(set, &[undo_sop(0, -1)], Some(record), &mut scratch) + .unwrap() + .ready_for_test(); + KernelSemSet::commit_semop(set, outcome, &scratch, None, Some(record)); + PreparedSemUndoRecordAction::Complete(()) + }); + + assert!(result.is_ok()); + assert_eq!(sem_values(&manager, semid), vec![3]); + assert_eq!(group.adjustment_for_test(semid, 0), 1); +} + +#[test] +fn setall_clear_between_prepare_and_commit_refreshes_stale_existing_record() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(63), &[4, 5]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[7, -3]); + manager.ensure_undo_group_registered(&group, semid).unwrap(); + + let record = group.prepare_record_for_test(semid, 2).unwrap(); + manager.clear_undo_for_setall(semid); + let mut scratch = SemopScratch::try_new(1).unwrap(); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + + let result = group.with_prepared_record_noalloc(record, |record| { + let outcome = + KernelSemSet::simulate_semop(set, &[undo_sop(0, -1)], Some(record), &mut scratch) + .unwrap() + .ready_for_test(); + KernelSemSet::commit_semop(set, outcome, &scratch, None, Some(record)); + PreparedSemUndoRecordAction::Complete(()) + }); + + assert!(result.is_ok()); + assert_eq!(sem_values(&manager, semid), vec![3, 5]); + assert_eq!(group.adjustment_for_test(semid, 0), 1); + assert_eq!(group.adjustment_for_test(semid, 1), 0); +} + +#[test] +fn stale_existing_prepared_record_refreshes_before_immediate_commit() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(65), &[4]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[7]); + manager.ensure_undo_group_registered(&group, semid).unwrap(); + + let record = group.prepare_record_for_test(semid, 1).unwrap(); + manager.clear_undo_for_setval(semid, 0); + let mut scratch = SemopScratch::try_new(1).unwrap(); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + + let result = group.with_prepared_record_noalloc(record, |record| { + let outcome = + KernelSemSet::simulate_semop(set, &[undo_sop(0, -1)], Some(record), &mut scratch) + .unwrap() + .ready_for_test(); + KernelSemSet::commit_semop(set, outcome, &scratch, None, Some(record)); + PreparedSemUndoRecordAction::Complete(()) + }); + + assert!(result.is_ok()); + assert_eq!(sem_values(&manager, semid), vec![3]); + assert_eq!(group.adjustment_for_test(semid, 0), 1); +} + +#[test] +fn consecutive_sem_undo_on_unchanged_existing_record_still_accumulates() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(64), &[5]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[2]); + + let record = group.prepare_record_for_test(semid, 1).unwrap(); + let mut scratch = SemopScratch::try_new(1).unwrap(); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + group + .with_prepared_record_noalloc(record, |record| { + let outcome = + KernelSemSet::simulate_semop(set, &[undo_sop(0, -2)], Some(record), &mut scratch) + .unwrap() + .ready_for_test(); + KernelSemSet::commit_semop(set, outcome, &scratch, None, Some(record)); + PreparedSemUndoRecordAction::Complete(()) + }) + .unwrap(); + + assert_eq!(group.adjustment_for_test(semid, 0), 4); +} + +#[test] +fn ordered_mixed_undo_ops_apply_each_adjustment_step() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(41), &[4]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let record = group.prepare_record_for_test(semid, 1).unwrap(); + let mut scratch = SemopScratch::try_new(3).unwrap(); + let sops = [undo_sop(0, 3), plain_sop(0, -1), undo_sop(0, -2)]; + + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + group + .with_prepared_record_noalloc(record, |record| { + let outcome = KernelSemSet::simulate_semop(set, &sops, Some(record), &mut scratch); + assert!(matches!(outcome, Ok(SemopOutcome::Ready(_)))); + KernelSemSet::commit_semop( + set, + outcome.unwrap().ready_for_test(), + &scratch, + None, + Some(record), + ); + PreparedSemUndoRecordAction::Complete(()) + }) + .unwrap(); + + assert_eq!(set.sems[0].val, 4); + assert_eq!(group.adjustment_for_test(semid, 0), -1); +} + +#[test] +fn intermediate_adjustment_overflow_is_erange_even_if_later_op_cancels_it() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(42), &[10]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[i16::MAX]); + let record = group.prepare_record_for_test(semid, 1).unwrap(); + let mut scratch = SemopScratch::try_new(2).unwrap(); + let sops = [undo_sop(0, -1), undo_sop(0, 1)]; + + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + group + .with_prepared_record_noalloc(record, |record| { + assert!(matches!( + KernelSemSet::simulate_semop(set, &sops, Some(record), &mut scratch), + Err(SystemError::ERANGE) + )); + PreparedSemUndoRecordAction::Keep(()) + }) + .unwrap(); + + assert_eq!(set.sems[0].val, 10); + assert_eq!(group.adjustment_for_test(semid, 0), i16::MAX); +} + +#[test] +fn blocked_or_nowait_failure_does_not_commit_semval_or_semadj_prefix() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(43), &[2]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + // Exercise the live record, not just an unpublished candidate. + group.insert_test_record(semid, &[0]); + let record = group.prepare_record_for_test(semid, 1).unwrap(); + let mut scratch = SemopScratch::try_new(2).unwrap(); + let sops = [undo_sop(0, -1), nowait_sop(0, -2)]; + + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + group + .with_prepared_record_noalloc(record, |record| { + assert!(matches!( + KernelSemSet::simulate_semop(set, &sops, Some(record), &mut scratch), + Ok(SemopOutcome::Blocked(SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: true, + })) + )); + PreparedSemUndoRecordAction::Keep(()) + }) + .unwrap(); + + assert_eq!(set.sems[0].val, 2); + assert_eq!(group.adjustment_for_test(semid, 0), 0); +} + +#[test] +fn zero_undo_op_can_prepare_zero_record_without_adjustment() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(44), &[0]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let record = group.prepare_record_for_test(semid, 1).unwrap(); + let mut scratch = SemopScratch::try_new(1).unwrap(); + let sops = [undo_sop(0, 0)]; + + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + group + .with_prepared_record_noalloc(record, |record| { + let outcome = KernelSemSet::simulate_semop(set, &sops, Some(record), &mut scratch); + assert!(matches!(outcome, Ok(SemopOutcome::Ready(_)))); + KernelSemSet::commit_semop( + set, + outcome.unwrap().ready_for_test(), + &scratch, + None, + Some(record), + ); + PreparedSemUndoRecordAction::Complete(()) + }) + .unwrap(); + + assert_eq!(set.sems[0].val, 0); + assert_eq!(group.record_count_for_test(), 1); +} + +#[test] +fn scratch_entry_for_returns_enomem_instead_of_extending_past_capacity() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(45), &[1, 1]); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let mut scratch = SemopScratch::try_new(1).unwrap(); + let sops = [plain_sop(0, -1), plain_sop(1, -1)]; + + assert!(matches!( + KernelSemSet::simulate_semop(set, &sops, None, &mut scratch), + Err(SystemError::ENOMEM) + )); +} diff --git a/kernel/src/ipc/sem/set/queue.rs b/kernel/src/ipc/sem/set/queue.rs new file mode 100644 index 0000000000..a6f0d1be36 --- /dev/null +++ b/kernel/src/ipc/sem/set/queue.rs @@ -0,0 +1,570 @@ +//! Intrusive pending queues, cached wait counts and one-way completion publication. +use super::*; +#[derive(Debug)] +enum SemQueueStatus { + Queued { + blocker: SemBlockedOp, + links: Option, + }, + Completed { + result: Result, + next_wake: Option>, + }, +} + +#[derive(Debug)] +struct SemWaitLinks { + prev: Option>, + next: Option>, +} + +/// Set-private FIFO. All structural changes require the namespace manager lock. +/// Strong forward / weak backward links avoid cycles; each operation holds at +/// most one entry status lock at a time. +#[derive(Debug, Default)] +pub(super) struct SemWaitQueue { + head: Option>, + tail: Option>, +} + +impl SemWaitQueue { + fn push_back(&mut self, entry: Arc) { + { + let mut status = entry.status.lock(); + let SemQueueStatus::Queued { links, .. } = &mut *status else { + panic!("cannot enqueue completed semaphore operation"); + }; + assert!(links.is_none(), "semaphore operation already linked"); + *links = Some(SemWaitLinks { + prev: self.tail.as_ref().map(Arc::downgrade), + next: None, + }); + } + if let Some(tail) = self.tail.take() { + let mut status = tail.status.lock(); + let SemQueueStatus::Queued { + links: Some(links), .. + } = &mut *status + else { + panic!("semaphore queue tail is not linked"); + }; + links.next = Some(entry.clone()); + } else { + self.head = Some(entry.clone()); + } + self.tail = Some(entry); + } + + fn remove(&mut self, entry: &Arc) -> bool { + let links = { + let mut status = entry.status.lock(); + match &mut *status { + SemQueueStatus::Queued { links, .. } => links.take(), + SemQueueStatus::Completed { .. } => None, + } + }; + let Some(SemWaitLinks { prev, next }) = links else { + return false; + }; + let prev = prev.map(|weak| weak.upgrade().expect("linked predecessor is live")); + if let Some(prev) = prev.as_ref() { + let mut status = prev.status.lock(); + let SemQueueStatus::Queued { + links: Some(links), .. + } = &mut *status + else { + panic!("semaphore predecessor is not linked"); + }; + links.next = next.clone(); + } else { + debug_assert!(self + .head + .as_ref() + .is_some_and(|head| Arc::ptr_eq(head, entry))); + self.head = next.clone(); + } + if let Some(next) = next { + let mut status = next.status.lock(); + let SemQueueStatus::Queued { + links: Some(links), .. + } = &mut *status + else { + panic!("semaphore successor is not linked"); + }; + links.prev = prev.as_ref().map(Arc::downgrade); + } else { + self.tail = prev; + } + true + } + + fn iter(&self) -> SemWaitIter { + SemWaitIter(self.head.clone()) + } + + #[cfg(test)] + fn is_empty(&self) -> bool { + self.head.is_none() + } +} + +impl Drop for SemWaitQueue { + fn drop(&mut self) { + // Detach iteratively, including namespace teardown, never recursively + // destroy an arbitrarily long chain of Arc-owned entries. + while let Some(entry) = self.head.clone() { + self.remove(&entry); + } + } +} + +struct SemWaitIter(Option>); + +impl Iterator for SemWaitIter { + type Item = Arc; + + fn next(&mut self) -> Option { + let entry = self.0.take()?; + self.0 = match &*entry.status.lock() { + SemQueueStatus::Queued { + links: Some(links), .. + } => links.next.clone(), + _ => None, + }; + Some(entry) + } +} + +/// An Arc-owned queue entry shared by the set and the blocked caller. +#[derive(Debug)] +pub(in crate::ipc::sem) struct SemQueueEntry { + sops: Vec, + pid: Option>, + undo_group: Option>, + pub(in crate::ipc::sem) undo_record: SpinLock>, + waker: Arc, + scratch: SpinLock, + status: SpinLock, +} + +impl SemQueueEntry { + pub(in crate::ipc::sem) fn new_prepared( + sops: Vec, + pid: Option>, + undo_group: Option>, + undo_record: Option, + waker: Arc, + scratch: SemopScratch, + blocker: SemBlockedOp, + ) -> Self { + debug_assert_eq!( + undo_group.is_some(), + undo_record.is_some(), + "queued SEM_UNDO group and prepared record must be captured together" + ); + debug_assert!( + undo_group.is_some() + || sops + .iter() + .all(|op| (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() == 0), + "queued SEM_UNDO entry requires a captured undo group" + ); + Self { + scratch: SpinLock::new(scratch), + sops, + pid, + undo_group, + undo_record: SpinLock::new(undo_record), + waker, + status: SpinLock::new(SemQueueStatus::Queued { + blocker, + links: None, + }), + } + } + + pub(in crate::ipc::sem) fn prepare_sops( + sops: &[PosixSemBuf], + ) -> Result, SystemError> { + let mut owned_sops = Vec::new(); + owned_sops + .try_reserve_exact(sops.len()) + .map_err(|_| SystemError::ENOMEM)?; + owned_sops.extend_from_slice(sops); + Ok(owned_sops) + } + + #[cfg(test)] + pub(in crate::ipc::sem) fn new( + sops: &[PosixSemBuf], + pid: Option>, + waker: Arc, + blocker: SemBlockedOp, + ) -> Self { + Self::new_prepared( + Self::prepare_sops(sops).unwrap(), + pid, + None, + None, + waker, + SemopScratch::try_new(sops.len()).unwrap(), + blocker, + ) + } + + pub(in crate::ipc::sem) fn completed_result(&self) -> Option> { + match &*self.status.lock() { + SemQueueStatus::Queued { .. } => None, + SemQueueStatus::Completed { result, .. } => Some(result.clone()), + } + } + + pub(in crate::ipc::sem) fn complete(&self, result: Result) -> bool { + let mut status = self.status.lock(); + if matches!(&*status, SemQueueStatus::Completed { .. }) { + return false; + } + assert!( + matches!(&*status, SemQueueStatus::Queued { links: None, .. }), + "semaphore operation must be unlinked before completion" + ); + *status = SemQueueStatus::Completed { + result, + next_wake: None, + }; + true + } + + #[cfg(test)] + fn is_waiting_on(&self, semnum: usize, wait_type: SemWaitType) -> bool { + matches!( + &*self.status.lock(), + SemQueueStatus::Queued { blocker, .. } + if blocker.semnum == semnum && blocker.wait_type == wait_type + ) + } +} + +/// Per-operation completion list. The existing entry status lock protects the +/// link; no allocation or scheduler operation is needed while the set is locked. +/// Declare this before manager guards so all exits wake only after unlocking. +#[derive(Default)] +pub(crate) struct SemWakeBatch { + head: Option>, + tail: Option>, +} + +impl SemWakeBatch { + fn push_completed(&mut self, entry: Arc) { + debug_assert!(entry.completed_result().is_some()); + if let Some(tail) = self.tail.take() { + if let SemQueueStatus::Completed { next_wake, .. } = &mut *tail.status.lock() { + *next_wake = Some(entry.clone()); + } + } else { + self.head = Some(entry.clone()); + } + self.tail = Some(entry); + } + + pub(crate) fn wake_all(&mut self) { + self.tail = None; + while let Some(entry) = self.head.take() { + self.head = match &mut *entry.status.lock() { + SemQueueStatus::Completed { next_wake, .. } => next_wake.take(), + SemQueueStatus::Queued { .. } => unreachable!("wake batch contains queued entry"), + }; + // Release the status lock before entering the scheduler. Detaching + // each link also prevents recursive Arc destruction for large batches. + entry.waker.wake(); + } + } +} + +impl Drop for SemWakeBatch { + fn drop(&mut self) { + self.wake_all(); + } +} + +/// Selects one of the set-global pending queues. +#[derive(Debug, Clone, Copy)] +enum SemPendingQueue { + Const, + Alter, +} + +impl KernelSemSet { + fn pending_queue_for(sops: &[PosixSemBuf]) -> SemPendingQueue { + if sops.iter().any(|op| op.sem_op != 0) { + SemPendingQueue::Alter + } else { + SemPendingQueue::Const + } + } + + /// The prepared entry itself owns the queue links; publication cannot allocate. + pub(in crate::ipc::sem) fn enqueue_waiter(&mut self, waiter: Arc) { + let blocker = match &*waiter.status.lock() { + SemQueueStatus::Queued { + blocker, + links: None, + } => *blocker, + _ => panic!("enqueue requires an unlinked semaphore operation"), + }; + let queue = match Self::pending_queue_for(&waiter.sops) { + SemPendingQueue::Const => &mut self.pending_const, + SemPendingQueue::Alter => &mut self.pending_alter, + }; + queue.push_back(waiter); + self.change_wait_count(blocker, true); + } + + fn change_wait_count(&mut self, blocker: SemBlockedOp, increase: bool) { + let sem = &mut self.sems[blocker.semnum]; + let count = match blocker.wait_type { + SemWaitType::Increase => &mut sem.ncnt, + SemWaitType::Zero => &mut sem.zcnt, + }; + *count = if increase { + count.checked_add(1).expect("semaphore wait count overflow") + } else { + count + .checked_sub(1) + .expect("semaphore wait count underflow") + }; + } + + /// Only linked entries contribute to counts; preparation uses this same + /// operation without accounting. Manager lock serializes both kinds. + pub(in crate::ipc::sem) fn update_blocker( + &mut self, + entry: &SemQueueEntry, + blocker: SemBlockedOp, + ) { + let mut status = entry.status.lock(); + if let SemQueueStatus::Queued { + blocker: old, + links, + } = &mut *status + { + if links.is_some() + && (old.semnum != blocker.semnum || old.wait_type != blocker.wait_type) + { + self.change_wait_count(*old, false); + self.change_wait_count(blocker, true); + } + *old = blocker; + } + } + + #[cfg(test)] + fn remove_waiter(&mut self, target: &Arc) { + self.remove_pending(Self::pending_queue_for(&target.sops), target); + } + + #[cfg(test)] + pub(super) fn pending_is_empty(&self) -> bool { + self.pending_const.is_empty() && self.pending_alter.is_empty() + } + + fn pending_iter(&self, queue: SemPendingQueue) -> SemWaitIter { + match queue { + SemPendingQueue::Const => self.pending_const.iter(), + SemPendingQueue::Alter => self.pending_alter.iter(), + } + } + + fn remove_pending(&mut self, queue: SemPendingQueue, entry: &Arc) { + let blocker = match &*entry.status.lock() { + SemQueueStatus::Queued { + blocker, + links: Some(_), + .. + } => *blocker, + _ => return, + }; + let removed = match queue { + SemPendingQueue::Const => self.pending_const.remove(entry), + SemPendingQueue::Alter => self.pending_alter.remove(entry), + }; + if removed { + self.change_wait_count(blocker, false); + } + } + + /// Publish removal under the manager lock; the caller wakes after unlocking. + pub(in crate::ipc::sem) fn complete_all_removed(&mut self, wakes: &mut SemWakeBatch) { + for entry in self.pending_const.iter() { + self.finish_and_wake( + SemPendingQueue::Const, + entry, + Err(SystemError::EIDRM), + wakes, + ); + } + for entry in self.pending_alter.iter() { + self.finish_and_wake( + SemPendingQueue::Alter, + entry, + Err(SystemError::EIDRM), + wakes, + ); + } + } + + #[cfg(test)] + fn ncnt(&self, semnum: usize) -> usize { + self.sems[semnum].ncnt + } + + #[cfg(test)] + fn zcnt(&self, semnum: usize) -> usize { + self.sems[semnum].zcnt + } + fn scan_pending_queue( + set: &mut KernelSemSet, + queue: SemPendingQueue, + wakes: &mut SemWakeBatch, + ) -> bool { + // Iterator captures the successor before the current entry is unlinked. + for entry in set.pending_iter(queue) { + if let Some(group) = entry.undo_group.as_ref() { + let result = { + let mut record_slot = entry.undo_record.lock_irqsave(); + let Some(record) = record_slot.take() else { + set.finish_and_wake(queue, entry.clone(), Err(SystemError::EINVAL), wakes); + continue; + }; + match group.with_prepared_record_noalloc(record, |record| { + match Self::retry_queued_undo_entry(set, &entry, record) { + Ok(Some(changed)) => { + PreparedSemUndoRecordAction::Complete(Ok(Some(changed))) + } + Ok(None) => PreparedSemUndoRecordAction::Keep(Ok(None)), + Err(error) => PreparedSemUndoRecordAction::Keep(Err(error)), + } + }) { + Ok((result, kept_record)) => { + *record_slot = kept_record; + result + } + Err(error) => Err(error), + } + }; + + match result { + Ok(Some(changed)) => { + set.finish_and_wake(queue, entry.clone(), Ok(0), wakes); + if changed { + return true; + } + } + Ok(None) => {} + Err(error) => { + set.finish_and_wake(queue, entry.clone(), Err(error), wakes); + } + } + continue; + } + + let mut scratch = entry.scratch.lock(); + match set.try_apply(&entry.sops, entry.pid.clone(), None, &mut scratch) { + Ok(SemAttempt::Completed { + values_changed: changed, + }) => { + set.finish_and_wake(queue, entry.clone(), Ok(0), wakes); + if changed { + return true; + } + } + Ok(SemAttempt::Blocked(blocker)) if blocker.nowait => { + set.finish_and_wake( + queue, + entry.clone(), + Err(SystemError::EAGAIN_OR_EWOULDBLOCK), + wakes, + ); + } + Ok(SemAttempt::Blocked(blocker)) => { + set.update_blocker(&entry, blocker); + } + Err(error) => { + set.finish_and_wake(queue, entry.clone(), Err(error), wakes); + } + } + } + false + } + + /// Complete executable const entries before altering entries. + pub(in crate::ipc::sem) fn update_queue(&mut self, wakes: &mut SemWakeBatch) { + let set = self; + loop { + let const_changed = Self::scan_pending_queue(set, SemPendingQueue::Const, wakes); + debug_assert!(!const_changed); + if !Self::scan_pending_queue(set, SemPendingQueue::Alter, wakes) { + return; + } + } + } + + fn retry_queued_undo_entry( + set: &mut KernelSemSet, + entry: &Arc, + record: &mut SemUndoRecord, + ) -> Result, SystemError> { + if record.adjustment_count() != set.sems.len() { + return Err(SystemError::EINVAL); + } + let mut scratch = entry.scratch.lock(); + match set.try_apply(&entry.sops, entry.pid.clone(), Some(record), &mut scratch) { + Ok(SemAttempt::Completed { values_changed }) => Ok(Some(values_changed)), + Ok(SemAttempt::Blocked(blocker)) => { + if blocker.nowait { + Err(SystemError::EAGAIN_OR_EWOULDBLOCK) + } else { + set.update_blocker(entry, blocker); + Ok(None) + } + } + Err(error) => Err(error), + } + } +} + +impl KernelSemSet { + /// Manager lock serializes unlink, counter removal and terminal publication. + /// A losing cancellation/completion preserves the first result and never double-wakes. + pub(in crate::ipc::sem) fn finish_waiter( + &mut self, + entry: &Arc, + result: Result, + ) -> bool { + self.finish_pending(Self::pending_queue_for(&entry.sops), entry, result) + } + + fn finish_pending( + &mut self, + queue: SemPendingQueue, + entry: &Arc, + result: Result, + ) -> bool { + self.remove_pending(queue, entry); + entry.complete(result) + } + fn finish_and_wake( + &mut self, + queue: SemPendingQueue, + entry: Arc, + result: Result, + wakes: &mut SemWakeBatch, + ) { + if self.finish_pending(queue, &entry, result) { + wakes.push_completed(entry); + } + } +} + +#[cfg(test)] +mod tests; diff --git a/kernel/src/ipc/sem/set/queue/tests.rs b/kernel/src/ipc/sem/set/queue/tests.rs new file mode 100644 index 0000000000..71fa2c2573 --- /dev/null +++ b/kernel/src/ipc/sem/set/queue/tests.rs @@ -0,0 +1,109 @@ +use super::*; +use crate::ipc::sem::manager::SemManager; +use crate::ipc::sem::set::test_support::*; +use crate::libs::wait_queue::Waiter; + +#[test] +fn pending_links_remove_middle_head_tail_and_preserve_fifo() { + let mut manager = SemManager::new(); + let id = insert_test_set(&mut manager, SemKey::new(153), &[0]); + let set = manager.get_by_semid_checked_mut(id).unwrap(); + let ops = [PosixSemBuf { + sem_num: 0, + sem_op: -1, + sem_flg: 0, + }]; + let mut entries = Vec::new(); + for _ in 0..4 { + entries.push(enqueue_test_waiter( + set, + &ops, + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: false, + }, + )); + } + set.remove_waiter(&entries[1]); + set.remove_waiter(&entries[1]); // An already detached node is harmless. + let remaining: Vec<_> = set.pending_alter.iter().collect(); + assert_eq!(remaining.len(), 3); + for (actual, expected) in remaining.iter().zip([0, 2, 3]) { + assert!(Arc::ptr_eq(actual, &entries[expected])); + } + set.remove_waiter(&entries[0]); + set.remove_waiter(&entries[3]); + assert!(Arc::ptr_eq( + set.pending_alter.head.as_ref().unwrap(), + &entries[2] + )); + assert!(Arc::ptr_eq( + set.pending_alter.tail.as_ref().unwrap(), + &entries[2] + )); + set.remove_waiter(&entries[2]); + assert!(set.pending_is_empty()); + for entry in entries { + assert!(entry.complete(Err(SystemError::EINTR))); + } +} + +#[test] +fn wait_counts_follow_only_linked_current_blockers() { + let mut manager = SemManager::new(); + let id = insert_test_set(&mut manager, SemKey::new(156), &[0, 1]); + let set = manager.get_by_semid_checked_mut(id).unwrap(); + let (_waiter, waker) = Waiter::new_pair(); + let increase = SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: false, + }; + let zero = SemBlockedOp { + semnum: 1, + wait_type: SemWaitType::Zero, + nowait: false, + }; + let entry = Arc::new(SemQueueEntry::new( + &[plain_sop(0, -1), plain_sop(1, 0)], + None, + waker, + increase, + )); + set.update_blocker(&entry, zero); + assert_eq!((set.ncnt(0), set.zcnt(1)), (0, 0)); + set.enqueue_waiter(entry.clone()); + assert_eq!((set.ncnt(0), set.zcnt(1)), (0, 1)); + set.update_blocker(&entry, increase); + assert_eq!((set.ncnt(0), set.zcnt(1)), (1, 0)); + set.update_blocker(&entry, increase); // No double-accounting. + let same_slot_zero = SemBlockedOp { semnum: 0, ..zero }; + set.update_blocker(&entry, same_slot_zero); + assert_eq!((set.ncnt(0), set.zcnt(0)), (0, 1)); + for semnum in 0..2 { + for (kind, cached) in [ + (SemWaitType::Increase, set.ncnt(semnum)), + (SemWaitType::Zero, set.zcnt(semnum)), + ] { + let scanned = set + .pending_const + .iter() + .chain(set.pending_alter.iter()) + .filter(|entry| entry.is_waiting_on(semnum, kind)) + .count(); + assert_eq!(cached, scanned); + } + } + set.remove_waiter(&entry); + set.remove_waiter(&entry); + assert_eq!((set.ncnt(0), set.zcnt(0)), (0, 0)); + set.enqueue_waiter(entry.clone()); + let mut wakes = SemWakeBatch::default(); + set.complete_all_removed(&mut wakes); + assert_eq!( + (set.ncnt(0), set.zcnt(0), set.ncnt(1), set.zcnt(1)), + (0, 0, 0, 0) + ); + assert_eq!(entry.completed_result(), Some(Err(SystemError::EIDRM))); +} diff --git a/kernel/src/ipc/sem/set/test_support.rs b/kernel/src/ipc/sem/set/test_support.rs new file mode 100644 index 0000000000..03f7f0dbf5 --- /dev/null +++ b/kernel/src/ipc/sem/set/test_support.rs @@ -0,0 +1,135 @@ +use super::*; +use crate::ipc::sem::manager::SemManager; +use crate::{libs::wait_queue::Waiter, process::namespace::ipc_namespace::IpcNamespace}; + +use crate::process::{ + cred::{Kgid, Kuid}, + fork::CloneFlags, + namespace::ipc_namespace::INIT_IPC_NAMESPACE, + KernelStack, ProcessControlBlock, +}; + +pub(in crate::ipc::sem) fn test_perm(id: SemId, key: SemKey, seq: usize) -> IpcPerm { + IpcPerm { + id: id.data(), + key: key.data(), + uid: Kuid::new(0), + gid: Kgid::new(0), + cuid: Kuid::new(0), + cgid: Kgid::new(0), + mode: 0o600, + seq, + } +} + +pub(in crate::ipc::sem) fn sem_values(manager: &SemManager, id: SemId) -> Vec { + manager + .get_by_semid_checked(id) + .unwrap() + .sems + .iter() + .map(|sem| sem.val) + .collect() +} + +pub(in crate::ipc::sem) fn test_ipc_ns() -> Arc { + INIT_IPC_NAMESPACE.copy_ipc_ns( + &CloneFlags::CLONE_NEWIPC, + INIT_IPC_NAMESPACE.user_ns.clone(), + ) +} + +pub(in crate::ipc::sem) fn test_pcb_with_group( + ipc_ns: &Arc, +) -> (Arc, Arc) { + let pcb = ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()); + let group = pcb.ensure_sem_undo_group(ipc_ns).unwrap(); + (pcb, group) +} + +pub(in crate::ipc::sem) fn enqueue_test_waiter( + set: &mut KernelSemSet, + sops: &[PosixSemBuf], + blocker: SemBlockedOp, +) -> Arc { + let (_waiter, waker) = Waiter::new_pair(); + let entry = Arc::new(SemQueueEntry::new(sops, None, waker, blocker)); + set.enqueue_waiter(entry.clone()); + entry +} + +pub(in crate::ipc::sem) fn enqueue_undo_waiter_for_test( + manager: &mut SemManager, + semid: SemId, + group: &Arc, +) -> Arc { + let (_waiter, waker) = Waiter::new_pair(); + let entry = Arc::new(SemQueueEntry::new_prepared( + SemQueueEntry::prepare_sops(&[undo_sop(0, -1)]).unwrap(), + None, + Some(Arc::clone(group)), + Some(group.prepare_record_for_test(semid, 1).unwrap()), + waker, + SemopScratch::try_new(1).unwrap(), + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: false, + }, + )); + manager + .get_by_semid_checked_mut(semid) + .unwrap() + .enqueue_waiter(entry.clone()); + entry +} + +pub(in crate::ipc::sem) fn undo_sop(sem_num: u16, sem_op: i16) -> PosixSemBuf { + PosixSemBuf { + sem_num, + sem_op, + sem_flg: SemFlags::SEM_UNDO.bits() as i16, + } +} + +pub(in crate::ipc::sem) fn plain_sop(sem_num: u16, sem_op: i16) -> PosixSemBuf { + PosixSemBuf { + sem_num, + sem_op, + sem_flg: 0, + } +} + +pub(in crate::ipc::sem) fn nowait_sop(sem_num: u16, sem_op: i16) -> PosixSemBuf { + PosixSemBuf { + sem_num, + sem_op, + sem_flg: SemFlags::IPC_NOWAIT.bits() as i16, + } +} + +pub(in crate::ipc::sem) fn insert_test_set( + manager: &mut SemManager, + key: SemKey, + vals: &[i32], +) -> SemId { + manager.insert_test_set(key, vals) +} +pub(in crate::ipc::sem) fn remove_test_set(manager: &mut SemManager, id: SemId) { + manager.remove_test_set(id); +} +impl KernelSemSet { + pub(in crate::ipc::sem) fn new_for_test(perm: IpcPerm, vals: &[i32]) -> Self { + let sems = Self::try_allocate_sems(vals.len()).unwrap(); + let mut set = Self::new(perm, sems); + for (sem, val) in set.sems.iter_mut().zip(vals.iter().copied()) { + sem.val = val; + } + set + } + pub(in crate::ipc::sem) fn undo_groups_for_test( + &self, + ) -> impl Iterator> { + self.undo_groups.iter().map(SemUndoAssociation::group) + } +} diff --git a/kernel/src/ipc/sem/set/tests.rs b/kernel/src/ipc/sem/set/tests.rs new file mode 100644 index 0000000000..80bbf60035 --- /dev/null +++ b/kernel/src/ipc/sem/set/tests.rs @@ -0,0 +1,837 @@ +use super::*; +use crate::ipc::sem::manager::SemManager; +use crate::ipc::sem::set::test_support::*; +use crate::{ipc::id::IpcIdAllocator, libs::wait_queue::Waiter}; +use crate::{ + ipc::sem_undo::detach_sem_undo, + process::{ + namespace::ipc_namespace::INIT_IPC_NAMESPACE, namespace::pid_namespace::INIT_PID_NAMESPACE, + KernelStack, ProcessControlBlock, RawPid, + }, +}; + +#[test] +fn last_owner_replays_adjustment_with_clamp_and_removes_record() { + let ipc_ns = test_ipc_ns(); + let semid = { + let mut manager = ipc_ns.sem.lock(); + insert_test_set(&mut manager, SemKey::new(31), &[32766]) + }; + let (pcb, group) = test_pcb_with_group(&ipc_ns); + group.insert_test_record(semid, &[4]); + + detach_sem_undo(&pcb); + + let manager = ipc_ns.sem.lock(); + assert_eq!(sem_values(&manager, semid), vec![SEMVMX]); + assert_eq!(group.record_count_for_test(), 0); + assert!(pcb.sem_undo_group().is_none()); +} + +#[test] +fn non_last_owner_does_not_replay() { + let ipc_ns = test_ipc_ns(); + let semid = { + let mut manager = ipc_ns.sem.lock(); + insert_test_set(&mut manager, SemKey::new(32), &[10]) + }; + let (owner_one, group) = test_pcb_with_group(&ipc_ns); + let owner_two = ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()); + let mut guard = owner_one + .prepare_shared_sem_undo_attachment(&ipc_ns) + .unwrap(); + guard.install_into(&owner_two); + guard.disarm(); + group.insert_test_record(semid, &[4]); + + detach_sem_undo(&owner_one); + + assert_eq!(sem_values(&ipc_ns.sem.lock(), semid), vec![10]); + assert_eq!(group.record_count_for_test(), 1); + + detach_sem_undo(&owner_two); + + assert_eq!(sem_values(&ipc_ns.sem.lock(), semid), vec![14]); + assert_eq!(group.record_count_for_test(), 0); +} + +#[test] +fn stale_full_semid_does_not_touch_reused_index() { + let ipc_ns = test_ipc_ns(); + let old_semid = { + let mut manager = ipc_ns.sem.lock(); + manager.reset_allocator_for_test(2); + insert_test_set(&mut manager, SemKey::new(33), &[7]) + }; + let (pcb, group) = test_pcb_with_group(&ipc_ns); + group.insert_test_record(old_semid, &[9]); + + let new_semid = { + let mut manager = ipc_ns.sem.lock(); + insert_test_set(&mut manager, SemKey::new(34), &[5]); + remove_test_set(&mut manager, old_semid); + insert_test_set(&mut manager, SemKey::new(35), &[21]) + }; + assert_ne!(old_semid, new_semid); + assert_eq!( + old_semid.data() & IpcIdAllocator::IPC_ID_IDX_MASK, + new_semid.data() & IpcIdAllocator::IPC_ID_IDX_MASK + ); + + detach_sem_undo(&pcb); + + assert_eq!(sem_values(&ipc_ns.sem.lock(), new_semid), vec![21]); + assert_eq!(group.record_count_for_test(), 0); +} + +#[test] +fn replay_updates_otime_and_rescans_waiter() { + let ipc_ns = test_ipc_ns(); + let (semid, entry) = { + let mut manager = ipc_ns.sem.lock(); + let semid = insert_test_set(&mut manager, SemKey::new(35), &[1, 2]); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + set.sem_otime = -1; + + let (_waiter, waker) = Waiter::new_pair(); + let blocker = SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }; + let entry = Arc::new(SemQueueEntry::new( + &[PosixSemBuf { + sem_num: 0, + sem_op: 0, + sem_flg: 0, + }], + None, + waker, + blocker, + )); + set.enqueue_waiter(entry.clone()); + (semid, entry) + }; + let (pcb, group) = test_pcb_with_group(&ipc_ns); + let exiting_tgid = Pid::new_for_test(RawPid::new(4242), INIT_PID_NAMESPACE.clone()); + pcb.install_pid_identity_for_test(exiting_tgid.clone()); + group.insert_test_record(semid, &[-1, 1]); + + detach_sem_undo(&pcb); + + let mut manager = ipc_ns.sem.lock(); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let values = [set.sems[0].val, set.sems[1].val]; + let waiter_pid_was_applied = set.sems[0].pid.is_none(); + let replay_pid_was_applied = set.sems[1] + .pid + .as_ref() + .is_some_and(|pid| Arc::ptr_eq(pid, &exiting_tgid)); + let replay_pid_vnr = set.sems[1] + .pid + .as_ref() + .map(|pid| pid.pid_nr_ns(&INIT_PID_NAMESPACE)); + let sem_otime = set.sem_otime; + let waiters_are_empty = set.pending_is_empty(); + let completed_result = entry.completed_result(); + let record_count = group.record_count_for_test(); + + for sem in &mut set.sems { + sem.pid = None; + } + drop(manager); + pcb.clear_pid_identity_for_test(); + exiting_tgid.clear_numbers_for_test(); + + assert_eq!(values, [0, 3]); + assert!(waiter_pid_was_applied); + assert!(replay_pid_was_applied); + assert_eq!(replay_pid_vnr, Some(RawPid::new(4242))); + assert_ne!(sem_otime, -1); + assert!(waiters_are_empty); + assert_eq!(completed_result, Some(Ok(0))); + assert_eq!(record_count, 0); +} + +#[test] +fn replay_rescans_and_updates_otime_when_clamp_does_not_change_value() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(36), &[SEMVMX, SEMVMX]); + let entry = { + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + set.sem_otime = -1; + enqueue_test_waiter( + set, + &[PosixSemBuf { + sem_num: 0, + sem_op: -1, + sem_flg: 0, + }], + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: false, + }, + ) + }; + let exiting_tgid = Pid::new_for_test(RawPid::new(4243), INIT_PID_NAMESPACE.clone()); + + manager.replay_sem_undo_adjustments( + semid, + &[1, 1], + Some(exiting_tgid.clone()), + &mut SemWakeBatch::default(), + ); + + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let values = [set.sems[0].val, set.sems[1].val]; + let replay_pid_was_applied = set.sems[1] + .pid + .as_ref() + .is_some_and(|pid| Arc::ptr_eq(pid, &exiting_tgid)); + let sem_otime = set.sem_otime; + let waiters_are_empty = set.pending_is_empty(); + let completed_result = entry.completed_result(); + for sem in &mut set.sems { + sem.pid = None; + } + exiting_tgid.clear_numbers_for_test(); + + assert_eq!(values, [SEMVMX - 1, SEMVMX]); + assert!(replay_pid_was_applied); + assert_ne!(sem_otime, -1); + assert!(waiters_are_empty); + assert_eq!(completed_result, Some(Ok(0))); +} + +#[test] +fn valid_all_zero_record_still_updates_otime_and_rescans_queue() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(37), &[0, 5]); + let entry = { + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + set.sem_otime = -1; + enqueue_test_waiter( + set, + &[PosixSemBuf { + sem_num: 0, + sem_op: 0, + sem_flg: 0, + }], + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }, + ) + }; + let exiting_tgid = Pid::new_for_test(RawPid::new(4244), INIT_PID_NAMESPACE.clone()); + + manager.replay_sem_undo_adjustments( + semid, + &[0, 0], + Some(exiting_tgid.clone()), + &mut SemWakeBatch::default(), + ); + + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let values = [set.sems[0].val, set.sems[1].val]; + let untouched_pid = set.sems[1].pid.is_none(); + let sem_otime = set.sem_otime; + let waiters_are_empty = set.pending_is_empty(); + let completed_result = entry.completed_result(); + for sem in &mut set.sems { + sem.pid = None; + } + exiting_tgid.clear_numbers_for_test(); + + assert_eq!(values, [0, 5]); + assert!(untouched_pid); + assert_ne!(sem_otime, -1); + assert!(waiters_are_empty); + assert_eq!(completed_result, Some(Ok(0))); +} + +#[test] +fn undo_registry_shrink_releases_empty_and_rechecks_concurrent_growth() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(152), &[0]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + set.undo_groups.reserve_exact(64); + set.undo_groups + .push(SemUndoAssociation::Group(Arc::downgrade(&group))); + let mut spare = Vec::new(); + let mut retired = Vec::new(); + let needed = set.shrink_undo_registry_prepared(&mut spare, &mut retired); + assert_eq!(needed, 4); + spare.try_reserve_exact(needed).unwrap(); + // Simulate new associations arriving during unlocked preparation. + for _ in 0..8 { + set.undo_groups + .push(SemUndoAssociation::Group(Arc::downgrade(&group))); + } + assert!(set.shrink_undo_registry_prepared(&mut spare, &mut retired) > 0); + assert_eq!(set.undo_groups.len(), 9); + assert_eq!(set.undo_groups.capacity(), 64); + set.undo_groups.truncate(1); + assert_eq!( + set.shrink_undo_registry_prepared(&mut spare, &mut retired), + 0 + ); + assert_eq!(set.undo_groups.len(), 1); + assert_eq!(set.undo_groups.capacity(), 4); + assert_eq!(spare.capacity(), 64); + // The spare is already allocated when another owner empties the set. + // It must not be installed back into the empty registry. + set.undo_groups.clear(); + assert_eq!( + set.shrink_undo_registry_prepared(&mut spare, &mut retired), + 0 + ); + assert_eq!(set.undo_groups.capacity(), 0); + assert_eq!(retired.capacity(), 4); +} + +#[test] +fn prepared_registry_growth_rechecks_registration_and_capacity() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(151), &[1]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let mut spare = Vec::new(); + let capacity = manager + .get_by_semid_checked_mut(semid) + .unwrap() + .ensure_undo_group_registered_prepared(&group, &mut spare) + .unwrap_err(); + assert!(!manager.undo_registry_contains_for_test(&group)); + assert!(manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .is_empty()); + spare.try_reserve(capacity).unwrap(); + + // Simulate other first-time groups consuming the prepared capacity + // while this caller has dropped the manager lock. + let mut owners = Vec::new(); + while manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .len() + < spare.capacity() + { + let owner = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + manager.ensure_undo_group_registered(&owner, semid).unwrap(); + owners.push(owner); + } + let capacity = manager + .get_by_semid_checked_mut(semid) + .unwrap() + .ensure_undo_group_registered_prepared(&group, &mut spare) + .unwrap_err(); + assert!(!manager.undo_registry_contains_for_test(&group)); + spare.try_reserve(capacity).unwrap(); + manager + .get_by_semid_checked_mut(semid) + .unwrap() + .ensure_undo_group_registered_prepared(&group, &mut spare) + .unwrap(); + assert!(spare.is_empty()); + assert!(manager.undo_registry_contains_for_test(&group)); + assert_eq!( + manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .len(), + owners.len() + 1 + ); + for (weak, owner) in manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .iter() + .zip(&owners) + { + assert!(weak.group().ptr_eq(&Arc::downgrade(owner))); + } + + // A concurrent CLONE_SYSVSEM sharer already registered this group. + let mut empty_spare = Vec::new(); + manager + .get_by_semid_checked_mut(semid) + .unwrap() + .ensure_undo_group_registered_prepared(&group, &mut empty_spare) + .unwrap(); + assert_eq!( + manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .len(), + owners.len() + 1 + ); +} + +#[test] +fn queued_undo_commits_to_captured_group_not_waker_current_task() { + let ipc_ns = test_ipc_ns(); + let group_a = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let group_b = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let mut manager = ipc_ns.sem.lock(); + let semid = insert_test_set(&mut manager, SemKey::new(46), &[0]); + manager + .prepare_undo_record_and_registry_for_test(&group_a, semid) + .unwrap(); + manager + .prepare_undo_record_and_registry_for_test(&group_b, semid) + .unwrap(); + + let (_waiter, waker) = Waiter::new_pair(); + let entry = Arc::new(SemQueueEntry::new_prepared( + SemQueueEntry::prepare_sops(&[undo_sop(0, -1)]).unwrap(), + None, + Some(group_a.clone()), + Some(group_a.prepare_record_for_test(semid, 1).unwrap()), + waker, + SemopScratch::try_new(1).unwrap(), + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Increase, + nowait: false, + }, + )); + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + set.enqueue_waiter(entry.clone()); + set.sems[0].val = 1; + + manager.update_queue_for_test(semid); + + assert_eq!(entry.completed_result(), Some(Ok(0))); + assert_eq!(sem_values(&manager, semid), vec![0]); + assert_eq!(group_a.adjustment_for_test(semid, 0), 1); + assert_eq!(group_b.adjustment_for_test(semid, 0), 0); +} + +#[test] +fn queued_timeout_signal_and_rmid_never_commit_adjustment() { + let ipc_ns = test_ipc_ns(); + let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let mut manager = ipc_ns.sem.lock(); + let timeout_semid = insert_test_set(&mut manager, SemKey::new(47), &[0]); + let signal_semid = insert_test_set(&mut manager, SemKey::new(48), &[0]); + let rmid_semid = insert_test_set(&mut manager, SemKey::new(49), &[0]); + for semid in [timeout_semid, signal_semid, rmid_semid] { + manager + .prepare_undo_record_and_registry_for_test(&group, semid) + .unwrap(); + } + + let timeout_entry = enqueue_undo_waiter_for_test(&mut manager, timeout_semid, &group); + assert_eq!( + manager.cancel_queued_entry( + timeout_semid, + &timeout_entry, + SystemError::EAGAIN_OR_EWOULDBLOCK, + ), + Err(SystemError::EAGAIN_OR_EWOULDBLOCK) + ); + assert_eq!(group.adjustment_for_test(timeout_semid, 0), 0); + + let signal_entry = enqueue_undo_waiter_for_test(&mut manager, signal_semid, &group); + assert_eq!( + manager.cancel_queued_entry(signal_semid, &signal_entry, SystemError::EINTR), + Err(SystemError::EINTR) + ); + assert_eq!(group.adjustment_for_test(signal_semid, 0), 0); + + let rmid_entry = enqueue_undo_waiter_for_test(&mut manager, rmid_semid, &group); + let mut wakes = SemWakeBatch::default(); + let removed = manager.ipc_rmid(rmid_semid, &mut wakes).unwrap(); + drop(manager); + wakes.wake_all(); + drop(removed); + assert_eq!(rmid_entry.completed_result(), Some(Err(SystemError::EIDRM))); + assert_eq!(group.adjustment_for_test(rmid_semid, 0), 0); +} + +#[test] +fn first_record_is_registry_visible_before_future_cleanup() { + let ipc_ns = test_ipc_ns(); + let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let mut manager = ipc_ns.sem.lock(); + let semid = insert_test_set(&mut manager, SemKey::new(50), &[3]); + + manager + .prepare_undo_record_and_registry_for_test(&group, semid) + .unwrap(); + + assert_eq!(manager.live_undo_group_count_for_test(), 1); + assert!(manager.undo_registry_contains_for_test(&group)); + assert_eq!(group.adjustment_for_test(semid, 0), 0); +} + +#[test] +fn stale_weak_entries_are_compacted_without_losing_live_group() { + let ipc_ns = test_ipc_ns(); + let live = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let candidate = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let stale = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let stale_weak = Arc::downgrade(&stale); + drop(stale); + + let mut manager = ipc_ns.sem.lock(); + let semid = insert_test_set(&mut manager, SemKey::new(51), &[1]); + manager + .get_by_semid_checked_mut(semid) + .unwrap() + .undo_groups + .push(SemUndoAssociation::Group(stale_weak)); + manager.ensure_undo_group_registered(&live, semid).unwrap(); + manager + .get_by_semid_checked_mut(semid) + .unwrap() + .undo_groups + .shrink_to_fit(); + + manager + .prepare_undo_record_and_registry_for_test(&candidate, semid) + .unwrap(); + + assert_eq!(manager.live_undo_group_count_for_test(), 2); + assert!(manager.undo_registry_contains_for_test(&live)); + assert!(manager.undo_registry_contains_for_test(&candidate)); +} + +#[test] +fn live_group_registration_survives_debt_removal_without_duplicates() { + let ipc_ns = test_ipc_ns(); + let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let mut manager = ipc_ns.sem.lock(); + let semid = insert_test_set(&mut manager, SemKey::new(150), &[1]); + manager + .prepare_undo_record_and_registry_for_test(&group, semid) + .unwrap(); + let capacity = manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .capacity(); + manager.clear_undo_for_setall(semid); + // Synthetic record removal exercises registration deduplication without + // putting a live set into the RMID-only retired association state. + group.remove_record(semid); + assert_eq!(group.record_count_for_test(), 0); + for _ in 0..32 { + manager.ensure_undo_group_registered(&group, semid).unwrap(); + } + assert!(manager.undo_registry_contains_for_test(&group)); + assert_eq!( + manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .len(), + 1 + ); + assert_eq!( + manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .capacity(), + capacity + ); + manager + .prepare_undo_record_and_registry_for_test(&group, semid) + .unwrap(); + assert_eq!( + manager + .get_by_semid_checked(semid) + .unwrap() + .undo_groups + .len(), + 1 + ); +} + +#[test] +fn queued_undo_entry_retains_group_after_external_owner_drops() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(52), &[1]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + manager + .prepare_undo_record_and_registry_for_test(&group, semid) + .unwrap(); + let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); + let group_weak = Arc::downgrade(&group); + drop(group); + assert!(group_weak.upgrade().is_some()); + manager.get_by_semid_checked_mut(semid).unwrap().sems[0].val = 1; + + manager.update_queue_for_test(semid); + + assert_eq!(entry.completed_result(), Some(Ok(0))); + assert_eq!(sem_values(&manager, semid), vec![0]); +} + +#[test] +fn queued_undo_record_length_mismatch_completes_with_internal_error() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(53), &[1, 1]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[0]); + let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); + manager.get_by_semid_checked_mut(semid).unwrap().sems[0].val = 1; + + manager.update_queue_for_test(semid); + + assert_eq!(entry.completed_result(), Some(Err(SystemError::EINVAL))); + assert_eq!(sem_values(&manager, semid), vec![1, 1]); + assert_eq!(group.adjustment_for_test(semid, 0), 0); +} + +#[test] +fn final_owner_detach_replays_against_setval_as_a_single_serial_order() { + let ipc_ns = test_ipc_ns(); + let semid = { + let mut manager = ipc_ns.sem.lock(); + insert_test_set(&mut manager, SemKey::new(62), &[2]) + }; + let (pcb, group) = test_pcb_with_group(&ipc_ns); + group.insert_test_record(semid, &[3]); + ipc_ns + .sem + .lock() + .ensure_undo_group_registered(&group, semid) + .unwrap(); + drop(pcb.take_sem_undo_attachment().unwrap()); + assert!(group.detach_last_owner_for_test()); + + { + let mut manager = ipc_ns.sem.lock(); + manager + .setval(semid, 0, 7, &mut SemWakeBatch::default()) + .unwrap(); + } + group.replay_marked_records_for_test(&pcb); + + let manager = ipc_ns.sem.lock(); + assert_eq!(sem_values(&manager, semid), vec![7]); + assert_eq!(group.record_count_for_test(), 0); + assert_eq!(group.replay_count_for_test(), 1); + assert!(pcb.sem_undo_group().is_none()); +} + +#[test] +fn rmid_before_final_owner_replay_skips_detached_record_once() { + let ipc_ns = test_ipc_ns(); + let semid = { + let mut manager = ipc_ns.sem.lock(); + insert_test_set(&mut manager, SemKey::new(63), &[2]) + }; + let (pcb, group) = test_pcb_with_group(&ipc_ns); + group.insert_test_record(semid, &[3]); + ipc_ns + .sem + .lock() + .ensure_undo_group_registered(&group, semid) + .unwrap(); + drop(pcb.take_sem_undo_attachment().unwrap()); + assert!(group.detach_last_owner_for_test()); + + let mut wakes = SemWakeBatch::default(); + let removed = { + let mut manager = ipc_ns.sem.lock(); + manager.ipc_rmid(semid, &mut wakes).unwrap() + }; + wakes.wake_all(); + drop(removed); + group.replay_marked_records_for_test(&pcb); + + let manager = ipc_ns.sem.lock(); + assert!(matches!( + manager.get_by_semid_checked(semid), + Err(SystemError::EINVAL) + )); + assert_eq!(group.record_count_for_test(), 0); + assert_eq!(group.replay_count_for_test(), 1); + assert!(pcb.sem_undo_group().is_none()); +} + +#[test] +fn prepare_existing_undo_record_length_mismatch_returns_einval_without_mutation() { + let semid = SemId::new(64); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[5]); + + let err = group.prepare_record_for_test(semid, 2).unwrap_err(); + + assert_eq!(err, SystemError::EINVAL); + assert_eq!(group.adjustment_for_test(semid, 0), 5); + assert_eq!(group.record_count_for_test(), 1); + assert_eq!(group.pending_record_reservations_for_test(), 0); +} + +#[test] +fn setval_clears_only_target_sem_adjustment_across_all_groups() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(54), &[4, 5]); + let other_semid = insert_test_set(&mut manager, SemKey::new(55), &[6, 7]); + let group_a = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let group_b = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group_a.insert_test_record(semid, &[11, 12]); + group_b.insert_test_record(semid, &[21, 22]); + group_a.insert_test_record(other_semid, &[31, 32]); + manager + .ensure_undo_group_registered(&group_a, semid) + .unwrap(); + manager + .ensure_undo_group_registered(&group_b, semid) + .unwrap(); + + manager + .setval(semid, 0, 9, &mut SemWakeBatch::default()) + .unwrap(); + + assert_eq!(sem_values(&manager, semid), vec![9, 5]); + assert_eq!(group_a.adjustment_for_test(semid, 0), 0); + assert_eq!(group_a.adjustment_for_test(semid, 1), 12); + assert_eq!(group_b.adjustment_for_test(semid, 0), 0); + assert_eq!(group_b.adjustment_for_test(semid, 1), 22); + assert_eq!(group_a.adjustment_for_test(other_semid, 0), 31); +} + +#[test] +fn setall_clears_entire_full_semid_record_across_all_groups() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(56), &[1, 2, 3]); + let other_semid = insert_test_set(&mut manager, SemKey::new(57), &[4, 5, 6]); + let group_a = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + let group_b = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group_a.insert_test_record(semid, &[1, 2, 3]); + group_b.insert_test_record(semid, &[4, 5, 6]); + group_b.insert_test_record(other_semid, &[7, 8, 9]); + manager + .ensure_undo_group_registered(&group_a, semid) + .unwrap(); + manager + .ensure_undo_group_registered(&group_b, semid) + .unwrap(); + let token = manager.prepare_setall(semid).unwrap(); + + manager + .setall(token, &[10, 11, 12], &mut SemWakeBatch::default()) + .unwrap(); + + assert_eq!(sem_values(&manager, semid), vec![10, 11, 12]); + assert_eq!(group_a.adjustment_for_test(semid, 0), 0); + assert_eq!(group_a.adjustment_for_test(semid, 1), 0); + assert_eq!(group_a.adjustment_for_test(semid, 2), 0); + assert_eq!(group_b.adjustment_for_test(semid, 0), 0); + assert_eq!(group_b.adjustment_for_test(semid, 1), 0); + assert_eq!(group_b.adjustment_for_test(semid, 2), 0); + assert_eq!(group_b.adjustment_for_test(other_semid, 1), 8); +} + +#[test] +fn setval_cleanup_precedes_value_write_and_queue_rescan() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(58), &[0]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[7]); + manager.ensure_undo_group_registered(&group, semid).unwrap(); + let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); + + manager + .setval(semid, 0, 1, &mut SemWakeBatch::default()) + .unwrap(); + + assert_eq!(entry.completed_result(), Some(Ok(0))); + assert_eq!(sem_values(&manager, semid), vec![0]); + assert_eq!(group.adjustment_for_test(semid, 0), 1); +} + +#[test] +fn rmid_discards_record_before_index_can_be_reused() { + let mut manager = SemManager::new(); + manager.reset_allocator_for_test(2); + let old_semid = insert_test_set(&mut manager, SemKey::new(59), &[3]); + let filler_semid = insert_test_set(&mut manager, SemKey::new(60), &[4]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(old_semid, &[9]); + manager + .ensure_undo_group_registered(&group, old_semid) + .unwrap(); + manager + .ipc_rmid(old_semid, &mut SemWakeBatch::default()) + .unwrap(); + + let new_semid = insert_test_set(&mut manager, SemKey::new(61), &[5]); + + assert_ne!(old_semid, new_semid); + assert_eq!( + old_semid.data() & IpcIdAllocator::IPC_ID_IDX_MASK, + new_semid.data() & IpcIdAllocator::IPC_ID_IDX_MASK + ); + assert_eq!(sem_values(&manager, new_semid), vec![5]); + assert_eq!(group.record_count_for_test(), 0); + assert_eq!(sem_values(&manager, filler_semid), vec![4]); +} + +#[test] +fn queued_stale_existing_record_retries_and_completes_without_error() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(66), &[0]); + let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + group.insert_test_record(semid, &[7]); + manager.ensure_undo_group_registered(&group, semid).unwrap(); + let entry = enqueue_undo_waiter_for_test(&mut manager, semid, &group); + + manager.clear_undo_for_setval(semid, 0); + manager.get_by_semid_checked_mut(semid).unwrap().sems[0].val = 2; + manager.update_queue_for_test(semid); + + assert_eq!(entry.completed_result(), Some(Ok(0))); + assert_eq!(sem_values(&manager, semid), vec![1]); + assert_eq!(group.adjustment_for_test(semid, 0), 1); +} + +#[test] +fn const_waiters_complete_before_altering_waiters() { + let mut manager = SemManager::new(); + let semid = insert_test_set(&mut manager, SemKey::new(67), &[1]); + let (altering, constant) = { + let set = manager.get_by_semid_checked_mut(semid).unwrap(); + let altering = enqueue_test_waiter( + set, + &[plain_sop(0, 0), plain_sop(0, 1)], + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }, + ); + let constant = enqueue_test_waiter( + set, + &[plain_sop(0, 0)], + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }, + ); + set.sems[0].val = 0; + (altering, constant) + }; + + manager.update_queue_for_test(semid); + + let set = manager.get_by_semid_checked(semid).unwrap(); + assert_eq!(constant.completed_result(), Some(Ok(0))); + assert_eq!(altering.completed_result(), Some(Ok(0))); + assert!(set.pending_is_empty()); + assert_eq!(set.sems[0].val, 1); +} diff --git a/kernel/src/ipc/sem_undo.rs b/kernel/src/ipc/sem_undo.rs deleted file mode 100644 index 72827e55b2..0000000000 --- a/kernel/src/ipc/sem_undo.rs +++ /dev/null @@ -1,1151 +0,0 @@ -use alloc::{ - boxed::Box, - sync::{Arc, Weak}, - vec::Vec, -}; -use system_error::SystemError; - -use crate::{ - ipc::sem::{SemId, SemManager, SemWakeBatch}, - libs::spinlock::SpinLock, - process::{namespace::ipc_namespace::IpcNamespace, pid::PidType, ProcessControlBlock}, -}; - -#[derive(Debug)] -pub struct SemUndoAttachment { - group: Arc, -} - -#[derive(Debug)] -pub struct SemUndoGroup { - ipc_ns: Weak, - #[allow(dead_code)] - inner: SpinLock, -} - -#[allow(dead_code)] -#[derive(Debug)] -struct SemUndoGroupState { - task_owners: usize, - records: Vec, - reserved_records: usize, - retired: bool, - replay_started: bool, - #[cfg(test)] - replay_count: usize, -} - -#[derive(Debug)] -struct PendingSemUndoRecordReservation { - group: Weak, - active: bool, -} - -#[derive(Debug)] -pub(crate) struct SemUndoRecord { - semid: SemId, - adjustments: Box<[i16]>, -} - -/// Existing records need no snapshot: simulation reads the current record -/// while holding the group lock. Only a first-use candidate owns dense storage. -#[derive(Debug)] -pub(crate) struct PreparedSemUndoRecord { - semid: SemId, - nsems: usize, - candidate: Option, - reservation: Option, -} - -pub(crate) enum PreparedSemUndoRecordAction { - Publish(R), - Keep(R), -} - -impl PendingSemUndoRecordReservation { - fn new(group: &Arc) -> Self { - Self { - group: Arc::downgrade(group), - active: true, - } - } - - fn disarm(&mut self) { - self.active = false; - } -} - -impl Drop for PendingSemUndoRecordReservation { - fn drop(&mut self) { - if !self.active { - return; - } - let Some(group) = self.group.upgrade() else { - return; - }; - let mut state = group.inner.lock_irqsave(); - state.reserved_records = state - .reserved_records - .checked_sub(1) - .expect("SEM_UNDO record reservation count underflow"); - self.active = false; - } -} - -fn prepare_records_storage_capacity( - records: &mut Vec, - required_capacity: usize, -) -> Result<(), SystemError> { - if records.capacity() >= required_capacity { - return Ok(()); - } - - let additional = required_capacity - .checked_sub(records.len()) - .ok_or(SystemError::ENOMEM)?; - records - .try_reserve_exact(additional) - .map_err(|_| SystemError::ENOMEM)?; - if records.capacity() < required_capacity { - return Err(SystemError::ENOMEM); - } - Ok(()) -} - -impl PreparedSemUndoRecord { - pub(crate) fn was_existing(&self) -> bool { - self.candidate.is_none() - } - - pub(crate) fn adjustment_count(&self) -> usize { - self.nsems - } -} - -impl SemUndoRecord { - #[cfg(test)] - fn new_live(semid: SemId, adjustments: Box<[i16]>) -> Self { - Self { semid, adjustments } - } - - pub(crate) fn adjustment(&self, semnum: usize) -> i16 { - self.adjustments[semnum] - } - - pub(crate) fn set_adjustment(&mut self, semnum: usize, adjustment: i16) { - if self.adjustments[semnum] != adjustment { - self.adjustments[semnum] = adjustment; - } - } - - pub(crate) fn clear_adjustment(&mut self, semnum: usize) { - self.set_adjustment(semnum, 0); - } - - pub(crate) fn clear_all_adjustments(&mut self) { - if self.adjustments.iter().any(|&adjustment| adjustment != 0) { - self.adjustments.fill(0); - } - } - - pub(crate) fn adjustment_count(&self) -> usize { - self.adjustments.len() - } - - #[cfg(test)] - pub(crate) fn adjustment_for_test(&self, semnum: usize) -> i16 { - self.adjustment(semnum) - } - - #[cfg(test)] - pub(crate) fn set_adjustment_for_test(&mut self, semnum: usize, adjustment: i16) { - self.set_adjustment(semnum, adjustment); - } -} - -pub(crate) struct UnpublishedSemUndoAttachmentGuard { - group: Arc, - attachment: Option, - installed_child: Option>, - armed: bool, -} - -impl SemUndoAttachment { - pub(crate) fn new(group: Arc) -> Self { - Self { group } - } - - pub(crate) fn group(&self) -> Arc { - self.group.clone() - } - - #[cfg(test)] - fn new_for_test(group: Arc) -> Self { - Self::new(group) - } - - #[cfg(test)] - fn group_for_test(&self) -> Arc { - self.group() - } -} - -impl Drop for SemUndoAttachment { - // Replay is an explicit lifecycle operation and must never run from Drop. - fn drop(&mut self) {} -} - -impl SemUndoGroup { - pub(crate) fn new(ipc_ns: &Arc) -> Result, SystemError> { - Arc::try_new(Self { - ipc_ns: Arc::downgrade(ipc_ns), - inner: SpinLock::new(SemUndoGroupState { - task_owners: 1, - records: Vec::new(), - reserved_records: 0, - retired: false, - replay_started: false, - #[cfg(test)] - replay_count: 0, - }), - }) - .map_err(|_| SystemError::ENOMEM) - } - - pub(crate) fn verify_ipc_ns(&self, ipc_ns: &Arc) -> Result<(), SystemError> { - let state = self.inner.lock_irqsave(); - if state.task_owners == 0 || state.retired { - return Err(SystemError::EINVAL); - } - if self.ipc_ns.ptr_eq(&Arc::downgrade(ipc_ns)) { - Ok(()) - } else { - Err(SystemError::EINVAL) - } - } - - fn detach_owner_and_mark_last(&self) -> bool { - let mut state = self.inner.lock_irqsave(); - debug_assert!( - state.task_owners > 0, - "SEM_UNDO detach requires an attached task owner" - ); - if state.task_owners == 0 { - return false; - } - - state.task_owners -= 1; - if state.task_owners != 0 { - return false; - } - - state.retired = true; - true - } - - /// Claim retirement exactly once without hiding pending debt from semctl. - fn begin_replay(&self) -> bool { - let mut state = self.inner.lock_irqsave(); - if !state.retired || state.replay_started { - return false; - } - state.replay_started = true; - #[cfg(test)] - { - state.replay_count += 1; - } - true - } - - /// The caller holds the namespace manager lock until this debt is applied. - /// Other records remain visible to SETVAL/SETALL and IPC_RMID between steps. - fn pop_retired_record(&self) -> Option { - let mut state = self.inner.lock_irqsave(); - debug_assert!(state.retired && state.replay_started); - state.records.pop() - } - - /// Only valid after the bound namespace can no longer be upgraded. - fn discard_retired_records(&self) -> Vec { - let mut state = self.inner.lock_irqsave(); - debug_assert!(state.retired && state.replay_started); - core::mem::take(&mut state.records) - } - - #[cfg(test)] - fn new_for_test() -> Arc { - Self::new(&crate::process::namespace::ipc_namespace::INIT_IPC_NAMESPACE).unwrap() - } - - #[cfg(test)] - fn new_for_test_bound_to_first_namespace() -> Arc { - Self::new_for_test() - } - - #[cfg(test)] - pub(crate) fn new_for_test_bound_to( - ipc_ns: &Arc, - ) -> Result, SystemError> { - Self::new(ipc_ns) - } - - #[allow(dead_code)] - pub(crate) fn prepare_record( - self: &Arc, - semid: SemId, - nsems: usize, - ) -> Result { - let mut adjustments = Vec::new(); - let mut reserved_storage = Vec::new(); - - loop { - let mut state = self.inner.lock_irqsave(); - if state.task_owners == 0 || state.retired { - return Err(SystemError::EINVAL); - } - - if let Some(existing) = state.records.iter().find(|item| item.semid == semid) { - if existing.adjustments.len() != nsems { - return Err(SystemError::EINVAL); - } - return Ok(PreparedSemUndoRecord { - semid, - nsems, - candidate: None, - reservation: None, - }); - } - - if adjustments.len() != nsems { - drop(state); - adjustments - .try_reserve_exact(nsems) - .map_err(|_| SystemError::ENOMEM)?; - adjustments.resize(nsems, 0); - continue; - } - - let required_capacity = state - .records - .len() - .checked_add(state.reserved_records) - .and_then(|capacity| capacity.checked_add(1)) - .ok_or(SystemError::ENOMEM)?; - - if state.records.capacity() < required_capacity { - if reserved_storage.capacity() < required_capacity { - drop(state); - prepare_records_storage_capacity(&mut reserved_storage, required_capacity)?; - continue; - } - - reserved_storage.append(&mut state.records); - core::mem::swap(&mut state.records, &mut reserved_storage); - } - - state.reserved_records = state - .reserved_records - .checked_add(1) - .ok_or(SystemError::ENOMEM)?; - // into_boxed_slice may shrink an allocation. Do it after releasing - // the group lock, with an armed reservation already owning the slot. - let reservation = PendingSemUndoRecordReservation::new(self); - drop(state); - return Ok(PreparedSemUndoRecord { - semid, - nsems, - candidate: Some(SemUndoRecord { - semid, - adjustments: adjustments.into_boxed_slice(), - }), - reservation: Some(reservation), - }); - } - } - - #[allow(dead_code)] - pub(crate) fn commit_record(&self, record: PreparedSemUndoRecord) -> Result<(), SystemError> { - self.commit_prepared_record_noalloc(record) - } - - pub(crate) fn commit_prepared_record_noalloc( - &self, - record: PreparedSemUndoRecord, - ) -> Result<(), SystemError> { - self.with_prepared_record_noalloc( - record, - |_record| PreparedSemUndoRecordAction::Publish(()), - ) - .map(|((), _)| ()) - } - - /// Borrow the current record under the group lock, never a stale snapshot. - /// The callback must simulate without writes and mutate only on Publish; - /// Keep (including errors/blocked operations) must leave the debt unchanged. - /// First use publishes a zero record before calling the simulation, as in - /// Linux find_alloc_undo; Keep retains only a lightweight existing token. - /// SemopScratch provides that separation without an additional transaction. - pub(crate) fn with_prepared_record_noalloc( - &self, - mut record: PreparedSemUndoRecord, - f: impl FnOnce(&mut SemUndoRecord) -> PreparedSemUndoRecordAction, - ) -> Result<(R, Option), SystemError> { - // A competing first publisher makes this candidate redundant. Keep - // its disposal outside the group lock and return a lightweight token. - let mut _retired_candidate = None; - let mut state = self.inner.lock_irqsave(); - if state.task_owners == 0 || state.retired { - return Err(SystemError::EINVAL); - } - - let existing_index = state - .records - .iter() - .position(|item| item.semid == record.semid); - - if let Some(index) = existing_index { - if state.records[index].adjustments.len() != record.nsems { - return Err(SystemError::EINVAL); - } - _retired_candidate = record.candidate.take(); - if let Some(mut reservation) = record.reservation.take() { - state.reserved_records = state - .reserved_records - .checked_sub(1) - .expect("SEM_UNDO record reservation count underflow"); - reservation.disarm(); - } - - return match f(&mut state.records[index]) { - PreparedSemUndoRecordAction::Publish(result) => Ok((result, None)), - PreparedSemUndoRecordAction::Keep(result) => Ok((result, Some(record))), - }; - } - - // Like Linux find_alloc_undo, publish a zero record before simulating: - // even a blocked/failed operation retains this group/set association. - // Existing tokens cannot recreate a record removed by RMID. - if record.candidate.is_none() { - return Err(SystemError::EINVAL); - } - if state.records.len() >= state.records.capacity() { - return Err(SystemError::ENOMEM); - } - if let Some(mut reservation) = record.reservation.take() { - state.reserved_records = state - .reserved_records - .checked_sub(1) - .expect("SEM_UNDO record reservation count underflow"); - reservation.disarm(); - } - state.records.push(record.candidate.take().unwrap()); - match f(state.records.last_mut().unwrap()) { - PreparedSemUndoRecordAction::Publish(result) => Ok((result, None)), - PreparedSemUndoRecordAction::Keep(result) => Ok((result, Some(record))), - } - } - - pub(crate) fn with_record_mut( - &self, - semid: SemId, - f: impl FnOnce(&mut SemUndoRecord) -> R, - ) -> Option { - let mut state = self.inner.lock_irqsave(); - state - .records - .iter_mut() - .find(|record| record.semid == semid) - .map(f) - } - - pub(crate) fn take_record(&self, semid: SemId) -> Option { - let mut state = self.inner.lock_irqsave(); - let index = state - .records - .iter() - .position(|record| record.semid == semid)?; - Some(state.records.swap_remove(index)) - } - - #[cfg(test)] - pub(crate) fn remove_record(&self, semid: SemId) { - drop(self.take_record(semid)); - } - - #[cfg(test)] - pub(crate) fn adjustment_for_test(&self, semid: SemId, semnum: usize) -> i16 { - self.inner - .lock_irqsave() - .records - .iter() - .find(|record| record.semid == semid) - .map(|record| record.adjustment(semnum)) - .unwrap_or_default() - } - - #[cfg(test)] - pub(crate) fn has_live_records_in_namespace_for_test( - &self, - ipc_ns: &Arc, - ) -> bool { - self.verify_ipc_ns(ipc_ns).is_ok() && !self.inner.lock_irqsave().records.is_empty() - } - - #[cfg(test)] - pub(crate) fn prepare_record_for_test( - self: &Arc, - semid: SemId, - nsems: usize, - ) -> Result { - self.prepare_record(semid, nsems) - } - - #[cfg(test)] - pub(crate) fn task_owners_for_test(&self) -> usize { - self.inner.lock_irqsave().task_owners - } - - #[cfg(test)] - pub(crate) fn replay_count_for_test(&self) -> usize { - self.inner.lock_irqsave().replay_count - } - - #[cfg(test)] - fn verify_ipc_ns_for_test(&self, ipc_ns: Arc) -> Result<(), SystemError> { - self.verify_ipc_ns(&ipc_ns) - } - - #[cfg(test)] - pub(crate) fn insert_test_record(&self, semid: SemId, adjustments: &[i16]) { - let mut state = self.inner.lock_irqsave(); - state.records.push(SemUndoRecord::new_live( - semid, - adjustments.to_vec().into_boxed_slice(), - )); - } - - #[cfg(test)] - pub(crate) fn record_count_for_test(&self) -> usize { - self.inner.lock_irqsave().records.len() - } - - #[cfg(test)] - pub(crate) fn record_capacity_for_test(&self) -> usize { - self.inner.lock_irqsave().records.capacity() - } - - #[cfg(test)] - pub(crate) fn set_record_capacity_for_test(&self, capacity: usize) { - let mut state = self.inner.lock_irqsave(); - assert!(state.records.is_empty()); - state.records = Vec::with_capacity(capacity); - assert_eq!(state.records.capacity(), capacity); - } - - #[cfg(test)] - pub(crate) fn pending_record_reservations_for_test(&self) -> usize { - self.inner.lock_irqsave().reserved_records - } - - #[cfg(test)] - pub(crate) fn detach_last_owner_for_test(&self) -> bool { - self.detach_owner_and_mark_last() - } - - #[cfg(test)] - pub(crate) fn replay_marked_records_for_test(self: &Arc, pcb: &Arc) { - replay_marked_records(pcb, self); - } -} - -pub(crate) fn detach_sem_undo(pcb: &Arc) { - let Some(attachment) = pcb.take_sem_undo_attachment() else { - return; - }; - let group = attachment.group(); - drop(attachment); - - if !group.detach_owner_and_mark_last() { - return; - } - - replay_marked_records(pcb, &group); -} - -fn replay_marked_records(pcb: &Arc, group: &Arc) { - if !group.begin_replay() { - return; - } - let exiting_tgid = pcb.try_active_pid_ns().and_then(|pid_ns| { - pcb.task_pid_nr_ns(PidType::TGID, Some(pid_ns)) - .filter(|tgid| tgid.data() != 0)?; - pcb.task_pid_ptr(PidType::TGID) - }); - - let Some(ipc_ns) = group.ipc_ns.upgrade() else { - for record in group.discard_retired_records() { - log::debug!( - "dropping SEM_UNDO record for semid {} after IPC namespace teardown", - record.semid.data() - ); - } - return; - }; - - loop { - let mut wakes = SemWakeBatch::default(); - let record = { - let mut manager = ipc_ns.sem.lock(); - let Some(record) = group.pop_retired_record() else { - break; - }; - SemManager::replay_sem_undo_adjustments( - &mut manager, - record.semid, - &record.adjustments, - exiting_tgid.clone(), - &mut wakes, - ); - manager.unregister_undo_group(record.semid, group); - record - }; - // Publish and notify one set at a time, like Linux exit_sem. Pending - // records stay in the group so interleaved semctl still clears them. - wakes.wake_all(); - SemManager::shrink_undo_registry(&ipc_ns, record.semid); - } -} - -impl UnpublishedSemUndoAttachmentGuard { - pub(crate) fn new(group: Arc) -> Self { - { - let mut state = group.inner.lock_irqsave(); - debug_assert!( - state.task_owners > 0 && !state.retired, - "SEM_UNDO shared owner must be acquired before final retirement" - ); - state.task_owners = state - .task_owners - .checked_add(1) - .expect("SEM_UNDO task owner count overflow"); - } - - Self { - attachment: Some(SemUndoAttachment::new(group.clone())), - group, - installed_child: None, - armed: true, - } - } - - pub(crate) fn install_into(&mut self, child: &ProcessControlBlock) { - assert!(self.armed, "cannot install a disarmed SEM_UNDO guard"); - assert!( - self.installed_child.is_none(), - "SEM_UNDO guard can only be installed once" - ); - let attachment = self - .attachment - .take() - .expect("SEM_UNDO guard attachment token is missing"); - self.installed_child = Some(child.install_unpublished_sem_undo_attachment(attachment)); - } - - pub(crate) fn disarm(mut self) { - debug_assert!( - self.attachment.is_none() && self.installed_child.is_some(), - "only an installed SEM_UNDO guard can be disarmed" - ); - self.armed = false; - } -} - -impl Drop for UnpublishedSemUndoAttachmentGuard { - fn drop(&mut self) { - if !self.armed { - return; - } - - if let Some(child) = self.installed_child.take() { - let attachment = child.take_sem_undo_attachment(); - debug_assert!(attachment.is_some(), "installed SEM_UNDO slot is empty"); - if let Some(attachment) = attachment { - debug_assert!(Arc::ptr_eq(&attachment.group, &self.group)); - drop(attachment); - } - } else { - drop(self.attachment.take()); - } - - let mut state = self.group.inner.lock_irqsave(); - debug_assert!( - state.task_owners > 1, - "unpublished SEM_UNDO rollback requires the parent owner" - ); - state.task_owners -= 1; - } -} - -#[cfg(test)] -mod tests { - use alloc::sync::Arc; - - use super::{ - detach_sem_undo, PreparedSemUndoRecord, PreparedSemUndoRecordAction, SemUndoAttachment, - SemUndoGroup, SemUndoRecord, UnpublishedSemUndoAttachmentGuard, - }; - use crate::ipc::sem::SemId; - use crate::process::{ - fork::CloneFlags, - namespace::ipc_namespace::{IpcNamespace, INIT_IPC_NAMESPACE}, - KernelStack, ProcessControlBlock, - }; - use system_error::SystemError; - - fn test_ipc_ns() -> &'static Arc { - &INIT_IPC_NAMESPACE - } - - fn test_unpublished_child() -> Arc { - ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()) - } - - fn test_pcb_with_group() -> Arc { - let pcb = test_unpublished_child(); - pcb.ensure_sem_undo_group(test_ipc_ns()).unwrap(); - pcb - } - - fn second_test_ipc_ns() -> Arc { - INIT_IPC_NAMESPACE.copy_ipc_ns( - &CloneFlags::CLONE_NEWIPC, - INIT_IPC_NAMESPACE.user_ns.clone(), - ) - } - - #[test] - fn missing_reservation_cannot_publish_after_final_drain() { - let pcb = test_pcb_with_group(); - let group = pcb.sem_undo_group().unwrap(); - let semid = SemId::new(208); - let record = group.prepare_record_for_test(semid, 1).unwrap(); - - detach_sem_undo(&pcb); - - assert_eq!(group.task_owners_for_test(), 0); - assert_eq!(group.commit_record(record), Err(SystemError::EINVAL)); - assert_eq!(group.record_count_for_test(), 0); - assert_eq!(group.pending_record_reservations_for_test(), 0); - assert_eq!( - group.prepare_record_for_test(semid, 1), - Err(SystemError::EINVAL) - ); - } - - #[test] - fn retired_pending_debt_stays_visible_between_replay_steps() { - use crate::ipc::sem::{SemFlags, SemWakeBatch, IPC_PRIVATE}; - for control in 0..3 { - let ipc_ns = second_test_ipc_ns(); - let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); - let (pending, first) = { - let mut manager = ipc_ns.sem.lock(); - let pending = SemId::new( - manager - .semget_for_test(IPC_PRIVATE, 2, SemFlags::IPC_CREAT) - .unwrap(), - ); - let first = SemId::new( - manager - .semget_for_test(IPC_PRIVATE, 1, SemFlags::IPC_CREAT) - .unwrap(), - ); - manager - .prepare_undo_record_and_registry_for_test(&group, pending) - .unwrap(); - manager - .prepare_undo_record_and_registry_for_test(&group, first) - .unwrap(); - group.with_record_mut(pending, |record| { - record.adjustments.copy_from_slice(&[7, -3]) - }); - group.with_record_mut(first, |record| record.adjustments[0] = 1); - (pending, first) - }; - assert!(group.detach_owner_and_mark_last()); - assert!(group.begin_replay()); - assert!(!group.begin_replay()); - // Vec.pop chooses the last inserted set. The other debt must - // remain in the same registry visited by the semctl primitives. - { - let mut wakes = SemWakeBatch::default(); - let record = { - let mut manager = ipc_ns.sem.lock(); - let record = group.pop_retired_record().unwrap(); - assert_eq!(record.semid, first); - manager.replay_sem_undo_adjustments( - record.semid, - &record.adjustments, - None, - &mut wakes, - ); - manager.unregister_undo_group(record.semid, &group); - record - }; - wakes.wake_all(); - drop(record); - } - assert_eq!(group.record_count_for_test(), 1); - let mut wakes = SemWakeBatch::default(); - let mut removed = None; - let mut manager = ipc_ns.sem.lock(); - match control { - 0 => manager.setval(pending, 0, 9, &mut wakes).unwrap(), - 1 => { - let token = manager.prepare_setall(pending).unwrap(); - manager.setall(token, &[9, 8], &mut wakes).unwrap(); - } - _ => removed = Some(manager.ipc_rmid(pending, &mut wakes).unwrap()), - } - let next = group.pop_retired_record(); - if control == 2 { - assert!(next.is_none()); - } else { - let next = next.as_ref().unwrap(); - assert_eq!(next.adjustment(0), 0); - assert_eq!(next.adjustment(1), if control == 0 { -3 } else { 0 }); - manager.replay_sem_undo_adjustments( - next.semid, - &next.adjustments, - None, - &mut wakes, - ); - manager.unregister_undo_group(next.semid, &group); - assert_eq!( - manager.getall(pending).unwrap(), - if control == 0 { vec![9, 0] } else { vec![9, 8] } - ); - } - assert!(group.pop_retired_record().is_none()); - assert!(!group.begin_replay()); - drop(manager); - wakes.wake_all(); - drop(removed); - drop(next); - } - } - - #[test] - fn retired_group_rejects_shared_owner_and_replays_only_once() { - let pcb = test_pcb_with_group(); - let group = pcb.sem_undo_group().unwrap(); - let child = test_unpublished_child(); - group.insert_test_record(SemId::new(209), &[1]); - - detach_sem_undo(&pcb); - - assert_eq!(group.replay_count_for_test(), 1); - assert_eq!(group.task_owners_for_test(), 0); - assert!(matches!( - pcb.prepare_shared_sem_undo_attachment(test_ipc_ns()), - Err(SystemError::EINVAL) - )); - group.replay_marked_records_for_test(&pcb); - detach_sem_undo(&child); - assert_eq!(group.replay_count_for_test(), 1); - } - - #[test] - fn prepared_existing_and_missing_records_cannot_publish_after_final_drain() { - let pcb = test_pcb_with_group(); - let group = pcb.sem_undo_group().unwrap(); - let existing_semid = SemId::new(210); - let missing_semid = SemId::new(211); - group.insert_test_record(existing_semid, &[1]); - let existing = group.prepare_record_for_test(existing_semid, 1).unwrap(); - let missing = group.prepare_record_for_test(missing_semid, 1).unwrap(); - - detach_sem_undo(&pcb); - - assert_eq!(group.commit_record(existing), Err(SystemError::EINVAL)); - assert_eq!(group.commit_record(missing), Err(SystemError::EINVAL)); - assert_eq!(group.record_count_for_test(), 0); - assert_eq!(group.pending_record_reservations_for_test(), 0); - } - - #[test] - fn prepare_existing_record_is_rejected_after_final_drain() { - let pcb = test_pcb_with_group(); - let group = pcb.sem_undo_group().unwrap(); - let semid = SemId::new(210); - group.insert_test_record(semid, &[1]); - - detach_sem_undo(&pcb); - - assert_eq!(group.record_count_for_test(), 0); - assert_eq!( - group.prepare_record_for_test(semid, 1), - Err(SystemError::EINVAL) - ); - } - - #[test] - fn namespace_lifecycle_invariant_has_no_live_record_at_final_drop() { - let ipc_ns = second_test_ipc_ns(); - let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); - let mut manager = ipc_ns.sem.lock(); - let semid = manager - .semget_for_test( - crate::ipc::sem::IPC_PRIVATE, - 1, - crate::ipc::sem::SemFlags::IPC_CREAT, - ) - .unwrap(); - manager - .prepare_undo_record_and_registry_for_test(&group, SemId::new(semid)) - .unwrap(); - - assert!(group.has_live_records_in_namespace_for_test(&ipc_ns)); - drop(manager); - drop(group); - assert!(ipc_ns.sem.lock().namespace_lifecycle_invariant_for_test()); - } - - #[test] - fn prepare_existing_record_borrows_current_adjustments() { - let group = SemUndoGroup::new_for_test(); - let semid = SemId::new(101); - group.insert_test_record(semid, &[7, -3]); - - let record = group.prepare_record_for_test(semid, 2).unwrap(); - - assert!(record.was_existing()); - group - .with_prepared_record_noalloc(record, |live| { - assert_eq!(live.adjustment_for_test(0), 7); - assert_eq!(live.adjustment_for_test(1), -3); - PreparedSemUndoRecordAction::Keep(()) - }) - .unwrap(); - } - - #[test] - fn concurrent_prepare_for_distinct_semids_reserves_each_missing_record_slot() { - let group = SemUndoGroup::new_for_test(); - let semid_one = SemId::new(201); - let semid_two = SemId::new(202); - - let record_one = group.prepare_record_for_test(semid_one, 1).unwrap(); - let record_two = group.prepare_record_for_test(semid_two, 1).unwrap(); - - assert_eq!(group.pending_record_reservations_for_test(), 2); - assert!(group.record_capacity_for_test() >= 2); - group.commit_record(record_one).unwrap(); - group.commit_record(record_two).unwrap(); - assert_eq!(group.record_count_for_test(), 2); - assert_eq!(group.pending_record_reservations_for_test(), 0); - } - - #[test] - fn prepare_missing_record_reserves_capacity_for_two_outstanding_reservations() { - let group = SemUndoGroup::new_for_test(); - group.set_record_capacity_for_test(1); - - let record_one = group - .prepare_record_for_test(SemId::new(204), 1) - .expect("first reservation must fit in the single free slot"); - let record_two = group - .prepare_record_for_test(SemId::new(205), 1) - .expect("second reservation must grow physical capacity"); - - assert_eq!(group.pending_record_reservations_for_test(), 2); - assert!(group.record_capacity_for_test() >= 2); - group.commit_record(record_one).unwrap(); - group.commit_record(record_two).unwrap(); - assert_eq!(group.record_count_for_test(), 2); - } - - #[test] - fn missing_record_reservation_loses_to_competing_insert_with_retry() { - let group = SemUndoGroup::new_for_test(); - let semid = SemId::new(206); - let stale = group.prepare_record_for_test(semid, 1).unwrap(); - group.insert_test_record(semid, &[9]); - - assert_eq!(group.commit_record(stale), Ok(())); - assert_eq!(group.adjustment_for_test(semid, 0), 9); - assert_eq!(group.pending_record_reservations_for_test(), 0); - } - - #[test] - fn missing_record_reservation_loses_to_rmid_generation_with_retry() { - let group = SemUndoGroup::new_for_test(); - let semid = SemId::new(207); - let stale = group.prepare_record_for_test(semid, 1).unwrap(); - group.remove_record(semid); - - assert_eq!(group.commit_record(stale), Ok(())); - assert_eq!(group.record_count_for_test(), 1); - assert_eq!(group.adjustment_for_test(semid, 0), 0); - assert_eq!(group.pending_record_reservations_for_test(), 0); - } - - #[test] - fn commit_unreserved_missing_record_returns_enomem_without_allocating() { - let group = SemUndoGroup::new_for_test(); - let before_capacity = group.record_capacity_for_test(); - let record = PreparedSemUndoRecord { - semid: SemId::new(203), - nsems: 1, - candidate: Some(SemUndoRecord { - semid: SemId::new(203), - adjustments: alloc::vec![0].into_boxed_slice(), - }), - reservation: None, - }; - - assert_eq!(group.commit_record(record), Err(SystemError::ENOMEM)); - assert_eq!(group.record_count_for_test(), 0); - assert_eq!(group.record_capacity_for_test(), before_capacity); - } - - #[test] - fn failed_first_operation_keeps_zero_record_without_another_reservation() { - let group = SemUndoGroup::new_for_test(); - let semid = SemId::new(214); - let prepared = group.prepare_record_for_test(semid, 2).unwrap(); - assert_eq!(group.record_count_for_test(), 0); // Preparation alone is not publication. - let (result, token) = group - .with_prepared_record_noalloc(prepared, |record| { - assert_eq!(record.adjustment(0), 0); - PreparedSemUndoRecordAction::Keep(Err::<(), _>(SystemError::EAGAIN_OR_EWOULDBLOCK)) - }) - .unwrap(); - assert_eq!(result, Err(SystemError::EAGAIN_OR_EWOULDBLOCK)); - assert!(token.unwrap().was_existing()); - assert_eq!(group.record_count_for_test(), 1); - assert_eq!(group.pending_record_reservations_for_test(), 0); - assert_eq!(group.adjustment_for_test(semid, 0), 0); - assert_eq!(group.adjustment_for_test(semid, 1), 0); - assert!(group - .prepare_record_for_test(semid, 2) - .unwrap() - .was_existing()); - } - - #[test] - fn existing_preparation_reads_current_debt_and_updates_only_one_slot() { - let group = SemUndoGroup::new_for_test(); - let semid = SemId::new(212); - group.insert_test_record(semid, &[3, 7]); - let prepared = group.prepare_record_for_test(semid, 2).unwrap(); - assert!(prepared.was_existing()); - assert!(prepared.candidate.is_none()); - group.with_record_mut(semid, |live| live.set_adjustment(0, 9)); - let (_, kept) = group - .with_prepared_record_noalloc(prepared, |live| { - assert_eq!(live.adjustment(0), 9); - PreparedSemUndoRecordAction::Keep(Err::<(), _>(SystemError::ERANGE)) - }) - .unwrap(); - assert_eq!(group.adjustment_for_test(semid, 0), 9); - assert_eq!(group.adjustment_for_test(semid, 1), 7); - group.with_record_mut(semid, |live| live.clear_all_adjustments()); - group - .with_prepared_record_noalloc(kept.unwrap(), |live| { - assert_eq!(live.adjustment(0), 0); - live.set_adjustment(1, 4); - PreparedSemUndoRecordAction::Publish(()) - }) - .unwrap(); - assert_eq!(group.adjustment_for_test(semid, 0), 0); - assert_eq!(group.adjustment_for_test(semid, 1), 4); - } - - #[test] - fn competing_first_publish_releases_candidate_on_keep() { - let group = SemUndoGroup::new_for_test(); - let semid = SemId::new(213); - let pending = group.prepare_record_for_test(semid, 2).unwrap(); - assert!(!pending.was_existing()); - group.insert_test_record(semid, &[6, 8]); - let (_, kept) = group - .with_prepared_record_noalloc(pending, |live| { - assert_eq!(live.adjustment(0), 6); - PreparedSemUndoRecordAction::Keep(()) - }) - .unwrap(); - assert!(kept.unwrap().was_existing()); - assert_eq!(group.pending_record_reservations_for_test(), 0); - assert_eq!(group.adjustment_for_test(semid, 1), 8); - } - - #[test] - fn observer_arc_does_not_change_task_owner_count() { - let group = SemUndoGroup::new_for_test(); - let attachment = SemUndoAttachment::new_for_test(group.clone()); - let observer = attachment.group_for_test(); - assert_eq!(group.task_owners_for_test(), 1); - drop(observer); - assert_eq!(group.task_owners_for_test(), 1); - } - - #[test] - fn attachment_is_taken_once_and_drop_never_replays() { - let attachment = SemUndoAttachment::new_for_test(SemUndoGroup::new_for_test()); - let mut slot = Some(attachment); - assert!(slot.take().is_some()); - assert!(slot.take().is_none()); - } - - #[test] - fn group_rejects_different_ipc_namespace() { - let group = SemUndoGroup::new_for_test_bound_to_first_namespace(); - assert_eq!( - group.verify_ipc_ns_for_test(second_test_ipc_ns()), - Err(SystemError::EINVAL) - ); - } - - #[test] - fn ordinary_fork_child_starts_without_attachment() { - let parent = test_pcb_with_group(); - let child = test_unpublished_child(); - - assert!(child.sem_undo_group().is_none()); - assert!(parent.sem_undo_group().is_some()); - } - - #[test] - fn sysvsem_guard_increments_once_then_install_moves_token() { - let parent = test_pcb_with_group(); - let group = parent.sem_undo_group().unwrap(); - let child = test_unpublished_child(); - - let mut guard = parent - .prepare_shared_sem_undo_attachment(test_ipc_ns()) - .unwrap(); - assert_eq!(group.task_owners_for_test(), 2); - guard.install_into(&child); - assert!(child.sem_undo_group().is_some()); - guard.disarm(); - assert_eq!(group.task_owners_for_test(), 2); - } - - #[test] - fn installed_guard_rollback_takes_child_slot_and_only_drops_owner() { - let parent = test_pcb_with_group(); - let group = parent.sem_undo_group().unwrap(); - let child = test_unpublished_child(); - - let mut guard = parent - .prepare_shared_sem_undo_attachment(test_ipc_ns()) - .unwrap(); - guard.install_into(&child); - drop(guard); - - assert!(child.sem_undo_group().is_none()); - assert_eq!(group.task_owners_for_test(), 1); - assert_eq!(group.replay_count_for_test(), 0); - } -} diff --git a/kernel/src/ipc/sem_undo/lifecycle.rs b/kernel/src/ipc/sem_undo/lifecycle.rs new file mode 100644 index 0000000000..cb87b068ea --- /dev/null +++ b/kernel/src/ipc/sem_undo/lifecycle.rs @@ -0,0 +1,157 @@ +//! Task attachment ownership and explicit, per-set exit replay. +use super::SemUndoGroup; +use crate::{ + ipc::sem::{SemManager, SemWakeBatch}, + process::{pid::PidType, ProcessControlBlock}, +}; +use alloc::sync::Arc; + +#[derive(Debug)] +pub struct SemUndoAttachment { + group: Arc, +} + +pub(crate) struct UnpublishedSemUndoAttachmentGuard { + group: Arc, + attachment: Option, + installed_child: Option>, + armed: bool, +} + +impl SemUndoAttachment { + pub(crate) fn new(group: Arc) -> Self { + Self { group } + } + + pub(crate) fn group(&self) -> Arc { + self.group.clone() + } + + #[cfg(test)] + pub(super) fn new_for_test(group: Arc) -> Self { + Self::new(group) + } + + #[cfg(test)] + pub(super) fn group_for_test(&self) -> Arc { + self.group() + } +} + +impl Drop for SemUndoAttachment { + // Replay is an explicit lifecycle operation and must never run from Drop. + fn drop(&mut self) {} +} + +pub(crate) fn detach_sem_undo(pcb: &Arc) { + let Some(attachment) = pcb.take_sem_undo_attachment() else { + return; + }; + let group = attachment.group(); + drop(attachment); + + if !group.detach_owner_and_mark_last() { + return; + } + + replay_marked_records(pcb, &group); +} + +pub(super) fn replay_marked_records(pcb: &Arc, group: &Arc) { + if !group.begin_replay() { + return; + } + let exiting_tgid = pcb.try_active_pid_ns().and_then(|pid_ns| { + pcb.task_pid_nr_ns(PidType::TGID, Some(pid_ns)) + .filter(|tgid| tgid.data() != 0)?; + pcb.task_pid_ptr(PidType::TGID) + }); + + let Some(ipc_ns) = group.ipc_ns.upgrade() else { + for record in group.discard_retired_records() { + log::debug!( + "dropping SEM_UNDO record for semid {} after IPC namespace teardown", + record.semid.data() + ); + } + return; + }; + + loop { + let mut wakes = SemWakeBatch::default(); + let record = { + let mut manager = ipc_ns.sem.lock(); + let Some(record) = group.pop_retired_record() else { + break; + }; + SemManager::replay_sem_undo_adjustments( + &mut manager, + record.semid, + &record.adjustments, + exiting_tgid.clone(), + &mut wakes, + ); + manager.unregister_undo_group(record.semid, group); + record + }; + // Publish and notify one set at a time, like Linux exit_sem. Pending + // records stay in the group so interleaved semctl still clears them. + wakes.wake_all(); + SemManager::shrink_undo_registry(&ipc_ns, record.semid); + } +} + +impl UnpublishedSemUndoAttachmentGuard { + pub(crate) fn new(group: Arc) -> Self { + group.acquire_shared_owner(); + + Self { + attachment: Some(SemUndoAttachment::new(group.clone())), + group, + installed_child: None, + armed: true, + } + } + + pub(crate) fn install_into(&mut self, child: &ProcessControlBlock) { + assert!(self.armed, "cannot install a disarmed SEM_UNDO guard"); + assert!( + self.installed_child.is_none(), + "SEM_UNDO guard can only be installed once" + ); + let attachment = self + .attachment + .take() + .expect("SEM_UNDO guard attachment token is missing"); + self.installed_child = Some(child.install_unpublished_sem_undo_attachment(attachment)); + } + + pub(crate) fn disarm(mut self) { + debug_assert!( + self.attachment.is_none() && self.installed_child.is_some(), + "only an installed SEM_UNDO guard can be disarmed" + ); + self.armed = false; + } +} + +impl Drop for UnpublishedSemUndoAttachmentGuard { + fn drop(&mut self) { + if !self.armed { + return; + } + + if let Some(child) = self.installed_child.take() { + let attachment = child.take_sem_undo_attachment(); + debug_assert!(attachment.is_some(), "installed SEM_UNDO slot is empty"); + if let Some(attachment) = attachment { + debug_assert!(Arc::ptr_eq(&attachment.group, &self.group)); + drop(attachment); + } + } else { + drop(self.attachment.take()); + } + + self.group.rollback_unpublished_owner(); + } +} diff --git a/kernel/src/ipc/sem_undo/mod.rs b/kernel/src/ipc/sem_undo/mod.rs new file mode 100644 index 0000000000..86830a6efa --- /dev/null +++ b/kernel/src/ipc/sem_undo/mod.rs @@ -0,0 +1,339 @@ +//! SEM_UNDO group state. The manager lock precedes the group lock whenever both are needed. +use crate::{ + ipc::sem::SemId, libs::spinlock::SpinLock, process::namespace::ipc_namespace::IpcNamespace, +}; +use alloc::{ + sync::{Arc, Weak}, + vec::Vec, +}; +use system_error::SystemError; + +mod lifecycle; +mod record; +pub use lifecycle::SemUndoAttachment; +pub(crate) use lifecycle::{detach_sem_undo, UnpublishedSemUndoAttachmentGuard}; +use record::PendingSemUndoRecordReservation; +pub(crate) use record::{PreparedSemUndoRecord, PreparedSemUndoRecordAction, SemUndoRecord}; + +#[derive(Debug)] +pub struct SemUndoGroup { + ipc_ns: Weak, + inner: SpinLock, +} + +#[derive(Debug)] +struct SemUndoGroupState { + task_owners: usize, + records: Vec, + reserved_records: usize, + phase: UndoPhase, + #[cfg(test)] + replay_count: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum UndoPhase { + Active, + Retired, + Replaying, +} + +fn prepare_records_storage_capacity( + records: &mut Vec, + required_capacity: usize, +) -> Result<(), SystemError> { + if records.capacity() >= required_capacity { + return Ok(()); + } + + let additional = required_capacity + .checked_sub(records.len()) + .ok_or(SystemError::ENOMEM)?; + records + .try_reserve_exact(additional) + .map_err(|_| SystemError::ENOMEM)?; + if records.capacity() < required_capacity { + return Err(SystemError::ENOMEM); + } + Ok(()) +} + +impl SemUndoGroup { + pub(crate) fn new(ipc_ns: &Arc) -> Result, SystemError> { + Arc::try_new(Self { + ipc_ns: Arc::downgrade(ipc_ns), + inner: SpinLock::new(SemUndoGroupState { + task_owners: 1, + records: Vec::new(), + reserved_records: 0, + phase: UndoPhase::Active, + #[cfg(test)] + replay_count: 0, + }), + }) + .map_err(|_| SystemError::ENOMEM) + } + + pub(crate) fn verify_ipc_ns(&self, ipc_ns: &Arc) -> Result<(), SystemError> { + let state = self.inner.lock_irqsave(); + if state.task_owners == 0 || state.phase != UndoPhase::Active { + return Err(SystemError::EINVAL); + } + if self.ipc_ns.ptr_eq(&Arc::downgrade(ipc_ns)) { + Ok(()) + } else { + Err(SystemError::EINVAL) + } + } + + fn acquire_shared_owner(&self) { + let mut state = self.inner.lock_irqsave(); + debug_assert!( + state.task_owners > 0 && state.phase == UndoPhase::Active, + "SEM_UNDO shared owner must be acquired before final retirement" + ); + state.task_owners = state + .task_owners + .checked_add(1) + .expect("SEM_UNDO task owner count overflow"); + } + + fn rollback_unpublished_owner(&self) { + let mut state = self.inner.lock_irqsave(); + debug_assert!( + state.task_owners > 1, + "unpublished SEM_UNDO rollback requires the parent owner" + ); + state.task_owners -= 1; + } + + fn detach_owner_and_mark_last(&self) -> bool { + let mut state = self.inner.lock_irqsave(); + debug_assert!( + state.task_owners > 0, + "SEM_UNDO detach requires an attached task owner" + ); + if state.task_owners == 0 { + return false; + } + + state.task_owners -= 1; + if state.task_owners != 0 { + return false; + } + + state.phase = UndoPhase::Retired; + true + } + + /// Claim retirement exactly once without hiding pending debt from semctl. + fn begin_replay(&self) -> bool { + let mut state = self.inner.lock_irqsave(); + if state.phase != UndoPhase::Retired { + return false; + } + state.phase = UndoPhase::Replaying; + #[cfg(test)] + { + state.replay_count += 1; + } + true + } + + /// The caller holds the namespace manager lock until this debt is applied. + /// Other records remain visible to SETVAL/SETALL and IPC_RMID between steps. + fn pop_retired_record(&self) -> Option { + let mut state = self.inner.lock_irqsave(); + debug_assert!(state.phase == UndoPhase::Replaying); + state.records.pop() + } + + /// Only valid after the bound namespace can no longer be upgraded. + fn discard_retired_records(&self) -> Vec { + let mut state = self.inner.lock_irqsave(); + debug_assert!(state.phase == UndoPhase::Replaying); + core::mem::take(&mut state.records) + } + + pub(crate) fn prepare_record( + self: &Arc, + semid: SemId, + nsems: usize, + ) -> Result { + let mut adjustments = Vec::new(); + let mut reserved_storage = Vec::new(); + + loop { + let mut state = self.inner.lock_irqsave(); + if state.task_owners == 0 || state.phase != UndoPhase::Active { + return Err(SystemError::EINVAL); + } + + if let Some(existing) = state.records.iter().find(|item| item.semid == semid) { + if existing.adjustments.len() != nsems { + return Err(SystemError::EINVAL); + } + return Ok(PreparedSemUndoRecord { + semid, + nsems, + candidate: None, + reservation: None, + }); + } + + if adjustments.len() != nsems { + drop(state); + adjustments + .try_reserve_exact(nsems) + .map_err(|_| SystemError::ENOMEM)?; + adjustments.resize(nsems, 0); + continue; + } + + let required_capacity = state + .records + .len() + .checked_add(state.reserved_records) + .and_then(|capacity| capacity.checked_add(1)) + .ok_or(SystemError::ENOMEM)?; + + if state.records.capacity() < required_capacity { + if reserved_storage.capacity() < required_capacity { + drop(state); + prepare_records_storage_capacity(&mut reserved_storage, required_capacity)?; + continue; + } + + reserved_storage.append(&mut state.records); + core::mem::swap(&mut state.records, &mut reserved_storage); + } + + state.reserved_records = state + .reserved_records + .checked_add(1) + .ok_or(SystemError::ENOMEM)?; + // into_boxed_slice may shrink an allocation. Do it after releasing + // the group lock, with an armed reservation already owning the slot. + let reservation = PendingSemUndoRecordReservation::new(self); + drop(state); + return Ok(PreparedSemUndoRecord { + semid, + nsems, + candidate: Some(SemUndoRecord { + semid, + adjustments: adjustments.into_boxed_slice(), + }), + reservation: Some(reservation), + }); + } + } + + #[cfg(test)] + pub(crate) fn commit_record(&self, record: PreparedSemUndoRecord) -> Result<(), SystemError> { + self.commit_prepared_record_noalloc(record) + } + + #[cfg(test)] + pub(crate) fn commit_prepared_record_noalloc( + &self, + record: PreparedSemUndoRecord, + ) -> Result<(), SystemError> { + self.with_prepared_record_noalloc(record, |_record| { + PreparedSemUndoRecordAction::Complete(()) + }) + .map(|((), _)| ()) + } + + /// Borrow the current record under the group lock, never a stale snapshot. + /// The callback must simulate without writes and mutate only on Complete; + /// Keep (including errors/blocked operations) must leave the debt unchanged. + /// First use publishes a zero record before calling the simulation, as in + /// Linux find_alloc_undo; Keep retains only a lightweight existing token. + /// SemopScratch provides that separation without an additional transaction. + pub(crate) fn with_prepared_record_noalloc( + &self, + mut record: PreparedSemUndoRecord, + f: impl FnOnce(&mut SemUndoRecord) -> PreparedSemUndoRecordAction, + ) -> Result<(R, Option), SystemError> { + // A competing first publisher makes this candidate redundant. Keep + // its disposal outside the group lock and return a lightweight token. + let mut _retired_candidate = None; + let mut state = self.inner.lock_irqsave(); + if state.task_owners == 0 || state.phase != UndoPhase::Active { + return Err(SystemError::EINVAL); + } + + let existing_index = state + .records + .iter() + .position(|item| item.semid == record.semid); + + if let Some(index) = existing_index { + if state.records[index].adjustments.len() != record.nsems { + return Err(SystemError::EINVAL); + } + _retired_candidate = record.candidate.take(); + if let Some(mut reservation) = record.reservation.take() { + state.reserved_records = state + .reserved_records + .checked_sub(1) + .expect("SEM_UNDO record reservation count underflow"); + reservation.disarm(); + } + + return match f(&mut state.records[index]) { + PreparedSemUndoRecordAction::Complete(result) => Ok((result, None)), + PreparedSemUndoRecordAction::Keep(result) => Ok((result, Some(record))), + }; + } + + // Like Linux find_alloc_undo, publish a zero record before simulating: + // even a blocked/failed operation retains this group/set association. + // Existing tokens cannot recreate a record removed by RMID. + if record.candidate.is_none() { + return Err(SystemError::EINVAL); + } + if state.records.len() >= state.records.capacity() { + return Err(SystemError::ENOMEM); + } + if let Some(mut reservation) = record.reservation.take() { + state.reserved_records = state + .reserved_records + .checked_sub(1) + .expect("SEM_UNDO record reservation count underflow"); + reservation.disarm(); + } + state.records.push(record.candidate.take().unwrap()); + match f(state.records.last_mut().unwrap()) { + PreparedSemUndoRecordAction::Complete(result) => Ok((result, None)), + PreparedSemUndoRecordAction::Keep(result) => Ok((result, Some(record))), + } + } + + pub(crate) fn with_record_mut( + &self, + semid: SemId, + f: impl FnOnce(&mut SemUndoRecord) -> R, + ) -> Option { + let mut state = self.inner.lock_irqsave(); + state + .records + .iter_mut() + .find(|record| record.semid == semid) + .map(f) + } + + pub(crate) fn take_record(&self, semid: SemId) -> Option { + let mut state = self.inner.lock_irqsave(); + let index = state + .records + .iter() + .position(|record| record.semid == semid)?; + Some(state.records.swap_remove(index)) + } +} + +#[cfg(test)] +mod test_support; +#[cfg(test)] +mod tests; diff --git a/kernel/src/ipc/sem_undo/record.rs b/kernel/src/ipc/sem_undo/record.rs new file mode 100644 index 0000000000..ef98ff3329 --- /dev/null +++ b/kernel/src/ipc/sem_undo/record.rs @@ -0,0 +1,115 @@ +//! Live undo debt and allocation reservations prepared before taking the manager lock. +use super::SemUndoGroup; +use crate::ipc::sem::SemId; +use alloc::{ + boxed::Box, + sync::{Arc, Weak}, +}; + +#[derive(Debug)] +pub(super) struct PendingSemUndoRecordReservation { + group: Weak, + active: bool, +} + +#[derive(Debug)] +pub(crate) struct SemUndoRecord { + pub(super) semid: SemId, + pub(super) adjustments: Box<[i16]>, +} + +/// Existing records need no snapshot: simulation reads the current record +/// while holding the group lock. Only a first-use candidate owns dense storage. +#[derive(Debug)] +pub(crate) struct PreparedSemUndoRecord { + pub(super) semid: SemId, + pub(super) nsems: usize, + pub(super) candidate: Option, + pub(super) reservation: Option, +} + +pub(crate) enum PreparedSemUndoRecordAction { + Complete(R), + Keep(R), +} + +impl PendingSemUndoRecordReservation { + pub(super) fn new(group: &Arc) -> Self { + Self { + group: Arc::downgrade(group), + active: true, + } + } + + pub(super) fn disarm(&mut self) { + self.active = false; + } +} + +impl Drop for PendingSemUndoRecordReservation { + fn drop(&mut self) { + if !self.active { + return; + } + let Some(group) = self.group.upgrade() else { + return; + }; + let mut state = group.inner.lock_irqsave(); + state.reserved_records = state + .reserved_records + .checked_sub(1) + .expect("SEM_UNDO record reservation count underflow"); + self.active = false; + } +} + +impl PreparedSemUndoRecord { + pub(crate) fn was_existing(&self) -> bool { + self.candidate.is_none() + } + + pub(crate) fn adjustment_count(&self) -> usize { + self.nsems + } +} + +impl SemUndoRecord { + #[cfg(test)] + pub(super) fn new_live(semid: SemId, adjustments: Box<[i16]>) -> Self { + Self { semid, adjustments } + } + + pub(crate) fn adjustment(&self, semnum: usize) -> i16 { + self.adjustments[semnum] + } + + pub(crate) fn set_adjustment(&mut self, semnum: usize, adjustment: i16) { + if self.adjustments[semnum] != adjustment { + self.adjustments[semnum] = adjustment; + } + } + + pub(crate) fn clear_adjustment(&mut self, semnum: usize) { + self.set_adjustment(semnum, 0); + } + + pub(crate) fn clear_all_adjustments(&mut self) { + if self.adjustments.iter().any(|&adjustment| adjustment != 0) { + self.adjustments.fill(0); + } + } + + pub(crate) fn adjustment_count(&self) -> usize { + self.adjustments.len() + } + + #[cfg(test)] + pub(crate) fn adjustment_for_test(&self, semnum: usize) -> i16 { + self.adjustment(semnum) + } + + #[cfg(test)] + pub(crate) fn set_adjustment_for_test(&mut self, semnum: usize, adjustment: i16) { + self.set_adjustment(semnum, adjustment); + } +} diff --git a/kernel/src/ipc/sem_undo/test_support.rs b/kernel/src/ipc/sem_undo/test_support.rs new file mode 100644 index 0000000000..e1a3fa5e74 --- /dev/null +++ b/kernel/src/ipc/sem_undo/test_support.rs @@ -0,0 +1,113 @@ +use super::lifecycle::replay_marked_records; +use super::*; +use crate::process::ProcessControlBlock; + +impl SemUndoGroup { + pub(super) fn new_for_test() -> Arc { + Self::new(&crate::process::namespace::ipc_namespace::INIT_IPC_NAMESPACE).unwrap() + } + + #[cfg(test)] + pub(super) fn new_for_test_bound_to_first_namespace() -> Arc { + Self::new_for_test() + } + + #[cfg(test)] + pub(crate) fn new_for_test_bound_to( + ipc_ns: &Arc, + ) -> Result, SystemError> { + Self::new(ipc_ns) + } + + pub(crate) fn remove_record(&self, semid: SemId) { + drop(self.take_record(semid)); + } + + #[cfg(test)] + pub(crate) fn adjustment_for_test(&self, semid: SemId, semnum: usize) -> i16 { + self.inner + .lock_irqsave() + .records + .iter() + .find(|record| record.semid == semid) + .map(|record| record.adjustment(semnum)) + .unwrap_or_default() + } + + #[cfg(test)] + pub(crate) fn has_live_records_in_namespace_for_test( + &self, + ipc_ns: &Arc, + ) -> bool { + self.verify_ipc_ns(ipc_ns).is_ok() && !self.inner.lock_irqsave().records.is_empty() + } + + #[cfg(test)] + pub(crate) fn prepare_record_for_test( + self: &Arc, + semid: SemId, + nsems: usize, + ) -> Result { + self.prepare_record(semid, nsems) + } + + #[cfg(test)] + pub(crate) fn task_owners_for_test(&self) -> usize { + self.inner.lock_irqsave().task_owners + } + + #[cfg(test)] + pub(crate) fn replay_count_for_test(&self) -> usize { + self.inner.lock_irqsave().replay_count + } + + #[cfg(test)] + pub(super) fn verify_ipc_ns_for_test( + &self, + ipc_ns: Arc, + ) -> Result<(), SystemError> { + self.verify_ipc_ns(&ipc_ns) + } + + #[cfg(test)] + pub(crate) fn insert_test_record(&self, semid: SemId, adjustments: &[i16]) { + let mut state = self.inner.lock_irqsave(); + state.records.push(SemUndoRecord::new_live( + semid, + adjustments.to_vec().into_boxed_slice(), + )); + } + + #[cfg(test)] + pub(crate) fn record_count_for_test(&self) -> usize { + self.inner.lock_irqsave().records.len() + } + + #[cfg(test)] + pub(crate) fn record_capacity_for_test(&self) -> usize { + self.inner.lock_irqsave().records.capacity() + } + + #[cfg(test)] + pub(crate) fn set_record_capacity_for_test(&self, capacity: usize) { + let mut state = self.inner.lock_irqsave(); + assert!(state.records.is_empty()); + state.records = Vec::with_capacity(capacity); + assert_eq!(state.records.capacity(), capacity); + } + + #[cfg(test)] + pub(crate) fn pending_record_reservations_for_test(&self) -> usize { + self.inner.lock_irqsave().reserved_records + } + + #[cfg(test)] + pub(crate) fn detach_last_owner_for_test(&self) -> bool { + self.detach_owner_and_mark_last() + } + + #[cfg(test)] + pub(crate) fn replay_marked_records_for_test(self: &Arc, pcb: &Arc) { + replay_marked_records(pcb, self); + } +} diff --git a/kernel/src/ipc/sem_undo/tests.rs b/kernel/src/ipc/sem_undo/tests.rs new file mode 100644 index 0000000000..72008feeaf --- /dev/null +++ b/kernel/src/ipc/sem_undo/tests.rs @@ -0,0 +1,455 @@ +use alloc::sync::Arc; + +use super::{ + detach_sem_undo, PreparedSemUndoRecord, PreparedSemUndoRecordAction, SemUndoAttachment, + SemUndoGroup, SemUndoRecord, +}; +use crate::ipc::sem::SemId; +use crate::process::{ + fork::CloneFlags, + namespace::ipc_namespace::{IpcNamespace, INIT_IPC_NAMESPACE}, + KernelStack, ProcessControlBlock, +}; +use system_error::SystemError; + +fn test_ipc_ns() -> &'static Arc { + &INIT_IPC_NAMESPACE +} + +fn test_unpublished_child() -> Arc { + ProcessControlBlock::new_idle(0, KernelStack::new().unwrap()) +} + +fn test_pcb_with_group() -> Arc { + let pcb = test_unpublished_child(); + pcb.ensure_sem_undo_group(test_ipc_ns()).unwrap(); + pcb +} + +fn second_test_ipc_ns() -> Arc { + INIT_IPC_NAMESPACE.copy_ipc_ns( + &CloneFlags::CLONE_NEWIPC, + INIT_IPC_NAMESPACE.user_ns.clone(), + ) +} + +#[test] +fn missing_reservation_cannot_publish_after_final_drain() { + let pcb = test_pcb_with_group(); + let group = pcb.sem_undo_group().unwrap(); + let semid = SemId::new(208); + let record = group.prepare_record_for_test(semid, 1).unwrap(); + + detach_sem_undo(&pcb); + + assert_eq!(group.task_owners_for_test(), 0); + assert_eq!(group.commit_record(record), Err(SystemError::EINVAL)); + assert_eq!(group.record_count_for_test(), 0); + assert_eq!(group.pending_record_reservations_for_test(), 0); + assert!(matches!( + group.prepare_record_for_test(semid, 1), + Err(SystemError::EINVAL) + )); +} + +#[test] +fn retired_pending_debt_stays_visible_between_replay_steps() { + use crate::ipc::sem::{SemFlags, SemWakeBatch, IPC_PRIVATE}; + for control in 0..3 { + let ipc_ns = second_test_ipc_ns(); + let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let (pending, first) = { + let mut manager = ipc_ns.sem.lock(); + let pending = SemId::new( + manager + .semget_for_test(IPC_PRIVATE, 2, SemFlags::IPC_CREAT) + .unwrap(), + ); + let first = SemId::new( + manager + .semget_for_test(IPC_PRIVATE, 1, SemFlags::IPC_CREAT) + .unwrap(), + ); + manager + .prepare_undo_record_and_registry_for_test(&group, pending) + .unwrap(); + manager + .prepare_undo_record_and_registry_for_test(&group, first) + .unwrap(); + group.with_record_mut(pending, |record| { + record.adjustments.copy_from_slice(&[7, -3]) + }); + group.with_record_mut(first, |record| record.adjustments[0] = 1); + (pending, first) + }; + assert!(group.detach_owner_and_mark_last()); + assert!(group.begin_replay()); + assert!(!group.begin_replay()); + // Vec.pop chooses the last inserted set. The other debt must + // remain in the same registry visited by the semctl primitives. + { + let mut wakes = SemWakeBatch::default(); + let record = { + let mut manager = ipc_ns.sem.lock(); + let record = group.pop_retired_record().unwrap(); + assert_eq!(record.semid, first); + manager.replay_sem_undo_adjustments( + record.semid, + &record.adjustments, + None, + &mut wakes, + ); + manager.unregister_undo_group(record.semid, &group); + record + }; + wakes.wake_all(); + drop(record); + } + assert_eq!(group.record_count_for_test(), 1); + let mut wakes = SemWakeBatch::default(); + let mut removed = None; + let mut manager = ipc_ns.sem.lock(); + match control { + 0 => manager.setval(pending, 0, 9, &mut wakes).unwrap(), + 1 => { + let token = manager.prepare_setall(pending).unwrap(); + manager.setall(token, &[9, 8], &mut wakes).unwrap(); + } + _ => removed = Some(manager.ipc_rmid(pending, &mut wakes).unwrap()), + } + let next = group.pop_retired_record(); + if control == 2 { + assert!(next.is_none()); + } else { + let next = next.as_ref().unwrap(); + assert_eq!(next.adjustment(0), 0); + assert_eq!(next.adjustment(1), if control == 0 { -3 } else { 0 }); + manager.replay_sem_undo_adjustments(next.semid, &next.adjustments, None, &mut wakes); + manager.unregister_undo_group(next.semid, &group); + assert_eq!( + manager.getall(pending).unwrap(), + if control == 0 { vec![9, 0] } else { vec![9, 8] } + ); + } + assert!(group.pop_retired_record().is_none()); + assert!(!group.begin_replay()); + drop(manager); + wakes.wake_all(); + drop(removed); + drop(next); + } +} + +#[test] +fn retired_group_rejects_shared_owner_and_replays_only_once() { + let pcb = test_pcb_with_group(); + let group = pcb.sem_undo_group().unwrap(); + let child = test_unpublished_child(); + group.insert_test_record(SemId::new(209), &[1]); + + detach_sem_undo(&pcb); + + assert_eq!(group.replay_count_for_test(), 1); + assert_eq!(group.task_owners_for_test(), 0); + assert!(matches!( + pcb.prepare_shared_sem_undo_attachment(test_ipc_ns()), + Err(SystemError::EINVAL) + )); + group.replay_marked_records_for_test(&pcb); + detach_sem_undo(&child); + assert_eq!(group.replay_count_for_test(), 1); +} + +#[test] +fn prepared_existing_and_missing_records_cannot_publish_after_final_drain() { + let pcb = test_pcb_with_group(); + let group = pcb.sem_undo_group().unwrap(); + let existing_semid = SemId::new(210); + let missing_semid = SemId::new(211); + group.insert_test_record(existing_semid, &[1]); + let existing = group.prepare_record_for_test(existing_semid, 1).unwrap(); + let missing = group.prepare_record_for_test(missing_semid, 1).unwrap(); + + detach_sem_undo(&pcb); + + assert_eq!(group.commit_record(existing), Err(SystemError::EINVAL)); + assert_eq!(group.commit_record(missing), Err(SystemError::EINVAL)); + assert_eq!(group.record_count_for_test(), 0); + assert_eq!(group.pending_record_reservations_for_test(), 0); +} + +#[test] +fn prepare_existing_record_is_rejected_after_final_drain() { + let pcb = test_pcb_with_group(); + let group = pcb.sem_undo_group().unwrap(); + let semid = SemId::new(210); + group.insert_test_record(semid, &[1]); + + detach_sem_undo(&pcb); + + assert_eq!(group.record_count_for_test(), 0); + assert!(matches!( + group.prepare_record_for_test(semid, 1), + Err(SystemError::EINVAL) + )); +} + +#[test] +fn namespace_lifecycle_invariant_has_no_live_record_at_final_drop() { + let ipc_ns = second_test_ipc_ns(); + let group = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); + let mut manager = ipc_ns.sem.lock(); + let semid = manager + .semget_for_test( + crate::ipc::sem::IPC_PRIVATE, + 1, + crate::ipc::sem::SemFlags::IPC_CREAT, + ) + .unwrap(); + manager + .prepare_undo_record_and_registry_for_test(&group, SemId::new(semid)) + .unwrap(); + + assert!(group.has_live_records_in_namespace_for_test(&ipc_ns)); + drop(manager); + drop(group); + assert!(ipc_ns.sem.lock().namespace_lifecycle_invariant_for_test()); +} + +#[test] +fn prepare_existing_record_borrows_current_adjustments() { + let group = SemUndoGroup::new_for_test(); + let semid = SemId::new(101); + group.insert_test_record(semid, &[7, -3]); + + let record = group.prepare_record_for_test(semid, 2).unwrap(); + + assert!(record.was_existing()); + group + .with_prepared_record_noalloc(record, |live| { + assert_eq!(live.adjustment_for_test(0), 7); + assert_eq!(live.adjustment_for_test(1), -3); + PreparedSemUndoRecordAction::Keep(()) + }) + .unwrap(); +} + +#[test] +fn concurrent_prepare_for_distinct_semids_reserves_each_missing_record_slot() { + let group = SemUndoGroup::new_for_test(); + let semid_one = SemId::new(201); + let semid_two = SemId::new(202); + + let record_one = group.prepare_record_for_test(semid_one, 1).unwrap(); + let record_two = group.prepare_record_for_test(semid_two, 1).unwrap(); + + assert_eq!(group.pending_record_reservations_for_test(), 2); + assert!(group.record_capacity_for_test() >= 2); + group.commit_record(record_one).unwrap(); + group.commit_record(record_two).unwrap(); + assert_eq!(group.record_count_for_test(), 2); + assert_eq!(group.pending_record_reservations_for_test(), 0); +} + +#[test] +fn prepare_missing_record_reserves_capacity_for_two_outstanding_reservations() { + let group = SemUndoGroup::new_for_test(); + group.set_record_capacity_for_test(1); + + let record_one = group + .prepare_record_for_test(SemId::new(204), 1) + .expect("first reservation must fit in the single free slot"); + let record_two = group + .prepare_record_for_test(SemId::new(205), 1) + .expect("second reservation must grow physical capacity"); + + assert_eq!(group.pending_record_reservations_for_test(), 2); + assert!(group.record_capacity_for_test() >= 2); + group.commit_record(record_one).unwrap(); + group.commit_record(record_two).unwrap(); + assert_eq!(group.record_count_for_test(), 2); +} + +#[test] +fn missing_record_reservation_loses_to_competing_insert_with_retry() { + let group = SemUndoGroup::new_for_test(); + let semid = SemId::new(206); + let stale = group.prepare_record_for_test(semid, 1).unwrap(); + group.insert_test_record(semid, &[9]); + + assert_eq!(group.commit_record(stale), Ok(())); + assert_eq!(group.adjustment_for_test(semid, 0), 9); + assert_eq!(group.pending_record_reservations_for_test(), 0); +} + +#[test] +fn missing_record_reservation_loses_to_rmid_generation_with_retry() { + let group = SemUndoGroup::new_for_test(); + let semid = SemId::new(207); + let stale = group.prepare_record_for_test(semid, 1).unwrap(); + group.remove_record(semid); + + assert_eq!(group.commit_record(stale), Ok(())); + assert_eq!(group.record_count_for_test(), 1); + assert_eq!(group.adjustment_for_test(semid, 0), 0); + assert_eq!(group.pending_record_reservations_for_test(), 0); +} + +#[test] +fn commit_unreserved_missing_record_returns_enomem_without_allocating() { + let group = SemUndoGroup::new_for_test(); + let before_capacity = group.record_capacity_for_test(); + let record = PreparedSemUndoRecord { + semid: SemId::new(203), + nsems: 1, + candidate: Some(SemUndoRecord { + semid: SemId::new(203), + adjustments: alloc::vec![0].into_boxed_slice(), + }), + reservation: None, + }; + + assert_eq!(group.commit_record(record), Err(SystemError::ENOMEM)); + assert_eq!(group.record_count_for_test(), 0); + assert_eq!(group.record_capacity_for_test(), before_capacity); +} + +#[test] +fn failed_first_operation_keeps_zero_record_without_another_reservation() { + let group = SemUndoGroup::new_for_test(); + let semid = SemId::new(214); + let prepared = group.prepare_record_for_test(semid, 2).unwrap(); + assert_eq!(group.record_count_for_test(), 0); // Preparation alone is not publication. + let (result, token) = group + .with_prepared_record_noalloc(prepared, |record| { + assert_eq!(record.adjustment(0), 0); + PreparedSemUndoRecordAction::Keep(Err::<(), _>(SystemError::EAGAIN_OR_EWOULDBLOCK)) + }) + .unwrap(); + assert_eq!(result, Err(SystemError::EAGAIN_OR_EWOULDBLOCK)); + assert!(token.unwrap().was_existing()); + assert_eq!(group.record_count_for_test(), 1); + assert_eq!(group.pending_record_reservations_for_test(), 0); + assert_eq!(group.adjustment_for_test(semid, 0), 0); + assert_eq!(group.adjustment_for_test(semid, 1), 0); + assert!(group + .prepare_record_for_test(semid, 2) + .unwrap() + .was_existing()); +} + +#[test] +fn existing_preparation_reads_current_debt_and_updates_only_one_slot() { + let group = SemUndoGroup::new_for_test(); + let semid = SemId::new(212); + group.insert_test_record(semid, &[3, 7]); + let prepared = group.prepare_record_for_test(semid, 2).unwrap(); + assert!(prepared.was_existing()); + assert!(prepared.candidate.is_none()); + group.with_record_mut(semid, |live| live.set_adjustment(0, 9)); + let (_, kept) = group + .with_prepared_record_noalloc(prepared, |live| { + assert_eq!(live.adjustment(0), 9); + PreparedSemUndoRecordAction::Keep(Err::<(), _>(SystemError::ERANGE)) + }) + .unwrap(); + assert_eq!(group.adjustment_for_test(semid, 0), 9); + assert_eq!(group.adjustment_for_test(semid, 1), 7); + group.with_record_mut(semid, |live| live.clear_all_adjustments()); + group + .with_prepared_record_noalloc(kept.unwrap(), |live| { + assert_eq!(live.adjustment(0), 0); + live.set_adjustment(1, 4); + PreparedSemUndoRecordAction::Complete(()) + }) + .unwrap(); + assert_eq!(group.adjustment_for_test(semid, 0), 0); + assert_eq!(group.adjustment_for_test(semid, 1), 4); +} + +#[test] +fn competing_first_publish_releases_candidate_on_keep() { + let group = SemUndoGroup::new_for_test(); + let semid = SemId::new(213); + let pending = group.prepare_record_for_test(semid, 2).unwrap(); + assert!(!pending.was_existing()); + group.insert_test_record(semid, &[6, 8]); + let (_, kept) = group + .with_prepared_record_noalloc(pending, |live| { + assert_eq!(live.adjustment(0), 6); + PreparedSemUndoRecordAction::Keep(()) + }) + .unwrap(); + assert!(kept.unwrap().was_existing()); + assert_eq!(group.pending_record_reservations_for_test(), 0); + assert_eq!(group.adjustment_for_test(semid, 1), 8); +} + +#[test] +fn observer_arc_does_not_change_task_owner_count() { + let group = SemUndoGroup::new_for_test(); + let attachment = SemUndoAttachment::new_for_test(group.clone()); + let observer = attachment.group_for_test(); + assert_eq!(group.task_owners_for_test(), 1); + drop(observer); + assert_eq!(group.task_owners_for_test(), 1); +} + +#[test] +fn attachment_is_taken_once_and_drop_never_replays() { + let attachment = SemUndoAttachment::new_for_test(SemUndoGroup::new_for_test()); + let mut slot = Some(attachment); + assert!(slot.take().is_some()); + assert!(slot.take().is_none()); +} + +#[test] +fn group_rejects_different_ipc_namespace() { + let group = SemUndoGroup::new_for_test_bound_to_first_namespace(); + assert_eq!( + group.verify_ipc_ns_for_test(second_test_ipc_ns()), + Err(SystemError::EINVAL) + ); +} + +#[test] +fn ordinary_fork_child_starts_without_attachment() { + let parent = test_pcb_with_group(); + let child = test_unpublished_child(); + + assert!(child.sem_undo_group().is_none()); + assert!(parent.sem_undo_group().is_some()); +} + +#[test] +fn sysvsem_guard_increments_once_then_install_moves_token() { + let parent = test_pcb_with_group(); + let group = parent.sem_undo_group().unwrap(); + let child = test_unpublished_child(); + + let mut guard = parent + .prepare_shared_sem_undo_attachment(test_ipc_ns()) + .unwrap(); + assert_eq!(group.task_owners_for_test(), 2); + guard.install_into(&child); + assert!(child.sem_undo_group().is_some()); + guard.disarm(); + assert_eq!(group.task_owners_for_test(), 2); +} + +#[test] +fn installed_guard_rollback_takes_child_slot_and_only_drops_owner() { + let parent = test_pcb_with_group(); + let group = parent.sem_undo_group().unwrap(); + let child = test_unpublished_child(); + + let mut guard = parent + .prepare_shared_sem_undo_attachment(test_ipc_ns()) + .unwrap(); + guard.install_into(&child); + drop(guard); + + assert!(child.sem_undo_group().is_none()); + assert_eq!(group.task_owners_for_test(), 1); + assert_eq!(group.replay_count_for_test(), 0); +} From a455a7301a5b4db7cd6ae86a19efaa5d7adfa2cf Mon Sep 17 00:00:00 2001 From: longjin Date: Sun, 6 Sep 2026 05:22:26 +0000 Subject: [PATCH 28/34] perf(ipc): preindex semaphore scratch and reclaim undo record storage Signed-off-by: longjin --- kernel/src/ipc/sem/operation.rs | 18 ++- kernel/src/ipc/sem/set/mod.rs | 9 ++ kernel/src/ipc/sem/set/operation.rs | 80 ++++++++----- kernel/src/ipc/sem/set/operation/tests.rs | 26 ++--- kernel/src/ipc/sem/set/queue.rs | 2 +- kernel/src/ipc/sem/set/test_support.rs | 2 +- kernel/src/ipc/sem/set/tests.rs | 2 +- kernel/src/ipc/sem_undo/mod.rs | 47 ++++++++ kernel/src/ipc/syscall/sys_semctl.rs | 1 + .../suites/normal/sysv_sem_semantics.cc | 107 ++++++++++++++++++ 10 files changed, 248 insertions(+), 46 deletions(-) diff --git a/kernel/src/ipc/sem/operation.rs b/kernel/src/ipc/sem/operation.rs index 5a301ff248..a5612c8214 100644 --- a/kernel/src/ipc/sem/operation.rs +++ b/kernel/src/ipc/sem/operation.rs @@ -110,7 +110,18 @@ impl SemManager { } else { None }; - let mut immediate_scratch = SemopScratch::try_new(sops.len())?; + // A cancelled first-use reservation can outlive the last RMID shrink. + // Declare cleanup before entries/guards so their reservations are gone + // before any allocation or buffer disposal performed by reclamation. + let prepared_missing = core::cell::Cell::new(false); + defer::defer!({ + if prepared_missing.get() { + if let Some(group) = undo_group.as_ref() { + group.shrink_records(); + } + } + }); + let mut immediate_scratch = SemopScratch::try_new(sops)?; let plain_prepared_entry = if has_undo { None } else { @@ -121,7 +132,7 @@ impl SemManager { None, None, waker.clone(), - SemopScratch::try_new(sops.len())?, + SemopScratch::try_new(sops)?, SemBlockedOp { semnum: 0, wait_type: SemWaitType::Zero, @@ -164,13 +175,14 @@ impl SemManager { let prepared_undo = if let Some(group) = undo_group.as_ref() { let record = group.prepare_record(semid, nsems)?; + prepared_missing.set(prepared_missing.get() || !record.was_existing()); let entry = Arc::try_new(SemQueueEntry::new_prepared( SemQueueEntry::prepare_sops(sops)?, pid.clone(), Some(group.clone()), Some(record), waker.clone(), - SemopScratch::try_new(sops.len())?, + SemopScratch::try_new(sops)?, SemBlockedOp { semnum: 0, wait_type: SemWaitType::Zero, diff --git a/kernel/src/ipc/sem/set/mod.rs b/kernel/src/ipc/sem/set/mod.rs index 7c4accefa3..1230bba5c4 100644 --- a/kernel/src/ipc/sem/set/mod.rs +++ b/kernel/src/ipc/sem/set/mod.rs @@ -79,6 +79,15 @@ pub struct KernelSemSet { } impl KernelSemSet { + /// Only call after RMID released the manager lock and delivered wakeups. + pub(crate) fn reclaim_removed_undo_storage(&self) { + for association in &self.undo_groups { + if let SemUndoAssociation::Retired { _group: group, .. } = association { + group.shrink_records(); + } + } + } + /// Live associations survive SETVAL/SETALL. Only RMID or dead groups /// remove them, so an existing undo record proves prior association. /// Insufficient spare capacity requests an unlocked preparation/retry. diff --git a/kernel/src/ipc/sem/set/operation.rs b/kernel/src/ipc/sem/set/operation.rs index f1a4951887..e32eb72040 100644 --- a/kernel/src/ipc/sem/set/operation.rs +++ b/kernel/src/ipc/sem/set/operation.rs @@ -18,6 +18,7 @@ pub(in crate::ipc::sem) struct SemBlockedOp { #[derive(Debug)] struct SemopScratchEntry { semnum: usize, + initialized: bool, initial_val: i32, virtual_val: i32, initial_adj: i16, @@ -27,50 +28,72 @@ struct SemopScratchEntry { #[derive(Debug)] pub(in crate::ipc::sem) struct SemopScratch { entries: Vec, + op_slots: Vec, } impl SemopScratch { - pub(in crate::ipc::sem) fn try_new(capacity: usize) -> Result { + /// Compile the immutable operation-to-slot mapping before locking the set. + /// Storage is proportional to nsops, never to the semaphore set size. + pub(in crate::ipc::sem) fn try_new(sops: &[PosixSemBuf]) -> Result { let mut entries = Vec::new(); entries - .try_reserve_exact(capacity) + .try_reserve_exact(sops.len()) .map_err(|_| SystemError::ENOMEM)?; - Ok(Self { entries }) + for op in sops { + entries.push(SemopScratchEntry { + semnum: op.sem_num as usize, + initialized: false, + initial_val: 0, + virtual_val: 0, + initial_adj: 0, + virtual_adj: 0, + }); + } + entries.sort_unstable_by_key(|entry| entry.semnum); + entries.dedup_by_key(|entry| entry.semnum); + let mut op_slots = Vec::new(); + op_slots + .try_reserve_exact(sops.len()) + .map_err(|_| SystemError::ENOMEM)?; + for op in sops { + op_slots.push( + entries + .binary_search_by_key(&(op.sem_num as usize), |entry| entry.semnum) + .expect("each operation has a prepared scratch slot"), + ); + } + Ok(Self { entries, op_slots }) } fn clear(&mut self) { - self.entries.clear(); + for entry in &mut self.entries { + entry.initialized = false; + } } fn entry_for( &mut self, set: &KernelSemSet, + op_index: usize, semnum: usize, undo: Option<&SemUndoRecord>, ) -> Result<&mut SemopScratchEntry, SystemError> { - if let Some(index) = self.entries.iter().position(|entry| entry.semnum == semnum) { - return Ok(&mut self.entries[index]); + let slot = *self.op_slots.get(op_index).ok_or(SystemError::EINVAL)?; + let entry = &mut self.entries[slot]; + // A scratch belongs to this operation array, including on queued retries. + if entry.semnum != semnum { + return Err(SystemError::EINVAL); } - - if self.entries.len() == self.entries.capacity() { - return Err(SystemError::ENOMEM); + if !entry.initialized { + entry.initial_val = set.sems[semnum].val; + entry.virtual_val = entry.initial_val; + entry.initial_adj = undo + .map(|record| record.adjustment(semnum)) + .unwrap_or_default(); + entry.virtual_adj = entry.initial_adj; + entry.initialized = true; } - - let initial_val = set.sems[semnum].val; - let initial_adj = undo - .map(|record| record.adjustment(semnum)) - .unwrap_or_default(); - self.entries.push(SemopScratchEntry { - semnum, - initial_val, - virtual_val: initial_val, - initial_adj, - virtual_adj: initial_adj, - }); - Ok(self - .entries - .last_mut() - .expect("SEM_UNDO scratch entry was just inserted")) + Ok(entry) } } @@ -112,16 +135,19 @@ impl KernelSemSet { undo: Option<&mut SemUndoRecord>, scratch: &mut SemopScratch, ) -> Result { + if sops.len() != scratch.op_slots.len() { + return Err(SystemError::EINVAL); + } scratch.clear(); - for op in sops { + for (op_index, op) in sops.iter().enumerate() { let idx = op.sem_num as usize; if idx >= set.sems.len() { return Err(SystemError::EFBIG); } let has_undo = (op.sem_flg as u32) & SemFlags::SEM_UNDO.bits() != 0; - let entry = scratch.entry_for(set, idx, undo.as_deref())?; + let entry = scratch.entry_for(set, op_index, idx, undo.as_deref())?; let current = entry.virtual_val; if op.sem_op == 0 { if current != 0 { diff --git a/kernel/src/ipc/sem/set/operation/tests.rs b/kernel/src/ipc/sem/set/operation/tests.rs index f883de6a03..5afd625f0e 100644 --- a/kernel/src/ipc/sem/set/operation/tests.rs +++ b/kernel/src/ipc/sem/set/operation/tests.rs @@ -6,7 +6,7 @@ fn try_apply_commits_metadata_even_when_values_are_unchanged() { let semid = insert_test_set(&mut manager, SemKey::new(301), &[4]); let set = manager.get_by_semid_checked_mut(semid).unwrap(); set.sem_otime = -1; - let mut scratch = SemopScratch::try_new(2).unwrap(); + let mut scratch = SemopScratch::try_new(&[plain_sop(0, 0); 2]).unwrap(); assert!(matches!( set.try_apply( &[plain_sop(0, -1), plain_sop(0, 1)], @@ -28,7 +28,7 @@ fn try_apply_rebuilds_scratch_after_a_blocked_attempt() { let semid = insert_test_set(&mut manager, SemKey::new(302), &[0]); let set = manager.get_by_semid_checked_mut(semid).unwrap(); set.sem_otime = -1; - let mut scratch = SemopScratch::try_new(1).unwrap(); + let mut scratch = SemopScratch::try_new(&[plain_sop(0, 0); 1]).unwrap(); assert!(matches!( set.try_apply(&[plain_sop(0, -1)], None, None, &mut scratch), Ok(SemAttempt::Blocked(_)) @@ -57,7 +57,7 @@ fn setval_clear_between_prepare_and_commit_refreshes_stale_existing_record() { let record = group.prepare_record_for_test(semid, 1).unwrap(); manager.clear_undo_for_setval(semid, 0); - let mut scratch = SemopScratch::try_new(1).unwrap(); + let mut scratch = SemopScratch::try_new(&[plain_sop(0, 0); 1]).unwrap(); let set = manager.get_by_semid_checked_mut(semid).unwrap(); let result = group.with_prepared_record_noalloc(record, |record| { @@ -84,7 +84,7 @@ fn setall_clear_between_prepare_and_commit_refreshes_stale_existing_record() { let record = group.prepare_record_for_test(semid, 2).unwrap(); manager.clear_undo_for_setall(semid); - let mut scratch = SemopScratch::try_new(1).unwrap(); + let mut scratch = SemopScratch::try_new(&[plain_sop(0, 0); 1]).unwrap(); let set = manager.get_by_semid_checked_mut(semid).unwrap(); let result = group.with_prepared_record_noalloc(record, |record| { @@ -112,7 +112,7 @@ fn stale_existing_prepared_record_refreshes_before_immediate_commit() { let record = group.prepare_record_for_test(semid, 1).unwrap(); manager.clear_undo_for_setval(semid, 0); - let mut scratch = SemopScratch::try_new(1).unwrap(); + let mut scratch = SemopScratch::try_new(&[plain_sop(0, 0); 1]).unwrap(); let set = manager.get_by_semid_checked_mut(semid).unwrap(); let result = group.with_prepared_record_noalloc(record, |record| { @@ -137,7 +137,7 @@ fn consecutive_sem_undo_on_unchanged_existing_record_still_accumulates() { group.insert_test_record(semid, &[2]); let record = group.prepare_record_for_test(semid, 1).unwrap(); - let mut scratch = SemopScratch::try_new(1).unwrap(); + let mut scratch = SemopScratch::try_new(&[plain_sop(0, 0); 1]).unwrap(); let set = manager.get_by_semid_checked_mut(semid).unwrap(); group .with_prepared_record_noalloc(record, |record| { @@ -159,7 +159,7 @@ fn ordered_mixed_undo_ops_apply_each_adjustment_step() { let semid = insert_test_set(&mut manager, SemKey::new(41), &[4]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); let record = group.prepare_record_for_test(semid, 1).unwrap(); - let mut scratch = SemopScratch::try_new(3).unwrap(); + let mut scratch = SemopScratch::try_new(&[plain_sop(0, 0); 3]).unwrap(); let sops = [undo_sop(0, 3), plain_sop(0, -1), undo_sop(0, -2)]; let set = manager.get_by_semid_checked_mut(semid).unwrap(); @@ -189,7 +189,7 @@ fn intermediate_adjustment_overflow_is_erange_even_if_later_op_cancels_it() { let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); group.insert_test_record(semid, &[i16::MAX]); let record = group.prepare_record_for_test(semid, 1).unwrap(); - let mut scratch = SemopScratch::try_new(2).unwrap(); + let mut scratch = SemopScratch::try_new(&[plain_sop(0, 0); 2]).unwrap(); let sops = [undo_sop(0, -1), undo_sop(0, 1)]; let set = manager.get_by_semid_checked_mut(semid).unwrap(); @@ -215,7 +215,7 @@ fn blocked_or_nowait_failure_does_not_commit_semval_or_semadj_prefix() { // Exercise the live record, not just an unpublished candidate. group.insert_test_record(semid, &[0]); let record = group.prepare_record_for_test(semid, 1).unwrap(); - let mut scratch = SemopScratch::try_new(2).unwrap(); + let mut scratch = SemopScratch::try_new(&[plain_sop(0, 0); 2]).unwrap(); let sops = [undo_sop(0, -1), nowait_sop(0, -2)]; let set = manager.get_by_semid_checked_mut(semid).unwrap(); @@ -243,7 +243,7 @@ fn zero_undo_op_can_prepare_zero_record_without_adjustment() { let semid = insert_test_set(&mut manager, SemKey::new(44), &[0]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); let record = group.prepare_record_for_test(semid, 1).unwrap(); - let mut scratch = SemopScratch::try_new(1).unwrap(); + let mut scratch = SemopScratch::try_new(&[plain_sop(0, 0); 1]).unwrap(); let sops = [undo_sop(0, 0)]; let set = manager.get_by_semid_checked_mut(semid).unwrap(); @@ -267,15 +267,15 @@ fn zero_undo_op_can_prepare_zero_record_without_adjustment() { } #[test] -fn scratch_entry_for_returns_enomem_instead_of_extending_past_capacity() { +fn scratch_rejects_an_incompatible_operation_array() { let mut manager = SemManager::new(); let semid = insert_test_set(&mut manager, SemKey::new(45), &[1, 1]); let set = manager.get_by_semid_checked_mut(semid).unwrap(); - let mut scratch = SemopScratch::try_new(1).unwrap(); + let mut scratch = SemopScratch::try_new(&[plain_sop(0, 0); 1]).unwrap(); let sops = [plain_sop(0, -1), plain_sop(1, -1)]; assert!(matches!( KernelSemSet::simulate_semop(set, &sops, None, &mut scratch), - Err(SystemError::ENOMEM) + Err(SystemError::EINVAL) )); } diff --git a/kernel/src/ipc/sem/set/queue.rs b/kernel/src/ipc/sem/set/queue.rs index a6f0d1be36..70eb76da99 100644 --- a/kernel/src/ipc/sem/set/queue.rs +++ b/kernel/src/ipc/sem/set/queue.rs @@ -207,7 +207,7 @@ impl SemQueueEntry { None, None, waker, - SemopScratch::try_new(sops.len()).unwrap(), + SemopScratch::try_new(sops).unwrap(), blocker, ) } diff --git a/kernel/src/ipc/sem/set/test_support.rs b/kernel/src/ipc/sem/set/test_support.rs index 03f7f0dbf5..11d6d4faa1 100644 --- a/kernel/src/ipc/sem/set/test_support.rs +++ b/kernel/src/ipc/sem/set/test_support.rs @@ -70,7 +70,7 @@ pub(in crate::ipc::sem) fn enqueue_undo_waiter_for_test( Some(Arc::clone(group)), Some(group.prepare_record_for_test(semid, 1).unwrap()), waker, - SemopScratch::try_new(1).unwrap(), + SemopScratch::try_new(&[plain_sop(0, 0); 1]).unwrap(), SemBlockedOp { semnum: 0, wait_type: SemWaitType::Increase, diff --git a/kernel/src/ipc/sem/set/tests.rs b/kernel/src/ipc/sem/set/tests.rs index 80bbf60035..e4e621d04a 100644 --- a/kernel/src/ipc/sem/set/tests.rs +++ b/kernel/src/ipc/sem/set/tests.rs @@ -396,7 +396,7 @@ fn queued_undo_commits_to_captured_group_not_waker_current_task() { Some(group_a.clone()), Some(group_a.prepare_record_for_test(semid, 1).unwrap()), waker, - SemopScratch::try_new(1).unwrap(), + SemopScratch::try_new(&[plain_sop(0, 0); 1]).unwrap(), SemBlockedOp { semnum: 0, wait_type: SemWaitType::Increase, diff --git a/kernel/src/ipc/sem_undo/mod.rs b/kernel/src/ipc/sem_undo/mod.rs index 86830a6efa..66b03b738d 100644 --- a/kernel/src/ipc/sem_undo/mod.rs +++ b/kernel/src/ipc/sem_undo/mod.rs @@ -331,6 +331,53 @@ impl SemUndoGroup { .position(|record| record.semid == semid)?; Some(state.records.swap_remove(index)) } + + /// Best-effort reclamation. Caller must hold neither manager nor group lock. + /// Preparation and old-buffer disposal stay outside both critical sections. + pub(crate) fn shrink_records(&self) { + let mut spare = Vec::new(); + let mut retired = Vec::new(); + let needed = { + let mut state = self.inner.lock_irqsave(); + state.shrink_records_prepared(&mut spare, &mut retired) + }; + if needed == 0 || spare.try_reserve_exact(needed).is_err() { + return; + } + let mut state = self.inner.lock_irqsave(); + state.shrink_records_prepared(&mut spare, &mut retired); + } +} + +impl SemUndoGroupState { + /// Pending Missing tokens own capacity even when no live records remain. + /// Recheck after unlocked allocation: another prepare may need more slots. + fn shrink_records_prepared( + &mut self, + spare: &mut Vec, + retired: &mut Vec, + ) -> usize { + debug_assert!(spare.is_empty()); + debug_assert!(retired.is_empty() && retired.capacity() == 0); + let Some(required) = self.records.len().checked_add(self.reserved_records) else { + return 0; + }; + if required == 0 { + core::mem::swap(&mut self.records, retired); + return 0; + } + if self.records.capacity() <= 4 || required > self.records.capacity() / 4 { + return 0; + } + if spare.capacity() < required { + return required.saturating_mul(2).max(4); + } + if spare.capacity() < self.records.capacity() { + spare.append(&mut self.records); + core::mem::swap(&mut self.records, spare); + } + 0 + } } #[cfg(test)] diff --git a/kernel/src/ipc/syscall/sys_semctl.rs b/kernel/src/ipc/syscall/sys_semctl.rs index 203b464a59..aa8864c114 100644 --- a/kernel/src/ipc/syscall/sys_semctl.rs +++ b/kernel/src/ipc/syscall/sys_semctl.rs @@ -88,6 +88,7 @@ pub(super) fn do_kernel_semctl( guard.ipc_rmid(semid, &mut wakes)? }; wakes.wake_all(); + removed.reclaim_removed_undo_storage(); drop(removed); Ok(0) } diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index bde36f1547..a49433d208 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -2890,6 +2890,113 @@ TEST(SysVSem, UndoGroupChurnAcrossPersistentSets) { } } +TEST(SysVSem, MaximumOperationArrayPreservesOrderAndRollback) { + SemSet sem(500, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + std::vector ops; + // Reverse order deliberately differs from the prepared scratch slot order. + for (int i = 499; i >= 0; --i) ops.push_back({static_cast(i), 1, 0}); + ASSERT_EQ(0, SemOp(sem.id(), ops.data(), ops.size())); + std::vector values(500); + ASSERT_EQ(0, SemCtl(sem.id(), 0, GETALL, reinterpret_cast(values.data()))); + EXPECT_TRUE(std::all_of(values.begin(), values.end(), [](auto v) { return v == 1; })); + for (auto& op : ops) op.sem_op = -1; + // Last operation fails after 499 virtual decrements: none may commit. + ops.back().sem_op = -2; + ops.back().sem_flg = IPC_NOWAIT; + errno = 0; + EXPECT_EQ(-1, SemOp(sem.id(), ops.data(), ops.size())); + EXPECT_EQ(EAGAIN, errno); + ASSERT_EQ(0, SemCtl(sem.id(), 0, GETALL, reinterpret_cast(values.data()))); + EXPECT_TRUE(std::all_of(values.begin(), values.end(), [](auto v) { return v == 1; })); + ops.clear(); + // Duplicate slots must observe preceding virtual values, not initial values. + for (int i = 249; i >= 0; --i) { + ops.push_back({static_cast(i), -1, SEM_UNDO}); + ops.push_back({static_cast(i), 1, SEM_UNDO}); + } + ASSERT_EQ(0, SemOp(sem.id(), ops.data(), ops.size())); + ASSERT_EQ(0, SemCtl(sem.id(), 0, GETALL, reinterpret_cast(values.data()))); + EXPECT_TRUE(std::all_of(values.begin(), values.end(), [](auto v) { return v == 1; })); +} + +TEST(SysVSem, MaximumOperationArrayRetryRefreshesEverySlot) { + SemSet sem(500, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + std::vector values(500, 1); + values[0] = 0; + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETALL, reinterpret_cast(values.data()))); + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + std::vector ops; + for (int i = 499; i >= 0; --i) ops.push_back({static_cast(i), -1, SEM_UNDO}); + const timespec timeout = {5, 0}; + _exit(SemTimedOp(sem.id(), ops.data(), ops.size(), &timeout) == 0 ? 0 : 180); + } + ChildGuard child(pid); + ASSERT_TRUE(WaitForNcnt(sem.id(), 0, 1)); + // Force a retry with a different blocker after almost the whole first pass. + ASSERT_EQ(0, SemCtl(sem.id(), 499, SETVAL, 0)); + ASSERT_TRUE(WaitForNcnt(sem.id(), 499, 1)); + values.assign(500, 2); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETALL, reinterpret_cast(values.data()))); + WaitChildOk(&child); + ASSERT_EQ(0, SemCtl(sem.id(), 0, GETALL, reinterpret_cast(values.data()))); + // Final-owner undo restores each successful decrement. + EXPECT_TRUE(std::all_of(values.begin(), values.end(), [](auto v) { return v == 2; })); +} + +TEST(SysVSem, PersistentUndoGroupSurvivesRemovalAndRecordRegrowth) { + constexpr int kSets = 128; + std::vector> sets; + for (int i = 0; i < kSets; ++i) { + sets.emplace_back(new SemSet(1, IPC_CREAT | 0600)); + ASSERT_TRUE(sets.back()->valid()); + } + int ready[2], resume[2]; + ASSERT_EQ(0, pipe(ready)); + FdGuard ready_read(ready[0]), ready_write(ready[1]); + ASSERT_EQ(0, pipe(resume)); + FdGuard resume_read(resume[0]), resume_write(resume[1]); + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + ready_read.Close(); + resume_write.Close(); + for (auto& sem : sets) if (!SemUndoOpMustSucceed(sem->id(), 0, 1)) _exit(181); + char token = 1; + if (!WriteExact(ready_write.get(), &token, 1) || + !ReadExact(resume_read.get(), &token, 1)) _exit(182); + // The same owner remains alive while most old records are removed. + if (!SemUndoOpMustSucceed(sets.back()->id(), 0, 1)) _exit(183); + { + std::vector> fresh_sets; + for (int i = 0; i < kSets; ++i) { + fresh_sets.emplace_back(new SemSet(1, IPC_CREAT | 0600)); + if (!fresh_sets.back()->valid() || + !SemUndoOpMustSucceed(fresh_sets.back()->id(), 0, 1)) _exit(184); + } + // Keep all new records live together to force growth after shrink, + // then remove them before final-owner replay of the survivor. + } + _exit(0); + } + ChildGuard child(pid); + ready_write.Close(); + resume_read.Close(); + char token; + ASSERT_TRUE(ReadExact(ready_read.get(), &token, 1)); + for (int i = 0; i < kSets - 1; ++i) { + ASSERT_EQ(0, SemCtl(sets[i]->id(), 0, IPC_RMID, 0)); + sets[i]->release(); + } + ASSERT_EQ(1, SemCtl(sets.back()->id(), 0, GETVAL, 0)); + ASSERT_TRUE(WriteExact(resume_write.get(), &token, 1)); + WaitChildOk(&child); + EXPECT_EQ(0, SemCtl(sets.back()->id(), 0, GETVAL, 0)); +} + TEST(SysVSem, ConcurrentWorkers) { constexpr int kWorkers = 8; SemSet sem(1, IPC_CREAT | 0600); From 55a48ef282664b86c069d83677c1c258bb810598 Mon Sep 17 00:00:00 2001 From: longjin Date: Sun, 6 Sep 2026 06:34:01 +0000 Subject: [PATCH 29/34] fix(ipc): correct semaphore ABI and index undo records Signed-off-by: longjin --- kernel/src/ipc/sem/abi.rs | 26 ++++- kernel/src/ipc/sem_undo/lifecycle.rs | 7 +- kernel/src/ipc/sem_undo/mod.rs | 98 +++++++------------ kernel/src/ipc/sem_undo/storage.rs | 91 +++++++++++++++++ kernel/src/ipc/sem_undo/test_support.rs | 12 ++- .../suites/normal/sysv_sem_semantics.cc | 87 ++++++++++++++++ 6 files changed, 246 insertions(+), 75 deletions(-) create mode 100644 kernel/src/ipc/sem_undo/storage.rs diff --git a/kernel/src/ipc/sem/abi.rs b/kernel/src/ipc/sem/abi.rs index d64464b6f6..63752b563a 100644 --- a/kernel/src/ipc/sem/abi.rs +++ b/kernel/src/ipc/sem/abi.rs @@ -118,25 +118,41 @@ pub struct PosixSemBuf { pub sem_flg: i16, } -/// Semaphore-set information matching Linux x86_64 `struct semid64_ds` (104 bytes, -/// including the high halves of 32-bit timestamp fields) +/// Linux `semid64_ds`: x86_64 has historical padding after each 64-bit time; +/// the supported asm-generic 64-bit architectures do not. #[repr(C)] #[derive(Debug, Clone, Copy, Default)] pub struct PosixSemIdDs { /// Permission information pub sem_perm: PosixIpcPerm, - /// Time of the last `semop` (64-bit; upper 32 bits are in `__sem_otime_high`) + /// Time of the last `semop` pub sem_otime: i64, - _sem_otime_high: i64, + #[cfg(target_arch = "x86_64")] + _otime_padding: u64, /// Time of the last metadata change pub sem_ctime: i64, - _sem_ctime_high: i64, + #[cfg(target_arch = "x86_64")] + _ctime_padding: u64, /// Number of semaphores in the set pub sem_nsems: usize, _unused1: usize, _unused2: usize, } +// Keep the copy size and field positions tied to the target Linux UAPI. +const _: () = { + assert!(core::mem::offset_of!(PosixSemIdDs, sem_otime) == 48); + if cfg!(target_arch = "x86_64") { + assert!(core::mem::size_of::() == 104); + assert!(core::mem::offset_of!(PosixSemIdDs, sem_ctime) == 64); + assert!(core::mem::offset_of!(PosixSemIdDs, sem_nsems) == 80); + } else { + assert!(core::mem::size_of::() == 88); + assert!(core::mem::offset_of!(PosixSemIdDs, sem_ctime) == 56); + assert!(core::mem::offset_of!(PosixSemIdDs, sem_nsems) == 64); + } +}; + /// Semaphore system information matching Linux `struct seminfo` (40 bytes) #[repr(C)] #[derive(Debug, Clone, Copy, Default)] diff --git a/kernel/src/ipc/sem_undo/lifecycle.rs b/kernel/src/ipc/sem_undo/lifecycle.rs index cb87b068ea..045bcfb560 100644 --- a/kernel/src/ipc/sem_undo/lifecycle.rs +++ b/kernel/src/ipc/sem_undo/lifecycle.rs @@ -68,12 +68,7 @@ pub(super) fn replay_marked_records(pcb: &Arc, group: &Arc< }); let Some(ipc_ns) = group.ipc_ns.upgrade() else { - for record in group.discard_retired_records() { - log::debug!( - "dropping SEM_UNDO record for semid {} after IPC namespace teardown", - record.semid.data() - ); - } + drop(group.discard_retired_records()); return; }; diff --git a/kernel/src/ipc/sem_undo/mod.rs b/kernel/src/ipc/sem_undo/mod.rs index 66b03b738d..cce1fb9026 100644 --- a/kernel/src/ipc/sem_undo/mod.rs +++ b/kernel/src/ipc/sem_undo/mod.rs @@ -10,10 +10,12 @@ use system_error::SystemError; mod lifecycle; mod record; +mod storage; pub use lifecycle::SemUndoAttachment; pub(crate) use lifecycle::{detach_sem_undo, UnpublishedSemUndoAttachmentGuard}; use record::PendingSemUndoRecordReservation; pub(crate) use record::{PreparedSemUndoRecord, PreparedSemUndoRecordAction, SemUndoRecord}; +use storage::UndoRecords; #[derive(Debug)] pub struct SemUndoGroup { @@ -24,7 +26,7 @@ pub struct SemUndoGroup { #[derive(Debug)] struct SemUndoGroupState { task_owners: usize, - records: Vec, + records: UndoRecords, reserved_records: usize, phase: UndoPhase, #[cfg(test)] @@ -38,33 +40,13 @@ enum UndoPhase { Replaying, } -fn prepare_records_storage_capacity( - records: &mut Vec, - required_capacity: usize, -) -> Result<(), SystemError> { - if records.capacity() >= required_capacity { - return Ok(()); - } - - let additional = required_capacity - .checked_sub(records.len()) - .ok_or(SystemError::ENOMEM)?; - records - .try_reserve_exact(additional) - .map_err(|_| SystemError::ENOMEM)?; - if records.capacity() < required_capacity { - return Err(SystemError::ENOMEM); - } - Ok(()) -} - impl SemUndoGroup { pub(crate) fn new(ipc_ns: &Arc) -> Result, SystemError> { Arc::try_new(Self { ipc_ns: Arc::downgrade(ipc_ns), inner: SpinLock::new(SemUndoGroupState { task_owners: 1, - records: Vec::new(), + records: UndoRecords::default(), reserved_records: 0, phase: UndoPhase::Active, #[cfg(test)] @@ -149,7 +131,7 @@ impl SemUndoGroup { } /// Only valid after the bound namespace can no longer be upgraded. - fn discard_retired_records(&self) -> Vec { + fn discard_retired_records(&self) -> UndoRecords { let mut state = self.inner.lock_irqsave(); debug_assert!(state.phase == UndoPhase::Replaying); core::mem::take(&mut state.records) @@ -161,7 +143,7 @@ impl SemUndoGroup { nsems: usize, ) -> Result { let mut adjustments = Vec::new(); - let mut reserved_storage = Vec::new(); + let mut reserved_storage = UndoRecords::default(); loop { let mut state = self.inner.lock_irqsave(); @@ -169,7 +151,7 @@ impl SemUndoGroup { return Err(SystemError::EINVAL); } - if let Some(existing) = state.records.iter().find(|item| item.semid == semid) { + if let Some(existing) = state.records.get(semid) { if existing.adjustments.len() != nsems { return Err(SystemError::EINVAL); } @@ -197,15 +179,23 @@ impl SemUndoGroup { .and_then(|capacity| capacity.checked_add(1)) .ok_or(SystemError::ENOMEM)?; - if state.records.capacity() < required_capacity { - if reserved_storage.capacity() < required_capacity { + if !state.records.can_hold(required_capacity) { + // Rebuild both together even when only hash tombstones exhaust + // insertion capacity. Dense capacity also bounds index residency. + let capacity = state.records.capacity(); + let target = if capacity < required_capacity { + required_capacity.max(capacity.saturating_mul(2)).max(4) + } else { + capacity + }; + if !reserved_storage.can_hold(target) { drop(state); - prepare_records_storage_capacity(&mut reserved_storage, required_capacity)?; + reserved_storage.prepare(target)?; continue; } - - reserved_storage.append(&mut state.records); - core::mem::swap(&mut state.records, &mut reserved_storage); + state + .records + .install_prepared(&mut reserved_storage, required_capacity); } state.reserved_records = state @@ -263,13 +253,8 @@ impl SemUndoGroup { return Err(SystemError::EINVAL); } - let existing_index = state - .records - .iter() - .position(|item| item.semid == record.semid); - - if let Some(index) = existing_index { - if state.records[index].adjustments.len() != record.nsems { + if let Some(existing) = state.records.get(record.semid) { + if existing.adjustments.len() != record.nsems { return Err(SystemError::EINVAL); } _retired_candidate = record.candidate.take(); @@ -281,7 +266,7 @@ impl SemUndoGroup { reservation.disarm(); } - return match f(&mut state.records[index]) { + return match f(state.records.get_mut(record.semid).unwrap()) { PreparedSemUndoRecordAction::Complete(result) => Ok((result, None)), PreparedSemUndoRecordAction::Keep(result) => Ok((result, Some(record))), }; @@ -293,7 +278,7 @@ impl SemUndoGroup { if record.candidate.is_none() { return Err(SystemError::EINVAL); } - if state.records.len() >= state.records.capacity() { + if !state.records.can_hold(state.records.len() + 1) { return Err(SystemError::ENOMEM); } if let Some(mut reservation) = record.reservation.take() { @@ -303,8 +288,10 @@ impl SemUndoGroup { .expect("SEM_UNDO record reservation count underflow"); reservation.disarm(); } - state.records.push(record.candidate.take().unwrap()); - match f(state.records.last_mut().unwrap()) { + state + .records + .push_prepared(record.candidate.take().unwrap()); + match f(state.records.get_mut(record.semid).unwrap()) { PreparedSemUndoRecordAction::Complete(result) => Ok((result, None)), PreparedSemUndoRecordAction::Keep(result) => Ok((result, Some(record))), } @@ -316,32 +303,24 @@ impl SemUndoGroup { f: impl FnOnce(&mut SemUndoRecord) -> R, ) -> Option { let mut state = self.inner.lock_irqsave(); - state - .records - .iter_mut() - .find(|record| record.semid == semid) - .map(f) + state.records.get_mut(semid).map(f) } pub(crate) fn take_record(&self, semid: SemId) -> Option { let mut state = self.inner.lock_irqsave(); - let index = state - .records - .iter() - .position(|record| record.semid == semid)?; - Some(state.records.swap_remove(index)) + state.records.remove(semid) } /// Best-effort reclamation. Caller must hold neither manager nor group lock. /// Preparation and old-buffer disposal stay outside both critical sections. pub(crate) fn shrink_records(&self) { - let mut spare = Vec::new(); - let mut retired = Vec::new(); + let mut spare = UndoRecords::default(); + let mut retired = UndoRecords::default(); let needed = { let mut state = self.inner.lock_irqsave(); state.shrink_records_prepared(&mut spare, &mut retired) }; - if needed == 0 || spare.try_reserve_exact(needed).is_err() { + if needed == 0 || spare.prepare(needed).is_err() { return; } let mut state = self.inner.lock_irqsave(); @@ -354,8 +333,8 @@ impl SemUndoGroupState { /// Recheck after unlocked allocation: another prepare may need more slots. fn shrink_records_prepared( &mut self, - spare: &mut Vec, - retired: &mut Vec, + spare: &mut UndoRecords, + retired: &mut UndoRecords, ) -> usize { debug_assert!(spare.is_empty()); debug_assert!(retired.is_empty() && retired.capacity() == 0); @@ -369,12 +348,11 @@ impl SemUndoGroupState { if self.records.capacity() <= 4 || required > self.records.capacity() / 4 { return 0; } - if spare.capacity() < required { + if !spare.can_hold(required) { return required.saturating_mul(2).max(4); } if spare.capacity() < self.records.capacity() { - spare.append(&mut self.records); - core::mem::swap(&mut self.records, spare); + self.records.install_prepared(spare, required); } 0 } diff --git a/kernel/src/ipc/sem_undo/storage.rs b/kernel/src/ipc/sem_undo/storage.rs new file mode 100644 index 0000000000..8d9e744973 --- /dev/null +++ b/kernel/src/ipc/sem_undo/storage.rs @@ -0,0 +1,91 @@ +//! Dense replay order with a full-ID lookup index. All index maintenance lives here. +use super::SemUndoRecord; +use crate::ipc::sem::SemId; +use alloc::vec::Vec; +use hashbrown::HashMap; +use system_error::SystemError; + +#[derive(Debug, Default)] +pub(super) struct UndoRecords { + records: Vec, + index: HashMap, +} + +impl UndoRecords { + pub(super) fn len(&self) -> usize { + self.records.len() + } + + pub(super) fn is_empty(&self) -> bool { + self.records.is_empty() + } + + /// Physical dense capacity drives reclamation; hash capacity can decrease + /// after deletion due to tombstones, without releasing its allocation. + pub(super) fn capacity(&self) -> usize { + self.records.capacity() + } + + pub(super) fn can_hold(&self, required: usize) -> bool { + self.records.capacity() >= required && self.index.capacity() >= required + } + + /// Only call on empty spare storage outside manager/group locks. + pub(super) fn prepare(&mut self, required: usize) -> Result<(), SystemError> { + debug_assert!(self.is_empty() && self.index.is_empty()); + self.records + .try_reserve_exact(required) + .map_err(|_| SystemError::ENOMEM)?; + self.index + .try_reserve(required) + .map_err(|_| SystemError::ENOMEM)?; + Ok(()) + } + + pub(super) fn get(&self, semid: SemId) -> Option<&SemUndoRecord> { + self.index.get(&semid).map(|&slot| &self.records[slot]) + } + + pub(super) fn get_mut(&mut self, semid: SemId) -> Option<&mut SemUndoRecord> { + let slot = *self.index.get(&semid)?; + Some(&mut self.records[slot]) + } + + /// The caller owns a reservation covering both allocations. + pub(super) fn push_prepared(&mut self, record: SemUndoRecord) { + assert!(self.can_hold(self.len() + 1)); + debug_assert!(!self.index.contains_key(&record.semid)); + self.index.insert(record.semid, self.records.len()); + self.records.push(record); + } + + pub(super) fn remove(&mut self, semid: SemId) -> Option { + let slot = self.index.remove(&semid)?; + let removed = self.records.swap_remove(slot); + if let Some(moved) = self.records.get(slot) { + *self + .index + .get_mut(&moved.semid) + .expect("live undo record is indexed") = slot; + } + Some(removed) + } + + pub(super) fn pop(&mut self) -> Option { + let record = self.records.pop()?; + self.index.remove(&record.semid); + Some(record) + } + + /// Both capacities must already cover live records AND pending reservations. + /// Keep the old allocations in spare so the caller can dispose of them unlocked. + pub(super) fn install_prepared(&mut self, spare: &mut Self, required: usize) { + assert!(spare.is_empty() && spare.index.is_empty() && spare.can_hold(required)); + assert!(required >= self.len()); + for record in self.records.drain(..) { + spare.push_prepared(record); + } + self.index.clear(); + core::mem::swap(self, spare); + } +} diff --git a/kernel/src/ipc/sem_undo/test_support.rs b/kernel/src/ipc/sem_undo/test_support.rs index e1a3fa5e74..51494c5cfb 100644 --- a/kernel/src/ipc/sem_undo/test_support.rs +++ b/kernel/src/ipc/sem_undo/test_support.rs @@ -28,8 +28,7 @@ impl SemUndoGroup { self.inner .lock_irqsave() .records - .iter() - .find(|record| record.semid == semid) + .get(semid) .map(|record| record.adjustment(semnum)) .unwrap_or_default() } @@ -72,7 +71,11 @@ impl SemUndoGroup { #[cfg(test)] pub(crate) fn insert_test_record(&self, semid: SemId, adjustments: &[i16]) { let mut state = self.inner.lock_irqsave(); - state.records.push(SemUndoRecord::new_live( + let required = state.records.len() + 1; + let mut spare = UndoRecords::default(); + spare.prepare(required).unwrap(); + state.records.install_prepared(&mut spare, required); + state.records.push_prepared(SemUndoRecord::new_live( semid, adjustments.to_vec().into_boxed_slice(), )); @@ -92,7 +95,8 @@ impl SemUndoGroup { pub(crate) fn set_record_capacity_for_test(&self, capacity: usize) { let mut state = self.inner.lock_irqsave(); assert!(state.records.is_empty()); - state.records = Vec::with_capacity(capacity); + state.records = UndoRecords::default(); + state.records.prepare(capacity).unwrap(); assert_eq!(state.records.capacity(), capacity); } diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index a49433d208..5d8ca8c200 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -454,6 +455,48 @@ TEST(SysVSem, SemStatAndSemStatAny) { EXPECT_EQ(1u, ds.sem_nsems); } +TEST(SysVSem, StatLayoutAndExactUserBufferBounds) { + SemSet sem(3, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + struct sembuf op = {0, 1, 0}; + ASSERT_EQ(0, SemOp(sem.id(), &op, 1)); + + struct Buffer { + struct semid_ds ds; + unsigned char canary[32]; + } buffer; + for (int cmd : {IPC_STAT, SEM_STAT, SEM_STAT_ANY}) { + memset(&buffer, 0xa5, sizeof(buffer)); + int id = cmd == IPC_STAT ? sem.id() : sem.id() & 0x7fff; + ASSERT_EQ(cmd == IPC_STAT ? 0 : sem.id(), + SemCtl(id, 0, cmd, reinterpret_cast(&buffer.ds))); + EXPECT_EQ(3u, buffer.ds.sem_nsems); + EXPECT_GT(buffer.ds.sem_otime, 0); + EXPECT_GT(buffer.ds.sem_ctime, 0); + for (unsigned char byte : buffer.canary) EXPECT_EQ(0xa5, byte); + } + + const long page = sysconf(_SC_PAGESIZE); + ASSERT_GT(page, static_cast(sizeof(struct semid_ds))); + void* mapping = mmap(nullptr, page * 2, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + ASSERT_NE(MAP_FAILED, mapping); + // Keep cleanup armed even if a following assertion fails. + auto unmap = [page](void* p) { munmap(p, page * 2); }; + std::unique_ptr guard(mapping, unmap); + ASSERT_EQ(0, mprotect(static_cast(mapping) + page, page, PROT_NONE)); + auto* ds = reinterpret_cast( + static_cast(mapping) + page - sizeof(struct semid_ds)); + for (int cmd : {IPC_STAT, SEM_STAT, SEM_STAT_ANY}) { + int id = cmd == IPC_STAT ? sem.id() : sem.id() & 0x7fff; + ASSERT_EQ(cmd == IPC_STAT ? 0 : sem.id(), + SemCtl(id, 0, cmd, reinterpret_cast(ds))) << errno; + EXPECT_EQ(3u, ds->sem_nsems); + } + ds->sem_perm.mode = 0640; + EXPECT_EQ(0, SemCtl(sem.id(), 0, IPC_SET, reinterpret_cast(ds))) << errno; +} + TEST(SysVSem, IpcSetMode) { SemSet sem(1, IPC_CREAT | 0600); ASSERT_TRUE(sem.valid()); @@ -2997,6 +3040,50 @@ TEST(SysVSem, PersistentUndoGroupSurvivesRemovalAndRecordRegrowth) { EXPECT_EQ(0, SemCtl(sets.back()->id(), 0, GETVAL, 0)); } +TEST(SysVSem, UndoLookupSurvivesInterleavedRecordRemoval) { + constexpr int kSets = 64; + std::vector> sets; + for (int i = 0; i < kSets; ++i) { + sets.emplace_back(new SemSet(1, IPC_CREAT | 0600)); + ASSERT_TRUE(sets.back()->valid()); + } + int ready[2], resume[2]; + ASSERT_EQ(0, pipe(ready)); + FdGuard ready_read(ready[0]), ready_write(ready[1]); + ASSERT_EQ(0, pipe(resume)); + FdGuard resume_read(resume[0]), resume_write(resume[1]); + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + ready_read.Close(); + resume_write.Close(); + for (int i = 0; i < kSets; ++i) + if (!SemUndoOpMustSucceed(sets[i]->id(), 0, i + 1)) _exit(185); + char token = 1; + if (!WriteExact(ready_write.get(), &token, 1) || + !ReadExact(resume_read.get(), &token, 1)) _exit(186); + // Removal swaps dense slots: every surviving full ID must still find + // its own debt, including subsequent control and final exit replay. + for (int i = kSets - 1; i >= 0; i -= 2) + if (!SemUndoOpMustSucceed(sets[i]->id(), 0, 2)) _exit(187); + _exit(0); + } + ChildGuard child(pid); + ready_write.Close(); + resume_read.Close(); + char token; + ASSERT_TRUE(ReadExact(ready_read.get(), &token, 1)); + for (int i = 0; i < kSets; i += 2) { + ASSERT_EQ(0, SemCtl(sets[i]->id(), 0, IPC_RMID, 0)); + sets[i]->release(); + } + ASSERT_EQ(0, SemCtl(sets[1]->id(), 0, SETVAL, 9)); + ASSERT_TRUE(WriteExact(resume_write.get(), &token, 1)); + WaitChildOk(&child); + for (int i = 1; i < kSets; i += 2) + EXPECT_EQ(i == 1 ? 9 : 0, SemCtl(sets[i]->id(), 0, GETVAL, 0)); +} + TEST(SysVSem, ConcurrentWorkers) { constexpr int kWorkers = 8; SemSet sem(1, IPC_CREAT | 0600); From 506bf42a68c35032366c3c004cd5f835325f07c5 Mon Sep 17 00:00:00 2001 From: longjin Date: Sun, 6 Sep 2026 07:37:32 +0000 Subject: [PATCH 30/34] fix(ipc): replay detached undo outside fs publication barrier Preserve the old IPC namespace and actor across namespace publication, then release the fs reference guard before replay. Validate copied SETALL values before checking for concurrent removal. Signed-off-by: longjin --- kernel/src/ipc/sem/manager/control.rs | 11 +- kernel/src/ipc/sem/manager/tests.rs | 12 ++ kernel/src/ipc/sem_undo/lifecycle.rs | 124 +++++++++++------- kernel/src/ipc/sem_undo/mod.rs | 4 +- kernel/src/process/namespace/nsproxy.rs | 26 ++-- kernel/src/process/namespace/setns.rs | 6 +- kernel/src/process/namespace/unshare.rs | 6 +- .../suites/normal/sysv_sem_semantics.cc | 26 ++++ 8 files changed, 150 insertions(+), 65 deletions(-) diff --git a/kernel/src/ipc/sem/manager/control.rs b/kernel/src/ipc/sem/manager/control.rs index 92efc7a554..caa861f780 100644 --- a/kernel/src/ipc/sem/manager/control.rs +++ b/kernel/src/ipc/sem/manager/control.rs @@ -142,11 +142,9 @@ impl SemManager { vals: &[u16], wakes: &mut SemWakeBatch, ) -> Result<(), SystemError> { - let set_nsems = self - .get_by_semid_checked(token.id) - .map_err(|_| SystemError::EIDRM)? - .nsems(); - if vals.len() != token.nsems || vals.len() != set_nsems { + // Linux validates the copied array before re-locking the target: + // an out-of-range value wins over removal during the user copy. + if vals.len() != token.nsems { return Err(SystemError::EINVAL); } if vals.iter().any(|&v| v as i32 > SEMVMX) { @@ -156,6 +154,9 @@ impl SemManager { let set = self .get_by_semid_checked_mut(token.id) .map_err(|_| SystemError::EIDRM)?; + if vals.len() != set.nsems() { + return Err(SystemError::EINVAL); + } set.setall(token.id, vals, wakes); Ok(()) } diff --git a/kernel/src/ipc/sem/manager/tests.rs b/kernel/src/ipc/sem/manager/tests.rs index 168b79c415..e443f7ff12 100644 --- a/kernel/src/ipc/sem/manager/tests.rs +++ b/kernel/src/ipc/sem/manager/tests.rs @@ -143,6 +143,18 @@ fn prepared_setall_token_returns_eidrm_after_rmid() { ); } +#[test] +fn prepared_setall_range_error_precedes_removal() { + let mut manager = SemManager::new(); + let id = insert_test_set(&mut manager, SemKey::new(12), &[1, 2]); + let token = SemSetAllToken::new(id, 2); + remove_test_set(&mut manager, id); + assert_eq!( + manager.setall(token, &[7, 32768], &mut SemWakeBatch::default()), + Err(SystemError::ERANGE) + ); +} + #[test] fn stale_prepared_setall_token_does_not_modify_reused_index() { let mut manager = SemManager::new(); diff --git a/kernel/src/ipc/sem_undo/lifecycle.rs b/kernel/src/ipc/sem_undo/lifecycle.rs index 045bcfb560..a2a7470927 100644 --- a/kernel/src/ipc/sem_undo/lifecycle.rs +++ b/kernel/src/ipc/sem_undo/lifecycle.rs @@ -2,7 +2,11 @@ use super::SemUndoGroup; use crate::{ ipc::sem::{SemManager, SemWakeBatch}, - process::{pid::PidType, ProcessControlBlock}, + process::{ + namespace::ipc_namespace::IpcNamespace, + pid::{Pid, PidType}, + ProcessControlBlock, + }, }; use alloc::sync::Arc; @@ -44,58 +48,90 @@ impl Drop for SemUndoAttachment { } pub(crate) fn detach_sem_undo(pcb: &Arc) { - let Some(attachment) = pcb.take_sem_undo_attachment() else { - return; - }; - let group = attachment.group(); - drop(attachment); - - if !group.detach_owner_and_mark_last() { - return; + if let Some(replay) = PendingSemUndoReplay::detach(pcb) { + replay.replay(); } +} - replay_marked_records(pcb, &group); +/// Logical detachment may happen under publication locks; actual replay must not. +/// Pin the old namespace and actor before publishing new task namespace state. +/// Execution is explicit: dropping this context never runs semaphore operations. +#[must_use = "detached final-owner undo debt must be replayed explicitly"] +pub(crate) struct PendingSemUndoReplay { + group: Arc, + ipc_ns: Option>, + actor: Option>, } -pub(super) fn replay_marked_records(pcb: &Arc, group: &Arc) { - if !group.begin_replay() { - return; +impl PendingSemUndoReplay { + pub(crate) fn detach(pcb: &Arc) -> Option { + let attachment = pcb.take_sem_undo_attachment()?; + let group = attachment.group(); + drop(attachment); + if !group.detach_owner_and_mark_last() { + return None; + } + Some(Self::new(pcb, group)) } - let exiting_tgid = pcb.try_active_pid_ns().and_then(|pid_ns| { - pcb.task_pid_nr_ns(PidType::TGID, Some(pid_ns)) - .filter(|tgid| tgid.data() != 0)?; - pcb.task_pid_ptr(PidType::TGID) - }); - - let Some(ipc_ns) = group.ipc_ns.upgrade() else { - drop(group.discard_retired_records()); - return; - }; - - loop { - let mut wakes = SemWakeBatch::default(); - let record = { - let mut manager = ipc_ns.sem.lock(); - let Some(record) = group.pop_retired_record() else { - break; - }; - SemManager::replay_sem_undo_adjustments( - &mut manager, - record.semid, - &record.adjustments, - exiting_tgid.clone(), - &mut wakes, - ); - manager.unregister_undo_group(record.semid, group); - record + + fn new(pcb: &Arc, group: Arc) -> Self { + let ipc_ns = group.ipc_ns.upgrade(); + let actor = pcb.try_active_pid_ns().and_then(|pid_ns| { + pcb.task_pid_nr_ns(PidType::TGID, Some(pid_ns)) + .filter(|tgid| tgid.data() != 0)?; + pcb.task_pid_ptr(PidType::TGID) + }); + Self { + group, + ipc_ns, + actor, + } + } + + pub(crate) fn replay(self) { + let Self { + group, + ipc_ns, + actor, + } = self; + if !group.begin_replay() { + return; + } + let Some(ipc_ns) = ipc_ns else { + drop(group.discard_retired_records()); + return; }; - // Publish and notify one set at a time, like Linux exit_sem. Pending - // records stay in the group so interleaved semctl still clears them. - wakes.wake_all(); - SemManager::shrink_undo_registry(&ipc_ns, record.semid); + + loop { + let mut wakes = SemWakeBatch::default(); + let record = { + let mut manager = ipc_ns.sem.lock(); + let Some(record) = group.pop_retired_record() else { + break; + }; + SemManager::replay_sem_undo_adjustments( + &mut manager, + record.semid, + &record.adjustments, + actor.clone(), + &mut wakes, + ); + manager.unregister_undo_group(record.semid, &group); + record + }; + // Publish and notify one set at a time, like Linux exit_sem. Pending + // records stay in the group so interleaved semctl still clears them. + wakes.wake_all(); + SemManager::shrink_undo_registry(&ipc_ns, record.semid); + } } } +#[cfg(test)] +pub(super) fn replay_marked_records(pcb: &Arc, group: &Arc) { + PendingSemUndoReplay::new(pcb, group.clone()).replay(); +} + impl UnpublishedSemUndoAttachmentGuard { pub(crate) fn new(group: Arc) -> Self { group.acquire_shared_owner(); diff --git a/kernel/src/ipc/sem_undo/mod.rs b/kernel/src/ipc/sem_undo/mod.rs index cce1fb9026..6580466ab6 100644 --- a/kernel/src/ipc/sem_undo/mod.rs +++ b/kernel/src/ipc/sem_undo/mod.rs @@ -12,7 +12,9 @@ mod lifecycle; mod record; mod storage; pub use lifecycle::SemUndoAttachment; -pub(crate) use lifecycle::{detach_sem_undo, UnpublishedSemUndoAttachmentGuard}; +pub(crate) use lifecycle::{ + detach_sem_undo, PendingSemUndoReplay, UnpublishedSemUndoAttachmentGuard, +}; use record::PendingSemUndoRecordReservation; pub(crate) use record::{PreparedSemUndoRecord, PreparedSemUndoRecordAction, SemUndoRecord}; use storage::UndoRecords; diff --git a/kernel/src/process/namespace/nsproxy.rs b/kernel/src/process/namespace/nsproxy.rs index e4bb27dab4..742852df06 100644 --- a/kernel/src/process/namespace/nsproxy.rs +++ b/kernel/src/process/namespace/nsproxy.rs @@ -4,7 +4,7 @@ use system_error::SystemError; use crate::{ filesystem::{fs::FsStruct, vfs::IndexNode}, - ipc::sem_undo::detach_sem_undo, + ipc::sem_undo::PendingSemUndoReplay, process::{ cred::Cred, fork::CloneFlags, @@ -294,7 +294,7 @@ pub fn exec_task_namespaces() -> Result<(), SystemError> { let fs_refs = crate::process::lock_fs_refs_copy(); let prepared = PreparedNamespaceInstall::prepare_for_setns(&tsk, new_nsproxy, None, false, &fs_refs)?; - prepared.commit(&tsk, &fs_refs)?; + prepared.commit(&tsk, fs_refs)?; return Ok(()); } @@ -408,7 +408,7 @@ impl PreparedNamespaceInstall { pub(crate) fn commit( self, tsk: &Arc, - fs_refs: &FsRefsReadGuard, + fs_refs: FsRefsReadGuard, ) -> Result<(), SystemError> { let Self { new_nsproxy, @@ -418,14 +418,22 @@ impl PreparedNamespaceInstall { nsproxy_retire, cred_retire, } = self; - if detach_sysvsem { - detach_sem_undo(tsk); - } + let undo_replay = if detach_sysvsem { + PendingSemUndoReplay::detach(tsk) + } else { + None + }; if let Some(new_fs) = new_fs { - tsk.set_fs_struct(new_fs, fs_refs); + tsk.set_fs_struct(new_fs, &fs_refs); } let prepared_cred = new_cred.zip(cred_retire); tsk.install_prepared_namespace_state(new_nsproxy, nsproxy_retire, prepared_cred); + // Keep copy-to-publication atomic against pivot_root, but do not make + // unrelated fs topology writers wait for semaphore replay/wakeup work. + drop(fs_refs); + if let Some(replay) = undo_replay { + replay.replay(); + } Ok(()) } } @@ -474,7 +482,7 @@ mod tests { ) .unwrap(); - prepared.commit(&pcb, &fs_refs).unwrap(); + prepared.commit(&pcb, fs_refs).unwrap(); assert!(Arc::ptr_eq(&pcb.nsproxy(), &new_nsproxy)); assert!(Arc::ptr_eq(&pcb.cred(), &old_cred)); } @@ -557,7 +565,7 @@ mod tests { .unwrap(); let unprepared_before = pcb.namespace_unprepared_rcu_stores_for_test(); - prepared.commit(&pcb, &fs_refs).unwrap(); + prepared.commit(&pcb, fs_refs).unwrap(); assert_eq!( pcb.namespace_unprepared_rcu_stores_for_test(), diff --git a/kernel/src/process/namespace/setns.rs b/kernel/src/process/namespace/setns.rs index 09fe49add7..8762ae7fe1 100644 --- a/kernel/src/process/namespace/setns.rs +++ b/kernel/src/process/namespace/setns.rs @@ -214,7 +214,7 @@ pub fn ksys_setns(fd: i32, nstype: i32) -> Result<(), SystemError> { let new_nsproxy = Arc::new(new_inner); let fs_refs = lock_fs_refs_copy(); let prepared = prepare_setns_install(¤t, new_nsproxy, None, flags, &fs_refs)?; - prepared.commit(¤t, &fs_refs)?; + prepared.commit(¤t, fs_refs)?; return Ok(()); } @@ -303,7 +303,7 @@ pub fn ksys_setns(fd: i32, nstype: i32) -> Result<(), SystemError> { installation_flags, &fs_refs, )?; - prepared.commit(¤t, &fs_refs)?; + prepared.commit(¤t, fs_refs)?; Ok(()) } @@ -359,7 +359,7 @@ mod tests { &fs_refs, ) .unwrap(); - prepared.commit(&pcb, &fs_refs).unwrap(); + prepared.commit(&pcb, fs_refs).unwrap(); assert!(Arc::ptr_eq(&pcb.nsproxy(), &old_nsproxy)); assert!(pcb.sem_undo_group().is_none()); diff --git a/kernel/src/process/namespace/unshare.rs b/kernel/src/process/namespace/unshare.rs index ce1427b0ad..56393dd631 100644 --- a/kernel/src/process/namespace/unshare.rs +++ b/kernel/src/process/namespace/unshare.rs @@ -29,7 +29,7 @@ pub fn ksys_unshare(flags: CloneFlags) -> Result<(), SystemError> { if let Some(prepared) = prepare_unshare_install(¤t_pcb, flags, new_fs, new_nsproxy, new_cred, &fs_refs)? { - prepared.commit(¤t_pcb, &fs_refs)?; + prepared.commit(¤t_pcb, fs_refs)?; } // TODO: 处理其他命名空间的 unshare 操作 @@ -235,7 +235,7 @@ mod tests { prepare_unshare_install(&pcb, CloneFlags::CLONE_SYSVSEM, None, None, None, &fs_refs) .unwrap() .unwrap(); - prepared.commit(&pcb, &fs_refs).unwrap(); + prepared.commit(&pcb, fs_refs).unwrap(); assert!(pcb.sem_undo_group().is_none()); let replacement = pcb.ensure_sem_undo_group(&ipc_ns).unwrap(); @@ -267,7 +267,7 @@ mod tests { ) .unwrap() .unwrap(); - prepared.commit(&pcb, &fs_refs).unwrap(); + prepared.commit(&pcb, fs_refs).unwrap(); assert!(pcb.sem_undo_group().is_none()); assert!(Arc::ptr_eq(&pcb.nsproxy().ipc_ns, &new_ipc_ns)); diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index 5d8ca8c200..69ebdd1219 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -1690,6 +1690,32 @@ TEST(SysVSem, SemUndoUnshareSysvsem) { << "last old shared owner replays old-group debt after its release"; } +TEST(SysVSem, MultiSetUnshareCompletesReplayBeforeReturning) { + std::vector> sets; + for (int i = 0; i < 128; ++i) { + sets.emplace_back(new SemSet(1, IPC_CREAT | 0600)); + ASSERT_TRUE(sets.back()->valid()); + } + pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + for (int round = 1; round <= 2; ++round) { + for (auto& sem : sets) + if (!SemUndoOpMustSucceed(sem->id(), 0, round)) _exit(188); + if (unshare(CLONE_SYSVSEM) != 0) _exit(189); + // Check in the live caller, not only after exit could replay debt. + for (auto& sem : sets) { + if (SemCtl(sem->id(), 0, GETVAL, 0) != 0 || + SemCtl(sem->id(), 0, GETPID, 0) != getpid()) _exit(190); + } + } + _exit(0); + } + ChildGuard child(pid); + WaitChildOk(&child); + for (auto& sem : sets) EXPECT_EQ(0, SemCtl(sem->id(), 0, GETVAL, 0)); +} + struct NamespaceUndoReport { int setns_result; int setns_errno; From 0424b7f82f8e620fb382979e6601e8bbe87cbc11 Mon Sep 17 00:00:00 2001 From: longjin Date: Sun, 6 Sep 2026 12:24:45 +0000 Subject: [PATCH 31/34] fix(ipc): skip semaphore queue scans when waiter state is unchanged Track both committed semaphore values and shared undo adjustments in the existing completion result. Preserve the result through the immediate undo path and use it for immediate scans and queued retries. Debt-only changes must still retry waiters because they can turn a blocked shared SEM_UNDO operation into ERANGE. Add user-space coverage for immediate and queued debt-only changes. Signed-off-by: longjin --- kernel/src/ipc/sem/operation.rs | 24 ++++-- kernel/src/ipc/sem/set/operation.rs | 13 ++-- kernel/src/ipc/sem/set/operation/tests.rs | 4 +- kernel/src/ipc/sem/set/queue.rs | 6 +- .../suites/normal/sysv_sem_semantics.cc | 75 +++++++++++++++++++ 5 files changed, 105 insertions(+), 17 deletions(-) diff --git a/kernel/src/ipc/sem/operation.rs b/kernel/src/ipc/sem/operation.rs index a5612c8214..211081514a 100644 --- a/kernel/src/ipc/sem/operation.rs +++ b/kernel/src/ipc/sem/operation.rs @@ -232,23 +232,27 @@ impl SemManager { let outcome = set.try_apply(sops, pid.clone(), Some(record), &mut immediate_scratch); match outcome { - Ok(SemAttempt::Completed { .. }) => { - PreparedSemUndoRecordAction::Complete(Ok(None)) + Ok(completed @ SemAttempt::Completed { .. }) => { + PreparedSemUndoRecordAction::Complete(Ok(completed)) } Ok(SemAttempt::Blocked(blocker)) => { - PreparedSemUndoRecordAction::Keep(Ok(Some(blocker))) + PreparedSemUndoRecordAction::Keep(Ok(SemAttempt::Blocked(blocker))) } Err(error) => PreparedSemUndoRecordAction::Keep(Err(error)), } })?; *record_slot = kept_record; match outcome? { - None => { + SemAttempt::Completed { + waiter_state_changed, + } => { drop(record_slot); - set.update_queue(&mut wakes); + if waiter_state_changed { + set.update_queue(&mut wakes); + } return Ok(0); } - Some(blocker) => { + SemAttempt::Blocked(blocker) => { if blocker.nowait || non_blocking { return Err(SystemError::EAGAIN_OR_EWOULDBLOCK); } @@ -264,8 +268,12 @@ impl SemManager { } else { let set = guard.get_by_semid_checked_mut(semid)?; match set.try_apply(sops, pid.clone(), None, &mut immediate_scratch)? { - SemAttempt::Completed { .. } => { - set.update_queue(&mut wakes); + SemAttempt::Completed { + waiter_state_changed, + } => { + if waiter_state_changed { + set.update_queue(&mut wakes); + } return Ok(0); } SemAttempt::Blocked(blocker) => { diff --git a/kernel/src/ipc/sem/set/operation.rs b/kernel/src/ipc/sem/set/operation.rs index e32eb72040..88e90831cd 100644 --- a/kernel/src/ipc/sem/set/operation.rs +++ b/kernel/src/ipc/sem/set/operation.rs @@ -195,26 +195,29 @@ impl KernelSemSet { pid: Option>, mut undo: Option<&mut SemUndoRecord>, ) -> bool { - let mut values_changed = false; + let mut waiter_state_changed = false; for entry in scratch.entries.iter().take(simulation.entry_count) { let sem = &mut set.sems[entry.semnum]; - values_changed |= entry.initial_val != entry.virtual_val; + waiter_state_changed |= entry.initial_val != entry.virtual_val; sem.val = entry.virtual_val; sem.pid = pid.clone(); if entry.virtual_adj != entry.initial_adj { if let Some(record) = undo.as_deref_mut() { record.set_adjustment(entry.semnum, entry.virtual_adj); + // Shared undo debt can change a queued operation's ERANGE result + // even when the semaphore value is unchanged. + waiter_state_changed = true; } } } set.sem_otime = PosixTimeSpec::now().tv_sec; - values_changed + waiter_state_changed } } #[derive(Debug)] pub(in crate::ipc::sem) enum SemAttempt { - Completed { values_changed: bool }, + Completed { waiter_state_changed: bool }, Blocked(SemBlockedOp), } impl KernelSemSet { @@ -229,7 +232,7 @@ impl KernelSemSet { ) -> Result { match Self::simulate_semop(self, sops, undo.as_deref_mut(), scratch)? { SemopOutcome::Ready(simulation) => Ok(SemAttempt::Completed { - values_changed: Self::commit_semop(self, simulation, scratch, pid, undo), + waiter_state_changed: Self::commit_semop(self, simulation, scratch, pid, undo), }), SemopOutcome::Blocked(blocker) => Ok(SemAttempt::Blocked(blocker)), } diff --git a/kernel/src/ipc/sem/set/operation/tests.rs b/kernel/src/ipc/sem/set/operation/tests.rs index 5afd625f0e..d30eba3fdf 100644 --- a/kernel/src/ipc/sem/set/operation/tests.rs +++ b/kernel/src/ipc/sem/set/operation/tests.rs @@ -15,7 +15,7 @@ fn try_apply_commits_metadata_even_when_values_are_unchanged() { &mut scratch ), Ok(SemAttempt::Completed { - values_changed: false + waiter_state_changed: false }) )); assert_eq!(set.sems[0].val, 4); @@ -38,7 +38,7 @@ fn try_apply_rebuilds_scratch_after_a_blocked_attempt() { assert!(matches!( set.try_apply(&[plain_sop(0, -1)], None, None, &mut scratch), Ok(SemAttempt::Completed { - values_changed: true + waiter_state_changed: true }) )); assert_eq!(set.sems[0].val, 0); diff --git a/kernel/src/ipc/sem/set/queue.rs b/kernel/src/ipc/sem/set/queue.rs index 70eb76da99..8e7efc612d 100644 --- a/kernel/src/ipc/sem/set/queue.rs +++ b/kernel/src/ipc/sem/set/queue.rs @@ -471,7 +471,7 @@ impl KernelSemSet { let mut scratch = entry.scratch.lock(); match set.try_apply(&entry.sops, entry.pid.clone(), None, &mut scratch) { Ok(SemAttempt::Completed { - values_changed: changed, + waiter_state_changed: changed, }) => { set.finish_and_wake(queue, entry.clone(), Ok(0), wakes); if changed { @@ -519,7 +519,9 @@ impl KernelSemSet { } let mut scratch = entry.scratch.lock(); match set.try_apply(&entry.sops, entry.pid.clone(), Some(record), &mut scratch) { - Ok(SemAttempt::Completed { values_changed }) => Ok(Some(values_changed)), + Ok(SemAttempt::Completed { + waiter_state_changed, + }) => Ok(Some(waiter_state_changed)), Ok(SemAttempt::Blocked(blocker)) => { if blocker.nowait { Err(SystemError::EAGAIN_OR_EWOULDBLOCK) diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index 69ebdd1219..da6ee757db 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -2176,6 +2176,81 @@ TEST(SysVSem, SharedUndoFailurePreservesQueuedAssociation) { } } +int DebtOnlyChangeWaiter(void* opaque) { + const int semid = *static_cast(opaque); + struct sembuf ops[] = {{0, -1, SEM_UNDO}, {1, -1, SEM_UNDO}}; + const timespec timeout = {5, 0}; + const int result = SemTimedOp(semid, ops, 2, &timeout); + // A missed queue scan must fail with the bounded timeout, not hang the suite. + return result == -1 && errno == ERANGE ? 0 : 191; +} + +int DebtOnlyChangeQueuedWriter(void* opaque) { + const int semid = *static_cast(opaque); + struct sembuf ops[] = {{2, 0, 0}, {0, 1, 0}, {0, -1, SEM_UNDO}}; + const timespec timeout = {5, 0}; + return SemTimedOp(semid, ops, 3, &timeout) == 0 ? 0 : 200; +} + +int RunDebtOnlyChangeSupervisor(int semid, bool queued_change) { + // semval stays 1 while this group's positive adjustment reaches 32766. + struct sembuf prepare[] = {{0, 32766, 0}, {0, -32766, SEM_UNDO}}; + if (SemOp(semid, prepare, 2) != 0) return 192; + alignas(16) char stack[16384]; + pid_t pid = clone(DebtOnlyChangeWaiter, stack + sizeof(stack), + CLONE_SYSVSEM | SIGCHLD, &semid); + if (pid < 0) return 193; + ChildGuard child(pid); + // Its first operation fits semadj=32767; the second blocks on semval=0. + if (!WaitForNcnt(semid, 1, 1)) return 194; + if (queued_change) { + alignas(16) char writer_stack[16384]; + pid_t writer_pid = clone(DebtOnlyChangeQueuedWriter, writer_stack + sizeof(writer_stack), + CLONE_SYSVSEM | SIGCHLD, &semid); + if (writer_pid < 0) return 201; + ChildGuard writer(writer_pid); + if (!WaitForZcnt(semid, 2, 1)) return 202; + // A is scanned first and still blocks; B then changes only undo debt. + // Queue processing must restart and discover A's new ERANGE condition. + if (SemCtl(semid, 2, SETVAL, 0) != 0) return 203; + int writer_status; + pid_t ret; + do { ret = waitpid(writer_pid, &writer_status, 0); } while (ret < 0 && errno == EINTR); + if (ret != writer_pid) return 204; + writer.MarkReaped(); + if (!WIFEXITED(writer_status) || WEXITSTATUS(writer_status) != 0) return 205; + } else { + struct sembuf change[] = {{0, 1, 0}, {0, -1, SEM_UNDO}}; + if (SemOp(semid, change, 2) != 0) return 195; + } + // Values are unchanged, but another -1 SEM_UNDO now overflows the shared + // adjustment. The queued operation must be retried without any value wakeup. + if (SemCtl(semid, 0, GETVAL, 0) != 1 || SemCtl(semid, 1, GETVAL, 0) != 0) return 196; + int status; + pid_t ret; + do { ret = waitpid(pid, &status, 0); } while (ret < 0 && errno == EINTR); + if (ret != pid) return 197; + child.MarkReaped(); + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) return 198; + return SemCtl(semid, 1, GETNCNT, 0) == 0 ? 0 : 199; +} + +TEST(SysVSem, SharedUndoDebtOnlyChangeRetriesQueuedOperation) { + for (bool queued_change : {false, true}) { + SCOPED_TRACE(queued_change ? "queued debt-only change" : "immediate debt-only change"); + SemSet sem(3, IPC_CREAT | 0600); + ASSERT_TRUE(sem.valid()); + ASSERT_EQ(0, SemCtl(sem.id(), 0, SETVAL, 1)); + ASSERT_EQ(0, SemCtl(sem.id(), 2, SETVAL, 1)); + pid_t pid = fork(); + ASSERT_GE(pid, 0); + // Isolate the shared undo group from the test runner's existing records. + if (pid == 0) _exit(RunDebtOnlyChangeSupervisor(sem.id(), queued_change)); + ChildGuard supervisor(pid); + WaitChildOk(&supervisor); + } +} + TEST(SysVSem, GetNcntCountsBlocked) { SemSet sem(1, IPC_CREAT | 0600); ASSERT_TRUE(sem.valid()); From 3e3843d074c0f747c5ab42665fa5a525b55666db Mon Sep 17 00:00:00 2001 From: longjin Date: Sun, 6 Sep 2026 12:43:39 +0000 Subject: [PATCH 32/34] test(ipc): preserve creator group access after semaphore IPC_SET Cover creator and current group membership through both primary and supplementary groups after changing gid. Check read/write access, unrelated-group denial, and the separate owner-only IPC_SET/IPC_RMID boundary. Linux 6.6 ipcperms checks both cgid and gid, so retain the existing kernel behavior. Signed-off-by: longjin --- .../suites/normal/sysv_sem_semantics.cc | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc index da6ee757db..2dd374a50a 100644 --- a/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -539,6 +540,62 @@ TEST(SysVSem, IpcSetInvalidGidDoesNotPartiallyUpdate) { EXPECT_EQ(before.sem_perm.mode & 0777, after.sem_perm.mode & 0777); } +TEST(SysVSem, CreatorAndCurrentGroupsRetainAccessAfterIpcSet) { + if (geteuid() != 0) { + GTEST_SKIP() << "requires root to establish independent reader credentials"; + } + SemSet sem(1, IPC_CREAT | 0660); + ASSERT_TRUE(sem.valid()); + struct semid_ds ds = {}; + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_STAT, reinterpret_cast(&ds))); + const gid_t creator_group = ds.sem_perm.cgid; + const gid_t current_group = creator_group == 1001 ? 1002 : 1001; + const gid_t unrelated_group = creator_group == 1003 ? 1004 : 1003; + ds.sem_perm.gid = current_group; + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_SET, reinterpret_cast(&ds))); + ASSERT_EQ(0, SemCtl(sem.id(), 0, IPC_STAT, reinterpret_cast(&ds))); + ASSERT_EQ(creator_group, ds.sem_perm.cgid); + ASSERT_EQ(current_group, ds.sem_perm.gid); + + // Linux ipcperms() uses both cgid and gid, for effective and supplementary + // membership. None of these readers is the owner/creator or retains root. + for (int access_case = 0; access_case < 5; ++access_case) { + SCOPED_TRACE(access_case); + const pid_t pid = fork(); + ASSERT_GE(pid, 0); + if (pid == 0) { + const gid_t group = access_case % 2 == 0 ? creator_group : current_group; + const bool supplementary = access_case == 2 || access_case == 3; + if (setgroups(supplementary ? 1 : 0, supplementary ? &group : nullptr) != 0 || + setgid(access_case < 2 ? group : unrelated_group) != 0 || + setuid(12345) != 0) { + _exit(210); + } + const bool allowed = access_case < 4; + errno = 0; + const int value = SemCtl(sem.id(), 0, GETVAL, 0); + if (allowed ? value != 0 : (value != -1 || errno != EACCES)) _exit(211); + struct sembuf zero = {0, 0, IPC_NOWAIT}; + errno = 0; + const int read_result = SemOp(sem.id(), &zero, 1); + if (allowed ? read_result != 0 : (read_result != -1 || errno != EACCES)) _exit(212); + struct sembuf alter[] = {{0, 1, IPC_NOWAIT}, {0, -1, IPC_NOWAIT}}; + errno = 0; + const int write_result = SemOp(sem.id(), alter, 2); + if (allowed ? write_result != 0 : (write_result != -1 || errno != EACCES)) _exit(213); + // Group access does not confer ownership/control privileges. + errno = 0; + if (SemCtl(sem.id(), 0, IPC_SET, reinterpret_cast(&ds)) != -1 || + errno != EPERM) _exit(214); + errno = 0; + if (SemCtl(sem.id(), 0, IPC_RMID, 0) != -1 || errno != EPERM) _exit(215); + _exit(0); + } + ChildGuard child(pid); + WaitChildOk(&child); + } +} + TEST(SysVSem, OwnerCanControlModeZeroSet) { const pid_t child = fork(); ASSERT_GE(child, 0); From fa5cdf934734577ca08482e4c97f1478ebfdba0c Mon Sep 17 00:00:00 2001 From: longjin Date: Sun, 6 Sep 2026 17:09:19 +0000 Subject: [PATCH 33/34] perf(ipc): index semaphore set undo associations Keep dense association storage and its group-identity index together. Prepare both buffers outside the manager lock, recheck capacity before publication, and repair moved slots on constant-time expected removal. Preserve Weak identity lifetimes, deferred RMID cleanup, geometric growth and best-effort unlocked reclamation. Extend registry regression coverage for removal, deduplication and concurrent capacity changes. Signed-off-by: longjin --- kernel/src/ipc/sem/manager/mod.rs | 8 +- kernel/src/ipc/sem/manager/test_support.rs | 7 +- kernel/src/ipc/sem/operation.rs | 13 +- kernel/src/ipc/sem/set/mod.rs | 67 ++-------- kernel/src/ipc/sem/set/tests.rs | 84 ++++++++---- kernel/src/ipc/sem/set/undo_registry.rs | 148 +++++++++++++++++++++ 6 files changed, 236 insertions(+), 91 deletions(-) create mode 100644 kernel/src/ipc/sem/set/undo_registry.rs diff --git a/kernel/src/ipc/sem/manager/mod.rs b/kernel/src/ipc/sem/manager/mod.rs index 45280b0d20..605f473403 100644 --- a/kernel/src/ipc/sem/manager/mod.rs +++ b/kernel/src/ipc/sem/manager/mod.rs @@ -1,7 +1,7 @@ //! Namespace identity, lookup, quota and semaphore creation. use super::{ abi::*, - set::{KernelSem, KernelSemSet, SemWakeBatch}, + set::{KernelSem, KernelSemSet, SemUndoRegistry, SemWakeBatch}, }; use crate::{ ipc::{ @@ -86,8 +86,8 @@ impl SemManager { /// the namespace lock. Concurrent growth can make a shrink unnecessary or /// consume the spare capacity; in either case leave the live registry intact. pub(crate) fn shrink_undo_registry(ipcns: &Arc, semid: SemId) { - let mut spare = Vec::new(); - let mut retired = Vec::new(); + let mut spare = SemUndoRegistry::default(); + let mut retired = SemUndoRegistry::default(); let needed = { let mut manager = ipcns.sem.lock(); let Ok(set) = manager.get_by_semid_checked_mut(semid) else { @@ -95,7 +95,7 @@ impl SemManager { }; set.shrink_undo_registry_prepared(&mut spare, &mut retired) }; - if needed == 0 || spare.try_reserve_exact(needed).is_err() { + if needed == 0 || spare.prepare(needed).is_err() { return; } // Allocation is best-effort reclamation, not part of syscall success. diff --git a/kernel/src/ipc/sem/manager/test_support.rs b/kernel/src/ipc/sem/manager/test_support.rs index d3eb83efb8..912bdffaae 100644 --- a/kernel/src/ipc/sem/manager/test_support.rs +++ b/kernel/src/ipc/sem/manager/test_support.rs @@ -1,5 +1,6 @@ use super::*; use crate::ipc::sem::set::test_support::test_perm; +use crate::ipc::sem::set::SemUndoRegistry; use alloc::sync::Weak; impl SemManager { @@ -11,11 +12,9 @@ impl SemManager { semid: SemId, ) -> Result<(), SystemError> { let set = self.get_by_semid_checked_mut(semid)?; - let mut spare = Vec::new(); + let mut spare = SemUndoRegistry::default(); if let Err(capacity) = set.ensure_undo_group_registered_prepared(group, &mut spare) { - spare - .try_reserve(capacity) - .map_err(|_| SystemError::ENOMEM)?; + spare.prepare(capacity)?; set.ensure_undo_group_registered_prepared(group, &mut spare) .unwrap(); } diff --git a/kernel/src/ipc/sem/operation.rs b/kernel/src/ipc/sem/operation.rs index 211081514a..a802f5e257 100644 --- a/kernel/src/ipc/sem/operation.rs +++ b/kernel/src/ipc/sem/operation.rs @@ -8,13 +8,16 @@ use crate::{ Duration, }, }; -use alloc::{sync::Arc, vec::Vec}; +use alloc::sync::Arc; use system_error::SystemError; use super::{ abi::*, manager::SemManager, - set::{SemAttempt, SemBlockedOp, SemQueueEntry, SemWaitType, SemWakeBatch, SemopScratch}, + set::{ + SemAttempt, SemBlockedOp, SemQueueEntry, SemUndoRegistry, SemWaitType, SemWakeBatch, + SemopScratch, + }, }; impl SemManager { pub(super) fn cancel_queued_entry( @@ -144,14 +147,12 @@ impl SemManager { }; let mut wakes = SemWakeBatch::default(); - let mut registry_spare = Vec::new(); + let mut registry_spare = SemUndoRegistry::default(); let mut registry_capacity_needed = 0; let entry = loop { // Revalidate after unlocked undo-registry preparation. if registry_capacity_needed != 0 { - registry_spare - .try_reserve(registry_capacity_needed) - .map_err(|_| SystemError::ENOMEM)?; + registry_spare.prepare(registry_capacity_needed)?; registry_capacity_needed = 0; } let nsems = { diff --git a/kernel/src/ipc/sem/set/mod.rs b/kernel/src/ipc/sem/set/mod.rs index 1230bba5c4..2a82d01ce5 100644 --- a/kernel/src/ipc/sem/set/mod.rs +++ b/kernel/src/ipc/sem/set/mod.rs @@ -22,10 +22,12 @@ use system_error::SystemError; use super::abi::*; mod operation; mod queue; +mod undo_registry; pub(in crate::ipc::sem) use operation::{SemAttempt, SemBlockedOp, SemWaitType, SemopScratch}; pub(in crate::ipc::sem) use queue::SemQueueEntry; use queue::SemWaitQueue; pub(crate) use queue::SemWakeBatch; +pub(in crate::ipc::sem) use undo_registry::SemUndoRegistry; /// A single semaphore (fields of Linux `struct sem`) #[derive(Debug, Clone)] @@ -63,7 +65,7 @@ impl SemUndoAssociation { #[derive(Debug)] pub struct KernelSemSet { /// Groups that can carry undo debt for this set, including queued operations. - undo_groups: Vec, + undo_groups: SemUndoRegistry, /// Permission information kern_ipc_perm: IpcPerm, /// Semaphores in the set @@ -81,7 +83,7 @@ pub struct KernelSemSet { impl KernelSemSet { /// Only call after RMID released the manager lock and delivered wakeups. pub(crate) fn reclaim_removed_undo_storage(&self) { - for association in &self.undo_groups { + for association in self.undo_groups.iter() { if let SemUndoAssociation::Retired { _group: group, .. } = association { group.shrink_records(); } @@ -95,62 +97,21 @@ impl KernelSemSet { pub(in crate::ipc::sem) fn ensure_undo_group_registered_prepared( &mut self, group: &Arc, - spare: &mut Vec, + spare: &mut SemUndoRegistry, ) -> Result<(), usize> { - debug_assert!(spare.is_empty()); - let candidate = Arc::downgrade(group); - if self - .undo_groups - .iter() - .any(|entry| entry.group().ptr_eq(&candidate)) - { - return Ok(()); - } - if self.undo_groups.len() == self.undo_groups.capacity() { - self.compact_undo_registry(); - } - if self.undo_groups.len() == self.undo_groups.capacity() { - if spare.capacity() <= self.undo_groups.len() { - return Err(self.undo_groups.len().saturating_mul(2).max(4)); - } - spare.append(&mut self.undo_groups); - core::mem::swap(&mut self.undo_groups, spare); - } - self.undo_groups.push(SemUndoAssociation::Group(candidate)); - Ok(()) + self.undo_groups.register_prepared(group, spare) } fn compact_undo_registry(&mut self) { - self.undo_groups - .retain(|entry| entry.group().strong_count() != 0); + self.undo_groups.compact(); } - /// Request/publish smaller storage without allocating under the manager - /// lock. Empty registries are moved into retired without installing spare. - /// The caller must drop both buffers after unlocking. pub(in crate::ipc::sem) fn shrink_undo_registry_prepared( &mut self, - spare: &mut Vec, - retired: &mut Vec, + spare: &mut SemUndoRegistry, + retired: &mut SemUndoRegistry, ) -> usize { - debug_assert!(spare.is_empty()); - debug_assert!(retired.is_empty() && retired.capacity() == 0); - let len = self.undo_groups.len(); - if len == 0 { - core::mem::swap(&mut self.undo_groups, retired); - return 0; - } - if self.undo_groups.capacity() <= 4 || len > self.undo_groups.capacity() / 4 { - return 0; - } - if spare.capacity() < len { - return len.saturating_mul(2).max(4); - } - if spare.capacity() < self.undo_groups.capacity() { - spare.append(&mut self.undo_groups); - core::mem::swap(&mut self.undo_groups, spare); - } - 0 + self.undo_groups.shrink_prepared(spare, retired) } pub(in crate::ipc::sem) fn try_allocate_sems( @@ -173,7 +134,7 @@ impl KernelSemSet { pub(in crate::ipc::sem) fn new(kern_ipc_perm: IpcPerm, sems: Vec) -> Self { KernelSemSet { - undo_groups: Vec::new(), + undo_groups: SemUndoRegistry::default(), kern_ipc_perm, sems, sem_otime: 0, @@ -235,13 +196,11 @@ impl KernelSemSet { Ok(vals) } pub(in crate::ipc::sem) fn unregister_undo_group(&mut self, group: &Arc) { - let target = Arc::downgrade(group); - self.undo_groups - .retain(|entry| !entry.group().ptr_eq(&target)); + self.undo_groups.unregister(group); } pub(in crate::ipc::sem) fn retire_undo_records(&mut self, id: SemId) { // Removed set owns all disposal until the caller releases the manager lock. - for association in &mut self.undo_groups { + for association in self.undo_groups.iter_mut() { if let Some(group) = association.group().upgrade() { let record = group.take_record(id); *association = SemUndoAssociation::Retired { diff --git a/kernel/src/ipc/sem/set/tests.rs b/kernel/src/ipc/sem/set/tests.rs index e4e621d04a..bb3ed18c16 100644 --- a/kernel/src/ipc/sem/set/tests.rs +++ b/kernel/src/ipc/sem/set/tests.rs @@ -258,23 +258,28 @@ fn undo_registry_shrink_releases_empty_and_rechecks_concurrent_growth() { let semid = insert_test_set(&mut manager, SemKey::new(152), &[0]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); let set = manager.get_by_semid_checked_mut(semid).unwrap(); - set.undo_groups.reserve_exact(64); - set.undo_groups - .push(SemUndoAssociation::Group(Arc::downgrade(&group))); - let mut spare = Vec::new(); - let mut retired = Vec::new(); + set.undo_groups.prepare(64).unwrap(); + set.ensure_undo_group_registered_prepared(&group, &mut SemUndoRegistry::default()) + .unwrap(); + let mut spare = SemUndoRegistry::default(); + let mut retired = SemUndoRegistry::default(); let needed = set.shrink_undo_registry_prepared(&mut spare, &mut retired); assert_eq!(needed, 4); - spare.try_reserve_exact(needed).unwrap(); + spare.prepare(needed).unwrap(); // Simulate new associations arriving during unlocked preparation. + let mut owners = Vec::new(); for _ in 0..8 { - set.undo_groups - .push(SemUndoAssociation::Group(Arc::downgrade(&group))); + let owner = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); + set.ensure_undo_group_registered_prepared(&owner, &mut SemUndoRegistry::default()) + .unwrap(); + owners.push(owner); } assert!(set.shrink_undo_registry_prepared(&mut spare, &mut retired) > 0); assert_eq!(set.undo_groups.len(), 9); assert_eq!(set.undo_groups.capacity(), 64); - set.undo_groups.truncate(1); + for owner in &owners { + set.unregister_undo_group(owner); + } assert_eq!( set.shrink_undo_registry_prepared(&mut spare, &mut retired), 0 @@ -284,7 +289,7 @@ fn undo_registry_shrink_releases_empty_and_rechecks_concurrent_growth() { assert_eq!(spare.capacity(), 64); // The spare is already allocated when another owner empties the set. // It must not be installed back into the empty registry. - set.undo_groups.clear(); + set.unregister_undo_group(&group); assert_eq!( set.shrink_undo_registry_prepared(&mut spare, &mut retired), 0 @@ -293,12 +298,51 @@ fn undo_registry_shrink_releases_empty_and_rechecks_concurrent_growth() { assert_eq!(retired.capacity(), 4); } +#[test] +fn indexed_registry_removal_preserves_moved_groups_and_allows_reregistration() { + let mut registry = SemUndoRegistry::default(); + registry.prepare(16).unwrap(); + let mut spare = SemUndoRegistry::default(); + let owners: Vec<_> = (0..6) + .map(|_| SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap()) + .collect(); + for owner in &owners { + registry.register_prepared(owner, &mut spare).unwrap(); + } + // First removal moves the tail into slot zero; then remove a middle slot + // and the current tail. Duplicate/missing operations must remain harmless. + for index in [0, 2, 3] { + registry.unregister(&owners[index]); + registry.unregister(&owners[index]); + } + assert_eq!(registry.len(), 3); + for index in [1, 4, 5] { + registry + .register_prepared(&owners[index], &mut spare) + .unwrap(); + assert_eq!(registry.len(), 3); + assert!(registry + .iter() + .any(|entry| { entry.group().ptr_eq(&Arc::downgrade(&owners[index])) })); + } + for index in [0, 2, 3] { + registry + .register_prepared(&owners[index], &mut spare) + .unwrap(); + } + assert_eq!(registry.len(), owners.len()); + for owner in &owners { + registry.unregister(owner); + } + assert!(registry.is_empty()); +} + #[test] fn prepared_registry_growth_rechecks_registration_and_capacity() { let mut manager = SemManager::new(); let semid = insert_test_set(&mut manager, SemKey::new(151), &[1]); let group = SemUndoGroup::new_for_test_bound_to(&INIT_IPC_NAMESPACE).unwrap(); - let mut spare = Vec::new(); + let mut spare = SemUndoRegistry::default(); let capacity = manager .get_by_semid_checked_mut(semid) .unwrap() @@ -310,7 +354,7 @@ fn prepared_registry_growth_rechecks_registration_and_capacity() { .unwrap() .undo_groups .is_empty()); - spare.try_reserve(capacity).unwrap(); + spare.prepare(capacity).unwrap(); // Simulate other first-time groups consuming the prepared capacity // while this caller has dropped the manager lock. @@ -332,7 +376,7 @@ fn prepared_registry_growth_rechecks_registration_and_capacity() { .ensure_undo_group_registered_prepared(&group, &mut spare) .unwrap_err(); assert!(!manager.undo_registry_contains_for_test(&group)); - spare.try_reserve(capacity).unwrap(); + spare.prepare(capacity).unwrap(); manager .get_by_semid_checked_mut(semid) .unwrap() @@ -359,7 +403,7 @@ fn prepared_registry_growth_rechecks_registration_and_capacity() { } // A concurrent CLONE_SYSVSEM sharer already registered this group. - let mut empty_spare = Vec::new(); + let mut empty_spare = SemUndoRegistry::default(); manager .get_by_semid_checked_mut(semid) .unwrap() @@ -479,22 +523,16 @@ fn stale_weak_entries_are_compacted_without_losing_live_group() { let live = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); let candidate = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); let stale = SemUndoGroup::new_for_test_bound_to(&ipc_ns).unwrap(); - let stale_weak = Arc::downgrade(&stale); - drop(stale); - let mut manager = ipc_ns.sem.lock(); let semid = insert_test_set(&mut manager, SemKey::new(51), &[1]); - manager - .get_by_semid_checked_mut(semid) - .unwrap() - .undo_groups - .push(SemUndoAssociation::Group(stale_weak)); + manager.ensure_undo_group_registered(&stale, semid).unwrap(); manager.ensure_undo_group_registered(&live, semid).unwrap(); + drop(stale); manager .get_by_semid_checked_mut(semid) .unwrap() .undo_groups - .shrink_to_fit(); + .compact(); manager .prepare_undo_record_and_registry_for_test(&candidate, semid) diff --git a/kernel/src/ipc/sem/set/undo_registry.rs b/kernel/src/ipc/sem/set/undo_registry.rs new file mode 100644 index 0000000000..788a21cb23 --- /dev/null +++ b/kernel/src/ipc/sem/set/undo_registry.rs @@ -0,0 +1,148 @@ +//! Set-local undo identities and their dense association slots. +use super::{SemUndoAssociation, SemUndoGroup}; +use alloc::{sync::Arc, vec::Vec}; +use hashbrown::HashMap; +use system_error::SystemError; + +#[derive(Debug, Default)] +pub(in crate::ipc::sem) struct SemUndoRegistry { + entries: Vec, + // Each live entry owns a Weak that pins the allocation used as its key. + // The address cannot be reused until both the entry and index are removed. + index: HashMap, +} + +impl SemUndoRegistry { + pub(super) fn len(&self) -> usize { + self.entries.len() + } + + pub(in crate::ipc::sem) fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + pub(super) fn capacity(&self) -> usize { + self.entries.capacity() + } + + fn can_hold(&self, required: usize) -> bool { + self.entries.capacity() >= required && self.index.capacity() >= required + } + + /// Prepare empty spare storage outside the manager lock. + pub(in crate::ipc::sem) fn prepare(&mut self, required: usize) -> Result<(), SystemError> { + debug_assert!(self.is_empty() && self.index.is_empty()); + self.entries + .try_reserve_exact(required) + .map_err(|_| SystemError::ENOMEM)?; + self.index + .try_reserve(required) + .map_err(|_| SystemError::ENOMEM)?; + Ok(()) + } + + pub(super) fn iter(&self) -> core::slice::Iter<'_, SemUndoAssociation> { + self.entries.iter() + } + + /// Only RMID may turn live slots into deferred cleanup, after removing the set. + pub(super) fn iter_mut(&mut self) -> core::slice::IterMut<'_, SemUndoAssociation> { + self.entries.iter_mut() + } + + fn push_prepared(&mut self, association: SemUndoAssociation) { + assert!(self.can_hold(self.len() + 1)); + let key = association.group().as_ptr() as usize; + debug_assert!(!self.index.contains_key(&key)); + self.index.insert(key, self.len()); + self.entries.push(association); + } + + fn remove(&mut self, key: usize) { + let Some(slot) = self.index.remove(&key) else { + return; + }; + let removed = self.entries.swap_remove(slot); + if let Some(moved) = self.entries.get(slot) { + *self + .index + .get_mut(&(moved.group().as_ptr() as usize)) + .expect("live undo association is indexed") = slot; + } + drop(removed); + } + + pub(super) fn compact(&mut self) { + let mut slot = 0; + while let Some(entry) = self.entries.get(slot) { + if entry.group().strong_count() == 0 { + self.remove(entry.group().as_ptr() as usize); + } else { + slot += 1; + } + } + } + + fn install_prepared(&mut self, spare: &mut Self) { + assert!(spare.is_empty() && spare.can_hold(self.len())); + for entry in self.entries.drain(..) { + spare.push_prepared(entry); + } + self.index.clear(); + core::mem::swap(self, spare); + } + + pub(super) fn register_prepared( + &mut self, + group: &Arc, + spare: &mut Self, + ) -> Result<(), usize> { + debug_assert!(spare.is_empty()); + if self.index.contains_key(&(Arc::as_ptr(group) as usize)) { + return Ok(()); + } + if !self.can_hold(self.len() + 1) { + self.compact(); + } + if !self.can_hold(self.len() + 1) { + if !spare.can_hold(self.len() + 1) { + // Rebuild at the dense capacity for hash tombstones; grow + // geometrically only when the dense storage is actually full. + return Err(if self.len() == self.capacity() { + self.len().saturating_mul(2).max(4) + } else { + self.capacity() + }); + } + self.install_prepared(spare); + } + self.push_prepared(SemUndoAssociation::Group(Arc::downgrade(group))); + Ok(()) + } + + pub(super) fn unregister(&mut self, group: &Arc) { + self.remove(Arc::as_ptr(group) as usize); + } + + /// Recheck unlocked preparation; return the next capacity request, if any. + /// Both replaced allocations remain caller-owned for unlocked disposal. + pub(super) fn shrink_prepared(&mut self, spare: &mut Self, retired: &mut Self) -> usize { + debug_assert!(spare.is_empty()); + debug_assert!(retired.is_empty() && retired.capacity() == 0); + let len = self.len(); + if len == 0 { + core::mem::swap(self, retired); + return 0; + } + if self.capacity() <= 4 || len > self.capacity() / 4 { + return 0; + } + if !spare.can_hold(len) { + return len.saturating_mul(2).max(4); + } + if spare.capacity() < self.capacity() { + self.install_prepared(spare); + } + 0 + } +} From a47366304c417852bf77b9a160ca59d50668dbf6 Mon Sep 17 00:00:00 2001 From: longjin Date: Sun, 6 Sep 2026 17:09:19 +0000 Subject: [PATCH 34/34] fix(ci): match syscall boot markers literally The rcS marker contains brackets that were incorrectly interpreted as a regex character class, causing the monitor to misclassify a system that had already entered userspace. Add host tests executing the actual predicate against literal, binary, CRLF, missing and misleading log inputs. Keep existing timeout and failure policies unchanged. Signed-off-by: longjin --- .../syscall/gvisor/monitor_test_results.sh | 4 +- .../gvisor/test_monitor_test_results.py | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100755 user/apps/tests/syscall/gvisor/test_monitor_test_results.py diff --git a/user/apps/tests/syscall/gvisor/monitor_test_results.sh b/user/apps/tests/syscall/gvisor/monitor_test_results.sh index 958ebaf5c7..8f52b836c1 100755 --- a/user/apps/tests/syscall/gvisor/monitor_test_results.sh +++ b/user/apps/tests/syscall/gvisor/monitor_test_results.sh @@ -123,8 +123,8 @@ check_qemu_alive() { # 检查系统是否已启动 check_boot_complete() { - [ -f "$SERIAL_FILE" ] && (grep -aq "[rcS] Running system init script..." "$SERIAL_FILE" 2>/dev/null || \ - grep -aq "开始运行gvisor系统调用测试" "$SERIAL_FILE" 2>/dev/null) + [ -f "$SERIAL_FILE" ] && (grep -aFq "[rcS] Running system init script..." "$SERIAL_FILE" 2>/dev/null || \ + grep -aFq "开始运行gvisor系统调用测试" "$SERIAL_FILE" 2>/dev/null) } # 检查测试是否已开始执行 diff --git a/user/apps/tests/syscall/gvisor/test_monitor_test_results.py b/user/apps/tests/syscall/gvisor/test_monitor_test_results.py new file mode 100755 index 0000000000..c7c2c96c2a --- /dev/null +++ b/user/apps/tests/syscall/gvisor/test_monitor_test_results.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Host regression tests for the monitor's actual boot-marker predicate. + +Run: python3 user/apps/tests/syscall/gvisor/test_monitor_test_results.py +Only the function under test is executed: sourcing the monitor would start its +process-management loop and could terminate an unrelated QEMU instance. +""" + +from pathlib import Path +import re +import subprocess +import tempfile +import unittest + + +class BootMarkerTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + source = Path(__file__).with_name("monitor_test_results.sh").read_text() + match = re.search(r"^check_boot_complete\(\) \{\n.*?^\}", source, re.M | re.S) + if match is None: + raise AssertionError("actual check_boot_complete function not found") + cls.function = match.group(0) + + def check_log(self, contents, expected): + with tempfile.TemporaryDirectory(prefix="gvisor-monitor-test-") as directory: + path = Path(directory) / "serial_opt.txt" + if contents is not None: + path.write_bytes(contents) + result = subprocess.run( + ["sh", "-c", self.function + '\nSERIAL_FILE="$1"\ncheck_boot_complete', + "monitor-fixture", str(path)], + capture_output=True, timeout=5, + ) + self.assertEqual(result.returncode, 0 if expected else 1, result.stderr) + + def test_real_rcs_marker(self): + self.check_log(b"[rcS] Running system init script...\n", True) + + def test_rcs_marker_with_nul_and_crlf(self): + self.check_log(b"early boot\x00\r\n[rcS] Running system init script...\r\n", True) + + def test_banner_alone_is_not_a_boot_marker(self): + self.check_log(b"DragonOS - Lightweight Cloud-Native Kernel\r\n", False) + + def test_regex_lookalike_is_not_the_literal_marker(self): + self.check_log(b"r Running system init scriptXYZ\n", False) + + def test_gvisor_start_marker(self): + self.check_log("开始运行gvisor系统调用测试\r\n".encode(), True) + + def test_missing_serial_file(self): + self.check_log(None, False) + + +if __name__ == "__main__": + unittest.main(verbosity=2)