feat: extend mmap with Vma
This commit is contained in:
1 parent
fd358f0fa6
commit
9e9c28fe80
2 files changed
+194
No files matched your search
+120
@@ -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
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user