diff --git a/src/schedTest.c b/src/schedTest.c index 84bee3e..702d965 100644 --- a/src/schedTest.c +++ b/src/schedTest.c @@ -310,3 +310,161 @@ KTEST(threadReapRejectsIdleOutOfRangeAndNeverCreatedSlots) if (neverUsed < threadCapacity()) KT_FALSE(t, threadReap(neverUsed)); + uint32 runnable = threadCreate("notDeadYet", noopThreadEntry); + KT_NE(t, runnable, threadInvalidId); + KT_FALSE(t, threadReap(runnable)); + KT_TRUE(t, threadDiscard(runnable)); +} + +KTEST(threadJoinRejectsInvalidIdsWithoutBlocking) +{ + /* All of these must return immediately (no schedBlockCurrent/halt()) + since there is no live scheduler to ever wake this thread back up -- + a wrong implementation here would hang the whole test battery, not + just fail an assertion. */ + KT_EQ(t, threadJoin(0), -1); + KT_EQ(t, threadJoin(threadCapacity()), -1); + + uint32 neverUsed = threadCount(); + if (neverUsed < threadCapacity()) + KT_EQ(t, threadJoin(neverUsed), -1); +} + +KTEST(threadJoinOnDiscardedSlotFailsCleanly) +{ + uint32 id = threadCreate("transient", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + KT_TRUE(t, threadDiscard(id)); + + /* The slot is threadUnused again -- joining it must behave exactly like + joining an id that was never created, not crash or block. */ + KT_EQ(t, threadJoin(id), -1); +} + +/* + * threadCreateUser/threadReap/threadDiscard user-memory reclamation + * (Thread.userSlotBase, sched.h). Every real spawn/exec used to leak its + * whole elfSlotSize slot's worth of physical frames on death, since + * frostbyte's single shared page table is never touched by thread teardown + * -- these tests drive that path directly with a synthetic "process" (its + * own fake slot, far above any real USER_BASE any binary is linked at, so + * it can never collide with a real program's mapping) rather than relying + * only on the live boot demo (kmain.c's proctest/spawntarget/exectarget + * chain, needles "user-mem-reclaimed-ok"/"exec-old-slot-freed-ok" in + * ci-test.sh) to prove it. + */ + +KTEST(threadCreateUserSetsUserSlotBaseFromEntry) +{ + uint64 entry = 0x320000000ull + 0x2000; + uint32 id = threadCreateUser("probe", entry, elfSlotStackTop(entry)); + KT_NE(t, id, threadInvalidId); + KT_EQU(t, threadUserSlotBase(id), elfSlotBase(entry)); + KT_EQU(t, threadUserSlotBase(id), 0x320000000ull); + KT_TRUE(t, threadDiscard(id)); +} + +KTEST(threadReapOnDeadUserThreadReclaimsItsMappedPages) +{ + uint64 fakeSlotBase = 0x330000000ull; + uint64 entry = fakeSlotBase + 0x2000; + uint64 stackTop = elfSlotStackTop(entry); + uint64 stackSize = 4 * frameSize; + uint64 before = pmmFreeFrames(); + + /* Mirror what a real loadImageByPath/spawnUser call maps for any user + thread: a sliver of "image" plus an eagerly-mapped stack, both well + inside the 256 MiB slot -- most of the slot stays unmapped, exactly + like a real small program. */ + /* At least the image page plus the stack pages -- entry and fakeSlotBase + are virgin address space, so mapping also pays for whatever PDPT/PD/PT + page-table pages do not exist yet (paging.c's nextTable create path), + which this test does not try to predict the exact count of. The reap/ + discard side below is the precise assertion: every frame spent here, + data or structural, must come all the way back. */ + KT_TRUE(t, pagingMapUserRange(entry, frameSize)); + KT_TRUE(t, pagingMapUserRange(stackTop - stackSize, stackSize)); + KT_LE(t, pmmFreeFrames(), before - 1 - (stackSize / frameSize)); + + uint32 id = threadCreateUser("fakeProc", entry, stackTop); + KT_NE(t, id, threadInvalidId); + KT_EQU(t, threadUserSlotBase(id), fakeSlotBase); + + /* No live scheduler to run threadExit through before schedStart() -- + poke the state directly, the same workaround + schedBlockCurrentMarksBlockedWithChannel uses above for the same + reason. */ + threadAt(id)->state = threadDead; + + KT_TRUE(t, threadReap(id)); + KT_EQU(t, pmmFreeFrames(), before); + KT_EQ(t, pagingResolve(entry), 0); + KT_EQ(t, pagingResolve(stackTop - 1), 0); +} + +KTEST(threadDiscardOnNeverRunUserThreadReclaimsItsMappedPages) +{ + uint64 fakeSlotBase = 0x340000000ull; + uint64 entry = fakeSlotBase + 0x2000; + uint64 stackTop = elfSlotStackTop(entry); + uint64 stackSize = 4 * frameSize; + uint64 before = pmmFreeFrames(); + + /* At least the image page plus the stack pages -- entry and fakeSlotBase + are virgin address space, so mapping also pays for whatever PDPT/PD/PT + page-table pages do not exist yet (paging.c's nextTable create path), + which this test does not try to predict the exact count of. The reap/ + discard side below is the precise assertion: every frame spent here, + data or structural, must come all the way back. */ + KT_TRUE(t, pagingMapUserRange(entry, frameSize)); + KT_TRUE(t, pagingMapUserRange(stackTop - stackSize, stackSize)); + KT_LE(t, pmmFreeFrames(), before - 1 - (stackSize / frameSize)); + + uint32 id = threadCreateUser("fakeProc2", entry, stackTop); + KT_NE(t, id, threadInvalidId); + + /* Runnable, never scheduled: the threadDiscard path, not threadReap. */ + KT_TRUE(t, threadDiscard(id)); + KT_EQU(t, pmmFreeFrames(), before); + KT_EQ(t, pagingResolve(entry), 0); + KT_EQ(t, pagingResolve(stackTop - 1), 0); +} + +KTEST(threadCreateKernelThreadHasNoUserSlotAndReapLeavesPmmUntouched) +{ + uint64 before = pmmFreeFrames(); + + uint32 id = threadCreate("kernelOnly", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + KT_EQU(t, threadUserSlotBase(id), 0); + + threadAt(id)->state = threadDead; + KT_TRUE(t, threadReap(id)); + KT_EQU(t, pmmFreeFrames(), before); +} + +/* + * threadKill (sched.c) -- the only way to recover a thread genuinely stuck + * forever, now that sysRead/sysWrite can really block on a shared pipe + * (pipe.h/vfs.h). The live proof that it tears down a thread ACTUALLY + * parked mid-syscall is killtest.c spawning killvictim.c (needles + * "killtest: killed victim"/"killtest: kill-ok" in tools/ci-test.sh); what + * is deterministic here is threadKill's own state-machine bookkeeping. + */ + +KTEST(threadKillRejectsIdleOutOfRangeAndNeverCreatedSlots) +{ + KT_FALSE(t, threadKill(0)); + KT_FALSE(t, threadKill(threadCapacity())); + KT_FALSE(t, threadKill(threadCapacity() + 1000)); + + uint32 neverUsed = threadCount(); + if (neverUsed < threadCapacity()) + KT_FALSE(t, threadKill(neverUsed)); +} + +KTEST(threadKillMarksRunnableThreadDeadWithKilledExitCode) +{ + uint32 id = threadCreate("killMe", noopThreadEntry); + KT_NE(t, id, threadInvalidId); +