Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
dff3185
linux: the signal frame is linux's rt_sigframe byte for byte
eKisNonos Sep 28, 2026
1b2ea57
linux: any handler can turn a kernel pid into the guest's number
eKisNonos Sep 28, 2026
3bd641c
linux: signals honour per-thread masks, alternate stacks and defaults
eKisNonos Sep 28, 2026
3af1d3c
linux: kill reaches every process of the family, not only the caller
eKisNonos Sep 28, 2026
294461d
linux: a leader's plain exit ends only the leader, status is linux's
eKisNonos Sep 28, 2026
88bd9a1
linux: fork copies a span larger than 1 MiB in pieces
eKisNonos Sep 28, 2026
4c5f99d
linux: clone can make a process, and vfork holds its parent
eKisNonos Sep 28, 2026
aaed193
linux: wait4 and waitid answer as linux's do, and SIGCHLD is raised
eKisNonos Sep 28, 2026
0c9b8fd
linux: sigpipe raises SIGPIPE for a write to a pipe with no reader
eKisNonos Sep 28, 2026
b27092c
linux: a caught signal ends the wait a thread is parked in
eKisNonos Sep 28, 2026
f2ce1fc
linux: alarm and setitimer arm ITIMER_REAL, which raises SIGALRM
eKisNonos Sep 28, 2026
6e6f705
linux: pause, sigsuspend, sigtimedwait and sigpending are served
eKisNonos Sep 28, 2026
949b452
linux: posix timers are served, with overruns counted as linux does
eKisNonos Sep 28, 2026
478e293
foreign: MkForeignFork takes an optional stack for the child
eKisNonos Sep 28, 2026
5f35d3b
libc: mk_foreign_fork_at forks a guest onto a stack it names
eKisNonos Sep 28, 2026
ac08871
linux: a process clone that names a stack starts the child on it
eKisNonos Sep 28, 2026
3f26b6d
linux: five guests check process lifecycle and signals against linux
eKisNonos Sep 28, 2026
2ed9096
linux: merge the waits, eventfd and pipe ends into lifecycle and signals
eKisNonos Sep 28, 2026
75d14d1
linux: answer lifecycle and signal calls before dispatch
eKisNonos Sep 29, 2026
240ee68
linux: a new thread starts with its creator's signal mask
eKisNonos Sep 29, 2026
2445669
linux: end the serve loop's wait at the next timer or signal wait
eKisNonos Sep 29, 2026
2c6af30
linux: a thread's death by signal keeps Linux's wait status
eKisNonos Sep 29, 2026
1b1e6ae
linux: raise SIGPIPE at a thread whose write answers EPIPE
eKisNonos Sep 29, 2026
3117b51
linux: serve /dev/null, /dev/zero, /dev/full and the random devices
eKisNonos Sep 29, 2026
9b551eb
linux: serve signalfd4 and signalfd
eKisNonos Sep 29, 2026
408e336
mk: pack only the Linux guests LINUX_GUEST_SET names
eKisNonos Sep 29, 2026
15067eb
linux: goexec checks the devices and os/exec, alarm checks signalfd
eKisNonos Sep 29, 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
31 changes: 20 additions & 11 deletions src/process/foreign/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@ use super::peer_guard::pid_arg;
use crate::process::core::ProcessState;
use crate::syscall::microkernel::errnos::{ERRNO_INVAL, ERRNO_NOENT, ERRNO_PERM};

/// The last address of the user half.
const USER_VA_MAX: u64 = 0x0000_7FFF_FFFF_FFFF;

/// `MkForeignFork`: a second process holding the first one's register state,
/// with zero in its return register so the two can tell each other apart,
/// which is the whole of fork's contract to the program.
pub fn sys_foreign_fork(pid: u64) -> i64 {
/// which is the whole of fork's contract to the program. A non-zero `rsp` is
/// the stack the child starts on instead of its parent's, as a clone that
/// names a stack gives it; it must lie in the user half.
pub fn sys_foreign_fork(pid: u64, rsp: u64) -> i64 {
if rsp > USER_VA_MAX {
return ERRNO_INVAL;
}
let Some(caller) = crate::process::current_pid() else {
return ERRNO_INVAL;
};
Expand All @@ -34,8 +42,8 @@ pub fn sys_foreign_fork(pid: u64) -> i64 {
if super::registry::supervisor_of(parent) != Some(caller) {
return ERRNO_PERM;
}
let Some(state) = saved_state(parent) else {
// A guest that is not parked inside a syscall has no frame to copy.
let Some(state) = super::trap_frame::parked_frame(parent) else {
/* A guest that is not parked inside a syscall has no frame to copy. */
return ERRNO_NOENT;
};
let child = match super::spawn::empty_guest(caller, b"fork") {
Expand All @@ -44,9 +52,14 @@ pub fn sys_foreign_fork(pid: u64) -> i64 {
};
let mut frame = state;
frame.rax = 0;
// The thread pointer is a register the frame does not carry, so the child
// takes its forking thread's, read from that thread's PCB. Without this a
// fork from a thread that set its own FS would give the child a zero one.
if rsp != 0 {
frame.rsp = rsp;
}
/*
* The thread pointer is a register the frame does not carry, so the child
* takes its forking thread's, read from that thread's PCB. Without this a
* fork from a thread that set its own FS would give the child a zero one.
*/
let parent_tls = crate::process::with_process(parent, |pcb| pcb.get_tls_base()).unwrap_or(0);
crate::process::with_process(child, |pcb| {
*pcb.saved_user_context.lock() = Some(frame);
Expand All @@ -57,7 +70,3 @@ pub fn sys_foreign_fork(pid: u64) -> i64 {
});
child as i64
}

fn saved_state(pid: u32) -> Option<crate::arch::context::SavedUser> {
super::trap_frame::parked_frame(pid)
}
2 changes: 1 addition & 1 deletion src/syscall/microkernel/dispatch/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ pub(super) fn handle(nr: u64, a: Args) -> Option<i64> {
SYS_PEER_PROTECT => sys_peer_protect(a.a0, a.a1, a.a2, a.a3),
SYS_FOREIGN_THREAD => sys_foreign_thread(a.a0, a.a1, a.a2, a.a3, a.a4),
SYS_PEER_TLS => sys_peer_tls(a.a0, a.a1),
SYS_FOREIGN_FORK => sys_foreign_fork(a.a0),
SYS_FOREIGN_FORK => sys_foreign_fork(a.a0, a.a1),
SYS_PEER_UNMAP => sys_peer_unmap(a.a0, a.a1, a.a2),
SYS_FOREIGN_EXEC => sys_foreign_exec(a.a0, a.a1, a.a2),
SYS_LOCAL_SIGN => sys_local_sign(a.a0, a.a1, a.a2, a.a3, a.a4),
Expand Down
1 change: 1 addition & 0 deletions userland/capsule_linux/src/linux/abi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ pub mod name;
pub mod nr;
pub mod nr_path;
pub mod nr_high;
pub mod nr_sig;
pub mod nr_sched;
39 changes: 39 additions & 0 deletions userland/capsule_linux/src/linux/abi/nr_sig.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// NONOS Operating System
// Copyright (C) 2026 NONOS Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Syscall numbers for process lifecycle, signals and timers, transcribed
//! from the x86_64 table.

pub const PAUSE: u64 = 34;
pub const GETITIMER: u64 = 36;
pub const ALARM: u64 = 37;
pub const SETITIMER: u64 = 38;
pub const KILL: u64 = 62;
pub const RT_SIGPENDING: u64 = 127;
pub const RT_SIGTIMEDWAIT: u64 = 128;
pub const RT_SIGQUEUEINFO: u64 = 129;
pub const RT_SIGSUSPEND: u64 = 130;
pub const TKILL: u64 = 200;
pub const TIMER_CREATE: u64 = 222;
pub const TIMER_SETTIME: u64 = 223;
pub const TIMER_GETTIME: u64 = 224;
pub const TIMER_GETOVERRUN: u64 = 225;
pub const TIMER_DELETE: u64 = 226;
pub const TGKILL: u64 = 234;
pub const WAITID: u64 = 247;
pub const RT_TGSIGQUEUEINFO: u64 = 297;
pub const SIGNALFD: u64 = 282;
pub const SIGNALFD4: u64 = 289;
3 changes: 3 additions & 0 deletions userland/capsule_linux/src/linux/call/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub fn write(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> u64 {
Some(Kind::Unix) => crate::linux::unix::send(guest, fd, buf, len),
Some(Kind::Pipe) => super::pipe_write(guest, fd, buf, len),
Some(Kind::Event) => file::event_write(guest, fd, buf, len),
Some(Kind::Device) => file::dev_write(guest, fd, len),
Some(Kind::Resolver) => net::dns::query(guest, fd, buf, len, LOOPBACK_53),
Some(Kind::Dir) => errno::fail(errno::EISDIR),
_ => errno::fail(errno::EBADF),
Expand All @@ -52,6 +53,8 @@ pub fn read(guest: &mut Guest, fd: u64, buf: u64, len: u64) -> u64 {
Some(Kind::Unix) => crate::linux::unix::recv(guest, fd, buf, len),
Some(Kind::Pipe) => super::pipe_read(guest, fd, buf, len),
Some(Kind::Event) => file::event_read(guest, fd, buf, len),
Some(Kind::Device) => file::dev_read(guest, fd, buf, len),
Some(Kind::Signal) => super::signalfd_now(guest, fd, buf, len),
Some(Kind::Resolver) => net::dns::answer_out(guest, fd, buf, len).0,
Some(Kind::Dir) => errno::fail(errno::EISDIR),
_ => errno::fail(errno::EBADF),
Expand Down
16 changes: 13 additions & 3 deletions userland/capsule_linux/src/linux/call/life.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! Ending a guest. The call never returns to the guest, so the answer
//! handed back is only what parks it until the supervisor tears it down.
//! Ending a thread or a guest. The call never returns to the guest, so the
//! answer handed back is only what parks it until the supervisor tears it
//! down. A process's end is kept as Linux's wait status: an exit's code in
//! the second byte, or the number of the signal that ended it in the first.

use nonos_libc::mk_kill;

Expand Down Expand Up @@ -58,7 +60,15 @@ pub fn set_tid_address(guest: &mut Guest, tid: u32, word: u64) -> Answer {
Answer::value(u64::from(tid))
}

/// exit_group: the whole process ends with `code`.
pub fn exit(guest: &mut Guest, code: u64) -> u64 {
guest.exited = Some(code as i32);
guest.exited = Some(((code & 0xff) << 8) as i32);
errno::ok(0)
}

/// The process ends on `signum`, unless something already ended it.
pub fn killed(guest: &mut Guest, signum: u8) {
if guest.exited.is_none() {
guest.exited = Some(i32::from(signum & 0x7f));
}
}
46 changes: 46 additions & 0 deletions userland/capsule_linux/src/linux/call/life_one.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// NONOS Operating System
// Copyright (C) 2026 NONOS Contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! A plain exit, which ends the calling thread only. The process ends with
//! the last of its threads, and that thread's code is its status.

use super::life::{exit, exit_thread};
use crate::linux::guest::Guest;
use crate::linux::serve::Answer;

/// A plain exit ends the calling thread only; the process ends with the last
/// of its threads, and that thread's code is its status. The leader cannot be
/// killed while others run: its pid is the address space every peer call
/// names. So it stays parked inside its exit, a zombie that runs nothing,
/// until the process ends and takes it.
pub fn exit_one(guest: &mut Guest, tid: u32, code: u64) -> Answer {
if tid == guest.pid {
if let Some(at) = guest.clear_tids.iter().position(|(t, _)| *t == tid) {
let (_, word) = guest.clear_tids.remove(at);
if guest.write(word, &0u32.to_le_bytes()) == 4 {
guest.wake(word, 1);
}
}
guest.signals.leader_gone = true;
} else {
let _ = exit_thread(guest, tid);
}
guest.forget_thread(tid);
if guest.live_threads().is_empty() {
let _ = exit(guest, code);
}
Answer::Park
}
43 changes: 39 additions & 4 deletions userland/capsule_linux/src/linux/call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mod io;
mod ioctl;
mod io_socket;
mod life;
mod life_one;
mod limits;
mod limits_table;
mod glibc;
Expand All @@ -43,12 +44,32 @@ mod pipe_read;
mod sched;
mod session;
pub mod sigframe;
pub mod sigframe_build;
mod sigframe_read;
mod signal;
mod signal_its;
mod signalfd;
mod signalfd_info;
mod signalfd_read;
mod signalfd_take;
mod signal_itv;
mod signal_mask;
mod signal_post;
mod signal_queue;
mod signal_real;
mod signal_send;
mod signal_stack;
mod signal_timedwait;
mod signal_timer;
mod signal_wait;
mod sigreturn;
mod sleep;
mod spawn;
mod thread;
mod timer_create;
mod timer_ops;
mod timer_set;
mod timer_sigev;
mod timeops;
mod umask;
mod uname;
Expand All @@ -61,7 +82,8 @@ pub use cwd::{chdir, fchdir, getcwd};
pub use futex::futex;
pub use ident::{getppid, setuid};
pub use io::{close, read, write};
pub use life::{exit, exit_thread, set_tid_address};
pub use life::{exit, exit_thread, killed, set_tid_address};
pub use life_one::exit_one;
pub use limits::{getrlimit, prlimit64};
pub use glibc::prctl;
pub use glibc_sched::{clone3, getcpu, membarrier, sched_getaffinity};
Expand All @@ -76,14 +98,27 @@ pub use sched::{
sched_setscheduler,
};
pub use session::{getpgid, getsid, setpgid, setsid};
pub use signal::{rt_sigaction, rt_sigprocmask, sigaltstack};
pub use signal::rt_sigaction;
pub use signal_mask::rt_sigprocmask;
pub use signal_queue::{rt_sigqueueinfo, rt_tgsigqueueinfo};
pub use signalfd::signalfd4;
pub use signalfd_read::signalfd_read;
pub use signalfd_take::{signalfd_bits, signalfd_now, signalfd_take};
pub use signal_send::{kill, kill_from, sigpipe, tgkill_from};
pub use signal_stack::sigaltstack;
pub use signal_timer::{alarm, getitimer, setitimer};
pub use signal_timedwait::rt_sigtimedwait;
pub use signal_wait::{pause, rt_sigpending, rt_sigsuspend};
pub use sigreturn::rt_sigreturn;
pub use signal_send::kill;
pub use sleep::{clock_nanosleep, nanosleep};
pub use spawn::{clone, execve, fork, reap_one, wait4};
pub use spawn::{clone, clone_process, execve, fork, vfork, wait4, wait4_usage, waitid};
pub use spawn::{WALL, WCLONE, WEXITED, WNOHANG, WNOWAIT};
pub use clock::{clock_getres, clock_gettime, now_ms};
pub use epoch::{family_ms, mark_start};
pub use thread::{arch_prctl, getrandom};
pub use timer_create::timer_create;
pub use timer_ops::{timer_delete, timer_getoverrun, timer_gettime};
pub use timer_set::timer_settime;
pub use timeops::{gettimeofday, time};
pub use umask::{umask, DEFAULT_UMASK};
pub use uname::uname;
Expand Down
85 changes: 29 additions & 56 deletions userland/capsule_linux/src/linux/call/sigframe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,62 +14,35 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! The `rt_sigframe` x86-64 puts on a thread's stack to enter a signal handler,
//! and where to read it back on return. Pure, so the layout is checked against
//! a round trip without a guest. It matches Linux `struct rt_sigframe`:
//! pretcode u64, ucontext at +8, siginfo at +312; the ucontext's sigcontext
//! holds the 18 words `mk_foreign_context` uses, in that order.
//! The `rt_sigframe` x86-64 puts on a thread's stack to enter a signal handler:
//! its layout, and what a handler is entered with (sigframe_build writes it,
//! sigframe_read reads it back). Pure, so the layout is checked against a
//! round trip without a guest. It matches Linux `struct rt_sigframe`:
//! pretcode u64, then the ucontext at +8 (uc_flags, uc_link, the 24-byte
//! uc_stack, the 256-byte sigcontext at +40, uc_sigmask at +296), then the
//! 128-byte siginfo at +312. The sigcontext starts with the 18 words
//! `mk_foreign_context` uses, in that order.

use alloc::vec::Vec;
pub const WORDS: usize = 18; /* r8..r15,rdi,rsi,rbp,rbx,rdx,rax,rcx,rsp,rip,rflags */
pub const FRAME_SIZE: usize = 440;
pub const UC_OFF: usize = 8;
pub const STACK_OFF: usize = 16; /* uc_stack within the ucontext */
pub const SIGCONTEXT_OFF: usize = 40; /* uc_mcontext within the ucontext */
pub const OLDMASK_WORD: usize = 21; /* sigcontext.oldmask, after cs/gs/fs/ss, err, trapno */
pub const SIGMASK_OFF: usize = 296; /* uc_sigmask within the ucontext */
pub const INFO_OFF: usize = 312;
pub const INFO_LEN: usize = 128;

pub const WORDS: usize = 18; // r8..r15,rdi,rsi,rbp,rbx,rdx,rax,rcx,rsp,rip,rflags
const FRAME_SIZE: usize = 440;
const UC_OFF: usize = 8;
pub const SIGCONTEXT_OFF: usize = 48; // sigcontext within the ucontext
const INFO_OFF: usize = 312;
const REDZONE: u64 = 128; // the System V red zone below rsp

fn put(buf: &mut [u8], at: usize, v: u64) {
buf[at..at + 8].copy_from_slice(&v.to_le_bytes());
}
/// Where the frame lands, the bytes to write there, and the registers that
/// enter the handler. `None` if the stack is too low to hold a frame.
pub fn build(
regs: &[u64; WORDS],
handler: u64,
restorer: u64,
signum: u32,
blocked: u64,
) -> Option<(u64, Vec<u8>, [u64; WORDS])> {
// Below the red zone, 16-aligned, then down 8 so the handler sees rsp+8
// aligned as a call would leave it.
let frame =
(regs[15].checked_sub(REDZONE)?.checked_sub(FRAME_SIZE as u64)? & !15u64).checked_sub(8)?;
let mut buf = alloc::vec![0u8; FRAME_SIZE];
put(&mut buf, 0, restorer);
let mc = UC_OFF + SIGCONTEXT_OFF; // the 18 words, then the sigmask
for (i, w) in regs.iter().enumerate() {
put(&mut buf, mc + i * 8, *w);
}
put(&mut buf, UC_OFF + SIGCONTEXT_OFF + WORDS * 8, blocked);
put(&mut buf, INFO_OFF, u64::from(signum)); // siginfo: si_signo
let mut out = [0u64; WORDS];
out[8] = u64::from(signum); // rdi
out[9] = frame + INFO_OFF as u64; // rsi, &siginfo
out[12] = frame + UC_OFF as u64; // rdx, &ucontext
out[15] = frame; // rsp at the frame; rax stays 0, no vector registers
out[16] = handler; // rip
out[17] = regs[17]; // rflags, the kernel masks it
Some((frame, buf, out))
}

/// The 18 words a returning frame carries, from the ucontext the guest's rsp
/// points at: the trampoline's `ret` left rsp there.
pub fn returned(uc: &[u8]) -> Option<[u64; WORDS]> {
let mut out = [0u64; WORDS];
for (i, slot) in out.iter_mut().enumerate() {
let at = SIGCONTEXT_OFF + i * 8;
*slot = u64::from_le_bytes(uc.get(at..at + 8)?.try_into().ok()?);
}
Some(out)
/// What a handler is entered with, beyond the registers it interrupts.
pub struct Entry<'a> {
pub handler: u64,
pub restorer: u64,
pub signum: u32,
/// The mask the thread had, restored by rt_sigreturn.
pub blocked: u64,
/// The top of the alternate stack when the frame goes there.
pub alt_top: Option<u64>,
/// uc_stack as Linux saves it: ss_sp, ss_flags, ss_size.
pub stack: [u64; 3],
pub info: &'a [u8; INFO_LEN],
}
Loading
Loading