From 9e9c28fe80da5222b92f45fc49ec05b4a74661c0 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 20:48:46 +0200 Subject: [PATCH] feat: extend mmap with Vma --- include/mmap.h | 120 +++++++++++++++++++++++++++++++++++++++++++++++ include/paging.h | 74 +++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 include/mmap.h diff --git a/include/mmap.h b/include/mmap.h new file mode 100644 index 0000000..e48aa50 --- /dev/null +++ b/include/mmap.h @@ -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 diff --git a/include/paging.h b/include/paging.h index 3cfce6b..62d23ca 100644 --- a/include/paging.h +++ b/include/paging.h @@ -7,8 +7,82 @@ #define pageWrite 0x2 #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); 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); +/* 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