rt: signal handling in yggdrasil-rt

This commit is contained in:
Mark Poliakov 2024-11-19 15:05:15 +02:00
parent cbd823e17b
commit ed7f6c2f46
18 changed files with 319 additions and 66 deletions
kernel
libk/src/task
src/syscall/imp
lib/runtime/src/process
test.c
userspace/lib/ygglibc

@ -1,5 +1,5 @@
use core::{ use core::{
sync::atomic::{AtomicU32, Ordering}, sync::atomic::{AtomicU32, AtomicUsize, Ordering},
task::{Context, Poll}, task::{Context, Poll},
}; };
@ -82,6 +82,7 @@ pub struct Process {
parent: Option<Weak<Process>>, parent: Option<Weak<Process>>,
inner: IrqSafeRwLock<ProcessInner>, inner: IrqSafeRwLock<ProcessInner>,
signal_entry: AtomicUsize,
pub(crate) exit: OneTimeEvent<ExitCode>, pub(crate) exit: OneTimeEvent<ExitCode>,
@ -107,6 +108,7 @@ impl Process {
id, id,
parent, parent,
signal_entry: AtomicUsize::new(0),
inner: IrqSafeRwLock::new(ProcessInner::new(id, group_id, Some(space.clone()), image)), inner: IrqSafeRwLock::new(ProcessInner::new(id, group_id, Some(space.clone()), image)),
exit: OneTimeEvent::new(), exit: OneTimeEvent::new(),
@ -323,6 +325,16 @@ impl Process {
inner.space = None; inner.space = None;
} }
/// Updates the signal entry point
pub fn set_signal_entry(&self, entry: usize) {
self.signal_entry.store(entry, Ordering::Release);
}
/// Retrieves current signal entry of the process
pub fn signal_entry(&self) -> usize {
self.signal_entry.load(Ordering::Acquire)
}
/// Raises a signal for the specified process /// Raises a signal for the specified process
/// ///
/// If sender is Some(id) - signal was sent by some thread in some (possibly this) process. /// If sender is Some(id) - signal was sent by some thread in some (possibly this) process.

@ -1,4 +1,9 @@
use core::{cell::Cell, mem::size_of, ops::Deref}; use core::{
cell::Cell,
mem::size_of,
ops::Deref,
sync::atomic::{AtomicUsize, Ordering},
};
use alloc::{ use alloc::{
collections::{btree_map, BTreeMap}, collections::{btree_map, BTreeMap},
@ -59,10 +64,6 @@ struct SignalEntry {
stack: usize, stack: usize,
} }
struct ThreadInner {
signal_entry: Option<SignalEntry>,
}
/// Describes a single thread within the system /// Describes a single thread within the system
pub struct Thread { pub struct Thread {
/// Unique thread ID /// Unique thread ID
@ -77,7 +78,8 @@ pub struct Thread {
space: Option<Arc<ProcessAddressSpace>>, space: Option<Arc<ProcessAddressSpace>>,
debug: IrqSafeSpinlock<ThreadDebuggingInfo>, debug: IrqSafeSpinlock<ThreadDebuggingInfo>,
inner: IrqSafeSpinlock<ThreadInner>, // inner: IrqSafeSpinlock<ThreadInner>,
signal_stack: AtomicUsize,
signal_queue: SegQueue<Signal>, signal_queue: SegQueue<Signal>,
pub exit: Arc<BoolEvent>, pub exit: Arc<BoolEvent>,
@ -133,7 +135,7 @@ impl Thread {
process, process,
space, space,
inner: IrqSafeSpinlock::new(ThreadInner { signal_entry: None }), signal_stack: AtomicUsize::new(0),
signal_queue: SegQueue::new(), signal_queue: SegQueue::new(),
exit: Arc::new(BoolEvent::new()), exit: Arc::new(BoolEvent::new()),
kill: BoolEvent::new(), kill: BoolEvent::new(),
@ -221,10 +223,9 @@ impl Thread {
self.affinity.set(affinity); self.affinity.set(affinity);
} }
/// Updates the thread signal entry/stack information /// Updates the thread signal stack information
pub fn set_signal_entry(&self, entry: usize, stack: usize) { pub fn set_signal_stack(&self, stack: usize) {
let mut inner = self.inner.lock(); self.signal_stack.store(stack, Ordering::Release);
inner.signal_entry.replace(SignalEntry { entry, stack });
} }
/// Returns the thread address space (usually provided by its parent process). If none exists, /// Returns the thread address space (usually provided by its parent process). If none exists,
@ -266,6 +267,7 @@ impl Thread {
/// Pushes a signal to the thread's signal queue /// Pushes a signal to the thread's signal queue
pub fn raise_signal(&self, signal: Signal) { pub fn raise_signal(&self, signal: Signal) {
log::debug!("{}: raise signal {signal:?}", self.id);
self.signal_queue.push(signal); self.signal_queue.push(signal);
self.enqueue(); self.enqueue();
} }
@ -622,6 +624,8 @@ impl CurrentThread {
} }
if let Some(signal) = self.signal_queue.pop() { if let Some(signal) = self.signal_queue.pop() {
log::debug!("{}: handle signal {signal:?}", self.id);
if signal == Signal::Debug { if signal == Signal::Debug {
frame.set_single_step(true); frame.set_single_step(true);
@ -638,19 +642,22 @@ impl CurrentThread {
return; return;
} }
let inner = self.inner.lock(); let ip = self.process().signal_entry();
let sp = self.signal_stack.load(Ordering::Acquire);
let Some(entry) = inner.signal_entry.as_ref() else { // Check if ip and sp are legible for signal entry
drop(inner); if ip == 0 || sp < 4096 {
log::warn!("No legible signal handler for {}", self.id);
self.exit_process(ExitCode::BySignal(signal)); self.exit_process(ExitCode::BySignal(signal));
}; }
// TODO check if really in a syscall, lol // TODO check if really in a syscall, lol
let syscall_return = -(u32::from(Error::Interrupted) as isize); let syscall_return = -(u32::from(Error::Interrupted) as isize);
frame.set_return_value(syscall_return as u64); frame.set_return_value(syscall_return as u64);
// Setup signal frame // Setup signal frame
let usp = ((entry.stack - size_of::<SignalEntryData>()) & !0xF) // FIXME use write_foreign_*** for this
let usp = ((sp - size_of::<SignalEntryData>()) & !0xF)
- TaskContextImpl::SIGNAL_STACK_EXTRA_ALIGN; - TaskContextImpl::SIGNAL_STACK_EXTRA_ALIGN;
let frame_ptr = usp as *mut SignalEntryData; let frame_ptr = usp as *mut SignalEntryData;
@ -666,13 +673,13 @@ impl CurrentThread {
// Setup return to signal handler // Setup return to signal handler
log::debug!( log::debug!(
"Signal entry @ pc={:#x}, sp={:#x} (top = {:#x})", "Signal entry @ pc={:#x}, sp={:#x} (top = {:#x})",
entry.entry, ip,
usp, usp,
entry.stack sp,
); );
frame.set_user_sp(usp); frame.set_user_sp(usp);
frame.set_user_ip(entry.entry); frame.set_user_ip(ip);
// Pass the frame pointer as an argument to signal handler entry // Pass the frame pointer as an argument to signal handler entry
frame.set_argument(usp as _); frame.set_argument(usp as _);

@ -201,7 +201,14 @@ pub(crate) fn nanosleep(duration: &Duration) -> Result<(), Error> {
pub(crate) fn set_signal_entry(ip: usize, sp: usize) { pub(crate) fn set_signal_entry(ip: usize, sp: usize) {
let thread = Thread::current(); let thread = Thread::current();
thread.set_signal_entry(ip, sp); let process = thread.process();
log::debug!("{}: set_signal_entry({:#x}, {:#x})", thread.id, ip, sp);
if ip != usize::MAX {
process.set_signal_entry(ip);
}
if sp != usize::MAX {
thread.set_signal_stack(sp);
}
} }
pub(crate) fn send_signal(pid: ProcessId, signal: Signal) -> Result<(), Error> { pub(crate) fn send_signal(pid: ProcessId, signal: Signal) -> Result<(), Error> {

@ -6,4 +6,5 @@ pub use abi::process::{
SpawnOption, SpawnOptions, StringArgIter, ThreadId, ThreadSpawnOptions, SpawnOption, SpawnOptions, StringArgIter, ThreadId, ThreadSpawnOptions,
}; };
pub mod signal;
pub mod thread_local; pub mod thread_local;

@ -0,0 +1,148 @@
//! Runtime utilities for signal handling
use core::ffi::c_int;
use abi::{
error::Error,
mem::{MappingFlags, MappingSource},
process::{ExitCode, Signal, SignalEntryData},
};
use alloc::boxed::Box;
/// Describes how a signal should be handled
#[derive(Debug, Clone, Copy)]
pub enum SignalHandler {
/// Terminate when received
Terminate,
/// Silently ignore the signal
Ignore,
/// Pass the signal to a custom function (Rust version)
Rust(fn(Signal)),
/// Pass the signal to a custom function (C compat version)
C(unsafe extern "C" fn(c_int)),
}
// TODO RwLock here
const MAX_SIGNALS: usize = 16;
static mut TABLE: [SignalHandler; MAX_SIGNALS] = [const { SignalHandler::Terminate }; MAX_SIGNALS];
unsafe extern "C" fn common_signal_entry(data: &SignalEntryData) -> ! {
let index = data.signal.into_raw() as usize;
if index >= MAX_SIGNALS {
terminate_by_signal(data.signal);
}
match TABLE[index] {
SignalHandler::Rust(function) => function(data.signal),
SignalHandler::C(function) => {
let signum = data.signal.into_raw() as i32;
function(signum);
}
SignalHandler::Terminate => terminate_by_signal(data.signal),
SignalHandler::Ignore => (),
}
crate::sys::exit_signal(data)
}
// TODO add signal print hook function to dump signal info here
fn terminate_by_signal(signal: Signal) -> ! {
crate::debug_trace!("runtime: terminated by signal: {signal:?}");
unsafe { crate::sys::exit_process(ExitCode::BySignal(signal)) };
}
/// Updates the handler for a particular signal. Returns the old handler used for that signal.
///
/// # Safety
///
/// Marked as unsafe due to being thread-unsafe. Will be lifted once I port RwLock into the runtime
/// crate.
pub unsafe fn set_handler(signal: Signal, handler: SignalHandler) -> SignalHandler {
let entry = &mut TABLE[signal.into_raw() as usize];
let old_handler = core::mem::replace(entry, handler);
old_handler
}
/// Sets the stack that will be used to handle signals **on this thread**.
///
/// # Safety
///
/// To be legible for signal handling, `sp` must be non-NULL and have at least 4K of space
/// below it. It is up to the caller to make sure `sp` points to a proper stack's top.
///
/// TLDR: just use [setup_signal_stack].
pub unsafe fn set_signal_stack(sp: usize) {
crate::sys::set_signal_entry(usize::MAX, sp);
}
fn allocate_signal_stack(mut size: usize) -> Result<usize, Error> {
if size == 0 {
size = 4096;
}
size = (size + 0xFFF) & !0xFFF;
let base = unsafe {
crate::sys::map_memory(None, size, MappingFlags::WRITE, &MappingSource::Anonymous)
}?;
let top = base + size;
Ok(top)
}
/// Allocates a signal stack and sets it **for this thread**. Returns an error if allocation fails.
///
/// # Note
///
/// This function may allocate more memory than requested in the `size` parameter. It will allocate
/// at least 4KiB of memory, rounding the size up to a page boundary. This is required for the
/// stack to be legible for entry from the kernel.
///
/// The allocated stack is expected to live forever or at least until it is replaced by another
/// one, so current implementation just leaks the stack. The implementation also provides no
/// guarantees on what will happen if a raw syscall to [crate::sys::set_signal_entry] is done
/// after calling this function.
pub fn setup_signal_stack(size: usize) -> Result<(), Error> {
let sp = allocate_signal_stack(size)?;
unsafe { set_signal_stack(sp) };
Ok(())
}
/// Performs full signal setup for the process:
///
/// * Allocates a signal stack for **the current thread**.
/// * Sets up a signal entry into the runtime-managed handler for the whole process.
///
/// # Note
///
/// This function is intended to be called from the main thread, while [setup_signal_stack] is
/// intended for setting up thread signal stacks. A caller *may* call this function elsewhere,
/// and this will be safe, but likely will not make any sense.
///
/// Also see notes for [setup_signal_stack].
pub fn setup_signal_full(size: usize) -> Result<(), Error> {
let sp = allocate_signal_stack(size)?;
unsafe { crate::sys::set_signal_entry(imp::signal_entry as usize, sp) };
Ok(())
}
// implementation details
#[cfg(any(target_arch = "x86", rust_analyzer))]
mod imp {
// i686 is a bit tricky: cdecl ABI requires the arguments to be passed on the stack, while
// the kernel does the easy thing and just puts it into %eax, so a fixup needs to be done
// before entering a proper handler. The wrapper also aligns the stack nicely.
#[naked]
pub(super) unsafe extern "C" fn signal_entry() -> ! {
// %eax - SignalEntryData pointer
core::arch::naked_asm!(
r#"
jmp .
"#,
options(att_syntax)
);
}
}
#[cfg(any(not(target_arch = "x86"), rust_analyzer))]
mod imp {
pub(super) use super::common_signal_entry as signal_entry;
}

17
test.c

@ -1,20 +1,3 @@
#include <pthread.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/yggdrasil.h>
static void *function(void *arg) {
abort();
return NULL;
}
int main(int argc, const char **argv) { int main(int argc, const char **argv) {
pthread_t thread;
assert(pthread_create(&thread, NULL, function, (void *) NULL) == 0);
pthread_join(thread, NULL);
return 0; return 0;
} }

@ -1,6 +1,10 @@
#ifndef _YGGDRASIL_SETJMP_H #ifndef _YGGDRASIL_SETJMP_H
#define _YGGDRASIL_SETJMP_H 1 #define _YGGDRASIL_SETJMP_H 1
int setjmp(jmp_buf buf);
int _setjmp(jmp_buf buf);
[[noreturn]] void longjmp(jmp_buf buf, int val);
[[noreturn]] void _longjmp(jmp_buf buf, int val);
#endif #endif

@ -11,7 +11,7 @@ typedef _Atomic int sig_atomic_t;
#define SIG_IGN __sig_ignore #define SIG_IGN __sig_ignore
// TODO // TODO
#define SIG_ERR __sig_terminate #define SIG_ERR ((sig_handler_t) 1)
// TODO // TODO
#define SIG_HOLD __sig_terminate #define SIG_HOLD __sig_terminate

@ -102,10 +102,13 @@ impl<T> EResult<T> {
} }
} }
#[track_caller]
pub fn expect(self, message: &str) -> T { pub fn expect(self, message: &str) -> T {
match self { match self {
Self::Ok(value) => value, Self::Ok(value) => value,
Self::Err(err) => panic!("expect() failed: {}, errno={:?}", message, err) Self::Err(err) => {
panic!("expect() failed: {}, errno={:?}", message, err)
}
} }
} }

@ -17,6 +17,4 @@ exclude = [
"longjmp", "longjmp",
"_setjmp", "_setjmp",
"_longjmp", "_longjmp",
"__x86_64_setjmp",
"__x86_64_longjmp"
] ]

@ -2,11 +2,10 @@ use core::ffi::{c_char, c_int, c_long, c_void};
use yggdrasil_rt::process::Signal; use yggdrasil_rt::process::Signal;
use crate::{error::CIntZeroResult, util::PointerExt}; use crate::{error::{self, CIntZeroResult, EResult, TryFromExt}, signal, util::PointerExt};
use super::{ use super::{
sys_time::__ygg_timespec_t, errno, sys_time::__ygg_timespec_t, sys_types::{pid_t, uid_t}
sys_types::{pid_t, uid_t},
}; };
pub type sig_handler_t = unsafe extern "C" fn(SigNumber); pub type sig_handler_t = unsafe extern "C" fn(SigNumber);
@ -104,16 +103,15 @@ impl SigNumber {
pub const INVALID: Self = Self(-65536); pub const INVALID: Self = Self(-65536);
} }
impl TryFrom<SigNumber> for Signal { impl TryFromExt<SigNumber> for Signal {
type Error = (); fn e_try_from(value: SigNumber) -> EResult<Self> {
fn try_from(value: SigNumber) -> Result<Self, Self::Error> {
match value { match value {
SIGSEGV => Ok(Signal::MemoryAccessViolation), SIGSEGV => EResult::Ok(Signal::MemoryAccessViolation),
SIGABRT => Ok(Signal::Aborted), SIGABRT => EResult::Ok(Signal::Aborted),
SIGKILL => Ok(Signal::Killed), SIGKILL => EResult::Ok(Signal::Killed),
SIGINT => Ok(Signal::Interrupted), SIGINT => EResult::Ok(Signal::Interrupted),
_ => Err(()), SIGTRAP => EResult::Ok(Signal::Debug),
_ => EResult::Err(errno::EINVAL),
} }
} }
} }
@ -229,8 +227,25 @@ unsafe extern "C" fn sigismember(_mask: *const sigset_t, _signum: c_int) -> c_in
} }
#[no_mangle] #[no_mangle]
unsafe extern "C" fn signal(_handler: sig_handler_t, _signum: c_int) -> sig_handler_t { unsafe extern "C" fn signal(handler: sig_handler_t, signum: c_int) -> sig_handler_t {
todo!() // Validate handler
let address: usize = core::mem::transmute(handler);
// NULL or SIG_ERR
if address == 0 || address == 1 {
yggdrasil_rt::debug_trace!("libc: signal() was passed an invalid handler");
error::errno = errno::EINVAL;
// SIG_ERR
return core::mem::transmute(1usize);
}
match signal::set(SigNumber(signum), handler) {
EResult::Ok(handler) => handler,
EResult::Err(err) => {
error::errno = err;
// SIG_ERR
core::mem::transmute(1usize)
}
}
} }
#[no_mangle] #[no_mangle]

@ -18,7 +18,7 @@ unsafe extern "C" fn nanosleep(
let amount = Duration::from(*rqtp); let amount = Duration::from(*rqtp);
process::sleep(amount)?; process::sleep(amount).expect("TODO interruptible sleep in nanosleep()");
CIntZeroResult::SUCCESS CIntZeroResult::SUCCESS
} }

@ -1,8 +1,14 @@
#![allow(unused)] #![allow(unused)]
use core::ffi::c_int;
mod exec; mod exec;
mod fs; mod fs;
mod getopt; mod getopt;
mod io; mod io;
mod process; mod process;
mod util; mod util;
pub const STDIN_FILENO: c_int = 0;
pub const STDOUT_FILENO: c_int = 1;
pub const STDERR_FILENO: c_int = 2;

@ -6,8 +6,8 @@ use core::{
use yggdrasil_rt::debug_trace; use yggdrasil_rt::debug_trace;
use crate::{ use crate::{
error::{CIntCountResult, CIntZeroResult}, error::{self, CIntCountResult, CIntZeroResult},
headers::sys_types::{gid_t, pid_t, uid_t}, headers::{errno, sys_types::{gid_t, pid_t, uid_t}},
process, process,
}; };
@ -121,6 +121,11 @@ unsafe extern "C" fn setuid(uid: uid_t) -> c_int {
#[no_mangle] #[no_mangle]
unsafe extern "C" fn sleep(seconds: c_uint) -> c_uint { unsafe extern "C" fn sleep(seconds: c_uint) -> c_uint {
let duration = Duration::from_secs(seconds.try_into().unwrap()); let duration = Duration::from_secs(seconds.try_into().unwrap());
process::sleep(duration).expect("TODO: sleep failed"); match process::sleep(duration) {
0 Ok(()) => 0,
Err(remaining) => {
error::errno = errno::EINTR;
remaining.as_secs() as _
}
}
} }

@ -42,6 +42,7 @@ mod sync;
mod types; mod types;
mod util; mod util;
mod thread; mod thread;
mod signal;
pub mod headers; pub mod headers;
@ -50,6 +51,7 @@ unsafe extern "C" fn __ygglibc_entry(
main: extern "C" fn(c_int, *const *const c_char, *const *const c_char) -> c_int, main: extern "C" fn(c_int, *const *const c_char, *const *const c_char) -> c_int,
arg: usize, arg: usize,
) { ) {
signal::init(true);
init::init(); init::init();
// Setup args // Setup args

@ -24,9 +24,9 @@ pub fn getpgrp() -> pid_t {
pgid.into_raw().try_into().unwrap() pgid.into_raw().try_into().unwrap()
} }
pub fn sleep(duration: Duration) -> EResult<()> { pub fn sleep(duration: Duration) -> Result<(), Duration> {
unsafe { syscall::nanosleep(&duration) }?; // TODO make nanosleep return duration
EResult::Ok(()) unsafe { syscall::nanosleep(&duration) }.map_err(|_| Duration::from_secs(1))
} }
pub fn abort() -> ! { pub fn abort() -> ! {

@ -0,0 +1,56 @@
use yggdrasil_rt::process::{
signal::{self, SignalHandler},
Signal,
};
use crate::{
error::{EResult, ResultExt, TryFromExt},
headers::{
self, errno, signal::{sig_handler_t, SigNumber}
},
};
const SIGNAL_STACK_SIZE: usize = 4096 * 8;
// These are just stubs for addresses, which get converted into Rust handlers
#[no_mangle]
unsafe extern "C" fn __sig_ignore(signum: SigNumber) {
unreachable!()
}
#[no_mangle]
unsafe extern "C" fn __sig_terminate(signum: SigNumber) {
unreachable!()
}
pub unsafe fn set(signum: SigNumber, handler: sig_handler_t) -> EResult<sig_handler_t> {
let signal = Signal::e_try_from(signum)?;
let handler = match handler {
// Transform special cases into Rust signal handlers instead
_ if handler == __sig_terminate => todo!(),
_ if handler == __sig_ignore => todo!(),
// This is safe: handler essentially has the same type, just in a wrapper
_ => core::mem::transmute(handler)
};
let old = signal::set_handler(signal, SignalHandler::C(handler));
let old = match old {
// Transform Rust special cases into C "handlers"
SignalHandler::Ignore => __sig_ignore,
SignalHandler::Terminate => __sig_terminate,
// libc doesn't set Rust signal handlers, return terminate just in case
SignalHandler::Rust(_) => __sig_terminate,
SignalHandler::C(handler) => core::mem::transmute(handler),
};
EResult::Ok(old)
}
pub fn init(main: bool) {
// TODO reset any non-default handlers possibly set by whatever libc might've been entered from
if main {
signal::setup_signal_full(SIGNAL_STACK_SIZE)
.expect("Couldn't setup signal handler for the main thread");
} else {
signal::setup_signal_stack(SIGNAL_STACK_SIZE).expect("Couldn't setup thread signal stack");
}
}

@ -17,7 +17,7 @@ use crate::{
error::{EResult, OptionExt}, headers::{ error::{EResult, OptionExt}, headers::{
errno::{self, Errno}, errno::{self, Errno},
sys_types::{pthread_attr_t, pthread_t}, sys_types::{pthread_attr_t, pthread_t},
}, process, sync::Mutex }, process, signal, sync::Mutex
}; };
pub mod tls; pub mod tls;
@ -183,7 +183,7 @@ impl Thread {
core::hint::spin_loop(); core::hint::spin_loop();
} }
unsafe { Self::set_this(argument.thread.clone()) }; unsafe { Self::setup_self(&argument.thread); }
let result = (argument.entry)(argument.argument); let result = (argument.entry)(argument.argument);
argument.thread.result.store(result, Ordering::Release); argument.thread.result.store(result, Ordering::Release);
@ -194,6 +194,12 @@ impl Thread {
// TODO call thread-local destructors // TODO call thread-local destructors
unsafe { yggdrasil_rt::sys::exit_thread() } unsafe { yggdrasil_rt::sys::exit_thread() }
} }
unsafe fn setup_self(self: &Arc<Self>) {
Self::set_this(self.clone());
// Setup signal stack
signal::init(false);
}
} }
impl Drop for Thread { impl Drop for Thread {