feat: extend signal

This commit is contained in:
dualfroz committed 2026-09-02 15:39:31 +02:00
1 parent 1ef1ab370a
commit 2b2252dac5
1 file changed
+217
+217
View File
@@ -149,3 +149,220 @@
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