#include "ktest.h" #include "sched.h" #include "signal.h" #include "ports.h" #include "string.h" #include "gdt.h" /* * Pure logic for signal.c's delivery decision (sigConsumePending) and its * supporting state (sigRaise/sigSetHandler/sigProcMask/sigResetState/ * sigOnSigReturn) -- all fully testable without a live scheduler or a real * ring-3 transition, since they only ever touch a Thread's own fields and a * caller-supplied Registers snapshot. The one thing this file cannot cover * (a real trapframe actually being rewritten so iretq lands ring 3 * execution inside the shared trampoline page, the handler genuinely * running, and sysSigReturn genuinely resuming afterwards) is proven live * instead by sigtest.c/sigtarget.c/sigdefault.c (ci-test.sh needles * "sigtest: handler-roundtrip-ok" / "sigtest: default-terminate-ok"). */ static void noopThreadEntry(void) { for (;;) halt(); } /* A plausible "interrupted in ring 3, mid-instruction" snapshot: distinct, recognizable values in every field sigConsumePending/sigOnSigReturn are expected to touch (rip/rax/rdi) or must leave alone until sigOnSigReturn restores them (rbx/rsp/cs/ss/rflags), so a test can tell "changed" from "still original" without ambiguity. */ static Registers makeUserRegs(void) { Registers regs; memset(®s, 0, sizeof(regs)); regs.rip = 0x40001234ull; regs.rax = 0xAAAAAAAAull; regs.rdi = 0xBBBBBBBBull; regs.rbx = 0xCCCCCCCCull; regs.rsp = 0x48003FF0ull; regs.cs = userCodeSelector | 3; regs.ss = userDataSelector | 3; regs.rflags = 0x202; return regs; } KTEST(sigRaiseSetsPendingBitAndIsIdempotent) { uint32 id = threadCreate("sigRaiseTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); KT_TRUE(t, sigRaise(id, sigUsr1)); KT_EQU(t, threadAt(id)->sigPending, (1ull << sigUsr1)); /* Raising the same signal again while it is still pending changes nothing -- there is exactly one bit per signal number, not a queue or a count, matching real Unix's non-queuing signals. */ KT_TRUE(t, sigRaise(id, sigUsr1)); KT_EQU(t, threadAt(id)->sigPending, (1ull << sigUsr1)); KT_TRUE(t, threadDiscard(id)); } KTEST(sigRaiseRejectsOutOfRangeSignumAndBadTarget) { uint32 id = threadCreate("sigRaiseBad", noopThreadEntry); KT_NE(t, id, threadInvalidId); KT_FALSE(t, sigRaise(id, 0)); KT_FALSE(t, sigRaise(id, -1)); KT_FALSE(t, sigRaise(id, sigMaxSignals)); KT_FALSE(t, sigRaise(0, sigUsr1)); KT_FALSE(t, sigRaise(threadCapacity(), sigUsr1)); KT_FALSE(t, sigRaise(threadCapacity() + 1000, sigUsr1)); KT_TRUE(t, threadDiscard(id)); /* Slot just discarded is threadUnused again -- a signal aimed at it must fail exactly like one aimed at a never-created id. */ KT_FALSE(t, sigRaise(id, sigUsr1)); } KTEST(sigRaiseSigKillDelegatesToThreadKill) { uint32 id = threadCreate("sigKillTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); /* threadKill only accepts Runnable/Sleeping/Blocked; a freshly created thread is Runnable, so this is the same live-target case threadKillMarksRunnableThreadDeadWithKilledExitCode already covers for threadKill directly -- this test is about sigRaise's OWN dispatch reaching it, not re-testing threadKill's state machine. */ KT_TRUE(t, sigRaise(id, sigKill)); KT_EQ(t, threadAt(id)->state, threadDead); KT_EQU(t, threadAt(id)->exitCode, threadKilledExitCode); /* sigKill never touches sigPending -- it bypasses the pending/blocked machinery entirely (see signal.h's file comment). */ KT_EQU(t, threadAt(id)->sigPending, 0ull); KT_TRUE(t, threadReap(id)); } KTEST(sigRaiseSigStopDelegatesToThreadStopAndBypassesPending) { uint32 id = threadCreate("sigStopTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); KT_TRUE(t, sigRaise(id, sigStop)); KT_EQ(t, threadAt(id)->state, threadStopped); /* Like sigKill, sigStop never touches sigPending -- it bypasses the pending/blocked/handler machinery entirely (see signal.h's file comment and sigRaiseSigKillDelegatesToThreadKill above). */ KT_EQU(t, threadAt(id)->sigPending, 0ull); /* Real kill(pid, SIGKILL) kills a stopped process too (threadKill was widened to accept threadStopped alongside threadStop's own introduction). */ KT_TRUE(t, threadKill(id)); KT_TRUE(t, threadReap(id)); } KTEST(sigRaiseSigStopRejectsInvalidTargetsLikeThreadStop) { KT_FALSE(t, sigRaise(0, sigStop)); KT_FALSE(t, sigRaise(threadCapacity(), sigStop)); KT_FALSE(t, sigRaise(threadCapacity() + 1000, sigStop)); } KTEST(sigRaiseSigContResumesStoppedTargetUnconditionally) { uint32 id = threadCreate("sigContTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); KT_TRUE(t, sigRaise(id, sigStop)); KT_EQ(t, threadAt(id)->state, threadStopped); /* sigCont, unlike sigStop, still falls through to the generic pending path afterwards -- but the resume itself (threadContinue) happens first, as an unconditional side effect, regardless of sigBlocked: blocking EVERYTHING (short of sigKill/sigStop, which sigProcMask always strips) must not stop the actual resume. */ sigProcMask(threadAt(id), sigMaskSetMask, sigAllBlocked); KT_TRUE(t, sigRaise(id, sigCont)); KT_EQ(t, threadAt(id)->state, threadRunnable); KT_TRUE(t, threadAt(id)->sigPending & (1ull << sigCont)); KT_TRUE(t, threadDiscard(id)); } KTEST(sigRaiseSigContOnNonStoppedTargetStillDeliversSignal) { uint32 id = threadCreate("sigContNotStopped", noopThreadEntry); KT_NE(t, id, threadInvalidId); /* Never stopped: SIGCONT here is a no-op at the process-state level (real Unix: continuing an already-running process does nothing extra), but the signal itself is still delivered -- an installed handler still runs through the normal sigConsumePending scan regardless. */ KT_TRUE(t, sigRaise(id, sigCont)); KT_EQ(t, threadAt(id)->state, threadRunnable); KT_TRUE(t, threadAt(id)->sigPending & (1ull << sigCont)); KT_TRUE(t, threadDiscard(id)); } KTEST(sigRaiseWakesSleepingOrBlockedTargetEarlyForADeliverableSignal) { uint32 id = threadCreate("sigWakeSleeping", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); /* No handler installed: sigTerm's default disposition is Terminate, which sigWouldDeliver treats as genuinely deliverable -- exactly the user/eintrtarget.c scenario (a thread blocked on a condition that will never naturally occur, recovered by an ORDINARY signal, not threadKill's direct bypass). */ thread->state = threadSleeping; KT_TRUE(t, sigRaise(id, sigTerm)); KT_EQ(t, thread->state, threadRunnable); thread->state = threadBlocked; KT_TRUE(t, sigRaise(id, sigTstp)); KT_EQ(t, thread->state, threadRunnable); KT_TRUE(t, threadDiscard(id)); } KTEST(sigRaiseDoesNotWakeSleepingOrBlockedTargetForABlockedOrIgnoredSignal) { uint32 id = threadCreate("sigNoWakeBlocked", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); /* sigUsr1 explicitly blocked: must not disturb the sleep at all, same as real Unix -- a blocked signal cannot interrupt anything. */ sigProcMask(thread, sigMaskBlock, (1ull << sigUsr1)); thread->state = threadSleeping; KT_TRUE(t, sigRaise(id, sigUsr1)); KT_EQ(t, thread->state, threadSleeping); KT_TRUE(t, thread->sigPending & (1ull << sigUsr1)); /* sigChld's real-world default is Ignore -- dismissed outright, never delivered, so it must not wake anything either. */ thread->state = threadBlocked; KT_TRUE(t, sigRaise(id, sigChld)); KT_EQ(t, thread->state, threadBlocked); /* An explicit sigHandlerIgnore on an otherwise-Terminate signal is the same story. */ sigSetHandler(thread, sigUsr2, sigHandlerIgnore, 0, 0); KT_TRUE(t, sigRaise(id, sigUsr2)); KT_EQ(t, thread->state, threadBlocked); threadAt(id)->state = threadRunnable; KT_TRUE(t, threadDiscard(id)); } KTEST(sigRaiseDoesNotEarlyWakeForSignalMaskedByActiveHandlersSaMask) { /* The gap the two tests above do NOT cover: sigWouldDeliver (and therefore threadWakeForSignal) reads thread->sigBlocked verbatim, with no idea whether any given bit got there via an explicit sigProcMask call (tested above) or via sigConsumePending's own handler-entry union of sigHandlerMasks[signum] (the file comment's per-signal sa_mask paragraph). This drives the SECOND path for real -- actually entering a handler via sigConsumePending, not just calling sigProcMask by hand -- to prove the early-wake mechanism (FALA 16) genuinely respects a mask that arrived that way too, the exact interaction FALA 19 flagged as unexamined before deferring this whole topic. user/masktarget.c is this same scenario live: its onUsr1 handler (sa_mask = sigHup's bit here, sigUsr2's there) blocks on a real read WHILE the mask is active, so an early wake bug here would hang it forever instead of just failing a check. */ uint32 id = threadCreate("sigMaskedNoEarlyWake", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); int signum = -1; uint64 usr1Handler = 0x40009000ull; sigSetHandler(thread, sigUsr1, usr1Handler, (1ull << sigHup), 0); sigRaise(id, sigUsr1); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); KT_TRUE(t, thread->sigBlocked & (1ull << sigHup)); /* onUsr1 is now "running" (as far as this thread's own state is concerned) and immediately blocks on something real, exactly like user/masktarget.c's own userRead(goFd) call from inside its handler. */ thread->state = threadBlocked; /* sigHup is covered by onUsr1's sa_mask -- sigWouldDeliver must say no, so threadWakeForSignal is never even called: the Blocked state (and therefore whatever real wait this models) must survive completely undisturbed, same as the explicit-sigProcMask case above. */ KT_TRUE(t, sigRaise(id, sigHup)); KT_EQ(t, thread->state, threadBlocked); KT_TRUE(t, thread->sigPending & (1ull << sigHup)); /* Once onUsr1 genuinely returns (sigOnSigReturn, restoring sigBlocked to what it was before -- no sigHup bit), the deferred sigHup is deliverable again, but this thread is STILL Blocked (the read it was modelling has not itself completed) -- sigOnSigReturn only unmasks, it is not itself a wake source, so nothing here should move the thread back to Runnable behind the scheduler's own back. */ KT_TRUE(t, sigOnSigReturn(thread, ®s)); KT_FALSE(t, thread->sigBlocked & (1ull << sigHup)); KT_EQ(t, thread->state, threadBlocked); threadAt(id)->state = threadRunnable; KT_TRUE(t, threadDiscard(id)); } KTEST(sigRaiseNeverWakesAStoppedTargetForAnOrdinarySignal) { uint32 id = threadCreate("sigStoppedNoWake", noopThreadEntry); KT_NE(t, id, threadInvalidId); KT_TRUE(t, threadStop(id)); KT_EQ(t, threadAt(id)->state, threadStopped); /* sigTerm would genuinely deliver against a Runnable/Sleeping/Blocked target (sigWouldDeliver says yes -- no handler, default Terminate, not blocked) but job control's "stopped means stopped" invariant must survive it existing at all: only threadContinue (an explicit SIGCONT) may ever move a Stopped thread, exactly like real Unix (a stopped process does not die of SIGTERM either, until continued or SIGKILLed). */ KT_TRUE(t, sigRaise(id, sigTerm)); KT_EQ(t, threadAt(id)->state, threadStopped); KT_TRUE(t, threadAt(id)->sigPending & (1ull << sigTerm)); KT_TRUE(t, threadKill(id)); KT_TRUE(t, threadReap(id)); } KTEST(sigSetHandlerInstallsAndReturnsPreviousDisposition) { uint32 id = threadCreate("sigHandlerTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); KT_EQU(t, sigSetHandler(thread, sigUsr1, 0x40005000ull, 0, 0), sigHandlerDefault); KT_EQU(t, thread->sigHandlers[sigUsr1], 0x40005000ull); KT_EQU(t, sigSetHandler(thread, sigUsr1, sigHandlerIgnore, 0, 0), 0x40005000ull); KT_EQU(t, thread->sigHandlers[sigUsr1], sigHandlerIgnore); KT_TRUE(t, threadDiscard(id)); } KTEST(sigSetHandlerRejectsSigKillAndOutOfRangeSignum) { uint32 id = threadCreate("sigHandlerBad", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); KT_EQU(t, sigSetHandler(thread, sigKill, 0x40005000ull, 0, 0), sigSetFailed); KT_EQU(t, thread->sigHandlers[sigKill], sigHandlerDefault); KT_EQU(t, sigSetHandler(thread, 0, 0x40005000ull, 0, 0), sigSetFailed); KT_EQU(t, sigSetHandler(thread, sigMaxSignals, 0x40005000ull, 0, 0), sigSetFailed); KT_TRUE(t, threadDiscard(id)); } KTEST(sigSetHandlerRejectsSigStopTooLikeRealEinval) { uint32 id = threadCreate("sigStopHandlerBad", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); KT_EQU(t, sigSetHandler(thread, sigStop, 0x40005000ull, 0, 0), sigSetFailed); KT_EQU(t, thread->sigHandlers[sigStop], sigHandlerDefault); KT_TRUE(t, threadDiscard(id)); } KTEST(sigSetHandlerStoresMaskStrippedOfSigKillAndSigStop) { uint32 id = threadCreate("sigMaskStoreTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); sigSetHandler(thread, sigUsr1, 0x40005000ull, (1ull << sigUsr2) | (1ull << sigKill) | (1ull << sigStop), 0); KT_EQU(t, thread->sigHandlerMasks[sigUsr1], (1ull << sigUsr2)); /* Installing a handler for a DIFFERENT signum must not disturb sigUsr1's own stored mask -- sigHandlerMasks[] is indexed by the signal being handled, one independent slot per signal number, same as sigHandlers[] itself already is. */ sigSetHandler(thread, sigTerm, 0x40006000ull, (1ull << sigHup), 0); KT_EQU(t, thread->sigHandlerMasks[sigUsr1], (1ull << sigUsr2)); KT_EQU(t, thread->sigHandlerMasks[sigTerm], (1ull << sigHup)); KT_TRUE(t, threadDiscard(id)); } KTEST(sigProcMaskBlockUnblockSetMaskReturnPreviousAndStripSigKill) { uint32 id = threadCreate("sigMaskTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); KT_EQU(t, sigProcMask(thread, sigMaskBlock, (1ull << sigUsr1)), 0ull); KT_EQU(t, thread->sigBlocked, (1ull << sigUsr1)); KT_EQU(t, sigProcMask(thread, sigMaskBlock, (1ull << sigUsr2)), (1ull << sigUsr1)); KT_EQU(t, thread->sigBlocked, (1ull << sigUsr1) | (1ull << sigUsr2)); KT_EQU(t, sigProcMask(thread, sigMaskUnblock, (1ull << sigUsr1)), (1ull << sigUsr1) | (1ull << sigUsr2)); KT_EQU(t, thread->sigBlocked, (1ull << sigUsr2)); KT_EQU(t, sigProcMask(thread, sigMaskSetMask, (1ull << sigTerm)), (1ull << sigUsr2)); KT_EQU(t, thread->sigBlocked, (1ull << sigTerm)); /* sigKill can never be blocked, matching real Unix: its bit is silently stripped from whatever mask is passed in, for BOTH sigMaskBlock and sigMaskSetMask. */ KT_EQU(t, sigProcMask(thread, sigMaskSetMask, sigAllBlocked), (1ull << sigTerm)); KT_FALSE(t, thread->sigBlocked & (1ull << sigKill)); /* sigStop can never be blocked either, same treatment as sigKill. */ KT_FALSE(t, thread->sigBlocked & (1ull << sigStop)); KT_TRUE(t, threadDiscard(id)); } KTEST(sigConsumePendingReturnsNoneWhenNothingIsPendingOrEverythingIsBlocked) { uint32 id = threadCreate("sigConsumeNone", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); int signum = -1; KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeNone); KT_EQ(t, signum, -1); sigRaise(id, sigUsr1); sigProcMask(thread, sigMaskBlock, (1ull << sigUsr1)); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeNone); /* Blocked, not delivered: the pending bit must survive untouched for whenever it eventually gets unblocked. */ KT_TRUE(t, thread->sigPending & (1ull << sigUsr1)); KT_TRUE(t, threadDiscard(id)); } KTEST(sigConsumePendingDismissesIgnoredSignalsAndKeepsScanning) { uint32 id = threadCreate("sigConsumeIgnore", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); int signum = -1; /* sigChld's real-world default disposition is Ignore -- with no handler installed at all it must be silently dismissed, not terminate the thread. */ sigRaise(id, sigChld); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeNone); KT_FALSE(t, thread->sigPending & (1ull << sigChld)); /* An explicit sigHandlerIgnore dismisses a signal whose default would otherwise be Terminate. */ sigSetHandler(thread, sigUsr1, sigHandlerIgnore, 0, 0); sigRaise(id, sigUsr1); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeNone); KT_FALSE(t, thread->sigPending & (1ull << sigUsr1)); /* An ignored signal must not mask a REAL one raised alongside it in the same scan: sigHup (explicit sigHandlerIgnore here) sits at a LOWER signal number than sigTerm (default Terminate, no handler), so the scan hits the dismissed one first and must keep going instead of stopping there. */ sigSetHandler(thread, sigHup, sigHandlerIgnore, 0, 0); sigRaise(id, sigHup); sigRaise(id, sigTerm); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeTerminate); KT_EQ(t, signum, sigTerm); KT_FALSE(t, thread->sigPending & (1ull << sigHup)); KT_FALSE(t, thread->sigPending & (1ull << sigTerm)); KT_TRUE(t, threadDiscard(id)); } KTEST(sigConsumePendingDefaultDispositionReturnsTerminate) { uint32 id = threadCreate("sigConsumeTerm", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); Registers before = regs; int signum = -1; sigRaise(id, sigTerm); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeTerminate); KT_EQ(t, signum, sigTerm); KT_FALSE(t, thread->sigPending & (1ull << sigTerm)); /* Terminate is reported, not carried out, here -- sigConsumePending has no way to hand back a new rsp for a thread it just killed, so it must not touch the trapframe at all; isr.c's deliverPendingSignals is the one that actually calls threadExit. */ KT_EQU(t, regs.rip, before.rip); KT_EQU(t, regs.rax, before.rax); KT_EQU(t, thread->sigHandlerDepth, 0u); KT_TRUE(t, threadDiscard(id)); } KTEST(sigConsumePendingSigTstpDefaultDispositionReturnsStopOutcome) { uint32 id = threadCreate("sigConsumeTstp", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); Registers before = regs; int signum = -1; /* SIGTSTP's real-world default disposition is Stop, not Terminate -- the catchable half of job control (see sigDefaultIsStop, signal.c). sigStop itself never reaches here at all (sigRaise bypasses this whole scan for it, see sigRaiseSigStopDelegatesToThreadStopAndBypassesPending above), so this is the ONLY way sigOutcomeStop is ever produced. */ sigRaise(id, sigTstp); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeStop); KT_EQ(t, signum, sigTstp); KT_FALSE(t, thread->sigPending & (1ull << sigTstp)); /* Reported, not carried out, here -- isr.c's deliverPendingSignals is the one that actually calls threadStop, the same contract sigOutcomeTerminate already has with threadExit. */ KT_EQU(t, regs.rip, before.rip); KT_EQU(t, regs.rax, before.rax); KT_EQU(t, thread->sigHandlerDepth, 0u); KT_EQ(t, thread->state, threadRunnable); KT_TRUE(t, threadDiscard(id)); } KTEST(sigConsumePendingSigTstpWithHandlerRunsHandlerInsteadOfStopping) { uint32 id = threadCreate("sigConsumeTstpCaught", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); int signum = -1; /* Unlike SIGSTOP, SIGTSTP is fully catchable: an installed handler overrides the default Stop disposition, exactly like any other catchable signal overriding a default Terminate. */ uint64 handlerAddr = 0x4000A000ull; sigSetHandler(thread, sigTstp, handlerAddr, 0, 0); sigRaise(id, sigTstp); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); KT_EQ(t, signum, sigTstp); KT_EQU(t, regs.rax, handlerAddr); KT_EQU(t, regs.rip, sigTrampolineVirtualAddr); KT_EQU(t, thread->sigHandlerDepth, 1u); KT_TRUE(t, threadDiscard(id)); } KTEST(sigConsumePendingSaMaskDefersDifferentSignalUntilHandlerReturns) { uint32 id = threadCreate("sigSaMaskTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); int signum = -1; /* sigUsr1's sa_mask explicitly includes sigHup -- unlike the nesting test below (mask 0), THIS is what should keep sigHup from interrupting sigUsr1's handler at all. */ uint64 usr1Handler = 0x40009000ull; uint64 hupHandler = 0x4000C000ull; sigSetHandler(thread, sigUsr1, usr1Handler, (1ull << sigHup), 0); sigSetHandler(thread, sigHup, hupHandler, 0, 0); sigRaise(id, sigUsr1); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); KT_EQ(t, signum, sigUsr1); KT_TRUE(t, thread->sigBlocked & (1ull << sigHup)); /* sigHup arrives while sigUsr1's handler is active -- masked by its sa_mask, so it must stay pending, NOT nest, unlike an unmasked signal would (see the nesting test below). */ sigRaise(id, sigHup); int deferredSignum = -1; KT_EQ(t, sigConsumePending(thread, ®s, &deferredSignum), sigOutcomeNone); KT_EQ(t, deferredSignum, -1); KT_TRUE(t, thread->sigPending & (1ull << sigHup)); KT_EQU(t, thread->sigHandlerDepth, 1u); /* Once sigUsr1's handler returns, its sa_mask's grip on sigHup ends with it (sigBlocked restored to what it was before) -- the deferred signal is finally deliverable, as its own TOP-LEVEL handler, not a nested one. */ KT_TRUE(t, sigOnSigReturn(thread, ®s)); KT_FALSE(t, thread->sigBlocked & (1ull << sigHup)); int finalSignum = -1; KT_EQ(t, sigConsumePending(thread, ®s, &finalSignum), sigOutcomeHandler); KT_EQ(t, finalSignum, sigHup); KT_EQU(t, regs.rax, hupHandler); KT_EQU(t, thread->sigHandlerDepth, 1u); KT_TRUE(t, threadDiscard(id)); } /* Default (sigFlagNoDefer NOT set) half of the contrast: a signal raised again against ITSELF while its own handler is still active must stay merely pending, not nest -- sigConsumePendingCustomHandlerRewritesTrapframeAndPushesContext above already asserts sigUsr1's own bit lands in sigBlocked once its handler starts; this test is the direct behavioral consequence of that bit actually being there, driven the same way sigConsumePendingSaMaskDefersDifferentSignalUntilHandlerReturns drives sa_mask's deferral. */ KTEST(sigConsumePendingDefersSameSignalRaisedAgainWhileItsOwnHandlerIsActive) { uint32 id = threadCreate("sigNoDeferOffTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); int signum = -1; uint64 handlerAddr = 0x40009000ull; sigSetHandler(thread, sigUsr1, handlerAddr, 0, 0); sigRaise(id, sigUsr1); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); KT_EQ(t, signum, sigUsr1); KT_TRUE(t, thread->sigBlocked & (1ull << sigUsr1)); KT_EQU(t, thread->sigHandlerDepth, 1u); /* Same signum, raised again while its own handler is still active -- self-blocked (no sigFlagNoDefer), so this must stay pending, not enter a second, nested instance of the same handler. */ sigRaise(id, sigUsr1); int deferredSignum = -1; KT_EQ(t, sigConsumePending(thread, ®s, &deferredSignum), sigOutcomeNone); KT_EQ(t, deferredSignum, -1); KT_TRUE(t, thread->sigPending & (1ull << sigUsr1)); KT_EQU(t, thread->sigHandlerDepth, 1u); /* Once the (only) active handler returns, its self-block lifts and the deferred re-raise is finally deliverable -- as a fresh TOP-LEVEL invocation (depth back to 1, not 2), never having nested at all. */ KT_TRUE(t, sigOnSigReturn(thread, ®s)); KT_FALSE(t, thread->sigBlocked & (1ull << sigUsr1)); int finalSignum = -1; KT_EQ(t, sigConsumePending(thread, ®s, &finalSignum), sigOutcomeHandler); KT_EQ(t, finalSignum, sigUsr1); KT_EQU(t, thread->sigHandlerDepth, 1u); KT_TRUE(t, threadDiscard(id)); } /* sigFlagNoDefer's whole point: registered for a signal, that signal's own bit is no longer added to sigBlocked when its handler starts, so raising the SAME signum again while that handler is still active genuinely nests a second instance of it -- the direct opposite outcome of the test just above, same signum, only the flag differs. */ KTEST(sigConsumePendingNoDeferFlagLetsSameSignalNestIntoItsOwnActiveHandler) { uint32 id = threadCreate("sigNoDeferOnTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); int signum = -1; uint64 handlerAddr = 0x40009000ull; sigSetHandler(thread, sigUsr1, handlerAddr, 0, sigFlagNoDefer); sigRaise(id, sigUsr1); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); KT_EQ(t, signum, sigUsr1); /* The one thing sigFlagNoDefer actually changes: sigUsr1's own bit is NOT in sigBlocked, unlike the default case above. */ KT_FALSE(t, thread->sigBlocked & (1ull << sigUsr1)); KT_EQU(t, thread->sigHandlerDepth, 1u); Registers midHandler = regs; sigRaise(id, sigUsr1); int nestedSignum = -1; KT_EQ(t, sigConsumePending(thread, ®s, &nestedSignum), sigOutcomeHandler); KT_EQ(t, nestedSignum, sigUsr1); KT_EQU(t, regs.rip, sigTrampolineVirtualAddr); KT_EQU(t, regs.rax, handlerAddr); KT_EQU(t, thread->sigHandlerDepth, 2u); /* Still no self-block even at the nested depth -- a third raise would nest again the same way, bounded only by sigMaxNestDepth, never by sigUsr1 blocking itself. */ KT_FALSE(t, thread->sigBlocked & (1ull << sigUsr1)); KT_EQ(t, memcmp(&thread->sigSavedRegs[1], &midHandler, sizeof(midHandler)), 0); /* Unwinding pops the nested instance first, resuming the outer one exactly where the re-raise interrupted it -- LIFO, same as any other nested delivery. */ KT_TRUE(t, sigOnSigReturn(thread, ®s)); KT_EQ(t, memcmp(®s, &midHandler, sizeof(midHandler)), 0); KT_EQU(t, thread->sigHandlerDepth, 1u); KT_TRUE(t, sigOnSigReturn(thread, ®s)); KT_EQU(t, thread->sigHandlerDepth, 0u); KT_TRUE(t, threadDiscard(id)); } KTEST(sigConsumePendingCustomHandlerRewritesTrapframeAndPushesContext) { uint32 id = threadCreate("sigConsumeHandler", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); Registers before = regs; int signum = -1; uint64 handlerAddr = 0x40009000ull; sigSetHandler(thread, sigUsr1, handlerAddr, 0, 0); sigProcMask(thread, sigMaskBlock, (1ull << sigUsr2)); uint64 blockedBefore = thread->sigBlocked; sigRaise(id, sigUsr1); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); KT_EQ(t, signum, sigUsr1); /* Trapframe rewritten to enter the trampoline: rip is the shared trampoline entry, rax is the handler address its `call *%rax` invokes, rdi is the signal number (the handler's sole argument). */ KT_EQU(t, regs.rip, sigTrampolineVirtualAddr); KT_EQU(t, regs.rax, handlerAddr); KT_EQU(t, regs.rdi, (uint64)sigUsr1); /* Untouched: the trampoline runs on the interrupted thread's own existing stack, no separate signal stack. */ KT_EQU(t, regs.rsp, before.rsp); /* sigUsr1's own sa_mask is empty (0 above), so the ONLY new bit added to sigBlocked is sigUsr1 itself (self-blocked, like real Unix) -- whatever was already blocked (sigUsr2, from sigProcMask) survives untouched, a union not a replacement. */ KT_EQU(t, thread->sigHandlerDepth, 1u); KT_EQU(t, thread->sigBlocked, blockedBefore | (1ull << sigUsr1)); KT_EQU(t, thread->sigSavedBlocked[0], blockedBefore); /* The ENTIRE pre-signal trapframe must be recoverable later -- not just the fields this test happens to check above. */ KT_EQ(t, memcmp(&thread->sigSavedRegs[0], &before, sizeof(before)), 0); /* An ALREADY-blocked signal (sigUsr2, blocked since before sigUsr1 was even raised) stays deferred while a handler is active, exactly like it would with no handler running at all -- the depth stack does not change what sigBlocked itself gates. */ sigRaise(id, sigUsr2); int blockedSignum = -1; KT_EQ(t, sigConsumePending(thread, ®s, &blockedSignum), sigOutcomeNone); KT_EQ(t, blockedSignum, -1); KT_TRUE(t, thread->sigPending & (1ull << sigUsr2)); /* A signal NOT covered by sigUsr1's mask nor its own self-block -- sigHup -- genuinely interrupts the active handler with a NESTED one of its own: this is the behavior a single "one handler at a time" bool (this code used to have) could never produce, since it gated delivery before sigBlocked was even consulted. */ Registers midUsr1Handler = regs; uint64 handlerAddrHup = 0x4000B000ull; sigSetHandler(thread, sigHup, handlerAddrHup, 0, 0); sigRaise(id, sigHup); int nestedSignum = -1; KT_EQ(t, sigConsumePending(thread, ®s, &nestedSignum), sigOutcomeHandler); KT_EQ(t, nestedSignum, sigHup); KT_EQU(t, regs.rip, sigTrampolineVirtualAddr); KT_EQU(t, regs.rax, handlerAddrHup); KT_EQU(t, regs.rdi, (uint64)sigHup); KT_EQU(t, thread->sigHandlerDepth, 2u); /* sigHup's own handler has no mask either, so its blocked set is whatever sigUsr1's handler already made it, plus sigHup's own bit. */ KT_EQU(t, thread->sigBlocked, blockedBefore | (1ull << sigUsr1) | (1ull << sigHup)); /* What got pushed for THIS (nested) frame is exactly the trapframe sigUsr1's handler was actually running with the instant sigHup preempted it -- not the original pre-sigUsr1 `before` snapshot. */ KT_EQ(t, memcmp(&thread->sigSavedRegs[1], &midUsr1Handler, sizeof(midUsr1Handler)), 0); KT_EQU(t, thread->sigSavedBlocked[1], blockedBefore | (1ull << sigUsr1)); /* Unwinding: first sigOnSigReturn resumes sigUsr1's still-active handler exactly where sigHup interrupted it, not the original program -- LIFO, like a real call stack of signal frames. */ KT_TRUE(t, sigOnSigReturn(thread, ®s)); KT_EQ(t, memcmp(®s, &midUsr1Handler, sizeof(midUsr1Handler)), 0); KT_EQU(t, thread->sigHandlerDepth, 1u); KT_EQU(t, thread->sigBlocked, blockedBefore | (1ull << sigUsr1)); /* Second sigOnSigReturn finally resumes the ORIGINAL interrupted program, restoring the exact pre-sigUsr1 trapframe and blocked mask. */ KT_TRUE(t, sigOnSigReturn(thread, ®s)); KT_EQ(t, memcmp(®s, &before, sizeof(before)), 0); KT_EQU(t, thread->sigHandlerDepth, 0u); KT_EQU(t, thread->sigBlocked, blockedBefore); KT_TRUE(t, threadDiscard(id)); } KTEST(sigConsumePendingRefusesNewHandlerAtMaxNestDepth) { uint32 id = threadCreate("sigNestDepthLimit", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); int signum = -1; /* Poke the depth field directly to the bound rather than actually nesting sigMaxNestDepth real handlers -- this test is about the GUARD, which the fault-injection check below confirms is load- bearing (removing it does not, by itself, corrupt anything a plain "one more handler" scenario would exercise; it needs the table genuinely full to matter). */ thread->sigHandlerDepth = sigMaxNestDepth; sigSetHandler(thread, sigUsr1, 0x40009000ull, 0, 0); sigRaise(id, sigUsr1); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeNone); KT_EQ(t, signum, -1); /* Left pending, not lost -- exactly like a blocked signal, it simply waits for room (a sigOnSigReturn dropping the depth back down). */ KT_TRUE(t, thread->sigPending & (1ull << sigUsr1)); KT_EQU(t, thread->sigHandlerDepth, (uint32)sigMaxNestDepth); thread->sigHandlerDepth = 0; KT_TRUE(t, threadDiscard(id)); } /* * SA_RESTART / EINTR (signalnum.h's sigFlagRestart, sched.h's * Thread.sigInRetryableSyscall) -- signal.h's file comment has the full * story. These four drive sigConsumePending directly instead of a live * syscall retry, the same division of labor the rest of this file already * uses: user/restarttarget.c/restarttest.c prove the real ring-3 round * trip, these prove the exact rip/rax rewrite (or lack of it) is gated on * precisely the right condition. */ KTEST(sigConsumePendingReturnsEintrForInterruptedRetryableSyscallWithoutRestartFlag) { uint32 id = threadCreate("sigEintrTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); /* Stand-in for the exact state sysRead/sysWrite/sysWait (syscall.c) leave a thread in right before blocking: rip rewound 2 bytes behind the `int $0x80` that trapped in, so re-entering ring 3 normally would re-execute it. */ regs.rip = 0x40001232ull; int signum = -1; uint64 handlerAddr = 0x4000A000ull; sigSetHandler(thread, sigUsr1, handlerAddr, 0, 0); thread->sigInRetryableSyscall = true; sigRaise(id, sigUsr1); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); KT_EQ(t, signum, sigUsr1); /* The handler still runs normally (trampoline entry) -- the rewrite only touches the SAVED, about-to-resume context below, never how this handler itself gets entered. */ KT_EQU(t, regs.rip, sigTrampolineVirtualAddr); KT_EQU(t, regs.rax, handlerAddr); /* What actually resumes once this handler returns: rip past the interrupted `int $0x80` entirely (not just past the rewind -- the retry itself is skipped), rax the raw negative "errno" a real syscall would return -- genuine EINTR, not a silent retry. */ KT_EQU(t, thread->sigSavedRegs[0].rip, 0x40001232ull + 2); KT_EQU(t, thread->sigSavedRegs[0].rax, (uint64)(int64)errInterrupted); /* Consumed -- a later, nested signal must never see this as still true (see the next test). */ KT_FALSE(t, thread->sigInRetryableSyscall); KT_TRUE(t, threadDiscard(id)); } KTEST(sigConsumePendingPreservesTransparentRetryWhenRestartFlagSet) { uint32 id = threadCreate("sigRestartTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); regs.rip = 0x40001232ull; Registers before = regs; int signum = -1; sigSetHandler(thread, sigUsr1, 0x4000A000ull, 0, sigFlagRestart); thread->sigInRetryableSyscall = true; sigRaise(id, sigUsr1); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); /* sigFlagRestart: the saved, about-to-resume context is untouched -- exactly the original retry point, so the syscall genuinely retries once this handler returns, same as every handler behaved before this flag existed. */ KT_EQ(t, memcmp(&thread->sigSavedRegs[0], &before, sizeof(before)), 0); KT_FALSE(t, thread->sigInRetryableSyscall); KT_TRUE(t, threadDiscard(id)); } KTEST(sigConsumePendingLeavesRegsAloneWhenNotInterruptingARetryableSyscall) { uint32 id = threadCreate("sigNoRetryTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); Registers before = regs; int signum = -1; /* No sigFlagRestart, AND this thread was never actually mid a retryable syscall (a fresh thread's sigInRetryableSyscall starts false) -- ordinary running code being interrupted, exactly like a plain sysSleep or no syscall at all. The gate must refuse to invent an EINTR here just because the flag is absent. */ sigSetHandler(thread, sigUsr1, 0x4000A000ull, 0, 0); KT_FALSE(t, thread->sigInRetryableSyscall); sigRaise(id, sigUsr1); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); KT_EQ(t, memcmp(&thread->sigSavedRegs[0], &before, sizeof(before)), 0); KT_TRUE(t, threadDiscard(id)); } KTEST(sigConsumePendingNestedHandlerNeverInheritsOuterRetryState) { uint32 id = threadCreate("sigNestedRetryTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); regs.rip = 0x40001232ull; int outerSignum = -1; sigSetHandler(thread, sigUsr1, 0x4000A000ull, 0, sigFlagRestart); sigSetHandler(thread, sigHup, 0x4000B000ull, 0, 0); thread->sigInRetryableSyscall = true; sigRaise(id, sigUsr1); KT_EQ(t, sigConsumePending(thread, ®s, &outerSignum), sigOutcomeHandler); KT_FALSE(t, thread->sigInRetryableSyscall); Registers midOuterHandler = regs; int nestedSignum = -1; sigRaise(id, sigHup); KT_EQ(t, sigConsumePending(thread, ®s, &nestedSignum), sigOutcomeHandler); /* sigHup has no sigFlagRestart, but it never interrupted a retryable syscall -- it interrupted sigUsr1's OWN already-running handler. thread->sigInRetryableSyscall was already consumed (cleared) by the outer delivery above, so this nested push must leave regs alone regardless of sigHup's own flags: the saved context is exactly the mid-outer-handler snapshot, not an invented EINTR. */ KT_EQ(t, memcmp(&thread->sigSavedRegs[1], &midOuterHandler, sizeof(midOuterHandler)), 0); KT_TRUE(t, threadDiscard(id)); } KTEST(sigOnSigReturnRestoresExactPreSignalContext) { uint32 id = threadCreate("sigReturnTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); Registers regs = makeUserRegs(); Registers before = regs; int signum = -1; sigSetHandler(thread, sigUsr1, 0x40009000ull, 0, 0); sigProcMask(thread, sigMaskBlock, (1ull << sigUsr2)); uint64 blockedBefore = thread->sigBlocked; sigRaise(id, sigUsr1); KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); /* regs now holds whatever the trampoline/handler would have left it as by the time it traps into sysSigReturn -- simulate that instead of actually running user code, since this file has no ring 3. */ regs.rax = 0xDEADBEEFull; regs.rdi = 0xFEEDFACEull; KT_TRUE(t, sigOnSigReturn(thread, ®s)); KT_EQ(t, memcmp(®s, &before, sizeof(before)), 0); KT_EQU(t, thread->sigHandlerDepth, 0u); KT_EQU(t, thread->sigBlocked, blockedBefore); /* Misuse: calling sigreturn while NOT actually inside a handler must be a safe no-op, not a crash or a silent corruption of an unrelated trapframe. */ Registers untouched = regs; KT_FALSE(t, sigOnSigReturn(thread, ®s)); KT_EQ(t, memcmp(®s, &untouched, sizeof(untouched)), 0); KT_TRUE(t, threadDiscard(id)); } /* A table slot recycled by threadDiscard/threadReap must never let a dead process's signal state leak into whatever unrelated process reuses its id next. Today that is guaranteed twice over -- threadReap/threadDiscard already memset() the whole Thread struct before any slot is handed back to allocSlot, and sigResetState does it again explicitly from threadCreate (see sched.h's field comment) -- so this test does not currently isolate sigResetState's own contribution; it locks in the observable end-to-end property regardless of which of the two is doing the work, the same way the console-reader/pipe-fd leak fixes (see keyboard.c/sched.c history) earned tests for state that lived OUTSIDE Thread and had no such blanket memset to fall back on. */ KTEST(threadCreateResetsSignalStateOnRecycledSlot) { uint32 first = threadCreate("sigLeakA", noopThreadEntry); KT_NE(t, first, threadInvalidId); Thread* a = threadAt(first); sigSetHandler(a, sigUsr1, 0x40009000ull, (1ull << sigUsr2), 0); sigProcMask(a, sigMaskBlock, (1ull << sigUsr2)); sigRaise(first, sigUsr1); Registers regs = makeUserRegs(); int signum = -1; /* Drives `a` into sigHandlerDepth==1 (via the installed handler above) so that field's reset gets exercised too, not just sigPending/sigBlocked/sigHandlers/sigHandlerMasks. */ KT_EQ(t, sigConsumePending(a, ®s, &signum), sigOutcomeHandler); KT_EQU(t, a->sigHandlerDepth, 1u); KT_TRUE(t, threadDiscard(first)); /* allocSlot's own round-robin cursor (sched.c) no longer guarantees the very next create reuses THIS exact slot -- drive it around until it does, discarding every other slot it hands back along the way, so this test still deterministically exercises a real recycle of `first` regardless of where the cursor currently sits. Bounded by threadCapacity(): the cursor sweeps every currently-live index at most once per full lap, and nothing else in this test holds a slot open forever, so `first` is guaranteed to come back around well within that many attempts. */ uint32 second = threadInvalidId; for (uint32 attempt = 0; attempt < threadCapacity(); attempt++) { uint32 candidate = threadCreate("sigLeakB", noopThreadEntry); KT_NE(t, candidate, threadInvalidId); if (candidate == first) { second = candidate; break; } KT_TRUE(t, threadDiscard(candidate)); } KT_EQ(t, second, first); Thread* b = threadAt(second); KT_EQU(t, b->sigPending, 0ull); KT_EQU(t, b->sigBlocked, 0ull); KT_EQU(t, b->sigHandlerDepth, 0u); KT_EQU(t, b->sigSavedBlocked[0], 0ull); for (int signumIndex = 1; signumIndex < sigMaxSignals; signumIndex++) { KT_EQU(t, b->sigHandlers[signumIndex], sigHandlerDefault); KT_EQU(t, b->sigHandlerMasks[signumIndex], 0ull); } KT_TRUE(t, threadDiscard(second)); } /* * sigFlagSigInfo / SigInfo (signalnum.h) -- signal.c's sigConsumePending * populating the shared sigInfoVirtualAddr page and rewriting rsi/rdx to * hand a sigFlagSigInfo handler a real (SigInfo*, NULL) pair, deterministic * counterpart to the live ring-3 round trip in siginfotest.c/ * siginfotarget.c (ci-test.sh needles "siginfotarget: siginfo-sender-pid-ok" * / "siginfotest: sigchld-siginfo-accurate-ok"), which cannot itself prove * the EXACT bytes sigConsumePending computed the way direct access to * `regs`/the dereferenced pointer here can. */ KTEST(sigConsumePendingPopulatesSigInfoForSigFlagSigInfoHandler) { uint32 id = threadCreate("sigInfoTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); sigSetHandler(thread, sigUsr1, 0x40009000ull, 0, sigFlagSigInfo); KT_TRUE(t, sigRaiseInfo(id, sigUsr1, siCodeUser, 4242, 99)); Registers regs = makeUserRegs(); int signum = -1; KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); KT_EQ(t, signum, sigUsr1); /* rdx is always NULL (no ucontext_t this kernel can expose) -- see signal.h's sigConsumePending doc comment. */ KT_EQU(t, regs.rdx, 0ull); /* rsi is a real pointer into the shared, kernel-populated page -- not just "some nonzero value", dereferencing it must show exactly what sigRaiseInfo attached above. */ KT_NE(t, regs.rsi, 0ull); SigInfo* info = (SigInfo*)regs.rsi; KT_EQ(t, info->siSignum, sigUsr1); KT_EQ(t, info->siCode, siCodeUser); KT_EQ(t, info->siPid, 4242); KT_EQ(t, info->siStatus, 99); KT_TRUE(t, threadDiscard(id)); } KTEST(sigConsumePendingLeavesRsiRdxUntouchedWithoutSigFlagSigInfo) { uint32 id = threadCreate("sigNoInfoTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); /* No sigFlagSigInfo this time -- a plain one-argument handler, exactly like every signal demo before this session. */ sigSetHandler(thread, sigUsr1, 0x40009000ull, 0, 0); KT_TRUE(t, sigRaise(id, sigUsr1)); Registers regs = makeUserRegs(); regs.rsi = 0x5151515151515151ull; regs.rdx = 0x6262626262626262ull; int signum = -1; KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); /* Untouched, exactly as documented -- a handler with no sigFlagSigInfo never reads either register, so sigConsumePending must not spend any effort computing values for them. */ KT_EQU(t, regs.rsi, 0x5151515151515151ull); KT_EQU(t, regs.rdx, 0x6262626262626262ull); KT_TRUE(t, threadDiscard(id)); } KTEST(sigRaisePlainWrapperDefaultsToSiCodeKernelWithNoSenderOrStatus) { uint32 id = threadCreate("sigInfoDefaultTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); sigSetHandler(thread, sigHup, 0x40009000ull, 0, sigFlagSigInfo); /* The plain, pre-existing sigRaise -- every one of this file's other ~50 call sites uses exactly this, and none of them supply detail, so sigRaise's own thin-wrapper default (siCodeKernel/0/0) is what they all still get. */ KT_TRUE(t, sigRaise(id, sigHup)); Registers regs = makeUserRegs(); int signum = -1; KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); SigInfo* info = (SigInfo*)regs.rsi; KT_EQ(t, info->siSignum, sigHup); KT_EQ(t, info->siCode, siCodeKernel); KT_EQ(t, info->siPid, 0); KT_EQ(t, info->siStatus, 0); KT_TRUE(t, threadDiscard(id)); } KTEST(sigRaiseInfoOverwritesStalePendingInfoOnReRaiseBeforeDelivery) { uint32 id = threadCreate("sigInfoOverwriteTarget", noopThreadEntry); KT_NE(t, id, threadInvalidId); Thread* thread = threadAt(id); sigSetHandler(thread, sigUsr2, 0x40009000ull, 0, sigFlagSigInfo); KT_TRUE(t, sigRaiseInfo(id, sigUsr2, siCodeUser, 111, 1)); /* Same signum raised again before it was ever delivered -- non-queuing signals coalesce, so only the SECOND raise's detail should ever be observable, matching sigPending itself staying a single bit rather than a count. */ KT_TRUE(t, sigRaiseInfo(id, sigUsr2, siCodeUser, 222, 2)); Registers regs = makeUserRegs(); int signum = -1; KT_EQ(t, sigConsumePending(thread, ®s, &signum), sigOutcomeHandler); SigInfo* info = (SigInfo*)regs.rsi; KT_EQ(t, info->siPid, 222); KT_EQ(t, info->siStatus, 2); KT_TRUE(t, threadDiscard(id)); } /* * sigRaiseGroup (signal.h) -- the group-targeted half of real kill(2)'s * pid<0/pid==0 convention (syscall.c's sysSigSend) and keyboard.c's own * real Ctrl-C/Ctrl-Z, delivered to the FOREGROUND process group * (sched.h's Thread.pgid) rather than one hardcoded target. The live * ring-3 proof that this scoping actually protects a DIFFERENT group from * a keyboard-generated signal is user/pgroupfg.c (killed by it) versus * user/pgroupbg.c (proven still alive afterwards) in kmain.c's boot * sequence; what is deterministic here is the plain fan-out-and-filter * logic itself, mirroring schedTest.c's threadKillGroup coverage for the * exact same shape of primitive one layer down. */ KTEST(sigRaiseGroupDeliversToEveryLiveMemberOfThatGroupOnly) { uint32 a = threadCreate("sigGroupA", noopThreadEntry); uint32 b = threadCreate("sigGroupB", noopThreadEntry); uint32 outsider = threadCreate("sigGroupOutsider", noopThreadEntry); KT_NE(t, a, threadInvalidId); KT_NE(t, b, threadInvalidId); KT_NE(t, outsider, threadInvalidId); uint32 pgid = threadAt(a)->pgid; KT_TRUE(t, threadSetPgid(b, pgid)); KT_NE(t, threadAt(outsider)->pgid, pgid); KT_TRUE(t, sigRaiseGroup(pgid, sigUsr1, siCodeKernel, 0)); KT_EQU(t, threadAt(a)->sigPending & (1ull << sigUsr1), (1ull << sigUsr1)); KT_EQU(t, threadAt(b)->sigPending & (1ull << sigUsr1), (1ull << sigUsr1)); /* The whole point: a DIFFERENT group must be completely untouched -- this is what keeps a real Ctrl-C from ever reaching a background job. */ KT_EQU(t, threadAt(outsider)->sigPending, 0ull); KT_TRUE(t, threadDiscard(a)); KT_TRUE(t, threadDiscard(b)); KT_TRUE(t, threadDiscard(outsider)); } KTEST(sigRaiseGroupReturnsFalseWhenNoLiveThreadHasThatPgid) { KT_FALSE(t, sigRaiseGroup(0xBEEF0001u, sigUsr1, siCodeKernel, 0)); } KTEST(sigRaiseGroupSkipsDeadOrUnusedMembersButStillFindsLiveOnes) { uint32 a = threadCreate("sigGroupDeadA", noopThreadEntry); uint32 b = threadCreate("sigGroupDeadB", noopThreadEntry); KT_NE(t, a, threadInvalidId); KT_NE(t, b, threadInvalidId); uint32 pgid = threadAt(a)->pgid; KT_TRUE(t, threadSetPgid(b, pgid)); KT_TRUE(t, threadKill(b)); KT_TRUE(t, sigRaiseGroup(pgid, sigUsr2, siCodeKernel, 0)); KT_EQU(t, threadAt(a)->sigPending & (1ull << sigUsr2), (1ull << sigUsr2)); /* Dead members are silently skipped, exactly like sigRaiseInfo's own Dead/Unused guard already refuses a single-target send against one -- no crash, no stale sigPending bit written into a slot that is no longer this thread's own. */ KT_EQU(t, threadAt(b)->sigPending, 0ull); KT_TRUE(t, threadDiscard(a)); KT_TRUE(t, threadReap(b)); } KTEST(sigRaiseGroupWithSigStopStopsEveryLiveMemberOfThatGroup) { uint32 a = threadCreate("sigGroupStopA", noopThreadEntry); uint32 b = threadCreate("sigGroupStopB", noopThreadEntry); KT_NE(t, a, threadInvalidId); KT_NE(t, b, threadInvalidId); uint32 pgid = threadAt(a)->pgid; KT_TRUE(t, threadSetPgid(b, pgid)); /* sigStop is delegated straight to threadStop per-target, bypassing sigPending entirely -- the same bypass sigRaiseInfo's own file comment documents for a single-target sigStop, now proven to apply independently to every member the group fan-out visits. */ KT_TRUE(t, sigRaiseGroup(pgid, sigStop, siCodeKernel, 0)); KT_EQ(t, threadAt(a)->state, threadStopped); KT_EQ(t, threadAt(b)->state, threadStopped); KT_TRUE(t, threadContinue(a)); KT_TRUE(t, threadContinue(b)); KT_TRUE(t, threadDiscard(a)); KT_TRUE(t, threadDiscard(b)); }