diff --git a/src/signal.c b/src/signal.c new file mode 100644 index 0000000..a5f144c --- /dev/null +++ b/src/signal.c @@ -0,0 +1,156 @@ +#include "signal.h" +#include "string.h" +#include "pmm.h" +#include "paging.h" + +/* Machine code for the shared trampoline page, sigTrampoline.S -- position + independent by construction (no absolute address references at all), so + copying its bytes anywhere and mapping them there is enough. */ +extern char sigTrampolineStart[]; +extern char sigTrampolineEnd[]; + +/* Physical frame addresses backing sigInfoVirtualAddr's page(s), filled in + by sigInstallInfoPage -- kept as separate frames (not assumed physically + contiguous, unlike their virtual mapping, which IS contiguous by + construction: pmmAlloc gives no contiguity guarantee across calls, so + sigConsumePending must look up the right frame for a given slot rather + than pointer-arithmetic across a single base). Physical frames below the + 1 GiB boot-time identity map (see sigTrampolineVirtualAddr's own comment) + are directly dereferenceable through their own physical value from + kernel code, exactly like sigInstallTrampoline's memcpy into `frame` + above already relies on. */ +static uint64 sigInfoFrames[sigInfoMaxPages]; +static uint32 sigInfoPageCount = 0; + +/* How many SigInfo structs fit in one frameSize page -- shared by + sigInstallInfoPage's sizing loop and sigConsumePending's slot lookup, so + the two can never disagree about the layout. */ +#define sigInfoSlotsPerPage (frameSize / sizeof(SigInfo)) + +void sigResetState(Thread* thread) +{ + thread->sigPending = 0; + thread->sigBlocked = 0; + thread->sigHandlerDepth = 0; + thread->sigInRetryableSyscall = false; + memset(thread->sigHandlers, 0, sizeof(thread->sigHandlers)); + memset(thread->sigHandlerMasks, 0, sizeof(thread->sigHandlerMasks)); + memset(thread->sigHandlerFlags, 0, sizeof(thread->sigHandlerFlags)); + memset(thread->sigPendingInfo, 0, sizeof(thread->sigPendingInfo)); + memset(thread->sigSavedBlocked, 0, sizeof(thread->sigSavedBlocked)); + memset(thread->sigSavedRegs, 0, sizeof(thread->sigSavedRegs)); +} + +/* Real-world default disposition for signals this kernel actually defines + that is Ignore rather than Terminate -- SIGCHLD (no handler installed: + nothing happens) and SIGCONT (its actual resume effect already happened + unconditionally in sigRaise -- see file comment -- so "no handler" here + just means no handler runs, not that nothing happened at all). SIGSTOP + never reaches this function to begin with (sigRaise bypasses the whole + scan for it, like sigKill), so it is deliberately absent from this list, + the same way sigKill already was. SIGTSTP is NOT here -- its default is + Stop, not Ignore, see sigDefaultIsStop below. Every other defined signal + defaults to Terminate. */ +static bool sigDefaultIsIgnore(int signum) +{ + switch (signum) { + case sigChld: + case sigCont: + return true; + default: + return false; + } +} + +/* Real-world default disposition that is Stop rather than Terminate: + SIGTSTP only. Unlike SIGSTOP (see file comment / sigRaise), SIGTSTP is + fully catchable/blockable/ignorable -- this default only applies when it + is neither, exactly like sigDefaultIsIgnore's entries. */ +static bool sigDefaultIsStop(int signum) +{ + return signum == sigTstp; +} + +/* Whether `signum` would genuinely be delivered (a real handler OR a + default disposition of Terminate/Stop -- anything sigConsumePending + itself would act on, not silently dismiss) if `thread` returned to ring 3 + right now. Deliberately mirrors sigConsumePending's own + blocked/ignored-dismissal logic for exactly one signal number rather than + sharing code with it -- sigConsumePending also mutates sigPending/ + sigHandlerDepth/the trapframe as it scans, none of which this pure + predicate may do, since it runs from sigRaise against a target that is + not currently resuming at all. Used solely to decide whether a Sleeping/ + Blocked target needs early-waking (threadWakeForSignal, sched.c) for this + signal -- if this ever drifted out of sync with sigConsumePending's own + logic, the failure mode is always benign either way: "wakes for a signal + that turns out not to be delivered" is a harmless spurious wake (the + target's own retry-syscall loop just re-blocks, exactly like a real + spurious wakeup), and "does not wake for one that would have delivered" + just falls back to today's behavior of waiting for a natural wake. */ +static bool sigWouldDeliver(const Thread* thread, int signum) +{ + uint64 bit = 1ull << (uint32)signum; + if (thread->sigBlocked & bit) + return false; + + uint64 handler = thread->sigHandlers[signum]; + if (handler == sigHandlerIgnore || + (handler == sigHandlerDefault && sigDefaultIsIgnore(signum))) + return false; + + return true; +} + +bool sigRaiseInfo(uint32 targetId, int signum, int siCode, int siPid, int siStatus) +{ + if (signum <= 0 || signum >= sigMaxSignals) + return false; + + /* SIGKILL never touches sigPending/sigBlocked at all -- it is + delegated straight to the pre-existing unconditional termination + path, exactly like real kill(pid, SIGKILL) bypassing every handler/ + mask a target could have installed. threadKill does its own id + validation (range, state), so none is duplicated here. Neither it + nor SIGSTOP just below ever reaches a sigHandlers[] entry that could + even be sigFlagSigInfo, so the caller-supplied SigInfo fields are + simply never used on either of these two paths. */ + if (signum == sigKill) + return threadKill(targetId); + + /* SIGSTOP: uncatchable, unblockable, unignorable, exactly like SIGKILL + above -- bypasses sigPending/sigHandlers/sigBlocked entirely and acts + on ThreadState directly. threadStop does its own id validation. */ + if (signum == sigStop) + return threadStop(targetId); + + if (targetId == 0 || targetId >= threadCapacity()) + return false; + + Thread* target = threadAt(targetId); + if (target->state == threadDead || target->state == threadUnused) + return false; + + /* SIGCONT's resume is likewise not something a mask or a missing + handler can defer -- called unconditionally before the generic + pending-bit path below, so a Stopped target is moved back to + threadRunnable regardless of whether it also has a handler + installed. Its return value is intentionally ignored: "target + was not actually Stopped" is a harmless no-op at the process-state + level (real SIGCONT to a running process), not a reason to skip + delivering the signal itself for any installed handler. */ + if (signum == sigCont) + threadContinue(targetId); + + target->sigPending |= (1ull << (uint32)signum); + + /* Attached regardless of whether signum's handler actually has + sigFlagSigInfo set (sigConsumePending is what decides whether this + ever gets read at all) -- cheap (4 ints), and simpler than threading + the handler's own flags in here just to skip it, matching how + sigHandlerMasks/sigHandlerFlags are likewise set unconditionally by + sigSetHandler and only ever consulted conditionally. */ + target->sigPendingInfo[signum].siSignum = signum; + target->sigPendingInfo[signum].siCode = siCode; + target->sigPendingInfo[signum].siPid = siPid; + target->sigPendingInfo[signum].siStatus = siStatus; +