summaryrefslogtreecommitdiff
path: root/mm/vma.cc
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-07-10 11:31:46 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-07-10 11:31:46 +0800
commite90b72ec3082588b296956730bd2930cf8d5d671 (patch)
tree36ff6916af9b4176ea5a9662219d11e721f0b691 /mm/vma.cc
parent1767d390fbf3d0c65389de46caf0b6d43ef3b6ad (diff)
downloadlume-e90b72ec3082588b296956730bd2930cf8d5d671.tar.gz
lume-e90b72ec3082588b296956730bd2930cf8d5d671.tar.xz
refactor: promote new architecture to repository root
Archive the deprecated implementation under lume-legacy, make the new kernel the default root-level build, and update repository entry points without changing kernel behavior.
Diffstat (limited to 'mm/vma.cc')
-rw-r--r--mm/vma.cc640
1 files changed, 640 insertions, 0 deletions
diff --git a/mm/vma.cc b/mm/vma.cc
new file mode 100644
index 0000000..da89e08
--- /dev/null
+++ b/mm/vma.cc
@@ -0,0 +1,640 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * vma.cc — Virtual Memory Area management (MI layer)
+ *
+ * Manages user address spaces using a red-black tree of VMA regions.
+ * Implements demand paging: vmm_map_user records intent only;
+ * physical pages are allocated on page fault via vmm_handle_page_fault.
+ *
+ * MI/MD separation: this file NEVER references pmap::PTE_* constants.
+ * Permission translation is delegated to pmap::vm_perm_to_pte().
+ *
+ * Reference: docs/specs/vmm.md
+ */
+
+#include <lume/vmm.h>
+#include <lume/addr.h>
+#include <lume/config.h>
+#include <lume/errno.h>
+#include <lume/pmm.h>
+#include <lume/new.h>
+#include <lume/klog.h>
+#include <arch/pmap.h>
+#include <lume/tlb.h>
+#include <lume/mm_layout.h>
+
+/* ------------------------------------------------------------------ */
+/* Rbtree callbacks for VMA interval tree */
+/* ------------------------------------------------------------------ */
+
+static bool vma_less(struct rb_node* node, const struct rb_node* parent) {
+ VmAreaStruct* n = reinterpret_cast<VmAreaStruct*>(
+ reinterpret_cast<char*>(node) - __builtin_offsetof(VmAreaStruct, rb));
+ const VmAreaStruct* p = reinterpret_cast<const VmAreaStruct*>(
+ reinterpret_cast<const char*>(parent) - __builtin_offsetof(VmAreaStruct, rb));
+ return n->start < p->start;
+}
+
+static int vma_cmp(const void* key, const struct rb_node* node) {
+ uint64 addr = *static_cast<const uint64*>(key);
+ const VmAreaStruct* n = reinterpret_cast<const VmAreaStruct*>(
+ reinterpret_cast<const char*>(node) - __builtin_offsetof(VmAreaStruct, rb));
+ if (addr < n->start) return -1;
+ if (addr >= n->end) return 1;
+ return 0;
+}
+
+static VmAreaStruct* node_to_vma(struct rb_node* node)
+{
+ return reinterpret_cast<VmAreaStruct*>(
+ reinterpret_cast<char*>(node) - __builtin_offsetof(VmAreaStruct, rb));
+}
+
+static uint64 align_up_any(uint64 value, uint64 align)
+{
+ if (align == 0) {
+ return value;
+ }
+ return ((value + align - 1) / align) * align;
+}
+
+static bool range_overlaps(uint64 a_start, uint64 a_end,
+ uint64 b_start, uint64 b_end)
+{
+ return a_start < b_end && b_start < a_end;
+}
+
+
+
+static bool vma_overlaps_locked(VmSpace* space, uint64 start, uint64 end)
+{
+ struct rb_node* node = rb_first(&space->vma_tree);
+ while (node) {
+ VmAreaStruct* vma = node_to_vma(node);
+ if (range_overlaps(start, end, vma->start, vma->end)) {
+ return true;
+ }
+ node = rb_next(node);
+ }
+ return false;
+}
+
+static bool vma_range_covered_locked(VmSpace* space, uint64 start, uint64 end)
+{
+ uint64 cursor = start;
+ struct rb_node* node = rb_first(&space->vma_tree);
+ while (node && cursor < end) {
+ VmAreaStruct* vma = node_to_vma(node);
+ if (vma->end <= cursor) {
+ node = rb_next(node);
+ continue;
+ }
+ if (vma->start > cursor) {
+ return false;
+ }
+ if (vma->end > cursor) {
+ cursor = vma->end;
+ }
+ node = rb_next(node);
+ }
+ return cursor >= end;
+}
+
+static void vma_try_merge_neighbors_locked(VmSpace* space, VmAreaStruct* vma)
+{
+ struct rb_node* prev_node = rb_prev(&vma->rb);
+ if (prev_node) {
+ VmAreaStruct* prev = node_to_vma(prev_node);
+ if (prev->end == vma->start && prev->perm == vma->perm) {
+ prev->end = vma->end;
+ rb_erase(&vma->rb, &space->vma_tree);
+ delete vma;
+ vma = prev;
+ }
+ }
+
+ struct rb_node* next_node = rb_next(&vma->rb);
+ if (next_node) {
+ VmAreaStruct* next = node_to_vma(next_node);
+ if (vma->end == next->start && vma->perm == next->perm) {
+ vma->end = next->end;
+ rb_erase(&next->rb, &space->vma_tree);
+ delete next;
+ }
+ }
+}
+
+static int vma_split_locked(VmSpace* space, VmAreaStruct* vma, uint64 split)
+{
+ if (split <= vma->start || split >= vma->end) {
+ return 0;
+ }
+
+ VmAreaStruct* right = new VmAreaStruct;
+ if (!right) {
+ return -ENOMEM;
+ }
+
+ right->start = split;
+ right->end = vma->end;
+ right->perm = vma->perm;
+ right->mm = space;
+
+ vma->end = split;
+ rb_add(&right->rb, &space->vma_tree, vma_less);
+ return 0;
+}
+
+static int vma_split_range_locked(VmSpace* space, uint64 start, uint64 end)
+{
+ if (end <= start) {
+ return 0;
+ }
+
+ struct rb_node* start_node = rb_find(&start, &space->vma_tree, vma_cmp);
+ if (!start_node) {
+ return -ENOMEM;
+ }
+ VmAreaStruct* start_vma = node_to_vma(start_node);
+ int ret = vma_split_locked(space, start_vma, start);
+ if (ret < 0) {
+ return ret;
+ }
+
+ uint64 end_key = end - 1;
+ struct rb_node* end_node = rb_find(&end_key, &space->vma_tree, vma_cmp);
+ if (!end_node) {
+ return -ENOMEM;
+ }
+ VmAreaStruct* end_vma = node_to_vma(end_node);
+ return vma_split_locked(space, end_vma, end);
+}
+
+/* ------------------------------------------------------------------ */
+/* VmSpace lifecycle */
+/* ------------------------------------------------------------------ */
+
+VmSpace* VmSpace::create() {
+ VmSpace* space = new VmSpace;
+ if (!space) return nullptr;
+
+ space->root_pa = pmap::create();
+ if (!space->root_pa) {
+ delete space;
+ return nullptr;
+ }
+
+ space->vma_tree = RB_ROOT;
+ space->lock.init("VmSpace");
+ space->refcount.store(1, __ATOMIC_RELAXED);
+ space->brk_start = 0;
+ space->brk_end = 0;
+ return space;
+}
+
+void VmSpace::incref() {
+ refcount.fetch_add(1, __ATOMIC_RELAXED);
+}
+
+void VmSpace::put() {
+ if (refcount.fetch_sub(1, __ATOMIC_ACQ_REL) == 1) {
+ destroy();
+ }
+}
+
+void VmSpace::destroy() {
+ /* Drain VMA tree under lock, then release BEFORE delete this */
+ lock.acquire();
+ uint64 root = root_pa;
+ struct rb_node* node = rb_first(&vma_tree);
+ while (node) {
+ VmAreaStruct* vma = reinterpret_cast<VmAreaStruct*>(
+ reinterpret_cast<char*>(node) - __builtin_offsetof(VmAreaStruct, rb));
+
+ /* Decref and dec mapcount for all mapped physical pages */
+ for (uint64 addr = vma->start; addr < vma->end; addr += kPageSize) {
+ uint64 pa;
+ if (pmap::lookup(root, addr, &pa)) {
+ Page* pg = pa_to_page(phys_addr(pa));
+ page_dec_mapcount(pg);
+ page_decref(pg);
+ }
+ }
+
+ node = rb_next(node);
+ rb_erase(&vma->rb, &vma_tree);
+ delete vma;
+ }
+ lock.release();
+
+ /* Destroy hardware page tables outside the lock */
+ pmap::destroy(root);
+
+ /* Finally free VmSpace itself */
+ delete this;
+}
+
+/* ------------------------------------------------------------------ */
+/* Public API: VMA manipulation */
+/* ------------------------------------------------------------------ */
+
+int vmm_map_user(VmSpace* space, uint64 va, uint64 len, uint64 perm) {
+ if (!space) return -EINVAL;
+
+ va = page_align_down(va);
+ len = page_align_up(len);
+ if (len == 0) return 0;
+ if (va < kUserVaBase) return -EINVAL;
+ if (va >= kUserVaLimit) return -EINVAL;
+ if (va + len < va || va + len > kUserVaLimit) return -EINVAL;
+
+ VmAreaStruct* vma = new VmAreaStruct;
+ if (!vma) return -ENOMEM;
+ vma->start = va;
+ vma->end = va + len;
+ vma->perm = perm | VM_USER; // Enforce user-mode access (MI flag)
+ vma->mm = space;
+
+ LockGuard guard(space->lock);
+ if (vma_overlaps_locked(space, vma->start, vma->end)) {
+ delete vma;
+ return -EINVAL;
+ }
+ rb_add(&vma->rb, &space->vma_tree, vma_less);
+ vma_try_merge_neighbors_locked(space, vma);
+ return 0;
+}
+
+int vmm_unmap_user(VmSpace* space, uint64 va, uint64 len) {
+ if (!space) return -EINVAL;
+ va = page_align_down(va);
+ len = page_align_up(len);
+ if (len == 0) return 0;
+ if (va < kUserVaBase) return -EINVAL;
+ if (va >= kUserVaLimit) return -EINVAL;
+ if (va + len < va || va + len > kUserVaLimit) return -EINVAL;
+ uint64 end = va + len;
+
+ LockGuard guard(space->lock);
+ struct rb_node* node = rb_first(&space->vma_tree);
+ while (node) {
+ VmAreaStruct* vma = node_to_vma(node);
+ struct rb_node* next = rb_next(node);
+
+ if (vma->end <= va) {
+ node = next;
+ continue;
+ }
+ if (vma->start >= end) {
+ break;
+ }
+
+ uint64 unmap_start = (vma->start > va) ? vma->start : va;
+ uint64 unmap_end = (vma->end < end) ? vma->end : end;
+
+ if (unmap_start < unmap_end) {
+ for (uint64 addr = unmap_start; addr < unmap_end; addr += kPageSize) {
+ uint64 pa;
+ if (pmap::lookup(space->root_pa, addr, &pa)) {
+ pmap::unmap(space->root_pa, addr);
+ Page* pg = pa_to_page(phys_addr(pa));
+ page_dec_mapcount(pg);
+ page_decref(pg);
+ }
+ }
+ }
+
+ if (unmap_start == vma->start && unmap_end == vma->end) {
+ rb_erase(&vma->rb, &space->vma_tree);
+ delete vma;
+ } else if (unmap_start == vma->start) {
+ rb_erase(&vma->rb, &space->vma_tree);
+ vma->start = unmap_end;
+ rb_add(&vma->rb, &space->vma_tree, vma_less);
+ } else if (unmap_end == vma->end) {
+ vma->end = unmap_start;
+ } else {
+ VmAreaStruct* right = new VmAreaStruct;
+ if (!right) {
+ return -ENOMEM;
+ }
+ right->start = unmap_end;
+ right->end = vma->end;
+ right->perm = vma->perm;
+ right->mm = space;
+
+ vma->end = unmap_start;
+ rb_add(&right->rb, &space->vma_tree, vma_less);
+ break;
+ }
+
+ node = next;
+ }
+ return 0;
+}
+
+int vmm_protect_user(VmSpace* space, uint64 va, uint64 len, uint64 perm)
+{
+ if (!space) return -EINVAL;
+ va = page_align_down(va);
+ len = page_align_up(len);
+ if (len == 0) return -EINVAL;
+ if (va < kUserVaBase) return -EINVAL;
+ if (va >= kUserVaLimit) return -EINVAL;
+ if (va + len < va || va + len > kUserVaLimit) return -EINVAL;
+
+ uint64 end = va + len;
+
+ LockGuard guard(space->lock);
+ if (!vma_range_covered_locked(space, va, end)) {
+ return -ENOMEM;
+ }
+ int ret = vma_split_range_locked(space, va, end);
+ if (ret < 0) {
+ return ret;
+ }
+
+ bool tlb_flush_needed = false;
+ struct rb_node* node = rb_find(&va, &space->vma_tree, vma_cmp);
+ while (node) {
+ VmAreaStruct* vma = node_to_vma(node);
+ if (vma->start >= end) {
+ break;
+ }
+ if (vma->end <= va) {
+ node = rb_next(node);
+ continue;
+ }
+
+ vma->perm = (perm | VM_USER);
+
+ for (uint64 addr = vma->start; addr < vma->end; addr += kPageSize) {
+ uint64 pa;
+ if (!pmap::lookup(space->root_pa, addr, &pa)) {
+ continue;
+ }
+
+ if (perm == 0) {
+ Page* pg = pa_to_page(phys_addr(pa));
+ page_dec_mapcount(pg);
+ page_decref(pg);
+ pmap::unmap(space->root_pa, addr);
+ tlb_flush_needed = true;
+ continue;
+ }
+
+ uint64 pte_perm = pmap::vm_perm_to_pte(vma->perm);
+ if ((vma->perm & VM_WRITE) && pmap::is_cow(space->root_pa, addr)) {
+ pte_perm &= ~pmap::PTE_W;
+ pte_perm |= pmap::PTE_COW;
+ }
+
+ int ret = pmap::protect(space->root_pa, addr, pte_perm);
+ if (ret < 0) {
+ return ret;
+ }
+ tlb_flush_needed = true;
+ }
+
+ node = rb_next(node);
+ }
+
+ if (tlb_flush_needed) {
+ tlb_shootdown_range(space, va, end);
+ }
+ return 0;
+}
+
+uint64 mm_find_free_va(VmSpace* space, uint64 length, uint64 align)
+{
+ if (!space) return 0;
+ length = page_align_up(length);
+ if (length == 0) return 0;
+ if (align < kPageSize) align = kPageSize;
+
+ LockGuard guard(space->lock);
+
+ /* Linux: arch_get_unmapped_area() starts from mm->mmap_base.
+ * RISC-V: TASK_UNMAPPED_BASE = PAGE_ALIGN(TASK_SIZE / 3).
+ * Fall back to kUserVaBase if mmap_base not set (kernel VmSpaces). */
+ uint64 base = space->mmap_base ? space->mmap_base : kUserVaBase;
+ uint64 cursor = align_up_any(base, align);
+ struct rb_node* node = rb_first(&space->vma_tree);
+ while (node) {
+ VmAreaStruct* vma = node_to_vma(node);
+ if (cursor <= vma->start) {
+ if (cursor > kUserVaLimit - length) {
+ return 0;
+ }
+ if (cursor + length <= vma->start) {
+ return cursor;
+ }
+ }
+ if (cursor < vma->end) {
+ cursor = vma->end;
+ }
+ cursor = align_up_any(cursor, align);
+ node = rb_next(node);
+ }
+
+ if (cursor > kUserVaLimit - length) {
+ return 0;
+ }
+ return cursor;
+}
+
+bool vma_range_covered(VmSpace* space, uint64 start, uint64 end)
+{
+ if (!space) return false;
+ start = page_align_down(start);
+ end = page_align_up(end);
+ if (end <= start) return true;
+
+ LockGuard guard(space->lock);
+ return vma_range_covered_locked(space, start, end);
+}
+
+/* ------------------------------------------------------------------ */
+/* Page fault handler (demand paging) */
+/* ------------------------------------------------------------------ */
+
+static bool vma_allows(const VmAreaStruct* vma, VmFaultCause cause)
+{
+ switch (cause) {
+ case VmFaultCause::Read:
+ return (vma->perm & VM_READ) != 0;
+ case VmFaultCause::Write:
+ return (vma->perm & VM_WRITE) != 0;
+ case VmFaultCause::Exec:
+ return (vma->perm & VM_EXEC) != 0;
+ default:
+ return false;
+ }
+}
+
+int vmm_handle_page_fault(VmSpace* space, uint64 fault_addr, VmFaultCause cause) {
+ if (!space) return -EINVAL;
+
+ uint64 aligned_addr = page_align_down(fault_addr);
+
+ LockGuard guard(space->lock);
+ struct rb_node* node = rb_find(&fault_addr, &space->vma_tree, vma_cmp);
+ if (!node) {
+ return -EFAULT; // No VMA covers this address → segfault
+ }
+
+ VmAreaStruct* vma = reinterpret_cast<VmAreaStruct*>(
+ reinterpret_cast<char*>(node) - __builtin_offsetof(VmAreaStruct, rb));
+
+ if (!vma_allows(vma, cause)) {
+ return -EFAULT;
+ }
+
+ /* Check if page is already mapped */
+ uint64 paddr;
+ if (pmap::lookup(space->root_pa, aligned_addr, &paddr)) {
+ if (cause == VmFaultCause::Write && pmap::is_cow(space->root_pa, aligned_addr)) {
+ /* Store Page Fault on a COW page */
+ Page* page = pmm_alloc_page();
+ if (!page) return -ENOMEM;
+ uint64 new_pa = page_to_pa(page).raw;
+
+ /* Copy page contents */
+ uint8* dst = reinterpret_cast<uint8*>(pa_to_va(phys_addr(new_pa)).raw);
+ const uint8* src = reinterpret_cast<const uint8*>(pa_to_va(phys_addr(paddr)).raw);
+ for (uint64 i = 0; i < kPageSize; i++) {
+ dst[i] = src[i];
+ }
+
+ /* Remap with write permission, clearing COW */
+ uint64 pte_perm = pmap::vm_perm_to_pte(vma->perm);
+ if (pmap::map(space->root_pa, aligned_addr, new_pa, pte_perm) != 0) {
+ pmm_free_page(page);
+ return -ENOMEM;
+ }
+
+ /* New page gets mapcount = 1 (we just mapped it) */
+ page_inc_mapcount(page);
+
+ /* Decrement mapcount and refcount of old page */
+ Page* old_page = pa_to_page(phys_addr(paddr));
+ page_dec_mapcount(old_page);
+ page_decref(old_page);
+
+ /* Flush TLB to remove old read-only cached translation */
+ tlb_shootdown_page(space, aligned_addr);
+ return 0;
+ }
+ /* DEBUG: page already mapped but still faulting — dump PTE */
+ {
+ uint64 cur_satp;
+ asm volatile("csrr %0, satp" : "=r"(cur_satp));
+ printk(KERN_ERR "fault: va=%llx pa=%llx cause=%d pte=%llx satp=%llx root=%llx\n",
+ aligned_addr, paddr, static_cast<int>(cause),
+ pmap::lookup_pte(space->root_pa, aligned_addr),
+ cur_satp, space->root_pa);
+ }
+ return 0; // Already mapped, maybe read fault on existing page
+ }
+
+ /* Allocate physical page and zero it.
+ * Linux: do_anonymous_page() uses alloc_zeroed_user_highpage_movable().
+ * Anonymous pages MUST be zeroed — userspace expects MAP_ANONYMOUS
+ * pages to contain all zeros (POSIX requirement). */
+ Page* page = pmm_alloc_page();
+ if (!page) return -ENOMEM;
+
+ uint64 pa = page_to_pa(page).raw;
+
+ /* Zero the page before mapping it to userspace */
+ /* Zero the page before mapping it to userspace */
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ for (uint64 i = 0; i < kPageSize; i++) kva[i] = 0;
+
+ /* Translate MI permission flags to hardware PTE bits (MD layer) */
+ uint64 pte_perm = pmap::vm_perm_to_pte(vma->perm);
+
+ if (pmap::map(space->root_pa, aligned_addr, pa, pte_perm) != 0) {
+ pmm_free_page(page);
+ return -ENOMEM;
+ }
+
+ /* Initial mapping: refcount is already 1 from pmm_alloc_page.
+ * Set mapcount = 1 to track this PTE mapping. */
+ page_inc_mapcount(page);
+
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Fork / Clone (COW infrastructure) */
+/* ------------------------------------------------------------------ */
+
+VmSpace* vmm_clone(VmSpace* parent) {
+ if (!parent) return nullptr;
+
+ VmSpace* child = VmSpace::create();
+ if (!child) return nullptr;
+
+ child->brk_start = parent->brk_start;
+ child->brk_end = parent->brk_end;
+
+ LockGuard parent_guard(parent->lock);
+
+ struct rb_node* node = rb_first(&parent->vma_tree);
+ while (node) {
+ VmAreaStruct* parent_vma = reinterpret_cast<VmAreaStruct*>(
+ reinterpret_cast<char*>(node) - __builtin_offsetof(VmAreaStruct, rb));
+
+ VmAreaStruct* child_vma = new VmAreaStruct;
+ if (!child_vma) {
+ child->destroy();
+ return nullptr;
+ }
+ child_vma->start = parent_vma->start;
+ child_vma->end = parent_vma->end;
+ child_vma->perm = parent_vma->perm;
+ child_vma->mm = child;
+
+ /* Copy pages and mark COW if writable */
+ for (uint64 va = child_vma->start; va < child_vma->end; va += kPageSize) {
+ uint64 pa;
+ if (pmap::lookup(parent->root_pa, va, &pa)) {
+ /* Make parent COW if it was writable */
+ if (child_vma->perm & VM_WRITE) {
+ pmap::make_cow(parent->root_pa, va);
+ /* Flush TLB for parent since we reduced permissions */
+ tlb_shootdown_page(parent, va);
+ }
+
+ /* Map in child as COW or read-only */
+ uint64 pte_perm = pmap::vm_perm_to_pte(child_vma->perm);
+ if (child_vma->perm & VM_WRITE) {
+ pte_perm &= ~pmap::PTE_W;
+ pte_perm |= pmap::PTE_COW;
+ }
+
+ if (pmap::map(child->root_pa, va, pa, pte_perm) != 0) {
+ /* If map fails, we leak what we did in this loop for now,
+ * but child->destroy() will clean it up safely since
+ * we increment refcounts as we go. */
+ delete child_vma;
+ child->destroy();
+ return nullptr;
+ }
+
+ /* Increment physical page refcount and mapcount */
+ Page* shared_page = pa_to_page(phys_addr(pa));
+ page_incref(shared_page);
+ page_inc_mapcount(shared_page);
+ }
+ }
+
+ rb_add(&child_vma->rb, &child->vma_tree, vma_less);
+ node = rb_next(node);
+ }
+
+ return child;
+}