Phase 19
Signal Handlers
Enable userspace programs to install and execute signal handlers. Implement the signal trampoline mechanism so ring-3 code can catch `SIGINT`, `SIGSEGV`, `SIGCHLD`, and other signals, run a user-supplied handler, and return cleanly to the interrupted execution without kernel assistance beyond the initial delivery and the `sigreturn` syscall.
Learning goal
Understand why the kernel must save the full interrupted register state before branching to a signal handler and how it reconstructs it on return.
Category
userspace
Components touched
Builds on
Unlocked by this phase
Why this phase matters
Enable userspace programs to install and execute signal handlers. Implement the signal trampoline mechanism so ring-3 code can catch SIGINT, SIGSEGV, SIGCHLD, and other signals, run a user-supplied handler, and return cleanly to the interrupted execution without kernel assistance beyond the initial delivery and the sigreturn syscall.
How it works in m3OS
The roadmap entry defines the learning scope for this phase; a deeper implementation doc has not been matched yet.
How it builds on earlier phases
This phase builds on Directory VFS and turns that foundation into Signal Handlers.
Implementation outline
- Define the
sigframelayout in the kernel (matches the Linuxucontext_t+siginfo_tlayout that musl expects): general-purpose registers,rflags,rip,rsp, the old signal mask, and the restorer address. - In
check_pending_signals(), after selecting a signal to deliver, check the process’sSignalDispositiontable for a user handler. If present, callsetup_signal_frame()instead of the default-action path. - Implement
setup_signal_frame(): read the current userRSP(or alt-stack base ifSS_ONSTACKand the signal hasSA_ONSTACKset), subtract the frame size, write thesigframestruct to user memory, then mutate the saved trap frame so thatIRET/SYSRETreturns to the handler. - Implement
sys_sigreturn: read thesigframepointer from the user stack (RSPat the time of the syscall), validate it is in user-space, copy saved registers back into the trap frame, and restore the old signal mask. - Implement
sys_rt_sigprocmask: updatetask.blocked_signalsaccording to thehowargument (SIG_BLOCK,SIG_UNBLOCK,SIG_SETMASK). Never allow blockingSIGKILLorSIGSTOP. - Extend
rt_sigactionto recordsa_restorer(theSA_RESTORERflag) and store it in theSignalActionentry; use this address as the return address written into thesigframe. - Implement
sys_sigaltstack(syscall 131): read / write thestack_tstruct in the process’s task block; set theSS_ONSTACKflag in the savedstack_twhile a handler using the alt stack is executing. - Write a test program that installs a
SIGINThandler viart_sigaction, raisesSIGINTviakill(getpid(), SIGINT), executes the handler, and returns; assert that execution continues after theraisecall. - Write a
SIGSEGVhandler test: map no guard page, overflow the stack, recover viasigaltstack-backed handler. - Audit
check_pending_signals()call sites: it must be invoked on every return to ring-3, including fromsys_read,sys_write,sys_waitpid, and any future blocking syscalls that can be interrupted by a signal (EINTRhandling). - Add a kernel-side assertion that
sigframewritten to user memory is 16-byte aligned (System V AMD64 ABI requires 16-byte stack alignment atCALL); misaligned frames cause SSE faults in musl startup code.
Task progress snapshot
No checklist tasks were discovered for this phase yet.
Documentation targets
- Documentation deliverables have not been listed yet.
Deferred follow-on work
- Real-time signals (
SIGRTMINthroughSIGRTMAX) and thesigqueueAPI signalfd— receiving signals as readable file descriptors- FPU / SSE / AVX state save and restore in the
sigframe - Per-thread signal masks and thread-directed signal delivery (requires
clone) SA_NODEFER,SA_RESETHANDflag semanticsSIGALRMandtimer_createtimer signalsptrace-stop signals
Source materials
Important code
Code spotlight cards
Code spotlight
The userspace/signal-test/signal-test.c binary validates four aspects:
The userspace/signal-test/signal-test.c binary validates four aspects:
Key files
- userspace/signal-test/signal-test.c — The userspace/signal-test/signal-test.c binary validates four aspects:
How real OSes differ
- Linux maintains a full `sigcontext` embedded inside `ucontext_t` for every supported architecture, including FPU / SSE / AVX state (managed via `XSAVE`).
- The in-kernel `copy_siginfo_to_user` and related helpers handle dozens of signal sources with different `siginfo` payloads.
- Linux also supports real-time signals (`SIGRTMIN` through `SIGRTMAX`) with a queued delivery model and `sigqueue` for passing an integer or pointer payload alongside the signal.
- This phase implements only the standard-signal subset with no FPU state save and no real-time signal queue.
Success criteria
- A statically linked musl binary that installs a `SIGINT` handler, calls `raise(SIGINT)`, prints inside the handler, and continues execution after `raise` returns runs correctly.
- `SIGSEGV` delivered to a process with a `sigaltstack`-backed handler executes the handler and does not triple-fault.
- A handler does not re-enter itself when the same signal fires during handler execution (blocked by automatic masking).
- `rt_sigprocmask(SIG_BLOCK, ...)` prevents delivery of the blocked signal until `SIG_UNBLOCK` is called; the blocked signal is held as pending and delivered immediately on unblock.
- After `sigreturn`, the process resumes at the exact instruction that was interrupted, with all registers restored to their pre-signal values.
- `SIGKILL` and `SIGSTOP` cannot be blocked or caught; `rt_sigaction` returns `EINVAL` for both.
- Nested signals with distinct numbers are handled correctly: signal A fires during signal B's handler (B is not in `sa_mask` of A), both handlers run, both frames are restored in reverse order.
- The kernel shell (Phase 9 / ring-0) continues to handle `SIGINT` via its existing default-action path; no regression.
- `sigaltstack` stack is marked `SS_ONSTACK` while the handler runs and cleared on `sigreturn`; a second call to `sigaltstack` while `SS_ONSTACK` returns `EPERM`.
Related phases
Keep exploring the roadmap
Phase 18
CompleteDirectory VFS
Make the filesystem navigable. Right now `getdents64` returns ENOSYS, directories cannot be opened as file descriptors, every process thinks its working directory is `/`, and the ramdisk is a flat array with no directory hierarchy. This phase fixes all of that: directories become first-class objects, paths resolve relative to a real per-process working directory, and the ramdisk gains a tree structure so binaries live under `/bin` instead of floating in a flat namespace.
Phase 20
CompleteUserspace Init Shell
Replace the kernel-resident `init_task` and `shell_task` ring-0 functions with real ring-3 userspace processes. After this phase PID 1 is a `no_std` Rust binary loaded from the ramdisk; the interactive shell is a userspace ELF that init spawns. The kernel is no longer responsible for parsing commands or managing the interactive session.