Process and Signals
This page covers two tightly-coupled modules:
-
posix/proc.rs(892 lines) —fork,execve,waitpid,exit, pgid/sid, credentials,clock_gettime,nanosleep. -
posix/signals.rs(386 lines) —sigaction,sigprocmask,sigsuspend,kill,raise, theposix_sigcheckcooperative poll, and the kernel-injected__signal_dispatcherframe.
Everything in proc.rs targets the procmgr endpoint (caps::procmgr_ep()) with PM_* labels.
signals.rs is split between PM_KILL / PM_SIGACTION calls to procmgr and a dispatcher path that rides on the per-thread signal notification.
Process lifecycle — proc.rs
fork
posix_fork() is unusual: most of its body is in assembly, not Rust.
The calling convention:
#[unsafe(no_mangle)]
pub unsafe extern "C" fn posix_fork() -> i32; // implemented in fork.S
The assembly trampoline is at posix/arch/x86_64/fork.S and posix/arch/aarch64/fork.S.
It saves the callee-saved GPRs (and the full SSE / NEON register file on x86_64 and aarch64 respectively), calls into _posix_fork_impl — the Rust function that issues PM_FORK to procmgr — and either returns the child PID to the parent or resumes at a fork_child_entry label in the child.
The Rust side of _posix_fork_impl does the usual work:
-
Send
PM_FORKto procmgr. procmgr creates a new TCB, clones the VSpace pages via mmsrv, and sets the new TCB’s entry point to the savedfork_child_entryaddress. -
If procmgr replies with a positive child PID, the parent returns that PID.
-
If the child TCB is resumed, control arrives at
fork_child_entry, which calls_trona_post_fork_childto reset per-process runtime state (thread pool, allocator counters, pending tables) and then returns0to the fork caller.
fork.S and Linker Scripts covers the assembly trampoline in detail.
execve
pub unsafe fn posix_execve(
path: *const u8,
argv: *const *const u8,
envp: *const *const u8,
) -> i32;
posix_execve packages the path, argv, and envp into a single PM_EXEC IPC and sends it to procmgr.
Procmgr re-reads the executable via VFS (using VFS_POSIX_STAT_FOR_EXEC to validate permissions), parses its ELF headers, lays out a fresh VA layout via substrate::layout, and then does an in-place VSpace swap: the old pages are unmapped, the new ones are mapped, and the TCB is resumed at the new entry point.
From the caller’s perspective, a successful exec never returns — control never comes back from the PM_EXEC call.
A failed exec returns a negative errno.
waitpid and exit
| Function | Label |
|---|---|
|
|
|
|
PM_EXIT is the last IPC the process ever makes — procmgr tears down the TCB, reclaims all capabilities via RES_RECLAIM_OWNER, and removes the PID from the list.
posix_exit is marked → ! because it cannot return.
PM_WAIT supports the usual POSIX flags:
-
WNOHANG— return immediately if no child has exited. -
WUNTRACED— also report stopped children. -
PID
-1— wait for any child. -
PID
0— wait for any child in the caller’s process group.
The returned status word encodes the exit status, termination signal, and stop signal in the traditional POSIX bit layout.
Process group and session
| Function | Label |
|---|---|
|
|
|
|
|
|
|
|
|
|
The _BADGE variants exist because procmgr can validate "the caller is asking about itself" via the IPC badge without looking up a PID table entry — a faster path for the common case.
Credentials
proc.rs exposes the full POSIX credential surface:
| Function | Label |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
All credential checks happen at procmgr. trona_posix just marshals the arguments and returns the errno.
Resource limits
| Function | Label |
|---|---|
|
|
|
|
resource is one of the RLIMIT_* constants defined in uapi/consts/posix.rs.
Enforcement happens at procmgr — trona_posix is pure wire.
Clock and sleep
| Function | Syscall |
|---|---|
|
|
|
Computed from |
|
|
|
|
|
|
These are the only posix_* functions in proc.rs that talk directly to the kernel rather than to procmgr.
The kernel clock is authoritative — no userspace caching.
CLOCK_REALTIME (0) and CLOCK_MONOTONIC (1) are the only two supported clock IDs; other POSIX clocks (CLOCK_PROCESS_CPUTIME_ID, CLOCK_THREAD_CPUTIME_ID) are not implemented.
Signals — signals.rs
SaltyOS’s signal delivery is cooperative: the kernel never preempts a running userspace thread to deliver a signal.
Instead, the signal arrives as a notification on the per-thread caps::signal_ntfn(), and the thread picks it up at one of two points:
-
posix_sigcheck()— an explicit check basaltc inserts in its C ABI wrappers around blocking syscalls. Before a blocking wait, basaltc callsposix_sigcheck(); if any signal bits are pending and not masked, the handler runs before the thread ever enters the syscall. -
__signal_dispatcher— a kernel-injected path used when a thread is already blocked in an IPC. The kernel arranges for the thread to resume at a well-known dispatcher address with aNotifFrameon the stack; the dispatcher runs any pending handlers and then returns to the interrupted code viaSYS_NOTIF_RETURN.
Both paths consult the same global state in posix/lib.rs:
static mut __sig_handlers: [AtomicU64; NSIG];
static mut __sig_blocked_mask: AtomicU64;
static mut __sig_sa_mask: [AtomicU64; NSIG];
static mut __sig_sa_flags: [AtomicU32; NSIG];
static mut __sig_last_restart: AtomicBool;
Signal installation
| Function | Label / action |
|---|---|
|
Updates the |
|
Updates |
|
Temporarily installs |
|
|
|
Local call to |
|
Block on the signal notification; return when any signal wakes the caller. |
The dispatcher path and NotifFrame
When the kernel needs to inject a signal into a thread that is currently blocked in an IPC, it:
-
Saves the TCB’s current register state.
-
Allocates a
NotifFrameon the thread’s stack containing the saved registers and some metadata (interrupted_syscall,syscall_cap_ptr,syscall_msg_info,magic = 0x5A17535349464652). -
Rewrites the TCB’s RIP to the user-registered
__signal_dispatcheraddress and resumes.
The __signal_dispatcher is a C ABI function in signals.rs that:
-
Reads the pending bitmask from the signal notification via
SYS_WAIT. -
For each pending bit, if the matching handler is not
SIG_DFL/SIG_IGNand not masked, calls the handler with the signal number. -
Clears the bits that were handled.
-
Either calls
SYS_NOTIF_RETURNto resume the interrupted operation (if any handler hadSA_RESTART) or abandons it and returns aTRONA_INTERRUPTEDerror to the original caller.
The __sig_last_restart flag records whether the most recently dispatched signal had SA_RESTART set; the kernel consults it when deciding whether to re-run the interrupted syscall.
The dispatcher is registered once per thread via TCB_SET_NOTIFICATION_DISPATCHER during thread creation.
Threads that have not registered a dispatcher have their NotifFrame dropped — the kernel silently resumes them at the original blocked location.
SA_RESTART is intentionally not implemented
The source has a comment near SA_RESTART handling noting that it is stored but intentionally not acted on.
The rationale is that SaltyOS’s cooperative delivery model already makes most system calls restartable implicitly — the kernel does not interrupt a syscall in the middle unless the TCB was actually blocked, and basaltc’s C wrappers check for pending signals before every blocking call.
So SA_RESTART ends up being a no-op: either the signal would have been caught by posix_sigcheck before the syscall started, or the kernel delivered it mid-block and the dispatcher will decide whether to restart.
Writing SA_RESTART via sigaction is accepted and the bit is stored — so POSIX test suites that set it do not error — but the bit does not change runtime behavior.
This matches what FreeBSD does for many of its "advisory" signal flags.
Related pages
-
Procmgr Protocol Labels — the
PM*labels this page references. -
fork.S and Linker Scripts — the assembly trampoline for
posix_fork. -
POSIX Threads — thread lifecycle, including
pthread_killwhich also usesPM_KILL. -
basalt: Processes and Signals — the C-side view of this code.