feat: extend mmap with Vma

This commit is contained in:
dualfroz committed 2026-09-01 20:48:46 +02:00
1 parent fd358f0fa6
commit 9e9c28fe80
2 files changed
+194

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
#ifndef FROSTBYTE_MMAP_H
#define FROSTBYTE_MMAP_H
#include "types.h"
#include "fs.h"
struct Thread;
/* Cap on live mappings per thread. Small and fixed like every other
per-Thread table in this kernel (maxOpenFiles=16, sigMaxSignals=32) --
nothing here needs more than a handful of distinct mappings at once, and
a hard cap keeps Vma[] a plain fixed-size array embedded directly in
Thread (sched.h), exactly like FileDescriptor fds[maxOpenFiles] already
is, rather than something that needs its own allocator. */
#define maxVmasPerThread 8
/* One live mapping inside a single thread's own elfSlotSize slot (see
elf.h) -- mmap.c reserves [start, end) at mmapReserve time but allocates
no physical frames and reads no file data yet; mmapFaultHandler below
fills each page lazily, the first time a real #PF (isr.c) proves it was
actually touched. `fd` (meaningful only when `anonymous` is false) names
a slot in the OWNING thread's own fd table (vfs.h) -- this mapping is
only ever resolved for its own owning thread (mmapFaultHandler's own
contract), so there is no cross-thread fd ambiguity. Unlike a real
kernel's struct vm_area_struct, there is no backing inode reference
independent of that fd: closing `fd` before every page has been faulted
in leaves this mapping unable to fill the rest (mmapFaultHandler treats
a since-closed fd as a fault it cannot resolve, turning into a real
SIGSEGV rather than silently serving zeroed pages for wrong reasons) --
an honestly scoped limitation (real mmap(2) lets a mapping outlive the
fd that created it), not a hidden bug, chosen because holding a
reference to a file independent of any thread's fd table would need a
refcounted VfsNode/inode layer this kernel does not otherwise have. */
typedef struct Vma {
bool used;
uint64 start;
uint64 end;
uint32 prot;
bool anonymous;
int fd;
uint64 fileOffset;
} Vma;
/* Reserves [addr, addr+length) -- or, when addr is 0, a first-fit region
of `length` bytes the kernel picks itself, real mmap(NULL, ...)-style --
inside `thread`'s own mmap region ([userSlotBase+elfSlotStackOffset,
userSlotBase+elfSlotSize), see elf.h: the 128 MiB of every elfSlotSize
slot that sits above the small fixed-size stack and is never otherwise
touched by an ELF image or brk). No physical frame is allocated and no
byte is read yet -- see mmapFaultHandler for when that actually happens.
On success, writes the mapping's real base address to *outAddr (matters
when addr was 0) and returns true. Returns false, touching *outAddr not
at all, if: length is 0, addr or length is not frameSize-aligned, a
non-anonymous mapping's fileOffset is not frameSize-aligned, the
resulting range would fall outside the mmap region, it would overlap an
existing Vma of this same thread, no addr was given and no free region
of that size exists, or every one of maxVmasPerThread's own slots is
already in use. */
bool mmapReserve(struct Thread* thread, uint64 addr, uint64 length, uint32 prot,
bool anonymous, int fd, uint64 fileOffset, uint64* outAddr);
/* Real munmap(2): removes every Vma of `thread` covering any part of
[addr, addr+length), giving back to the pmm whatever physical frames
were actually faulted in for the removed range (pagingUnmapUserRange,
paging.h -- the same "only what is actually present costs anything"
contract that function already documents for itself). A length that only
overlaps one end of an existing Vma shrinks it in place; a length that
lands entirely inside one Vma's interior, touching neither end, splits
it into two (consuming one more Vma slot for the upper half) -- since
Vmas of the same thread never overlap each other (mmapReserve's own
refusal), at most one Vma can ever need splitting per call, so this
either finds the one free slot it needs or refuses the WHOLE call up
front, leaving every Vma of this thread completely untouched, rather
than leaving the caller's mapping table half-modified by a resource
failure it could not have predicted from addr/length alone. A range that
overlaps no Vma of this thread at all is a no-op returning true, real
munmap(2)'s own "unmapping already-unmapped memory is not an error"
behavior. Returns false only for a misaligned addr, a zero length, or
the above split-with-no-free-slot case. */
bool mmapRelease(struct Thread* thread, uint64 addr, uint64 length);
/* Looks up the (at most one, since a thread's own Vmas never overlap each
other) Vma of `thread` covering `addr`, or nullptr if none does. */
Vma* mmapFind(struct Thread* thread, uint64 addr);
/* The real page-fault resolver: called by isr.c for a ring-3 #PF whose
faulting address might belong to `thread`'s own mmap region. Looks up
the covering Vma (mmapFind); returns false, mapping nothing, if there is
none, or if `forWrite` is true but the Vma's own prot lacks
mmapProtWrite (isr.c turns either case into a real SIGSEGV, see its own
comment) or (mmapProtRead is missing on a read fault, refused the same
way). Otherwise allocates one physical frame, fills it -- zeroed for an
anonymous Vma; for a file-backed one, read from the Vma's own `fd`
starting at the file offset that corresponds to this page (whatever
vfsRead returns short of a full frameSize read, e.g. a page straddling
end-of-file, is left zero-padded, since the frame was already
zero-filled before the read) -- maps it at the containing page with
pageUser | (mmapProtWrite set ? pageWrite : 0), and returns true so
isr.c can simply retry the faulting instruction, exactly like any other
successfully resolved demand-paging fault. Only ever meaningful when
`thread` is the CURRENTLY RUNNING thread (it reads/restores that
thread's own fd offset through vfsRead/vfsLseek, which resolve fd
numbers against currentThread() -- true for both isr.c's real caller and
mmapTest.c's own KTEST coverage, which runs entirely on the idle thread,
itself always currentThread() while KTEST executes). */
bool mmapFaultHandler(struct Thread* thread, uint64 faultAddr, bool forWrite);
/* Zeroes every Vma of `thread` without touching any physical frame or
page-table entry -- callers that already reclaim frames some other way
(doExec's own pagingUnmapUserRange over the relevant range, syscall.c)
call this afterward purely to stop a later mmapReserve on the same
thread from ever finding a stale Vma left over from an image this
thread no longer runs. threadReap/threadDiscard (sched.c) do NOT need
to call this themselves: both already memset() the whole Thread struct
before a slot is handed back to allocSlot, which zeroes the embedded
Vma[] array as a side effect, exactly like every other per-thread table
in it. */
void mmapReleaseAll(struct Thread* thread);
#endif
+74
View File
@@ -7,8 +7,82 @@
#define pageWrite 0x2 #define pageWrite 0x2
#define pageUser 0x4 #define pageUser 0x4
/* Real x86_64 PTE bit 63 (XD/NX) -- refuses instruction fetch against a
page that has it set, enforced by the CPU itself once
pagingNxSupported() is true (boot.S enables IA32_EFER.NXE at boot,
gated on CPUID.80000001H:EDX.NX so this never sets a reserved bit on
hardware/hypervisors that predate it). pagingMap silently drops this
bit from the leaf entry it writes when NX is not supported -- the page
stays executable rather than risk a reserved-bit #PF, an honest
best-effort rather than a promise this bit is always enforced. */
#define pageNoExecute 0x8000000000000000ull
/* True once boot.S has confirmed both that the CPU reports
CPUID.80000001H:EDX.NX and that IA32_EFER.NXE was actually set --
callers that care whether pageNoExecute is real hardware enforcement
(as opposed to a bit pagingMap quietly ignored) check this. */
bool pagingNxSupported(void);
bool pagingMap(uint64 virtualAddr, uint64 physicalAddr, uint64 flags); bool pagingMap(uint64 virtualAddr, uint64 physicalAddr, uint64 flags);
uint64 pagingResolve(uint64 virtualAddr); uint64 pagingResolve(uint64 virtualAddr);
/* True if virtualAddr is currently mapped AND executable (present, and
either pageNoExecute was never asked for or the CPU cannot enforce it
-- see pagingNxSupported). False for an unmapped address or one mapped
with pageNoExecute successfully applied. Introspection only, used by
pagingTest.c/mmapTest.c to verify pagingMap/pagingMapUserRangeFlags
actually wrote the bit they were asked to, without needing a live
instruction-fetch fault to prove it indirectly. */
bool pagingIsExecutable(uint64 virtualAddr);
/* Same shape as pagingIsExecutable, for pageWrite instead of pageNoExecute:
true if virtualAddr is currently mapped AND writable, false for an
unmapped address or one mapped without pageWrite. Introspection only,
added alongside pagingMapUserRangeExact so pagingTest.c can prove a
range came back genuinely read-only (e.g. an ELF binary's own .text/
.rodata, elf.c's elfLoad) directly, without needing a live user-mode
write fault to prove it indirectly the way user/elfwritevictim.c's
SIGSEGV does. */
bool pagingIsWritable(uint64 virtualAddr);
bool pagingMapUserRange(uint64 virtualAddr, uint64 size); bool pagingMapUserRange(uint64 virtualAddr, uint64 size);
/* Same as pagingMapUserRange, plus extraFlags (pageNoExecute, currently
the only bit worth passing here) OR'd into every leaf PTE this call
creates -- pagingMapUserRange itself is a thin wrapper passing 0.
Callers that want a non-executable range (a stack, a brk-grown heap --
see kmain.c/syscall.c/jobctl.c's stack setup and sched.c's
currentThreadSbrk) pass pageNoExecute. A thin wrapper itself, over
pagingMapUserRangeExact below, that always forces pageWrite on --
every caller of this entry point (the ones just named, plus
pagingMapUserRange) wants a writable range and only ever varies whether
it is also executable. elf.c's own per-ELF-segment loader is the one
caller in this kernel that needs a range which may be READ-ONLY too (a
binary's own .text/.rodata), so it calls pagingMapUserRangeExact
directly instead. */
bool pagingMapUserRangeFlags(uint64 virtualAddr, uint64 size, uint64 extraFlags);
/* Lowest-level of the three pagingMapUserRange* entry points: flags is
the exact set of extra leaf PTE bits (pageWrite, pageNoExecute) OR'd
onto pageUser for every page in the range -- unlike
pagingMapUserRangeFlags, pageWrite is NOT forced on, so a caller that
omits it gets a genuinely read-only mapping. elf.c's elfLoad is the
only caller today: each ELF program header's own p_flags (via
elfSegmentPagingFlags) decides per-segment whether the range this call
maps is writable, executable, both, or (read-only, non-executable)
neither -- real per-segment W^X instead of one blanket writable+
executable range for a whole binary's image. */
bool pagingMapUserRangeExact(uint64 virtualAddr, uint64 size, uint64 flags);
/* Inverse of pagingMapUserRange: walks [virtualAddr, virtualAddr+size) one
frame at a time and, for every page that is actually present, gives its
physical frame back to the pmm and clears the PTE (present bit gone,
TLB invalidated). Pages that were never mapped -- including whole page-
table levels that were never allocated -- are silently skipped rather
than treated as an error: callers (threadReap/threadDiscard in sched.c,
doExec in syscall.c) always unmap a thread's whole elfSlotSize-sized
slot, most of which a typical small user program never touches. Safe to
call on a range that overlaps no mapping at all (a no-op). */
void pagingUnmapUserRange(uint64 virtualAddr, uint64 size);
#endif #endif