Files
frostbyte/src/signal.c
T

403 lines
17 KiB
C

#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;
/* Real Unix's interruptible sleep/EINTR (deliberately out of scope
until now, see the file comment's history): a Sleeping/Blocked
target whose sleep timer or wait-channel condition would otherwise
never naturally fire again (e.g. user/eintrtarget.c's read that no
writer will ever satisfy) must still actually receive a signal that
would genuinely be delivered, not sit on it forever. Skipped
entirely for a blocked or ignored signal -- see sigWouldDeliver --
so a masked/ignored signal never disturbs a thread's sleep/wait at
all, matching real Unix. threadWakeForSignal itself refuses anything
other than Sleeping/Blocked (Stopped is excluded on purpose -- only
threadContinue may move it), so this is safe to call unconditionally
whenever sigWouldDeliver agrees. */
if (sigWouldDeliver(target, signum))
threadWakeForSignal(targetId);
return true;
}
bool sigRaise(uint32 targetId, int signum)
{
return sigRaiseInfo(targetId, signum, siCodeKernel, 0, 0);
}
bool sigRaiseGroup(uint32 pgid, int signum, int siCode, int siPid)
{
bool anyTarget = false;
for (uint32 id = 1; id < threadCapacity(); id++) {
Thread* candidate = threadAt(id);
if (candidate->state == threadDead || candidate->state == threadUnused)
continue;
if (candidate->pgid != pgid)
continue;
anyTarget = true;
sigRaiseInfo(id, signum, siCode, siPid, 0);
}
return anyTarget;
}
uint64 sigSetHandler(Thread* thread, int signum, uint64 handlerAddr, uint64 mask, uint64 flags)
{
if (signum <= 0 || signum >= sigMaxSignals)
return sigSetFailed;
if (signum == sigKill || signum == sigStop)
return sigSetFailed;
uint64 previous = thread->sigHandlers[signum];
thread->sigHandlers[signum] = handlerAddr;
/* Same stripping sigProcMask already does to a mask it is handed: a
mask cannot make sigKill/sigStop blockable just because it asked. */
mask &= ~(1ull << sigKill);
mask &= ~(1ull << sigStop);
thread->sigHandlerMasks[signum] = mask;
thread->sigHandlerFlags[signum] = flags;
return previous;
}
uint64 sigProcMask(Thread* thread, int how, uint64 mask)
{
uint64 previous = thread->sigBlocked;
mask &= ~(1ull << sigKill);
mask &= ~(1ull << sigStop);
switch (how) {
case sigMaskBlock:
thread->sigBlocked |= mask;
break;
case sigMaskUnblock:
thread->sigBlocked &= ~mask;
break;
case sigMaskSetMask:
thread->sigBlocked = mask;
break;
default:
break;
}
return previous;
}
SigOutcome sigConsumePending(Thread* thread, Registers* regs, int* outSignum)
{
if (thread->sigHandlerDepth >= sigMaxNestDepth)
return sigOutcomeNone;
for (int signum = 1; signum < sigMaxSignals; signum++) {
uint64 bit = 1ull << (uint32)signum;
if (!(thread->sigPending & bit) || (thread->sigBlocked & bit))
continue;
thread->sigPending &= ~bit;
uint64 handler = thread->sigHandlers[signum];
if (handler == sigHandlerIgnore ||
(handler == sigHandlerDefault && sigDefaultIsIgnore(signum)))
continue;
if (outSignum)
*outSignum = signum;
if (handler == sigHandlerDefault) {
if (sigDefaultIsStop(signum))
return sigOutcomeStop;
return sigOutcomeTerminate;
}
/* SA_RESTART / EINTR (see file comment): consumed here, exactly
once, regardless of which way the decision goes -- a signal
nested INSIDE the handler this delivery is about to enter is
interrupting that handler's own code, not a retryable syscall
retry point, so it must never see this as still true. Only
rewrites `regs` (the context about to be saved below) when this
thread was genuinely mid one of sysRead/sysWrite/sysWait's own
retry-on-block pattern AND this particular signal was not
registered with sigFlagRestart -- anything else (plain running
code, a sleep, an already-nested handler, or sigFlagRestart
itself) leaves `regs` completely untouched, preserving the
unconditional-retry behavior this kernel had before the flag
existed. */
bool interruptedRetry = thread->sigInRetryableSyscall;
thread->sigInRetryableSyscall = false;
if (interruptedRetry && !(thread->sigHandlerFlags[signum] & sigFlagRestart)) {
regs->rip += 2;
regs->rax = (uint64)(int64)errInterrupted;
}
/* Push the whole interrupted trapframe (now possibly rewritten
above) onto this thread's small context stack (the only copy of
it that exists anywhere -- unlike a real kernel this never pushes
one onto the user stack) so sigOnSigReturn can restore it byte
for byte, even if a DIFFERENT, unmasked signal nests another
handler on top before this one ever returns. */
uint32 depth = thread->sigHandlerDepth;
thread->sigSavedRegs[depth] = *regs;
thread->sigSavedBlocked[depth] = thread->sigBlocked;
thread->sigHandlerDepth = depth + 1;
/* sa_mask (sigHandlerMasks[signum], set by sigSetHandler) plus the
signal's own bit (self-blocked, like real Unix's implicit
default) are ADDED to whatever was already blocked -- a union,
not a replacement, so an outer handler's own blocked set stays
blocked for this nested one too. sigFlagNoDefer (signalnum.h)
opts out of exactly the self-block half of that union: a signal
registered with it does not gain its own bit here, so a second
raise of the SAME signum while this handler is still active
finds sigBlocked genuinely clear for it and nests, instead of
merely staying pending until this handler returns. Stripped of
sigKill/sigStop for the same reason every other mask operation in
this file is: neither ever consults sigBlocked at all, so this is
purely cosmetic, but leaving either bit set here would visibly
contradict "can never be blocked" for anything that inspects
sigBlocked. */
uint64 newBlocked = thread->sigBlocked | thread->sigHandlerMasks[signum];
if (!(thread->sigHandlerFlags[signum] & sigFlagNoDefer))
newBlocked |= bit;
newBlocked &= ~(1ull << sigKill);
newBlocked &= ~(1ull << sigStop);
thread->sigBlocked = newBlocked;
/* Rewrite `regs` in place to enter the shared trampoline: rip is
the trampoline's entry, rax is the handler address its
`call *%rax` invokes, rdi is signum (the handler's sole SysV
argument). regs->rsp (the interrupted user stack) is left
untouched -- the trampoline runs on the target's own stack,
right where it already was. */
regs->rax = handler;
regs->rdi = (uint64)signum;
regs->rip = sigTrampolineVirtualAddr;
/* sigFlagSigInfo (signalnum.h): copy the SigInfo sigRaiseInfo
attached to this signum into THIS (thread, depth) pair's own
slot of the shared sigInfoVirtualAddr page -- a different slot
than any still-active outer handler's own siginfo pointer, by
the exact same depth-indexed reasoning sigSavedRegs/
sigSavedBlocked already use, so a nested sigFlagSigInfo delivery
can never clobber an outer one's still-unread copy. rdx is
always 0 (no ucontext_t this kernel can expose). Left completely
untouched otherwise -- see this function's own doc comment. */
if (thread->sigHandlerFlags[signum] & sigFlagSigInfo && sigInfoPageCount > 0) {
uint32 slot = thread->id * sigMaxNestDepth + depth;
uint32 page = slot / sigInfoSlotsPerPage;
uint32 offset = (slot % sigInfoSlotsPerPage) * (uint32)sizeof(SigInfo);
SigInfo* dst = (SigInfo*)(sigInfoFrames[page] + offset);
*dst = thread->sigPendingInfo[signum];
regs->rsi = sigInfoVirtualAddr + (uint64)page * frameSize + offset;
regs->rdx = 0;
}
return sigOutcomeHandler;
}
return sigOutcomeNone;
}
bool sigOnSigReturn(Thread* thread, Registers* regs)
{
if (thread->sigHandlerDepth == 0)
return false;
uint32 depth = thread->sigHandlerDepth - 1;
*regs = thread->sigSavedRegs[depth];
thread->sigBlocked = thread->sigSavedBlocked[depth];
thread->sigHandlerDepth = depth;
return true;
}
void sigInstallTrampoline(void)
{
uint64 frame = pmmAlloc();
if (frame == 0)
return;
uint64 length = (uint64)(sigTrampolineEnd - sigTrampolineStart);
memset((void*)frame, 0, frameSize);
memcpy((void*)frame, sigTrampolineStart, length);
pagingMap(sigTrampolineVirtualAddr, frame, pagePresent | pageUser);
}
void sigInstallInfoPage(void)
{
uint64 totalSlots = (uint64)threadCapacity() * sigMaxNestDepth;
uint64 pagesNeeded = (totalSlots + sigInfoSlotsPerPage - 1) / sigInfoSlotsPerPage;
if (pagesNeeded > sigInfoMaxPages)
pagesNeeded = sigInfoMaxPages;
for (uint32 i = 0; i < pagesNeeded; i++) {
uint64 frame = pmmAlloc();
if (frame == 0)
return;
memset((void*)frame, 0, frameSize);
/* pageUser, no pageWrite: a handler may only ever READ the SigInfo
it was handed a pointer to, exactly like the trampoline page
above is read+execute, never write -- the kernel is the only
writer, via `frame`'s own physical address (see the frame
comment above sigInfoFrames), never through this user-facing
mapping. */
pagingMap(sigInfoVirtualAddr + (uint64)i * frameSize, frame, pagePresent | pageUser);
sigInfoFrames[i] = frame;
}
sigInfoPageCount = (uint32)pagesNeeded;
}