test: extend coverage of schedTest

This commit is contained in:
dualfroz committed 2026-09-11 15:10:29 +02:00
1 parent 6c6c8d8b1a
commit f7c3e005a1
1 file changed
+159
+159
View File
@@ -1095,3 +1095,162 @@ KTEST(threadKillAcceptsStoppedThreadLikeRealSigkillOnAStoppedProcess)
KT_TRUE(t, threadReap(id)); 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));
}