Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
44b822c
refactor(ipc): extract generic SysV IPC permission module
mistcoversmyeyes Aug 12, 2026
22dcdd8
feat(ipc): implement System V semaphore syscalls
mistcoversmyeyes Aug 12, 2026
7dabc9e
test(ipc): add SysV semaphore dunitest suite
mistcoversmyeyes Aug 12, 2026
a807b3c
style(ipc): satisfy kernel formatting checks
mistcoversmyeyes Aug 17, 2026
49075ea
style(ipc): translate SysV semaphore comments to English
mistcoversmyeyes Aug 23, 2026
0650199
fix(ipc): make IPC_SET permission updates atomic
mistcoversmyeyes Aug 30, 2026
4d4346c
fix(ipc): make semaphore set allocation fallible
mistcoversmyeyes Aug 30, 2026
1257bce
feat(ipc): implement SysV SEM_UNDO lifecycle
mistcoversmyeyes Sep 4, 2026
7e9b503
fix(ipc): mark allocated IDs in release builds
mistcoversmyeyes Sep 5, 2026
8678779
fix(ipc): prioritize const semaphore waiters
mistcoversmyeyes Sep 5, 2026
26da368
fix(process): preserve reaped PID identity
mistcoversmyeyes Sep 5, 2026
73467d1
fix(process): preserve exec locking in prepared namespace publication
fslongjin Sep 5, 2026
03c0e03
fix(ipc): harden semaphore allocation and Linux syscall semantics
fslongjin Sep 5, 2026
27c67d5
test(fuse): handle background writeback in direct-drain assertions
fslongjin Sep 5, 2026
b3f637c
fix(ipc): make bulk semaphore buffers fallible and prepare queue grow…
fslongjin Sep 5, 2026
dbcd42f
fix(ipc): prepare undo registry growth outside the namespace lock
fslongjin Sep 5, 2026
e150302
fix(ipc): scope semaphore undo cleanup and defer wakeups
fslongjin Sep 5, 2026
d313722
fix(ipc): reclaim unused semaphore undo registry storage
fslongjin Sep 5, 2026
72b3749
fix(ipc): skip empty unshare installs and cache the maximum ID index
fslongjin Sep 5, 2026
8d1e231
fix(namespace): make prepared fs copies fallible
fslongjin Sep 5, 2026
2c64d68
fix(ipc): prepare semaphore storage outside the lock and unlink waite…
fslongjin Sep 5, 2026
b2717d7
fix(dunitest): use real deadlines when collecting child output
fslongjin Sep 5, 2026
dabd730
fix(ipc): reuse live undo records and replay one set at a time
fslongjin Sep 5, 2026
277351e
fix(ipc): defer semaphore removal reclamation until after unlock
fslongjin Sep 5, 2026
26adf2c
fix(ipc): cache wait counts and retain zero undo records
fslongjin Sep 6, 2026
41ebf5a
docs: remove sem undo implementation plan and design spec
fslongjin Sep 6, 2026
303064c
refactor(ipc): separate semaphore state, operations and undo lifecycle
fslongjin Sep 6, 2026
a455a73
perf(ipc): preindex semaphore scratch and reclaim undo record storage
fslongjin Sep 6, 2026
55a48ef
fix(ipc): correct semaphore ABI and index undo records
fslongjin Sep 6, 2026
506bf42
fix(ipc): replay detached undo outside fs publication barrier
fslongjin Sep 6, 2026
0424b7f
fix(ipc): skip semaphore queue scans when waiter state is unchanged
fslongjin Sep 6, 2026
3e3843d
test(ipc): preserve creator group access after semaphore IPC_SET
fslongjin Sep 6, 2026
fa5cdf9
perf(ipc): index semaphore set undo associations
fslongjin Sep 6, 2026
a473663
fix(ci): match syscall boot markers literally
fslongjin Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions kernel/crates/rust-slabmalloc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 8 additions & 2 deletions kernel/crates/rust-slabmalloc/src/pages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,13 @@ pub struct ObjectPage<'a> {
}
impl<'a> ObjectPage<'a> {
pub fn new() -> Box<ObjectPage<'a>> {
let mut page = Box::<ObjectPage<'a>>::new_uninit();
Self::try_new()
.unwrap_or_else(|_| alloc::alloc::handle_alloc_error(Layout::new::<ObjectPage<'a>>()))
}

/// Allocate a slab page without aborting when its backing allocation fails.
pub fn try_new() -> Result<Box<ObjectPage<'a>>, alloc::alloc::AllocError> {
let mut page = Box::<ObjectPage<'a>>::try_new_uninit()?;
unsafe {
// The data area is intentionally uninitialized object storage. It
// is wrapped in MaybeUninit so constructing ObjectPage is sound;
Expand All @@ -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())
}
}
}
Expand Down
51 changes: 51 additions & 0 deletions kernel/crates/rust-slabmalloc/tests/fallible_page.rs
Original file line number Diff line number Diff line change
@@ -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<bool> = const { Cell::new(false) };
}

struct PageFailureAllocator;

unsafe impl GlobalAlloc for PageFailureAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let fail = layout == Layout::new::<ObjectPage<'static>>()
&& 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::<ObjectPage<'_>>(), 0);
drop(page);

// Existing callers still use the same metadata initialization through new().
drop(ObjectPage::new());
}
133 changes: 125 additions & 8 deletions kernel/src/ipc/id.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use ida::IdAllocator;
use bitmap::{traits::BitMapOps, StaticBitmap};
use system_error::SystemError;

/// Linux-compatible SysV IPC id allocator.
Expand All @@ -7,39 +7,58 @@ 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<const CAPACITY: usize, const WORDS: usize> {
used: StaticBitmap<CAPACITY, WORDS>,
max_ids: usize,
next_idx: usize,
seq: usize,
last_idx: Option<usize>,
/// Highest currently allocated index, not the cyclic allocation cursor.
max_used_idx: Option<usize>,
}

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,
pub idx: usize,
pub seq: usize,
}

impl IpcIdAllocator {
impl<const CAPACITY: usize, const WORDS: usize> FixedIpcIdAllocator<CAPACITY, WORDS> {
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<Self, SystemError> {
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::<CAPACITY>()
{
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<IpcId, SystemError> {
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;
Expand All @@ -57,8 +76,33 @@ impl IpcIdAllocator {
})
}

fn find_free_idx(&self) -> Option<usize> {
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<usize> {
self.max_used_idx
}

pub fn decode(raw: usize) -> Result<IpcId, SystemError> {
Expand All @@ -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);
}
}
Loading
Loading