Files
frostbyte/src/mmapTest.c
T
2026-09-10 18:10:30 +02:00

321 lines
11 KiB
C

#include "ktest.h"
#include "mmap.h"
#include "sched.h"
#include "elf.h"
#include "paging.h"
#include "pmm.h"
#include "vfs.h"
#include "fs.h"
#include "string.h"
/*
* Coverage for mmap.c's real demand-paged mmap(2)/munmap(2) -- run entirely
* on the idle thread (KTEST always runs there, before schedStart(), see
* ktest.h's own file comment), whose real userSlotBase is 0 (a kernel
* thread owns no user slot). Every test below temporarily repoints it at
* mmapTestSlotBase, a 256 MiB (elfSlotSize)-aligned scratch address far
* above any real binary's USER_BASE (Makefile's highest is currently under
* 0x600000000, ~24 GiB) and far above pagingTest.c's own scratch range
* (0x200000000-0x202000000, ~8 GiB) so the two files can never collide even
* if their tests interleave, then restores it to 0 before returning --
* exactly the "explicit, restorable state" every other KTEST here already
* insists on rather than depending on hidden global setup.
*/
#define mmapTestSlotBase 0x300000000000ull
static uint64 setupSlot(Thread* t)
{
uint64 previous = t->userSlotBase;
t->userSlotBase = mmapTestSlotBase;
mmapReleaseAll(t);
return previous;
}
static void teardownSlot(Thread* t, uint64 previous)
{
pagingUnmapUserRange(mmapTestSlotBase, elfSlotSize);
mmapReleaseAll(t);
t->userSlotBase = previous;
}
KTEST(mmapReserveRejectsMisalignedZeroAndOverlap)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 addr;
KT_FALSE(t, mmapReserve(self, 1, frameSize, mmapProtRead, true, -1, 0, &addr));
KT_FALSE(t, mmapReserve(self, 0, 0, mmapProtRead, true, -1, 0, &addr));
KT_FALSE(t, mmapReserve(self, mmapTestSlotBase + elfSlotStackOffset, frameSize + 1, mmapProtRead, true, -1, 0, &addr));
uint64 base = mmapTestSlotBase + elfSlotStackOffset;
KT_TRUE(t, mmapReserve(self, base, 2 * frameSize, mmapProtRead | mmapProtWrite, true, -1, 0, &addr));
KT_EQU(t, addr, base);
/* Overlapping the just-reserved [base, base+2*frameSize) from either
side must be refused -- mmapReserve never silently merges/replaces. */
KT_FALSE(t, mmapReserve(self, base + frameSize, frameSize, mmapProtRead, true, -1, 0, &addr));
KT_FALSE(t, mmapReserve(self, base - frameSize, 2 * frameSize, mmapProtRead, true, -1, 0, &addr));
/* Outside the mmap region entirely (below stackTop, where the real
stack itself lives) is refused regardless of alignment. */
KT_FALSE(t, mmapReserve(self, mmapTestSlotBase, frameSize, mmapProtRead, true, -1, 0, &addr));
teardownSlot(self, previous);
}
KTEST(mmapReserveAutoPlacementIsFirstFitAndNonOverlapping)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 firstAddr, secondAddr;
KT_TRUE(t, mmapReserve(self, 0, 3 * frameSize, mmapProtRead, true, -1, 0, &firstAddr));
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, true, -1, 0, &secondAddr));
KT_EQU(t, firstAddr, mmapTestSlotBase + elfSlotStackOffset);
KT_EQU(t, secondAddr, firstAddr + 3 * frameSize);
teardownSlot(self, previous);
}
KTEST(mmapFaultHandlerAnonymousDemandZeroFillThenWritable)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead | mmapProtWrite, true, -1, 0, &addr));
/* Nothing mapped yet -- mmapReserve never eagerly allocates. */
KT_EQ(t, pagingResolve(addr), 0);
KT_TRUE(t, mmapFaultHandler(self, addr + 42, false));
KT_NE(t, pagingResolve(addr), 0);
/* Demand-zero: untouched content must actually be zero, not whatever
garbage the frame's previous owner (if any) left behind. */
uint8* bytes = (uint8*)addr;
for (int i = 0; i < 64; i++)
KT_EQ(t, bytes[i], 0);
/* A write fault on an already-present page (mmapProtWrite granted at
reserve time) is simply a no-op success -- the page was already
mapped pageWrite by the read fault above. */
KT_TRUE(t, mmapFaultHandler(self, addr, true));
bytes[0] = 0xAB;
KT_EQ(t, bytes[0], 0xAB);
teardownSlot(self, previous);
}
KTEST(mmapFaultHandlerSetsNoExecuteUnlessProtExecGranted)
{
if (!pagingNxSupported())
return;
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 noExecAddr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead | mmapProtWrite, true, -1, 0, &noExecAddr));
KT_TRUE(t, mmapFaultHandler(self, noExecAddr, false));
KT_FALSE(t, pagingIsExecutable(noExecAddr));
uint64 execAddr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead | mmapProtWrite | mmapProtExec, true, -1, 0, &execAddr));
KT_TRUE(t, mmapFaultHandler(self, execAddr, false));
KT_TRUE(t, pagingIsExecutable(execAddr));
teardownSlot(self, previous);
}
KTEST(mmapFaultHandlerRefusesWriteAgainstReadOnlyMapping)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, true, -1, 0, &addr));
/* A read fault is fine (mmapProtRead granted)... */
KT_TRUE(t, mmapFaultHandler(self, addr, false));
/* ...but a write fault against the SAME already-reserved range, on a
fresh page that never got mapped by the read above, must be refused
-- this is exactly the case isr.c turns into a real SIGSEGV. */
uint64 secondAddr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, true, -1, 0, &secondAddr));
KT_FALSE(t, mmapFaultHandler(self, secondAddr, true));
KT_EQ(t, pagingResolve(secondAddr), 0);
teardownSlot(self, previous);
}
KTEST(mmapFaultHandlerRefusesAddressOutsideAnyVma)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
KT_FALSE(t, mmapFaultHandler(self, mmapTestSlotBase + elfSlotStackOffset + 5 * frameSize, false));
teardownSlot(self, previous);
}
KTEST(mmapFaultHandlerFileBackedReadsRealFileContent)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
const char* payload = "mmapTest file-backed page content, byte-for-byte.";
int fd = vfsOpen("/tmp/mmaptest-src.txt", oReadWrite | oCreate | oTrunc);
KT_TRUE(t, fd >= 0);
KT_EQ(t, vfsWrite(fd, payload, strlen(payload)), (int64)strlen(payload));
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, false, fd, 0, &addr));
KT_TRUE(t, mmapFaultHandler(self, addr, false));
KT_TRUE(t, memcmp((const void*)addr, payload, strlen(payload)) == 0);
/* Past end-of-file within the same page: zero-padded, not garbage. */
KT_EQ(t, ((const uint8*)addr)[strlen(payload)], 0);
/* The fd's own seek offset must be exactly what it was before the
fault -- mmapFaultHandler saves/restores it around its own read, so
a caller's ordinary vfsRead/vfsLseek use of the same fd is never
disturbed by an unrelated page fault. */
KT_EQ(t, vfsLseek(fd, 0, seekCur), (int64)strlen(payload));
vfsClose(fd);
teardownSlot(self, previous);
}
KTEST(mmapFaultHandlerRefusesFileBackedReadAfterFdClosed)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
int fd = vfsOpen("/tmp/mmaptest-closed.txt", oReadWrite | oCreate | oTrunc);
KT_TRUE(t, fd >= 0);
KT_EQ(t, vfsWrite(fd, "x", 1), 1);
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, false, fd, 0, &addr));
vfsClose(fd);
/* Honestly scoped limitation (see mmap.h's own Vma doc comment): this
mapping cannot outlive the fd that backs it. A fault against it once
the fd is gone must fail cleanly, never silently serve zeroed or
stale data as if it had succeeded. */
KT_FALSE(t, mmapFaultHandler(self, addr, false));
KT_EQ(t, pagingResolve(addr), 0);
teardownSlot(self, previous);
}
KTEST(mmapReleaseFreesFramesAndAllowsReReservation)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 framesBefore = pmmFreeFrames();
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, 2 * frameSize, mmapProtRead | mmapProtWrite, true, -1, 0, &addr));
KT_TRUE(t, mmapFaultHandler(self, addr, false));
KT_TRUE(t, mmapFaultHandler(self, addr + frameSize, false));
KT_LE(t, pmmFreeFrames(), framesBefore - 2);
KT_TRUE(t, mmapRelease(self, addr, 2 * frameSize));
KT_EQU(t, pmmFreeFrames(), framesBefore);
KT_EQ(t, pagingResolve(addr), 0);
KT_EQ(t, pagingResolve(addr + frameSize), 0);
KT_NULL(t, mmapFind(self, addr));
/* The address range is genuinely free again, not just unmapped. */
uint64 secondAddr;
KT_TRUE(t, mmapReserve(self, addr, 2 * frameSize, mmapProtRead, true, -1, 0, &secondAddr));
KT_EQU(t, secondAddr, addr);
teardownSlot(self, previous);
}
KTEST(mmapReleaseOnNeverMappedRangeIsSafeNoop)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 framesBefore = pmmFreeFrames();
uint64 probe = mmapTestSlotBase + elfSlotStackOffset + 10 * frameSize;
KT_TRUE(t, mmapRelease(self, probe, 3 * frameSize));
KT_EQU(t, pmmFreeFrames(), framesBefore);
teardownSlot(self, previous);
}
KTEST(mmapReleaseSplitsMiddleOfAVmaIntoTwo)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, 3 * frameSize, mmapProtRead | mmapProtWrite, true, -1, 0, &addr));
KT_TRUE(t, mmapFaultHandler(self, addr, false));
KT_TRUE(t, mmapFaultHandler(self, addr + frameSize, false));
KT_TRUE(t, mmapFaultHandler(self, addr + 2 * frameSize, false));
/* Unmap only the middle page -- the low and high pages must survive,
untouched, as two now-independent Vmas. */
KT_TRUE(t, mmapRelease(self, addr + frameSize, frameSize));
KT_NE(t, pagingResolve(addr), 0);
KT_EQ(t, pagingResolve(addr + frameSize), 0);
KT_NE(t, pagingResolve(addr + 2 * frameSize), 0);
KT_NOTNULL(t, mmapFind(self, addr));
KT_NULL(t, mmapFind(self, addr + frameSize));
KT_NOTNULL(t, mmapFind(self, addr + 2 * frameSize));
/* The middle gap is genuinely free address space again. */
uint64 refilled;
KT_TRUE(t, mmapReserve(self, addr + frameSize, frameSize, mmapProtRead, true, -1, 0, &refilled));
teardownSlot(self, previous);
}
KTEST(mmapReserveFailsOnceVmaTableIsFull)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 addr;
for (int i = 0; i < maxVmasPerThread; i++)
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, true, -1, 0, &addr));
KT_FALSE(t, mmapReserve(self, 0, frameSize, mmapProtRead, true, -1, 0, &addr));
teardownSlot(self, previous);
}
KTEST(mmapReleaseAllClearsBookkeepingOnly)
{
Thread* self = currentThread();
uint64 previous = setupSlot(self);
uint64 addr;
KT_TRUE(t, mmapReserve(self, 0, frameSize, mmapProtRead, true, -1, 0, &addr));
KT_TRUE(t, mmapFaultHandler(self, addr, false));
KT_NE(t, pagingResolve(addr), 0);
mmapReleaseAll(self);
KT_NULL(t, mmapFind(self, addr));
/* mmapReleaseAll deliberately does not touch the page table -- callers
that need the frame back reclaim it their own way first (doExec).
The frame is still really mapped; teardownSlot's own whole-region
pagingUnmapUserRange (not mmapReleaseAll again) is what reclaims it
for this test. */
KT_NE(t, pagingResolve(addr), 0);
teardownSlot(self, previous);
}