From f7c3e005a1130de4d16a985c64cab9887c47df53 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 15:10:29 +0200 Subject: [PATCH] test: extend coverage of schedTest --- src/schedTest.c | 159 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) diff --git a/src/schedTest.c b/src/schedTest.c index 4463cdf..707bc8d 100644 --- a/src/schedTest.c +++ b/src/schedTest.c @@ -1095,3 +1095,162 @@ KTEST(threadKillAcceptsStoppedThreadLikeRealSigkillOnAStoppedProcess) KT_TRUE(t, threadReap(id)); } +/* + * threadReap/threadDiscard's own fd cleanup (sched.c's reclaimOpenFds) -- a + * thread that dies while still holding an open fdPipeRead/fdPipeWrite fd + * must give pipe.c's readers/writers count back exactly like an orderly + * vfsClose would, or the pipe's OTHER end (a second, independent process, + * e.g. fifomaster.c's children) would wait forever for an EOF/broken-pipe + * that can now never come -- the same shape of leak FALA 13's bug#2 found + * for console readers, just against pipe.c's own refcounts. These fabricate + * the fd the same way the threadCreateUser tests above fabricate a user + * slot: poke threadAt(id)->fds[] directly (bypassing vfsPipeAttach's own + * allocFd/currentThread(), since the thread under test is never actually + * running) with a real pipeCreateDetached handle, so the underlying pipe.c + * refcounts driving the assertions are the genuine article, not a mock. + */ + +KTEST(threadDiscardClosesOpenReadPipeFdSoWriterSeesBrokenPipe) +{ + int handle = pipeCreateDetached(); + KT_GE(t, handle, 0); + + uint32 id = threadCreate("pipeReadHolder", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + + /* Simulate this (never-run) thread having opened the read end via + vfsPipeAttach: a real reader attached to the handle, referenced only + from this thread's own fd table (index 3: 0-2 are always fdConsole, + see vfsFdInit). */ + pipeAddReader(handle); + threadAt(id)->fds[3].kind = fdPipeRead; + threadAt(id)->fds[3].pipeHandle = handle; + + /* A second, independent side attaches the write end -- tracked purely + by handle, with no fd of its own, exactly like pipeShareTest.c's + coverage of the same primitives. */ + pipeAddWriter(handle); + KT_FALSE(t, pipeWriteBroken(handle)); + + /* Runnable, never scheduled: threadDiscard's path (e.g. rolling back a + thread that never got to run its own vfsClose). */ + KT_TRUE(t, threadDiscard(id)); + + /* The only reader was this thread's fd -- once reclaimOpenFds has run, + the writer must see the pipe as genuinely, permanently broken, not + "try again" forever. */ + KT_TRUE(t, pipeWriteBroken(handle)); + KT_EQ(t, pipeWrite(handle, "x", 1), -1); + + pipeCloseWrite(handle); +} + +KTEST(threadReapClosesOpenWritePipeFdSoReaderSeesEof) +{ + int handle = pipeCreateDetached(); + KT_GE(t, handle, 0); + + uint32 id = threadCreate("pipeWriteHolder", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + + pipeAddWriter(handle); + threadAt(id)->fds[3].kind = fdPipeWrite; + threadAt(id)->fds[3].pipeHandle = handle; + + pipeAddReader(handle); + char buffer[4]; + /* Ring empty, a writer is still attached: "try again", not EOF yet. */ + KT_EQ(t, pipeRead(handle, buffer, sizeof(buffer)), -1); + + /* No live scheduler to run threadExit through before schedStart() -- + poke the state directly, the same workaround + threadReapOnDeadUserThreadReclaimsItsMappedPages uses above for the + same reason. */ + threadAt(id)->state = threadDead; + KT_TRUE(t, threadReap(id)); + + /* The only writer was this thread's fd -- once reclaimOpenFds has run, + the reader must see real end-of-file (0), not "try again" (-1) + forever. */ + KT_EQ(t, pipeRead(handle, buffer, sizeof(buffer)), 0); + + pipeCloseRead(handle); +} + +/* threadSetAlarm (sched.c) always targets currentThread() -- there is no + target-id argument, exactly like real alarm(2) -- so these tests exercise + it exactly the way syscall.c's sysAlarm does, against whatever thread is + "current" in this pre-schedStart() harness (the permanent idle thread, + id 0), rather than a freshly created one the way most other tests here + fabricate their own target. That is safe: deliverPendingSignals (isr.c) + is a no-op for anything that is not a ring-3 return context (`(regs->cs & + 3) != 3`), so a pending sigAlrm bit left on the idle thread by an + in-progress deadline is never actually consumed, and schedule()'s own + alarm-expiry scan (see its file comment) clears alarmAt back to 0 the + first tick it notices the deadline has passed either way -- but each test + below still explicitly cancels (threadSetAlarm(0)) before returning, so + no stray deadline is left for a LATER test or real boot to trip over. */ +KTEST(threadSetAlarmReturnsZeroWithNoPriorAlarmAndRecordsAFutureDeadline) +{ + Thread* self = threadAt(currentThreadId()); + KT_EQU(t, self->alarmAt, 0ull); + + uint64 now = pitTicks(); + KT_EQU(t, threadSetAlarm(5), 0ull); + KT_TRUE(t, self->alarmAt > now); + KT_EQU(t, self->alarmAt, now + 5ull * pitFrequency()); + + threadSetAlarm(0); + KT_EQU(t, self->alarmAt, 0ull); +} + +KTEST(threadSetAlarmReplacingOrCancelingAPendingAlarmReturnsItsRemainingSecondsRoundedUp) +{ + Thread* self = threadAt(currentThreadId()); + + /* Replace a 10s alarm with a 3s one: the return value describes the + OLD (10s) deadline, not the new one, and the struct itself now holds + the new one -- matching real alarm()'s "at most one pending alarm, + setting a new one silently discards whatever was left of the old" + contract. No real time passes between these two calls (interrupts + are off, there is no scheduler to preempt this function), so the + ceiling in threadSetAlarm's own comment reports the full, exact 10, + not 9 or 11 -- a flaky "off by the current tick's remainder" result + here would itself be a bug in that rounding, not a timing accident. */ + KT_EQU(t, threadSetAlarm(10), 0ull); + KT_EQU(t, threadSetAlarm(3), 10ull); + + /* Canceling (0) reports the 3s alarm's remaining time and clears the + deadline back to "none pending" -- the other real alarm(0) case, + exercised separately from the plain replace case above. */ + KT_EQU(t, threadSetAlarm(0), 3ull); + KT_EQU(t, self->alarmAt, 0ull); + + /* Canceling an ALREADY-canceled (none pending) alarm reports 0, the + same "nothing was pending" value a fresh thread starts with. */ + KT_EQU(t, threadSetAlarm(0), 0ull); +} + +/* + * Thread.pgid (sched.h) -- process groups, the scope real job control and + * kill(2)/sigSend's pid<0 convention actually target (signal.c's + * sigRaiseGroup, sched.c's threadKillGroup, syscall.c's sysKill/sysSigSend, + * keyboard.c's real Ctrl-C/Ctrl-Z). ktestRunAll always runs as the + * permanent idle thread (id 0) -- exactly the one context initialPgid + * (sched.c) treats as "boot-time", per this file's own header comment -- + * so every plain threadCreate/threadCreateUser call here exercises the + * "becomes its own new group leader" default; the OTHER default (a + * runtime child inheriting a genuinely running, non-idle parent's pgid) + * can only ever be exercised by a real ring-3 userSpawn, which is why + * user/jobtest.c's own pgid check (spawning user/jobtarget.c at runtime) + * is this feature's live proof instead. + */ + +KTEST(threadCreateDefaultsPgidToItsOwnIdWhenCurrentIsTheIdleThread) +{ + uint32 id = threadCreate("pgidDefaultKernel", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + KT_EQU(t, threadAt(id)->pgid, id); + KT_TRUE(t, threadDiscard(id)); +} +