feat: extend isr with deliverPendingSignals and tryResolveUserPageFault

This commit is contained in:
dualfroz committed 2026-09-02 22:28:12 +02:00
1 parent 8df641fbf7
commit f59833733f
1 file changed
+118 -4
+118 -4
View File
@@ -5,6 +5,8 @@
#include "keyboard.h"
#include "sched.h"
#include "ports.h"
#include "signal.h"
#include "mmap.h"
static const char* exceptionNames[32] = {
"divide by zero",
@@ -52,16 +54,126 @@ static void panicOnException(Registers* regs)
__asm__ volatile("cli; hlt");
}
/* Runs at every single return-to-ring3 point this kernel has (a completed
syscall, a timer reschedule, any other IRQ resuming whoever it
interrupted) -- the one choke point isrDispatch funnels its result
through below, regardless of which of those three paths produced it.
`rsp` is whatever Registers* the caller is about to resume; if that
context is not ring 3 (a kernel thread, e.g. workerA/shell -- see
sched.h's userCodeSelector|3 vs kernelCodeSelector), signals are not a
concept that applies to it at all and this is a no-op. Otherwise
currentThread() is guaranteed to be exactly the owner of `rsp` (schedule()
already updated currentIndex before returning it; the syscall/other-IRQ
paths never changed currentIndex away from whoever trapped in), so
sigConsumePending always inspects the right thread's pending/blocked
state. A default-disposition Terminate (or job-control Stop, sigTstp's
default -- see signal.h) signal cannot be handled here directly -- it
needs a NEW thread's rsp, which only schedule() can produce -- so the
loop below reuses the exact same "flip state, then reschedule" pattern
sysExit/sysSleep already use (threadExit for Terminate, threadStop for
Stop), and loops again in case the freshly chosen thread also has a
signal of its own waiting (e.g. two threads both made Runnable by the
same timer tick and both signaled). */
static uint64 deliverPendingSignals(uint64 rsp)
{
for (;;) {
Registers* regs = (Registers*)rsp;
if ((regs->cs & 3) != 3)
return rsp;
Thread* thread = currentThread();
int signum = 0;
SigOutcome outcome = sigConsumePending(thread, regs, &signum);
if (outcome == sigOutcomeNone)
return rsp;
if (outcome == sigOutcomeHandler)
return (uint64)regs;
if (outcome == sigOutcomeStop) {
kprintf("[sig] pid=%u '%s' stopped by signal %d\n",
thread->id, thread->name, signum);
threadStop(thread->id);
rsp = schedule(rsp);
continue;
}
kprintf("[sig] pid=%u '%s' terminated by uncaught signal %d\n",
thread->id, thread->name, signum);
threadExit(128u + (uint64)signum);
rsp = schedule(rsp);
}
}
/* Attempts to resolve a ring-3 #PF through mmap.c's own demand-paging
machinery: cr2 is the real faulting address (same register the panic
path above already reads), errCode bit 1 (0x2) is the CPU's own
"this was a write access" flag -- the exact distinction
mmapFaultHandler needs to refuse a write against a read-only mapping
instead of silently allowing it. Returns whatever mmapFaultHandler
itself returns: true means a real frame is now mapped and the faulting
instruction can simply be retried (the CPU never advanced rip past a
#PF to begin with); false means this address is not a mapping this
kernel's mmap.c even knows about, or the access violates its own prot
bits -- the caller (isrDispatch below) turns that into a real SIGSEGV
rather than the kernel-wide panic every OTHER exception still gets. */
static bool tryResolveUserPageFault(Registers* regs)
{
/* errCode bit 4 (I/D) is the CPU's own "this fault was an instruction
fetch" flag -- only ever set once paging.h's pageNoExecute is real
hardware enforcement (IA32_EFER.NXE, see paging.c's
pagingNxSupported/boot.S), meaning a PRESENT page's own NX bit
refused execution. This can never be something mmapFaultHandler's
demand-paging logic fixes: there is no lazy version of "make this
page executable" the way there is for a not-yet-faulted-in page, and
mmapFaultHandler's own "pagingResolve != 0 -> already resolved, just
retry" shortcut (mmap.c) would otherwise see the very same present
page, report success, and send the CPU straight back to retry the
identical faulting instruction fetch -- an infinite fault loop that
hangs the whole kernel rather than the one offending thread. Refuse
outright instead; isrDispatch below turns this into a real SIGSEGV
exactly like any other refusal, whether the address belongs to an
mmap Vma, or (mmapFind would never even find it) a stack/brk page
mapped NX by kmain.c/syscall.c/jobctl.c/sched.c. */
if (regs->errCode & 0x10)
return false;
uint64 faultAddress;
__asm__ volatile("mov %%cr2, %0" : "=r"(faultAddress));
bool forWrite = (regs->errCode & 0x2) != 0;
return mmapFaultHandler(currentThread(), faultAddress, forWrite);
}
uint64 isrDispatch(Registers* regs)
{
/* A ring-3 (user-mode) page fault gets one real chance at recovery
before falling through to the unconditional panic below: an
instruction inside SOME OTHER thread's buggy user program should
never be able to take the whole machine down with it, exactly like
a real kernel's own per-process fault isolation -- this kernel's
flat, single-shared-address-space design (see paging.c's own file
comments) cannot isolate a bad ACCESS the way separate page tables
would, but it can still isolate the CONSEQUENCE, by killing only the
one thread that caused it. A kernel-mode fault (ring 0, cs&3==0 --
this kernel's own bug, not a user program's) still panics
unconditionally below, exactly as before this existed. */
if (regs->intNo == 14 && (regs->cs & 3) == 3) {
if (!tryResolveUserPageFault(regs))
sigRaiseInfo(currentThreadId(), sigSegv, siCodeKernel, 0, 0);
return deliverPendingSignals((uint64)regs);
}
if (regs->intNo < 32)
panicOnException(regs);
if (regs->intNo == 128)
return syscallDispatch(regs);
uint64 nextRsp;
if (regs->intNo == 128) {
nextRsp = syscallDispatch(regs);
} else {
uint8 irq = (uint8)(regs->intNo - 32);
uint64 nextRsp = (uint64)regs;
nextRsp = (uint64)regs;
if (irq == 0) {
pitTick();
@@ -71,5 +183,7 @@ uint64 isrDispatch(Registers* regs)
}
picSendEoi(irq);
return nextRsp;
}
return deliverPendingSignals(nextRsp);
}