Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
487 changes: 486 additions & 1 deletion abi/snapshot.json

Large diffs are not rendered by default.

46 changes: 44 additions & 2 deletions crates/kernel/src/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::fd::{FdEntry, FdTable, OpenFileDescRef};
use crate::lock::LockTable;
use crate::memory::{MappedRegion, MemoryLayoutMetadata, MemoryManager};
use crate::ofd::{FileType, OfdTable, OpenFileDesc};
use crate::process::{Process, ProcessState};
use crate::process::{Process, ProcessState, ShmMapping};
use crate::signal::{SignalAction, SignalHandler, SignalState};
use crate::socket::SocketTable;
use crate::terminal::{NCCS, TerminalState, WinSize};
Expand All @@ -40,6 +40,7 @@ const MAX_OFDS: u32 = 65536;
const MAX_ENV_VARS: u32 = 65536;
const MAX_ARGV: u32 = 65536;
const MAX_PATH_LEN: usize = 1048576; // 1 MiB
const MAX_SHM_MAPPINGS: usize = 4096;
const MAX_STRING_LEN: usize = 1048576; // 1 MiB

// ── Writer helper ───────────────────────────────────────────────────────────
Expand Down Expand Up @@ -546,6 +547,14 @@ pub fn serialize_fork_state(proc: &Process, buf: &mut [u8]) -> Result<usize, Err
}
}

// ── SysV shared-memory attachments (v7) ──
w.write_u32(proc.shm_mappings.len() as u32)?;
for mapping in &proc.shm_mappings {
w.write_u32(mapping.addr as u32)?;
w.write_u32(mapping.shmid as u32)?;
w.write_u32(mapping.size as u32)?;
}

// ── Patch total_size ──
let total = w.pos as u32;
w.patch_u32(total_size_offset, total);
Expand Down Expand Up @@ -955,6 +964,22 @@ pub fn deserialize_fork_state(buf: &[u8], child_pid: u32) -> Result<Process, Err
}
}

// ── SysV shared-memory attachments (v7) ──
let mut shm_mappings = Vec::new();
if r.remaining() >= 4 {
let count = r.read_u32()? as usize;
if count > MAX_SHM_MAPPINGS {
return Err(Errno::EINVAL);
}
shm_mappings = Vec::with_capacity(count);
for _ in 0..count {
let addr = r.read_u32()? as usize;
let shmid = r.read_u32()? as i32;
let size = r.read_u32()? as usize;
shm_mappings.push(ShmMapping { addr, shmid, size });
}
}

Ok(Process {
pid: child_pid,
ppid,
Expand Down Expand Up @@ -1016,6 +1041,7 @@ pub fn deserialize_fork_state(buf: &[u8], child_pid: u32) -> Result<Process, Err
// registered as a host display target. fbDOOM doesn't fork
// mid-game; documented limitation in the design doc.
fb_binding: None,
shm_mappings,
fork_count: 0,
})
}
Expand Down Expand Up @@ -1408,6 +1434,7 @@ pub fn deserialize_exec_state(buf: &[u8], pid: u32) -> Result<Process, Errno> {
// exec wipes any prior framebuffer binding — the new program
// must open and mmap /dev/fb0 itself.
fb_binding: None,
shm_mappings: Vec::new(),
// The fork counter exists as a kernel-side regression guardrail.
// Resetting on exec keeps semantics simple: the next spawn-from-this-pid
// test starts from a clean slate. The plan's regression check inspects
Expand Down Expand Up @@ -1555,6 +1582,19 @@ mod tests {
}
}

#[test]
fn test_fork_state_preserves_shm_mappings() {
let mut proc = Process::new(1);
proc.record_shm_mapping(0x20000, 17, 4096);

let mut buf = vec![0u8; 64 * 1024];
let written = serialize_fork_state(&proc, &mut buf).unwrap();
let child = deserialize_fork_state(&buf[..written], 42).unwrap();

assert_eq!(child.shm_mapping_at(0x20000).unwrap().shmid, 17);
assert_eq!(child.shm_mapping_at(0x20000).unwrap().size, 4096);
}

#[test]
fn test_buffer_too_small() {
let proc = Process::new(1);
Expand All @@ -1574,7 +1614,8 @@ mod tests {

#[test]
fn test_exec_roundtrip_default_process() {
let proc = Process::new(1);
let mut proc = Process::new(1);
proc.record_shm_mapping(0x20000, 17, 4096);
let mut buf = vec![0u8; 64 * 1024];
let written = serialize_exec_state(&proc, &mut buf).unwrap();
assert!(written > 12);
Expand All @@ -1584,6 +1625,7 @@ mod tests {
assert_eq!(restored.pid, 1);
assert_eq!(restored.ppid, 0); // default ppid
assert_eq!(restored.signals.pending, 0);
assert!(restored.shm_mappings.is_empty());
}

#[test]
Expand Down
19 changes: 19 additions & 0 deletions crates/kernel/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,17 @@ impl IpcTable {
Ok(seg.segsz)
}

/// Inherit an existing attachment across fork without rechecking
/// permissions. The parent already passed `shmat`; fork only creates
/// another process reference to the same segment.
pub fn shm_attach_inherited(&mut self, shmid: i32, pid: u32) -> Result<(), Errno> {
let seg = self.shm_segments.get_mut(&shmid).ok_or(Errno::EINVAL)?;
seg.nattch += 1;
seg.lpid = pid as i32;
seg.atime = crate::current_time_secs();
Ok(())
}

/// Read a chunk of shared memory segment data into a buffer.
/// Returns bytes written.
pub fn shm_read_chunk(&self, shmid: i32, offset: u32, buf: &mut [u8]) -> Result<u32, Errno> {
Expand Down Expand Up @@ -1446,9 +1457,17 @@ mod tests {
assert_eq!(info.nattch, 1);
assert_eq!(info.lpid, 42);

t.shm_attach_inherited(id, 43).unwrap();
let info = t.shmctl(id, IPC_STAT, 1, 0, 0).unwrap().unwrap();
assert_eq!(info.nattch, 2);
assert_eq!(info.lpid, 43);

// Detach
t.shmdt(id, 42).unwrap();
let info = t.shmctl(id, IPC_STAT, 1, 0, 0).unwrap().unwrap();
assert_eq!(info.nattch, 1);
t.shmdt(id, 43).unwrap();
let info = t.shmctl(id, IPC_STAT, 1, 0, 0).unwrap().unwrap();
assert_eq!(info.nattch, 0);
}

Expand Down
122 changes: 122 additions & 0 deletions crates/kernel/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,20 @@ pub struct FbBinding {
pub fmt: u32,
}

/// Per-process SysV shared-memory attachment.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShmMapping {
pub addr: usize,
pub shmid: i32,
pub size: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostTimerCleanup {
pub cancel_alarm: bool,
pub posix_timer_ids: Vec<usize>,
}

/// Per-thread state within a process.
#[derive(Debug, Clone)]
pub struct ThreadInfo {
Expand Down Expand Up @@ -424,6 +438,9 @@ pub struct Process {
/// Live mmap of `/dev/fb0`, if any. `Some` between successful
/// `mmap` and the matching `munmap`/process-exit/exec.
pub fb_binding: Option<FbBinding>,
/// SysV shared-memory attachments keyed by the process virtual address
/// returned from `shmat`.
pub shm_mappings: Vec<ShmMapping>,
/// Counts how many times this process has called fork() (parent side, on success).
/// Read-only from outside the kernel via `kernel_get_fork_count`.
/// Used as a regression guardrail by the spawn test suite to confirm
Expand Down Expand Up @@ -508,6 +525,7 @@ impl Process {
procfs_bufs: Vec::new(),
has_exec: false,
fb_binding: None,
shm_mappings: Vec::new(),
fork_count: 0,
}
}
Expand Down Expand Up @@ -588,6 +606,47 @@ impl Process {
self.threads.iter_mut().find(|t| t.tid == tid)
}

/// Record or replace a SysV shared-memory attachment for an address.
pub fn record_shm_mapping(&mut self, addr: usize, shmid: i32, size: usize) {
if let Some(mapping) = self.shm_mappings.iter_mut().find(|m| m.addr == addr) {
*mapping = ShmMapping { addr, shmid, size };
} else {
self.shm_mappings.push(ShmMapping { addr, shmid, size });
}
}

/// Find a SysV shared-memory attachment by its process address.
pub fn shm_mapping_at(&self, addr: usize) -> Option<ShmMapping> {
self.shm_mappings.iter().copied().find(|m| m.addr == addr)
}

/// Remove and return a SysV shared-memory attachment by its process address.
pub fn remove_shm_mapping(&mut self, addr: usize) -> Option<ShmMapping> {
let idx = self.shm_mappings.iter().position(|m| m.addr == addr)?;
Some(self.shm_mappings.swap_remove(idx))
}

/// Return host timer handles that should be cancelled for this process and
/// clear the Rust timer state that made them live.
pub fn take_host_timer_cleanup(&mut self) -> HostTimerCleanup {
let cancel_alarm = self.alarm_deadline_ns != 0 || self.alarm_interval_ns != 0;
self.alarm_deadline_ns = 0;
self.alarm_interval_ns = 0;

let mut posix_timer_ids = Vec::new();
for (timer_id, slot) in self.posix_timers.iter_mut().enumerate() {
if slot.is_some() {
posix_timer_ids.push(timer_id);
*slot = None;
}
}

HostTimerCleanup {
cancel_alarm,
posix_timer_ids,
}
}

/// True if `tid` names the process's main thread. The main thread's TID
/// equals the process PID (Linux convention) and is not tracked in
/// [`Process::threads`]; per-thread signal state for the main thread lives
Expand Down Expand Up @@ -964,6 +1023,69 @@ mod tests {
assert_eq!(proc.fork_count(), 0);
}

#[test]
fn shm_mapping_bookkeeping_is_keyed_by_process_addr() {
let mut proc = Process::new(1);

proc.record_shm_mapping(0x20000, 7, 4096);
assert_eq!(
proc.shm_mapping_at(0x20000),
Some(ShmMapping {
addr: 0x20000,
shmid: 7,
size: 4096,
})
);

proc.record_shm_mapping(0x20000, 8, 8192);
assert_eq!(proc.shm_mappings.len(), 1);
assert_eq!(
proc.remove_shm_mapping(0x20000),
Some(ShmMapping {
addr: 0x20000,
shmid: 8,
size: 8192,
})
);
assert_eq!(proc.shm_mapping_at(0x20000), None);
}

#[test]
fn host_timer_cleanup_drains_alarm_and_posix_timer_state() {
let mut proc = Process::new(1);
proc.alarm_deadline_ns = 10;
proc.alarm_interval_ns = 5;
proc.posix_timers.push(Some(PosixTimerState {
clock_id: 0,
sigev_signo: 14,
sigev_value: 0,
interval_sec: 0,
interval_nsec: 0,
value_sec: 1,
value_nsec: 0,
overrun: 0,
}));
proc.posix_timers.push(None);
proc.posix_timers.push(Some(PosixTimerState {
clock_id: 0,
sigev_signo: 15,
sigev_value: 0,
interval_sec: 1,
interval_nsec: 0,
value_sec: 1,
value_nsec: 0,
overrun: 0,
}));

let cleanup = proc.take_host_timer_cleanup();

assert!(cleanup.cancel_alarm);
assert_eq!(cleanup.posix_timer_ids, alloc::vec![0, 2]);
assert_eq!(proc.alarm_deadline_ns, 0);
assert_eq!(proc.alarm_interval_ns, 0);
assert!(proc.posix_timers.iter().all(|slot| slot.is_none()));
}

#[test]
fn spawn_child_basic_inherits_cwd_and_returns_pid() {
use crate::process_table::ProcessTable;
Expand Down
Loading
Loading