From 2b2252dac514eb01f0599be93ced5bb4edaa9ee3 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 15:39:31 +0200 Subject: [PATCH] feat: extend signal --- include/signal.h | 217 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) diff --git a/include/signal.h b/include/signal.h index 7048fac..cfd6db1 100644 --- a/include/signal.h +++ b/include/signal.h @@ -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