diff --git a/src/schedTest.c b/src/schedTest.c index 12c7e21..d4cd652 100644 --- a/src/schedTest.c +++ b/src/schedTest.c @@ -784,3 +784,161 @@ KTEST(threadStopAcceptsSleepingBlockedAndRunningStates) uint32 id = threadCreate("stopFromEveryState", noopThreadEntry); KT_NE(t, id, threadInvalidId); + /* Fabricated states, the same way threadReapClosesOpenWritePipeFdSoReaderSeesEof + below pokes threadDead directly: this thread is never actually + scheduled in this pre-schedStart() harness, so its `state` field + stands in for "genuinely Sleeping/Blocked/Running" -- exactly the + states a real SIGSTOP target could be caught in. */ + threadAt(id)->state = threadSleeping; + KT_TRUE(t, threadStop(id)); + KT_EQ(t, threadAt(id)->state, threadStopped); + KT_TRUE(t, threadContinue(id)); + + threadAt(id)->state = threadBlocked; + KT_TRUE(t, threadStop(id)); + KT_EQ(t, threadAt(id)->state, threadStopped); + KT_TRUE(t, threadContinue(id)); + + /* Running -- the self-stop case (sysSigSend targeting the caller's own + pid, syscall.c): threadStop does not special-case "am I the one + calling this" the way threadKill implicitly does (Running is simply + absent from threadKill's accepted set); a thread can stop itself. */ + threadAt(id)->state = threadRunning; + KT_TRUE(t, threadStop(id)); + KT_EQ(t, threadAt(id)->state, threadStopped); + + KT_TRUE(t, threadContinue(id)); + KT_TRUE(t, threadDiscard(id)); +} + +KTEST(threadStopOnDeadOrUnusedSlotFails) +{ + uint32 id = threadCreate("stopDead", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + + threadAt(id)->state = threadDead; + KT_FALSE(t, threadStop(id)); + KT_EQ(t, threadAt(id)->state, threadDead); + KT_TRUE(t, threadReap(id)); + + /* threadUnused again after reap: still rejected. */ + KT_FALSE(t, threadStop(id)); +} + +KTEST(threadContinueRejectsOutOfRangeAndNonStoppedThreads) +{ + KT_FALSE(t, threadContinue(threadCapacity())); + KT_FALSE(t, threadContinue(threadCapacity() + 1000)); + + uint32 id = threadCreate("notStopped", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + + /* Runnable, never stopped: SIGCONT against a process that was never + stopped is a harmless no-op at this level (see signal.c's + sigRaise, which still delivers the signal itself regardless). */ + KT_FALSE(t, threadContinue(id)); + KT_EQ(t, threadAt(id)->state, threadRunnable); + + KT_TRUE(t, threadDiscard(id)); +} + +KTEST(threadStopExcludesTargetFromUnrelatedWaitChannelWake) +{ + int channel; + + uint32 id = threadCreate("stoppedWhileWaiting", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + + /* Fabricate exactly what schedBlockCurrent would have left behind had + this thread been the one calling it (see + threadDiscardClosesOpenReadPipeFdSoWriterSeesBrokenPipe below for + the same fabricate-then-assert convention). */ + threadAt(id)->state = threadBlocked; + threadAt(id)->waitChannel = &channel; + + KT_TRUE(t, threadStop(id)); + KT_EQ(t, threadAt(id)->state, threadStopped); + + /* The defining difference from threadBlocked: schedWakeChannel must + NOT match a Stopped thread even though its waitChannel field still + names this exact channel -- only threadContinue (an explicit + SIGCONT) may move it back to threadRunnable, never "the resource it + was originally waiting on became available". */ + schedWakeChannel(&channel); + KT_EQ(t, threadAt(id)->state, threadStopped); + + KT_TRUE(t, threadContinue(id)); + KT_EQ(t, threadAt(id)->state, threadRunnable); + + KT_TRUE(t, threadDiscard(id)); +} + +/* + * threadStop/threadContinue's own new SIGCHLD-on-stop/SIGCHLD-on-continue + * (sched.c, mirroring threadExit/threadKill's parentId/sigChld block + * above): real job control notifies the parent asynchronously the instant + * a child stops or resumes, not only when it eventually dies. The live + * ring-3 proof that this reaches a genuinely blocked-mid-syscall target + * and a real installed handler is user/stopchldtest.c spawning + * user/stopchldtarget.c (needle "stopchldtest: job-control-sigchld-ok" in + * tools/ci-test.sh); what is deterministic here is the same plumbing shape + * threadKillRaisesSigChldAgainstParentId/ + * threadKillEarlyWakesAParentGenuinelyBlockedWaitingForSigChld above + * already establish for the death case. + */ + +KTEST(threadStopRaisesSigChldAgainstParentIdOnTheActualStopTransition) +{ + uint32 parent = threadCreate("stopChldParent", noopThreadEntry); + KT_NE(t, parent, threadInvalidId); + uint32 child = threadCreate("stopChldVictim", noopThreadEntry); + KT_NE(t, child, threadInvalidId); + threadAt(child)->parentId = parent; + + KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), 0ull); + KT_TRUE(t, threadStop(child)); + KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), (1ull << sigChld)); + + KT_TRUE(t, threadContinue(child)); + KT_TRUE(t, threadDiscard(child)); + KT_TRUE(t, threadDiscard(parent)); +} + +KTEST(threadStopDoesNotReRaiseSigChldAgainstAnAlreadyStoppedTarget) +{ + uint32 parent = threadCreate("stopChldParent2", noopThreadEntry); + KT_NE(t, parent, threadInvalidId); + uint32 child = threadCreate("stopChldVictim2", noopThreadEntry); + KT_NE(t, child, threadInvalidId); + threadAt(child)->parentId = parent; + + KT_TRUE(t, threadStop(child)); + KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), (1ull << sigChld)); + + /* Simulate the parent's own handler already having consumed that one + notification (sigConsumePending, signal.c, clears the bit on + delivery) -- exactly the "next natural ring-3 return" a real + sigchldtest.c-style handler would have already run through before a + second, redundant sigStop against the same still-Stopped target + could possibly arrive. */ + threadAt(parent)->sigPending &= ~(1ull << sigChld); + + KT_TRUE(t, threadStop(child)); + KT_EQ(t, threadAt(child)->state, threadStopped); + KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), 0ull); + + KT_TRUE(t, threadContinue(child)); + KT_TRUE(t, threadDiscard(child)); + KT_TRUE(t, threadDiscard(parent)); +} + +KTEST(threadStopEarlyWakesAParentGenuinelyBlockedWaitingForSigChldOnStop) +{ + int channel; + + uint32 parent = threadCreate("stopChldBlockedParent", noopThreadEntry); + KT_NE(t, parent, threadInvalidId); + uint32 child = threadCreate("stopChldVictim3", noopThreadEntry); + KT_NE(t, child, threadInvalidId); + threadAt(child)->parentId = parent; +