Files
frostbyte/include/signal.h
T
2026-09-02 15:39:31 +02:00

369 lines
23 KiB
C

#ifndef FROSTBYTE_SIGNAL_H
#define FROSTBYTE_SIGNAL_H
#include "types.h"
#include "isr.h"
#include "sched.h"
#include "signalnum.h"
/*
* Real (catchable) signal delivery on top of the single-address-space,
* ring0/ring3 kernel: a thread can install a handler for a signal number,
* mask/unmask signals, and have another thread (or itself) raise one --
* delivery happens the moment the target thread next returns to ring3
* through the interrupt-return path (isr.c's deliverPendingSignals), by
* rewriting its saved trapframe to enter a small shared trampoline page
* (sigInstallTrampoline below) instead of resuming where it was
* interrupted. The trampoline calls the handler, then traps back in via
* sysSigReturn, which restores the exact pre-signal register snapshot this
* kernel keeps in the Thread struct itself (sigSavedRegs) -- no signal
* frame is ever pushed onto the user stack, since there is no need to: the
* saved context already lives in kernel memory, unlike a real kernel with
* per-process address spaces where the handler must run in a stack the
* kernel does not otherwise own a copy of.
*
* Job control (SIGSTOP/SIGCONT/SIGTSTP) is real, on top of a genuine
* threadStopped ThreadState (sched.h): SIGSTOP, like SIGKILL, bypasses the
* pending/blocked/handler machinery entirely -- sigRaise delegates it
* straight to threadStop (sched.c), so it cannot be caught, blocked, or
* ignored, matching real Unix. SIGCONT is NOT bypassed the same way: it
* still sets its pending bit and runs through the normal scan (so an
* installed handler for it genuinely fires), but sigRaise ALSO
* unconditionally calls threadContinue as a side effect, independent of
* sigBlocked -- the resume itself is not something a handler or a mask can
* defer, only whether a handler additionally runs is. SIGTSTP is the
* catchable half of job control: its real-world default disposition is
* Stop rather than Terminate (sigDefaultIsStop), reported as
* sigOutcomeStop for isr.c to act on the same way it already acts on
* sigOutcomeTerminate, but -- unlike SIGSTOP -- an installed handler for it
* runs instead of stopping, and it can be blocked/ignored like any other
* catchable signal.
*
* Per-signal sa_mask (POSIX sigaction's sa_mask, not sigprocmask's whole-
* process mask): sigSetHandler takes a `mask` alongside the handler address,
* stored in Thread.sigHandlerMasks[signum] (sched.h) -- while that signum's
* handler is running, sigBlocked is the union of whatever was already
* blocked, this mask, and (unless sigFlagNoDefer -- signalnum.h -- is set
* for it) the signal itself, self-blocked like real Unix's implicit
* default. A DIFFERENT, unmasked signal arriving while a handler runs is
* therefore genuinely deliverable, not blanket-blocked the way an earlier
* version of this file (see git history) used to: sigConsumePending is
* gated by Thread.sigHandlerDepth against sigMaxNestDepth (sched.h), not by
* a single "one handler at a time" bool, so a second handler can run
* NESTED inside the first (its own context pushed onto the small
* sigSavedRegs/sigSavedBlocked stack) and the first resumes, via
* sigOnSigReturn popping that same stack, exactly where it left off once
* the nested one returns. user/nesttarget.c is the live ring-3 proof for
* two DIFFERENT signals; user/nodefertest.c is the same mechanism proving
* the SAME signal number can nest into its own still-active handler when
* registered with sigFlagNoDefer (and, by contrast, genuinely cannot
* without it); sigTest.c drives the stack directly for fully deterministic
* versions of both.
*
* Early wake of a Sleeping/Blocked target (real Unix's interruptible
* sleep/EINTR): sigRaise calls sched.c's threadWakeForSignal for any
* signal signal.c's sigWouldDeliver determines would genuinely be
* delivered (not blocked, not ignored) -- forcing the target straight to
* threadRunnable so the NEXT schedule() eventually resumes it, reaching a
* return-to-ring3 point (isr.c's deliverPendingSignals) it might otherwise
* NEVER naturally reach again (e.g. user/eintrtarget.c's read, which no
* writer will ever satisfy). A blocked or ignored signal leaves a
* Sleeping/Blocked target's wait completely undisturbed, exactly like real
* Unix -- sigWouldDeliver is checked first. threadWakeForSignal itself
* excludes threadStopped on purpose: job control's "stopped means stopped"
* must survive an ordinary signal arriving too, so only threadContinue (an
* explicit SIGCONT) may ever move a Stopped thread, the same asymmetry
* schedWakeChannel already has for the same reason.
*
* SA_RESTART / EINTR (signalnum.h's sigFlagRestart, sched.h's
* Thread.sigInRetryableSyscall): being woken early (or simply signaled
* while already Blocked) answers "does the target wake up at all", not
* "what does the interrupted syscall return once its handler is done" --
* that second question is what sa_flags settles. Before this existed,
* every retryable syscall (sysRead/sysWrite/sysWait, syscall.c) restarted
* unconditionally once a handler returned, since sysSigReturn always
* restores the exact rewound rip sigConsumePending captured. Now
* sigConsumePending checks, at the moment it is about to push a custom
* handler's context, whether the thread is genuinely mid one of those three
* (sigInRetryableSyscall) and the signal being delivered lacks
* sigFlagRestart for its own handler; if so, it rewrites the ABOUT-TO-BE-
* SAVED context in place (rip past the interrupted `int $0x80`, rax to
* errInterrupted) before pushing it, so sigOnSigReturn's later, byte-for-
* byte-faithful restore resumes the caller having genuinely observed an
* interrupted syscall, not a silently retried one -- real EINTR, not an
* approximation of it. user/restarttarget.c is the live ring-3 proof of
* both halves (with and without the flag); sigTest.c drives
* sigConsumePending directly for the deterministic one.
*
* A thread self-targeting SIGSTOP through sysSigSend (syscall.c) is
* reasoned about and exercised by
* schedTest.c's state-machine coverage of threadStop accepting
* threadRunning, but -- unlike the external-stop/continue scenario
* user/jobtest.c demonstrates live -- has no live ring-3 demo of its own
* this session: proving it would need a second, independent process to
* race-freely learn "the target has now actually stopped" before sending
* SIGCONT, which is exactly the kind of guess-free rendezvous this file's
* OWN sigtarget.c comment already had to solve once for a different race,
* and doing it again here was judged not worth the added live-boot
* surface for a self-stop path whose actual mechanism (threadStop, called
* on the current thread) is not otherwise unusual. SIGKILL is intentionally
* NOT routed through this pending/blocked machinery at all: sigRaise
* delegates it straight to the existing threadKill (sched.c), the one
* unconditional, unmaskable, uncatchable termination path this kernel
* already had (now also accepting threadStopped, since a real
* kill(pid, SIGKILL) kills a stopped process too) -- exactly like
* real kill(pid, SIGKILL) never goes through a handler either.
*/
/* Signal numbers, handler sentinels, sigSetFailed and the sigMask* "how"
values all live in signalnum.h, shared verbatim with userspace (see that
header for why it is split out). */
/* sigMaxSignals (one bit per signal number in Thread.sigPending/
sigBlocked, and the size of Thread.sigHandlers[]) is defined in sched.h,
next to the Thread struct that actually needs it as an array bound --
signal.h cannot define it itself without sched.h including signal.h
right back, which would be circular. */
/* Every signal bit set -- a plain "block literally everything" convenience
constant for callers/tests that want to exercise sigProcMask's own
sigKill/sigStop stripping, or job control's "resume happens regardless of
sigBlocked" guarantee, with the widest possible mask. Not used by
sigConsumePending's own handler-entry logic any more (see the file
comment's per-signal sa_mask paragraph) -- that is real, per-handler
Thread.sigHandlerMasks[signum] now, not this one blanket value. */
#define sigAllBlocked (~0ull)
/* Fixed virtual address of the one shared, read+execute, never-write,
user-accessible trampoline page every process's signal handlers are
entered through (sigInstallTrampoline maps it once at boot). Chosen
deliberately far from both the boot-time 1 GiB identity-mapped range
([0, 0x40000000), see boot.S's bootPd -- pagingMap refuses to touch
anything inside a huge-page-mapped range anyway) and the per-binary
elfSlotSize=0x10000000 slots USER_BASE hands out one at a time starting
at 0x40000000 (syscall.c's userSpaceMin) -- at the current growth rate
(Makefile) that sequence would need roughly three thousand more user
binaries to ever reach here, and every process already shares this one
address space regardless (see the architecture note in sched.h), so one
fixed shared page for ALL of them, like a real kernel's vDSO, is exactly
as much mapping as this needs. */
#define sigTrampolineVirtualAddr 0x0000700000000000ull
/* Fixed virtual address of the shared, read-only (never write, never
execute), user-accessible SigInfo (signalnum.h) data page(s) every
process's sigFlagSigInfo handlers are handed a pointer into --
sigInstallInfoPage maps it once at boot, right after the trampoline page
above, in the same reserved high range for the same reasons that page's
own comment already gives (nowhere near either the boot-time identity
map or any real per-binary elfSlotSize slot). One frameSize-sized page
holds frameSize/sizeof(SigInfo) slots; sigInstallInfoPage maps as many
consecutive pages, starting here, as threadCapacity()*sigMaxNestDepth
slots actually need (today: 96*4 = 384 slots, comfortably inside 2
pages) -- see sigConsumePending for how a (thread id, handler-nesting
depth) pair maps to one specific slot, the same indexing scheme
sigSavedRegs/sigSavedBlocked (sched.h) already use for the exact same
pair, just kept in a user-readable page here instead of Thread itself
since a handler running in ring 3 has no way to reach into kernel
memory. */
#define sigInfoVirtualAddr 0x0000700000010000ull
/* How many consecutive frameSize pages, starting at sigInfoVirtualAddr, a
fixed-size static array of physical frame addresses (signal.c) can hold
-- sigInstallInfoPage only ever maps as many as threadCapacity()*
sigMaxNestDepth slots actually need (2 today), so this is deliberately
generous headroom, not a tight fit: maxThreads (sched.c) has grown by a
fixed +8 roughly once per session for a long history of falas, and this
comfortably absorbs many more such bumps (8 pages * frameSize/sizeof
(SigInfo) slots-per-page / sigMaxNestDepth == room for hundreds of
threads) before ever needing to be revisited, the same "generous
headroom, not exact" reasoning sigMaxNestDepth's own comment (sched.h)
already gives for itself. */
#define sigInfoMaxPages 8
typedef enum {
sigOutcomeNone = 0,
sigOutcomeHandler = 1,
sigOutcomeTerminate = 2,
/* Default disposition of a catchable job-control signal (SIGTSTP only
-- SIGSTOP never reaches sigConsumePending at all, see the file
comment). Same contract as sigOutcomeTerminate: this function only
REPORTS it, the caller (isr.c) must actually call threadStop and
reschedule, since only schedule() can produce the next thread's own
rsp to resume with. */
sigOutcomeStop = 3
} SigOutcome;
/* Resets ALL signal state on a Thread slot to fresh defaults: no pending or
blocked signals, no installed handlers or their masks, handler-nesting
depth zero. Called by threadCreate/threadCreateUser (sched.c) on every
(re)use of a table slot,
the same explicit-field-by-field convention threadCreate/threadCreateUser
already use for every other Thread field -- threadReap/threadDiscard's
own memset() of the whole struct already guarantees this before a slot
is ever reused, so this is defensive documentation of the invariant, not
a fix for a leak that could otherwise happen (unlike the console-reader/
pipe-fd leaks this subsystem's sibling code had to actually fix, which
lived in tables OUTSIDE Thread that no memset here ever touched). */
void sigResetState(Thread* thread);
/* Raises signal `signum` against thread `targetId`. sigKill and sigStop are
both delegated straight to threadKill/threadStop (see file comment) and
never touch sigPending at all. sigCont ALSO calls threadContinue as an
unconditional side effect (resuming a Stopped target regardless of
sigBlocked) but, unlike sigKill/sigStop, still falls through to set its
own pending bit afterwards, so an installed handler still runs through
the normal scan. Any other signum just sets the pending bit, then --
if sigWouldDeliver agrees this signal is not blocked or ignored --
calls threadWakeForSignal (sched.c) to force a Sleeping/Blocked target
straight to Runnable early, real Unix's interruptible sleep/EINTR (see
file comment); actual delivery itself still only ever happens later,
off the interrupt-return path (isr.c), once that thread genuinely
resumes. Returns false, changing nothing, for an out-of-range signum, an
invalid/out-of-range targetId, or a target that is Dead/Unused (mirrors
threadKill's own validation for the non-SIGKILL path, and threadKill's/
threadStop's own for their delegated paths). */
bool sigRaise(uint32 targetId, int signum);
/* Exactly sigRaise above, plus attaching a real SigInfo (signalnum.h) to
this specific raise -- `siCode`/`siPid`/`siStatus` become
target->sigPendingInfo[signum] (sched.h) the moment the pending bit
itself is set, later handed to a sigFlagSigInfo handler by
sigConsumePending. sigRaise itself is now a thin wrapper around this
(siCodeKernel, 0, 0 -- "no specific detail available/tracked", the
honest default for the many pre-existing callers, including sigTest.c's
whole KTEST suite, that only ever cared about signum/pending-bit
semantics and never plumbed real sender/child detail through). Real
production call sites needing genuine detail (sched.c's SIGALRM/SIGCHLD
raises, syscall.c's sysSigSend) call this directly instead. Same return
value, same validation, same sigKill/sigStop delegation, same early-wake
side effect as sigRaise -- see that function's own doc comment for all
of it, not repeated here. */
bool sigRaiseInfo(uint32 targetId, int signum, int siCode, int siPid, int siStatus);
/* Real kill(-pgid, signum)/kill(0, signum)'s group-targeted half (sched.h's
Thread.pgid; userSigSend/userKill's negative/zero-pid convention,
syscall.c's sysSigSend/sysKill) and keyboard.c's own real Ctrl-C/Ctrl-Z
(SIGINT/SIGTSTP delivered to the FOREGROUND process group only, never a
single target thread): scans every currently live thread once and calls
sigRaiseInfo above against each one whose pgid matches, independently --
SIGKILL/SIGSTOP/SIGCONT's own special-cased bypasses, per-target
early-wake, and SigInfo attachment therefore all apply exactly as they
already do for a plain, single-target send, simply repeated once per
group member, rather than this function sharing or reimplementing any of
that delivery logic itself. A pgid with no currently live member at all
(nobody has ever claimed the foreground yet, or every process that once
shared it has since died) is a harmless no-op, matching real kill(2)'s
ESRCH for an empty target set -- reported back as false, exactly like a
single-target sigRaiseInfo already reports false for an invalid/dead
target. */
bool sigRaiseGroup(uint32 pgid, int signum, int siCode, int siPid);
/* Installs `handlerAddr` (a real user-code address, or sigHandlerDefault /
sigHandlerIgnore) as thread's disposition for `signum`, `mask` as its
sa_mask -- the additional signals sigConsumePending blocks for as long as
THIS handler is active (stripped of sigKill/sigStop first, same as
sigProcMask: neither can ever be blocked, so a mask cannot "achieve" it
either) -- and `flags` (signalnum.h's sigFlagRestart, or 0) as its
sa_flags. Returns the PREVIOUS disposition (like real signal(2)/
sigaction(2)'s oldact->sa_handler) or sigSetFailed for an out-of-range
signum or an attempt to touch sigKill or sigStop (both uncatchable, like
real EINVAL on SIGKILL/SIGSTOP) -- `mask`/`flags` are stored regardless of
whether `handlerAddr` is a real handler or a sentinel, but only ever
consulted for a real one (sigHandlerDefault/sigHandlerIgnore never reach
the custom-handler branch of sigConsumePending). */
uint64 sigSetHandler(Thread* thread, int signum, uint64 handlerAddr, uint64 mask, uint64 flags);
/* Blocks/unblocks/replaces thread's blocked-signal mask according to `how`
(sigMaskBlock/sigMaskUnblock/sigMaskSetMask; any other value is a no-op),
returning the mask as it was BEFORE this call (like real sigprocmask's
oldset). sigKill's and sigStop's bits are silently stripped from `mask`
first -- neither can ever be blocked, matching real Unix (sigCont CAN be
blocked here, since only whether its handler runs is gated by
sigBlocked -- the resume side effect in sigRaise never consults it). */
uint64 sigProcMask(Thread* thread, int how, uint64 mask);
/* The delivery decision, called from isr.c's deliverPendingSignals on
every return-to-ring3 point for whichever thread is about to resume.
Refuses to deliver anything (returns sigOutcomeNone, touching nothing)
once thread->sigHandlerDepth already reached sigMaxNestDepth -- the small
fixed context stack is full, so whatever is pending simply waits for a
sigOnSigReturn to free a slot; this is the only depth-independent gate,
unlike the old single "one handler at a time" bool it replaced. Otherwise
scans sigPending & ~sigBlocked from signum 1 upward. An ignored signal
(explicit sigHandlerIgnore, or sigHandlerDefault whose real-world default
is Ignore -- see sigDefaultIsIgnore in signal.c) is dismissed and the
scan continues, so it can never mask a real one behind it. Otherwise:
default disposition -> sigOutcomeStop for sigTstp (sigDefaultIsStop) or
sigOutcomeTerminate for everything else (caller must actually stop/
terminate the thread, e.g. via threadStop/threadExit, and reschedule --
this function never does, since it has no way to hand back a new rsp).
Custom handler -> the interrupted context is pushed onto
thread->sigSavedRegs[depth]/sigSavedBlocked[depth] (depth being
sigHandlerDepth's value on entry, then incremented), sigBlocked becomes
the union of what it already was, sigHandlerMasks[signum] (the sa_mask
sigSetHandler stored), and signum's own bit (self-blocked, stripped of
sigKill/sigStop like every other mask operation in this file), and `regs`
is rewritten in place to enter the trampoline page (rip), call the
handler (rax) with signum as its sole argument (rdi) -- sigOutcomeHandler.
If sigHandlerFlags[signum] also has sigFlagSigInfo set, thread->
sigPendingInfo[signum] (whatever sigRaiseInfo last stashed there for this
signum, or sigRaise's own siCodeKernel/0/0 default) is additionally
copied into this (thread id, depth) pair's slot in the shared
sigInfoVirtualAddr page (signal.h), and rsi/rdx are rewritten too -- rsi
to that slot's real user-visible address, rdx to 0 (no ucontext_t this
kernel can expose) -- so the handler genuinely receives (signum,
SigInfo*, NULL) exactly as sigaction()'s SA_SIGINFO promises. Without the
flag, rsi/rdx are left completely untouched (whatever they already held),
exactly as before this existed, since a plain one-argument handler never
reads either. Before any of that pushing happens, thread->sigInRetryableSyscall is
consumed (read, then unconditionally cleared): if it was true (this
thread is genuinely mid a sysRead/sysWrite/sysWait retry, see sched.h)
and signum's sigHandlerFlags lacks sigFlagRestart, `regs` -- the
interrupted context about to be saved -- is rewritten first (rip past
the interrupted `int $0x80`, rax to errInterrupted) so the eventual
sigOnSigReturn resumes the caller having observed a genuine EINTR-style
failure instead of transparently retrying; sigFlagRestart (or
sigInRetryableSyscall already being false, e.g. a signal that interrupts
plain running code or a nested handler) leaves `regs` untouched here,
preserving today's transparent-retry behavior exactly.
A DIFFERENT signal not covered by that union can still be delivered on a
later call while this one is active -- pushed onto depth+1 as a genuinely
NESTED handler, sigOnSigReturn popping it back off once it returns, the
interrupted (still mid-handler) context resuming exactly where it was.
Either way `*outSignum` (if non-null) is set to whichever signal was
consumed. sigStop itself never appears here at all (see file comment --
sigRaise bypasses this whole scan for it). */
SigOutcome sigConsumePending(Thread* thread, Registers* regs, int* outSignum);
/* Undoes exactly what the sigOutcomeHandler branch of sigConsumePending did
for the MOST RECENTLY entered still-active handler (LIFO, like a real call
stack of signal frames): pops thread->sigHandlerDepth, restoring `*regs`
from thread->sigSavedRegs[depth] and thread->sigBlocked from
thread->sigSavedBlocked[depth] at the popped index -- resuming the
interrupted program (which may itself be a still-active OUTER handler, if
this was a nested one) at the exact instruction, with the exact
registers, it had before that signal arrived, as if it had never been
interrupted at all.
Returns false, changing nothing, if the thread was not actually inside a
handler (a user program calling the sigreturn syscall on its own, never
having been signaled, is a misuse this kernel simply ignores rather than
trusting). */
bool sigOnSigReturn(Thread* thread, Registers* regs);
/* Allocates one physical frame, fills it with the trampoline's machine
code (sigTrampolineStart..sigTrampolineEnd, sigTrampoline.S -- position
independent by construction, see that file), and maps it read+execute+
user (never write) at sigTrampolineVirtualAddr. Called exactly once from
kmain.c, before any user thread can possibly receive a signal. */
void sigInstallTrampoline(void);
/* Allocates and maps sigInfoVirtualAddr's data page(s) -- see that
constant's own comment for the layout and sizing. Called exactly once
from kmain.c, alongside sigInstallTrampoline (before any user thread can
possibly receive a sigFlagSigInfo delivery), and before ktestRunAll so
sigTest.c's own deterministic coverage can read the same shared page a
live ring-3 handler would. */
void sigInstallInfoPage(void);
#endif