168 lines
5.7 KiB
C
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));
|
|
}
|