From 67d097e51bfff305c5a78fe5f68a32fe8df2582c Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 14:31:44 +0200 Subject: [PATCH] test: add more cases for schedTest --- src/schedTest.c | 161 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/src/schedTest.c b/src/schedTest.c index 702d965..fdb27ad 100644 --- a/src/schedTest.c +++ b/src/schedTest.c @@ -468,3 +468,164 @@ KTEST(threadKillMarksRunnableThreadDeadWithKilledExitCode) uint32 id = threadCreate("killMe", noopThreadEntry); KT_NE(t, id, threadInvalidId); + KT_TRUE(t, threadKill(id)); + KT_EQ(t, threadAt(id)->state, threadDead); + KT_EQU(t, threadAt(id)->exitCode, threadKilledExitCode); + + /* A killed thread reaps exactly like one that called threadExit on its + own -- no separate "killed" bookkeeping survives past exitCode. */ + KT_TRUE(t, threadReap(id)); +} + +KTEST(threadKillOnAlreadyDeadOrUnusedSlotFails) +{ + uint32 id = threadCreate("killTwice", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + + KT_TRUE(t, threadKill(id)); + /* Already Dead: a second kill must not succeed (nor reset exitCode). */ + KT_FALSE(t, threadKill(id)); + KT_EQU(t, threadAt(id)->exitCode, threadKilledExitCode); + + KT_TRUE(t, threadReap(id)); + /* threadUnused again after reap: still rejected. */ + KT_FALSE(t, threadKill(id)); +} + +KTEST(threadKillWakesAThreadParkedInJoin) +{ + ThreadState saved = currentThread()->state; + + uint32 victim = threadCreate("victim", noopThreadEntry); + KT_NE(t, victim, threadInvalidId); + + /* Mirrors exactly what threadJoin (sched.c) itself does while waiting + for `victim`: block the current thread (the idle thread, id 0, at + this point in boot) on the victim slot's own address -- the same + channel threadExit/threadKill both wake. */ + schedBlockCurrent(threadAt(victim)); + KT_EQ(t, currentThread()->state, threadBlocked); + + KT_TRUE(t, threadKill(victim)); + KT_EQ(t, currentThread()->state, threadRunnable); + + currentThread()->state = saved; + KT_TRUE(t, threadReap(victim)); +} + +/* + * Thread.parentId (sched.h) and the real, asynchronous SIGCHLD it drives + * from threadExit/threadKill (signal.h/signal.c's sigRaise) -- the live + * ring-3 proof that a genuinely blocked parent actually wakes for its own + * child's death is user/sigchldtest.c spawning user/sigchldchild.c (needle + * "sigchldtest: sigchld-delivered-to-parent-ok" in tools/ci-test.sh); what + * is deterministic here is the plumbing itself: threadCreate/ + * threadCreateUser attributing parentId to whoever is truly running at + * creation time, and threadKill (parameterized by an explicit id, unlike + * threadExit which only ever acts on the CURRENT thread and so cannot be + * driven this way with ktestRunAll's single, never-context-switched + * thread -- see this file's own header comment) actually raising sigChld + * against exactly that id when the id it names dies. + */ + +KTEST(threadCreateAttributesParentIdToWhicheverThreadIsActuallyCurrent) +{ + uint32 id = threadCreate("childOfCurrent", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + KT_EQU(t, threadAt(id)->parentId, currentThreadId()); + KT_TRUE(t, threadDiscard(id)); +} + +KTEST(threadKillRaisesSigChldAgainstParentId) +{ + uint32 parent = threadCreate("chldParent", noopThreadEntry); + KT_NE(t, parent, threadInvalidId); + uint32 child = threadCreate("chldVictim", noopThreadEntry); + KT_NE(t, child, threadInvalidId); + + /* ktestRunAll runs single-threaded (see file header) -- threadCreate + always attributes parentId to THIS KTEST runner itself, so the + "child was spawned by parent" relationship this test actually wants + is set up directly here, the same white-box technique + threadStopExcludesTargetFromUnrelatedWaitChannelWake below already + uses to reach a state the real call path would only reach through + an actual live sysSpawn. */ + threadAt(child)->parentId = parent; + + KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), 0ull); + KT_TRUE(t, threadKill(child)); + KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), (1ull << sigChld)); + + KT_TRUE(t, threadReap(child)); + KT_TRUE(t, threadDiscard(parent)); +} + +KTEST(threadKillEarlyWakesAParentGenuinelyBlockedWaitingForSigChld) +{ + int channel; + + uint32 parent = threadCreate("chldBlockedParent", noopThreadEntry); + KT_NE(t, parent, threadInvalidId); + uint32 child = threadCreate("chldVictim2", noopThreadEntry); + KT_NE(t, child, threadInvalidId); + threadAt(child)->parentId = parent; + + /* sigChld's real-world default disposition is Ignore (sigDefaultIsIgnore, + signal.c), which sigWouldDeliver (also signal.c) treats exactly like + an installed sigHandlerIgnore -- correctly declining to early-wake a + thread for a signal that would just be silently dismissed on + delivery anyway, real Unix's own "SIGCHLD does nothing without a + handler" behavior. A parent that actually wants this notification + (like user/sigchldtest.c) always installs a real handler first, so + this test must too, or threadWakeForSignal below would (correctly) + never fire and this would be testing the wrong thing. */ + sigSetHandler(threadAt(parent), sigChld, 0x40005000ull, 0, 0); + + /* Fabricate exactly what schedBlockCurrent would have left behind had + parent been the one calling it (see + threadStopExcludesTargetFromUnrelatedWaitChannelWake above for the + same convention) -- e.g. a real sigchldtest.c genuinely blocked in + userRead on a pipe with no writer (see user/sigchldtest.c) while + its own sigChld handler is installed. threadWakeForSignal's own + state-machine bookkeeping is already proven generically by + threadWakeForSignalMovesSleepingOrBlockedThreadToRunnable above (for + any signal); this proves specifically that threadKill's own new + sigRaise(parentId, sigChld) call actually reaches that same + early-wake path for THIS signal, not just the pending-bit half. */ + threadAt(parent)->state = threadBlocked; + threadAt(parent)->waitChannel = &channel; + + KT_TRUE(t, threadKill(child)); + KT_EQ(t, threadAt(parent)->state, threadRunnable); + + KT_TRUE(t, threadReap(child)); + /* Runnable and never actually scheduled (ktestRunAll runs + single-threaded, see file header) -- threadDiscard, not threadReap, + is the correct teardown here, same as every other fabricated-but- + never-run thread in this file. */ + KT_TRUE(t, threadDiscard(parent)); +} + +KTEST(threadKillAgainstAnOrphanWithADeadParentIdRaisesNothing) +{ + uint32 deadParent = threadCreate("deadParent", noopThreadEntry); + KT_NE(t, deadParent, threadInvalidId); + KT_TRUE(t, threadKill(deadParent)); + KT_EQ(t, threadAt(deadParent)->state, threadDead); + + uint32 orphan = threadCreate("orphan", noopThreadEntry); + KT_NE(t, orphan, threadInvalidId); + threadAt(orphan)->parentId = deadParent; + + /* sigRaise's own targetId-state guard (signal.c) is what makes this + safe, not anything special here -- an orphan whose spawning parent + is already gone simply has nowhere for sigChld to land, the same + way a real kernel would reparent it to init instead of crashing or + misdelivering into whatever unrelated thread later reuses + deadParent's slot; this kernel does not implement reparenting (one + shared address space, no true fork(), see doSpawn's own file + comment in syscall.c), so the signal is just silently dropped. */ + KT_TRUE(t, threadKill(orphan)); + KT_EQ(t, threadAt(orphan)->state, threadDead); + KT_EQU(t, threadAt(deadParent)->sigPending & (1ull << sigChld), 0ull); +