From febf5254a4f37ca5c69f6e9ae4132eddcd09b935 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 15:43:05 +0200 Subject: [PATCH] test: strengthen tests for sigTest --- src/sigTest.c | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/src/sigTest.c b/src/sigTest.c index 46b1070..f7230f3 100644 --- a/src/sigTest.c +++ b/src/sigTest.c @@ -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, ®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; +