Files
frostbyte/src/pidfdTest.c
T
2026-09-10 19:31:20 +02:00

168 lines
5.7 KiB
C

#include "ktest.h"
#include "sched.h"
#include "pidfd.h"
#include "ports.h"
/*
* Pure logic tests for pidfd.h/pidfd.c: no live scheduler needed (same
* "runs before schedStart(), current thread is idle, every created thread
* stays Runnable/never-run for its whole life" setup schedTest.c's own
* threadCreate/threadDiscard tests already use), since every property here
* is about sched.c's Thread.generation bookkeeping, not about actually
* scheduling anything.
*
* The marquee test below (pidfdTargetDetectsSlotRecycledByAnUnrelatedThread)
* is the deterministic, pure-logic reproduction of the exact scenario
* killtest.c's own long-standing comment warns about but never guards
* against ("killing it again must fail cleanly, not resurrect a slot that
* has since been reused") and sched.c's own allocSlot doc comment
* documents as a real, previously-unfixed race: a stale handle must never
* silently act on whichever unrelated, live thread has since been handed
* the exact same numeric slot. A live boot cannot reliably force this
* exact collision on demand (allocSlot's round-robin cursor needs the
* WHOLE table full before the very next create is forced back onto one
* specific freed slot, see its own doc comment) -- exactly why this test
* uses the same fill-the-table technique schedTest.c's own
* threadCreateReusesTheOnlyFreedSlotOnceTheTableIsFull already established
* for proving deterministic slot reuse, rather than a live demo (see
* user/pidfdtest.c instead for the real ring-3 syscall-path proof of
* post-reap staleness detection alone).
*/
static void noopThreadEntry(void)
{
for (;;)
halt();
}
KTEST(pidfdOpenRejectsIdleOutOfRangeAndUnusedTargets)
{
KT_EQ(t, pidfdOpen(0), -1);
KT_EQ(t, pidfdOpen(threadCapacity()), -1);
KT_EQ(t, pidfdOpen(threadCapacity() + 1000), -1);
/* Same "freed slot stays genuinely Unused until the round-robin cursor
wraps back around to it" setup as schedTest.c's own
threadCreateSkipsAFreshlyFreedLowSlotInFavorOfMovingForward. */
uint32 a = threadCreate("pfA", noopThreadEntry);
uint32 b = threadCreate("pfB", noopThreadEntry);
uint32 c = threadCreate("pfC", noopThreadEntry);
KT_TRUE(t, threadDiscard(b));
KT_EQ(t, pidfdOpen(b), -1);
KT_TRUE(t, threadDiscard(a));
KT_TRUE(t, threadDiscard(c));
}
KTEST(pidfdOpenAndTargetRoundtripThenClose)
{
uint32 x = threadCreate("pfX", noopThreadEntry);
int handle = pidfdOpen(x);
KT_GE(t, handle, 0);
KT_EQU(t, pidfdTarget(handle), x);
pidfdClose(handle);
KT_EQU(t, pidfdTarget(handle), threadInvalidId);
/* Closing twice must not corrupt anything else's slot. */
pidfdClose(handle);
KT_EQU(t, pidfdTarget(handle), threadInvalidId);
KT_TRUE(t, threadDiscard(x));
}
KTEST(pidfdTargetDetectsDiscardedTarget)
{
uint32 x = threadCreate("pfDiscarded", noopThreadEntry);
int handle = pidfdOpen(x);
KT_GE(t, handle, 0);
KT_TRUE(t, threadDiscard(x));
/* threadDiscard's memset reset Thread.generation for slot x back to 0
-- pidfdTarget must notice immediately, with no new thread ever
having claimed the slot yet. */
KT_EQU(t, pidfdTarget(handle), threadInvalidId);
pidfdClose(handle);
}
KTEST(pidfdTargetDetectsSlotRecycledByAnUnrelatedThread)
{
uint32 capacity = threadCapacity();
uint32 created[capacity];
uint32 count = 0;
while (count < capacity) {
uint32 id = threadCreate("pfFill", noopThreadEntry);
if (id == threadInvalidId)
break;
created[count++] = id;
}
KT_EQU(t, threadCreate("pfShouldFail", noopThreadEntry), threadInvalidId);
uint32 victim = created[count / 2];
int handle = pidfdOpen(victim);
KT_GE(t, handle, 0);
KT_EQU(t, pidfdTarget(handle), victim);
/* victim is the ONLY free slot afterward -- the very next create is
forced onto it regardless of allocSlot's own cursor position, the
exact same proof schedTest.c's
threadCreateReusesTheOnlyFreedSlotOnceTheTableIsFull already
establishes for the underlying thread-table mechanics alone. */
KT_TRUE(t, threadDiscard(victim));
uint32 impostor = threadCreate("pfImpostor", noopThreadEntry);
KT_EQ(t, impostor, victim);
/* The old handle must NOT follow the slot to its new, completely
unrelated occupant -- this is the entire point of pidfd.h over a
raw cached pid, which would now (harmlessly, in this test, but not
in general) apply any further kill/wait/sigSend straight to
impostor while believing it was still acting on victim. */
KT_EQU(t, pidfdTarget(handle), threadInvalidId);
/* A FRESH pidfd opened on the same numeric id now correctly targets
the new occupant -- the slot itself is not broken, only the OLD
handle's own pinned incarnation is (rightly) stale. */
int freshHandle = pidfdOpen(impostor);
KT_GE(t, freshHandle, 0);
KT_EQU(t, pidfdTarget(freshHandle), impostor);
pidfdClose(handle);
pidfdClose(freshHandle);
for (uint32 i = 0; i < count; i++) {
if (created[i] != victim)
KT_TRUE(t, threadDiscard(created[i]));
}
KT_TRUE(t, threadDiscard(impostor));
}
KTEST(pidfdOpenExhaustsThenClosesFreeSlotsBackForReuse)
{
uint32 x = threadCreate("pfTableFill", noopThreadEntry);
int handles[maxPidfds];
int count = 0;
while (count < maxPidfds) {
int h = pidfdOpen(x);
if (h < 0)
break;
handles[count++] = h;
}
KT_EQ(t, count, maxPidfds);
KT_EQ(t, pidfdOpen(x), -1);
pidfdClose(handles[0]);
int reused = pidfdOpen(x);
KT_GE(t, reused, 0);
KT_EQU(t, pidfdTarget(reused), x);
pidfdClose(reused);
for (int i = 1; i < count; i++)
pidfdClose(handles[i]);
KT_TRUE(t, threadDiscard(x));
}