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/id.rs b/kernel/src/ipc/id.rs index b11d93b3bb..d89e01c60c 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,19 @@ 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, + /// Highest currently allocated index, not the cyclic allocation cursor. + max_used_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 +27,38 @@ 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, + max_used_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)?; + let was_used = self.used.set(idx, true); + debug_assert_eq!(was_used, Some(false)); + self.max_used_idx = Some(self.max_used_idx.map_or(idx, |max| max.max(idx))); + 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 +76,33 @@ 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); + if self.max_used_idx == Some(idx) { + self.max_used_idx = self.used.prev_index(idx); + } + } + } + + /// Constant-time query; only removing the maximum searches the existing bitmap. + pub fn max_used_index(&self) -> Option { + self.max_used_idx } pub fn decode(raw: usize) -> Result { @@ -76,3 +120,76 @@ 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); + } + + #[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/ipc_perm.rs b/kernel/src/ipc/ipc_perm.rs new file mode 100644 index 0000000000..0b3c08383a --- /dev/null +++ b/kernel/src/ipc/ipc_perm.rs @@ -0,0 +1,271 @@ +// 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> { + 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(()) + } +} + +/// 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..a03bfaaa74 100644 --- a/kernel/src/ipc/mod.rs +++ b/kernel/src/ipc/mod.rs @@ -1,7 +1,10 @@ pub mod generic_signal; pub mod id; +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/abi.rs b/kernel/src/ipc/sem/abi.rs new file mode 100644 index 0000000000..63752b563a --- /dev/null +++ b/kernel/src/ipc/sem/abi.rs @@ -0,0 +1,209 @@ +// 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, +} + +/// 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` + pub sem_otime: i64, + #[cfg(target_arch = "x86_64")] + _otime_padding: u64, + /// Time of the last metadata change + pub sem_ctime: 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)] +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..caa861f780 --- /dev/null +++ b/kernel/src/ipc/sem/manager/control.rs @@ -0,0 +1,179 @@ +//! 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> { + // 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) { + return Err(SystemError::ERANGE); + } + + 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(()) + } + + /// # 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..605f473403 --- /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, SemUndoRegistry, 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 = 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 { + return; + }; + set.shrink_undo_registry_prepared(&mut spare, &mut retired) + }; + if needed == 0 || spare.prepare(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..912bdffaae --- /dev/null +++ b/kernel/src/ipc/sem/manager/test_support.rs @@ -0,0 +1,109 @@ +use super::*; +use crate::ipc::sem::set::test_support::test_perm; +use crate::ipc::sem::set::SemUndoRegistry; +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 = SemUndoRegistry::default(); + if let Err(capacity) = set.ensure_undo_group_registered_prepared(group, &mut spare) { + spare.prepare(capacity)?; + 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..e443f7ff12 --- /dev/null +++ b/kernel/src/ipc/sem/manager/tests.rs @@ -0,0 +1,177 @@ +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 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(); + 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..a802f5e257 --- /dev/null +++ b/kernel/src/ipc/sem/operation.rs @@ -0,0 +1,323 @@ +//! 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; +use system_error::SystemError; + +use super::{ + abi::*, + manager::SemManager, + set::{ + SemAttempt, SemBlockedOp, SemQueueEntry, SemUndoRegistry, 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 + }; + // 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 { + Some( + Arc::try_new(SemQueueEntry::new_prepared( + SemQueueEntry::prepare_sops(sops)?, + pid.clone(), + None, + None, + waker.clone(), + SemopScratch::try_new(sops)?, + SemBlockedOp { + semnum: 0, + wait_type: SemWaitType::Zero, + nowait: false, + }, + )) + .map_err(|_| SystemError::ENOMEM)?, + ) + }; + + let mut wakes = SemWakeBatch::default(); + 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.prepare(registry_capacity_needed)?; + 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)?; + 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)?, + 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(completed @ SemAttempt::Completed { .. }) => { + PreparedSemUndoRecordAction::Complete(Ok(completed)) + } + Ok(SemAttempt::Blocked(blocker)) => { + PreparedSemUndoRecordAction::Keep(Ok(SemAttempt::Blocked(blocker))) + } + Err(error) => PreparedSemUndoRecordAction::Keep(Err(error)), + } + })?; + *record_slot = kept_record; + match outcome? { + SemAttempt::Completed { + waiter_state_changed, + } => { + drop(record_slot); + if waiter_state_changed { + 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); + } + 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 { + waiter_state_changed, + } => { + if waiter_state_changed { + 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..2a82d01ce5 --- /dev/null +++ b/kernel/src/ipc/sem/set/mod.rs @@ -0,0 +1,294 @@ +//! 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; +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)] +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: SemUndoRegistry, + /// 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 { + /// 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.iter() { + 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. + /// 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 SemUndoRegistry, + ) -> Result<(), usize> { + self.undo_groups.register_prepared(group, spare) + } + + fn compact_undo_registry(&mut self) { + self.undo_groups.compact(); + } + + pub(in crate::ipc::sem) fn shrink_undo_registry_prepared( + &mut self, + spare: &mut SemUndoRegistry, + retired: &mut SemUndoRegistry, + ) -> usize { + self.undo_groups.shrink_prepared(spare, retired) + } + + 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: SemUndoRegistry::default(), + 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) { + 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 self.undo_groups.iter_mut() { + 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..88e90831cd --- /dev/null +++ b/kernel/src/ipc/sem/set/operation.rs @@ -0,0 +1,243 @@ +//! 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, + initialized: bool, + initial_val: i32, + virtual_val: i32, + initial_adj: i16, + virtual_adj: i16, +} + +#[derive(Debug)] +pub(in crate::ipc::sem) struct SemopScratch { + entries: Vec, + op_slots: Vec, +} + +impl SemopScratch { + /// 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(sops.len()) + .map_err(|_| SystemError::ENOMEM)?; + 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) { + 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> { + 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 !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; + } + Ok(entry) + } +} + +/// 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 { + if sops.len() != scratch.op_slots.len() { + return Err(SystemError::EINVAL); + } + scratch.clear(); + + 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, op_index, 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 waiter_state_changed = false; + for entry in scratch.entries.iter().take(simulation.entry_count) { + let sem = &mut set.sems[entry.semnum]; + 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; + waiter_state_changed + } +} + +#[derive(Debug)] +pub(in crate::ipc::sem) enum SemAttempt { + Completed { waiter_state_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 { + waiter_state_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..d30eba3fdf --- /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(&[plain_sop(0, 0); 2]).unwrap(); + assert!(matches!( + set.try_apply( + &[plain_sop(0, -1), plain_sop(0, 1)], + None, + None, + &mut scratch + ), + Ok(SemAttempt::Completed { + waiter_state_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(&[plain_sop(0, 0); 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 { + waiter_state_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(&[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| { + 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(&[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| { + 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(&[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| { + 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(&[plain_sop(0, 0); 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(&[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(); + 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(&[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(); + 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(&[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(); + 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(&[plain_sop(0, 0); 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_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(&[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::EINVAL) + )); +} diff --git a/kernel/src/ipc/sem/set/queue.rs b/kernel/src/ipc/sem/set/queue.rs new file mode 100644 index 0000000000..8e7efc612d --- /dev/null +++ b/kernel/src/ipc/sem/set/queue.rs @@ -0,0 +1,572 @@ +//! 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).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 { + waiter_state_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 { + waiter_state_changed, + }) => Ok(Some(waiter_state_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..11d6d4faa1 --- /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(&[plain_sop(0, 0); 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..bb3ed18c16 --- /dev/null +++ b/kernel/src/ipc/sem/set/tests.rs @@ -0,0 +1,875 @@ +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.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.prepare(needed).unwrap(); + // Simulate new associations arriving during unlocked preparation. + let mut owners = Vec::new(); + for _ in 0..8 { + 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); + for owner in &owners { + set.unregister_undo_group(owner); + } + 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.unregister_undo_group(&group); + 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 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 = SemUndoRegistry::default(); + 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.prepare(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.prepare(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 = SemUndoRegistry::default(); + 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(&[plain_sop(0, 0); 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 mut manager = ipc_ns.sem.lock(); + let semid = insert_test_set(&mut manager, SemKey::new(51), &[1]); + 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 + .compact(); + + 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/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 + } +} diff --git a/kernel/src/ipc/sem_undo/lifecycle.rs b/kernel/src/ipc/sem_undo/lifecycle.rs new file mode 100644 index 0000000000..a2a7470927 --- /dev/null +++ b/kernel/src/ipc/sem_undo/lifecycle.rs @@ -0,0 +1,188 @@ +//! Task attachment ownership and explicit, per-set exit replay. +use super::SemUndoGroup; +use crate::{ + ipc::sem::{SemManager, SemWakeBatch}, + process::{ + namespace::ipc_namespace::IpcNamespace, + pid::{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) { + if let Some(replay) = PendingSemUndoReplay::detach(pcb) { + replay.replay(); + } +} + +/// 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>, +} + +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)) + } + + 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; + }; + + 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(); + + 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..6580466ab6 --- /dev/null +++ b/kernel/src/ipc/sem_undo/mod.rs @@ -0,0 +1,366 @@ +//! 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; +mod storage; +pub use lifecycle::SemUndoAttachment; +pub(crate) use lifecycle::{ + detach_sem_undo, PendingSemUndoReplay, UnpublishedSemUndoAttachmentGuard, +}; +use record::PendingSemUndoRecordReservation; +pub(crate) use record::{PreparedSemUndoRecord, PreparedSemUndoRecordAction, SemUndoRecord}; +use storage::UndoRecords; + +#[derive(Debug)] +pub struct SemUndoGroup { + ipc_ns: Weak, + inner: SpinLock, +} + +#[derive(Debug)] +struct SemUndoGroupState { + task_owners: usize, + records: UndoRecords, + reserved_records: usize, + phase: UndoPhase, + #[cfg(test)] + replay_count: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum UndoPhase { + Active, + Retired, + Replaying, +} + +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: UndoRecords::default(), + 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) -> UndoRecords { + 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 = UndoRecords::default(); + + 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.get(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.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); + reserved_storage.prepare(target)?; + continue; + } + state + .records + .install_prepared(&mut reserved_storage, required_capacity); + } + + 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); + } + + if let Some(existing) = state.records.get(record.semid) { + if existing.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(state.records.get_mut(record.semid).unwrap()) { + 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.can_hold(state.records.len() + 1) { + 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_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))), + } + } + + 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.get_mut(semid).map(f) + } + + pub(crate) fn take_record(&self, semid: SemId) -> Option { + let mut state = self.inner.lock_irqsave(); + 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 = 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.prepare(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 UndoRecords, + retired: &mut UndoRecords, + ) -> 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.can_hold(required) { + return required.saturating_mul(2).max(4); + } + if spare.capacity() < self.records.capacity() { + self.records.install_prepared(spare, required); + } + 0 + } +} + +#[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/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 new file mode 100644 index 0000000000..51494c5cfb --- /dev/null +++ b/kernel/src/ipc/sem_undo/test_support.rs @@ -0,0 +1,117 @@ +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 + .get(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(); + 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(), + )); + } + + #[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 = UndoRecords::default(); + state.records.prepare(capacity).unwrap(); + 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); +} diff --git a/kernel/src/ipc/shm.rs b/kernel/src/ipc/shm.rs index 430292171a..69028dcae5 100644 --- a/kernel/src/ipc/shm.rs +++ b/kernel/src/ipc/shm.rs @@ -8,15 +8,15 @@ use crate::{ InodeId, }, }, - ipc::id::IpcIdAllocator, + ipc::{ + id::{IpcIdAllocator, ShmIpcIdAllocator}, + 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}, - }, + namespace::{ipc_namespace::IpcNamespace, user_namespace::UserNamespace}, resource::RLimitID, ProcessManager, RawPid, }, @@ -34,7 +34,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); @@ -356,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表 @@ -378,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, @@ -456,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); @@ -559,75 +568,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 +602,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 +637,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 +749,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 +794,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 +806,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 +822,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 +854,9 @@ 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 +874,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 +1037,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 +1192,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 mode(&self) -> u32 { + self.mode.bits() } - fn to_posix(&self, user_ns: &Arc) -> Result { - let key = self.key.data() as u32 as i32; + fn key(&self) -> usize { + self.key.data() + } - 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 seq(&self) -> usize { + self.seq } } @@ -1413,37 +1338,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, -} 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..aa8864c114 --- /dev/null +++ b/kernel/src/ipc/syscall/sys_semctl.rs @@ -0,0 +1,198 @@ +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, SemWakeBatch}, + 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 syscall: control a semaphore set +/// +/// ## Parameters +/// +/// - `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 +/// +/// 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, + cmd: SemCtlCmd, + arg: usize, + from_user: bool, +) -> 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 + 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) + } + // 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(); + 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) + } + // Set permissions + 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) + } + // Remove the semaphore set + SemCtlCmd::IpcRmid => { + let removed = { + let mut guard = ipcns.sem.lock(); + guard.ipc_rmid(semid, &mut wakes)? + }; + wakes.wake_all(); + removed.reclaim_removed_undo_storage(); + drop(removed); + 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) + } + // 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, &mut wakes)?; + Ok(0) + } + // Get values of all semaphores in the set + 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) + } + // Set values of all semaphores in the set + SemCtlCmd::SetAll => { + let token = { + let guard = ipcns.sem.lock(); + guard.prepare_setall(semid)? + }; + 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::(), + 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, &mut wakes)?; + Ok(0) + } + // Invalid command + SemCtlCmd::Default => Err(SystemError::EINVAL), + } +} + +impl SysSemctlHandle { + #[inline(always)] + fn semid(args: &[usize]) -> SemId { + SemId::new(args[0] as u32 as usize) + } + + #[inline(always)] + fn semnum(args: &[usize]) -> usize { + args[1] as u32 as usize + } + + #[inline(always)] + fn cmd(args: &[usize]) -> SemCtlCmd { + SemCtlCmd::from(args[2] as u32 as usize) + } + + #[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] as i32) < 0 { + 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..838d668e4f --- /dev/null +++ b/kernel/src/ipc/syscall/sys_semget.rs @@ -0,0 +1,52 @@ +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, SemManager}, + process::ProcessManager, + syscall::table::Syscall, +}; +use syscall_table_macros::declare_syscall; +use system_error::SystemError; + +pub struct SysSemgetHandle; + +/// # SYS_SEMGET syscall: create or get a semaphore set +/// +/// ## Parameters +/// +/// - `key`: semaphore-set key +/// - `nsems`: number of semaphores in the set +/// - `semflg`: flags (IPC_CREAT/IPC_EXCL/permission bits) +/// +/// ## Return value +/// +/// On success: semaphore set ID. +/// On failure: error code +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] as i32; + if nsems < 0 { + return Err(SystemError::EINVAL); + } + let semflg = SemFlags::from_bits_truncate(args[2] as u32); + let ipcns = ProcessManager::current_ipcns(); + SemManager::semget(&ipcns, key, nsems as usize, 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..09d90132de --- /dev/null +++ b/kernel/src/ipc/syscall/sys_semop.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_SEMOP, syscall::table::Syscall}; +use syscall_table_macros::declare_syscall; +use system_error::SystemError; + +use super::sys_semtimedop::do_user_semtimedop; + +pub struct SysSemopHandle; + +/// # SYS_SEMOP syscall: atomically execute a group of semaphore operations indefinitely +/// +/// Shares its implementation with SYS_SEMTIMEDOP with a NULL timeout. +/// +/// ## Parameters +/// +/// - `semid`: semaphore set ID +/// - `sops`: userspace `sembuf` array pointer +/// - `nsops`: number of operations +/// +/// ## Return value +/// +/// On success: 0. +/// On failure: error code +impl Syscall for SysSemopHandle { + fn num_args(&self) -> usize { + 3 + } + + fn handle(&self, args: &[usize], frame: &mut TrapFrame) -> Result { + do_user_semtimedop( + args[0] as i32, + args[1] as *const u8, + args[2] as u32 as usize, + None, + frame.is_from_user(), + ) + } + + 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..2a4090ad4c --- /dev/null +++ b/kernel/src/ipc/syscall/sys_semtimedop.rs @@ -0,0 +1,156 @@ +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 syscall: atomically execute a group of semaphore operations with an +/// optional timeout +/// +/// ## Parameters +/// +/// - `semid`: semaphore set ID +/// - `sops`: userspace `sembuf` array pointer +/// - `nsops`: number of operations +/// - `timeout`: pointer to `struct timespec`; NULL waits indefinitely +/// +/// ## Return value +/// +/// 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], + 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) +} + +// 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]) -> i32 { + args[0] as i32 + } + + #[inline(always)] + fn sops(args: &[usize]) -> *const u8 { + args[1] as *const u8 + } + + #[inline(always)] + fn nsops(args: &[usize]) -> usize { + args[2] as u32 as usize + } + + #[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 timeout_ptr = Self::timeout(args); + let from_user = frame.is_from_user(); + + 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_user_semtimedop(semid, sops_ptr, nsops, timeout, from_user) + } + + fn entry_format(&self, args: &[usize]) -> Vec { + vec![ + 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)), + ] + } +} + +declare_syscall!(SYS_SEMTIMEDOP, SysSemtimedopHandle); 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/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/ipc_namespace.rs b/kernel/src/process/namespace/ipc_namespace.rs index 9bb126ecd4..298048ccb5 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::{ @@ -7,20 +8,22 @@ 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 semaphore manager + pub sem: SpinLock, } impl NamespaceOps for IpcNamespace { @@ -36,10 +39,11 @@ 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()), }) } - /// 复制/创建 IPC 命名空间 + /// Copy or create an IPC namespace pub fn copy_ipc_ns( &self, clone_flags: &crate::process::fork::CloneFlags, @@ -49,12 +53,13 @@ impl IpcNamespace { if !clone_flags.contains(CloneFlags::CLONE_NEWIPC) { return self.self_ref.upgrade().unwrap(); } - // 创建新的 IPC 命名空间,SHM 空间独立 + // 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(), user_ns, shm: SpinLock::new(ShmManager::new()), + sem: SpinLock::new(SemManager::new()), }) } } diff --git a/kernel/src/process/namespace/nsproxy.rs b/kernel/src/process/namespace/nsproxy.rs index b8a38c1cd6..742852df06 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::PendingSemUndoReplay, 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,150 @@ 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::try_new((*current_fs).clone()).map_err(|_| SystemError::ENOMEM)?); + } + + 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; + 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); + } + 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(()) + } } type ReboundFsPaths = (Arc, Arc); @@ -370,3 +456,120 @@ 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_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(); + 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..8762ae7fe1 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 { @@ -94,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)?; @@ -112,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(); @@ -185,7 +213,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 +234,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 +251,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 +287,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 +296,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 +333,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..56393dd631 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,27 +21,15 @@ 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())?; + 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 操作 @@ -53,6 +39,32 @@ 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, 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, + new_fs, + new_cred, + detach_sysvsem, + fs_refs, + ) + .map(Some) +} + #[inline(always)] fn normalize_unshare_flags(mut flags: CloneFlags) -> CloneFlags { if flags.contains(CloneFlags::CLONE_NEWUSER) { @@ -189,3 +201,77 @@ 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 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(); + 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() + .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() + .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..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,12 +96,27 @@ 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(), }); 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) } @@ -247,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); + } } } } @@ -302,6 +323,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) { @@ -487,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) => { @@ -511,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; } } @@ -524,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, @@ -646,7 +682,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..161d0274e1 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,47 @@ 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)>, + ) { + // 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); + 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 +1102,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/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()); } 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); 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..2dd374a50a --- /dev/null +++ b/user/apps/tests/dunitest/suites/normal/sysv_sem_semantics.cc @@ -0,0 +1,3285 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include + +#include + +#include +#include +#include +#include +#include +#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 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 +#endif +#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 ============ + +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)) {} + + // Adopt an existing 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; +} + +// Wait until the target semaphore has the expected number of waiters, confirming that +// 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) { + 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; + } + 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; + } + } +} + +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 ============ + +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, 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()); + + 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, 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, 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); + 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, 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); + + 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()); + + 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, 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()); + + 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); + + // 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]); + 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"; +} + +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) { + 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: return ERANGE immediately without blocking or modifying values. + 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: succeed immediately + 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 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)); + 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); +} + +// ============ 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)); + + 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(), 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]; + 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); + 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(), old_owner_ready[1], old_owner_release[0]}; + pid_t old_owner = clone(CloneSysvsemUndoChild, stack + sizeof(stack), + CLONE_SYSVSEM | SIGCHLD, &args); + 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); + } + 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(); + 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)) + << "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"; +} + +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; + 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, 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()); + 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); + + 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"; + } +} + +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 ============ + +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) { + // The child blocks on -1 while 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) { + // The child waits for val to become zero. + 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); +} + +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)); + } +} + +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()); + + 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)); + + // 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) { + 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); + // An interrupted blocked operation must return 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, 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()); + 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, 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}) { + 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, RemoveWakesBothWaitQueuesInBulk) { + constexpr int kPairs = 16; + 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); + 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) { + 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"; + } +} + +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; + 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, UndoGroupChurnAcrossPersistentSets) { + constexpr int kSets = 16; + constexpr int kWorkers = 16; + std::vector> 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, 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, 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); + 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) { + // 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) { + 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) { + 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(); +} 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 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)