test: strengthen tests for sigTest

This commit is contained in:
dualfroz committed 2026-09-12 15:43:05 +02:00
1 parent 978d2ad905
commit febf5254a4
1 file changed
+155
+155
View File
@@ -626,3 +626,158 @@ KTEST(sigConsumePendingDefersSameSignalRaisedAgainWhileItsOwnHandlerIsActive)
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, &regs, &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, &regs, &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, &regs));
KT_EQ(t, memcmp(&regs, &midHandler, sizeof(midHandler)), 0);
KT_EQU(t, thread->sigHandlerDepth, 1u);
KT_TRUE(t, sigOnSigReturn(thread, &regs));
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, &regs, &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, &regs, &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, &regs, &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, &regs));
KT_EQ(t, memcmp(&regs, &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, &regs));
KT_EQ(t, memcmp(&regs, &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;