1248 lines
50 KiB
C
1248 lines
50 KiB
C
#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));
|
|
}
|