summaryrefslogtreecommitdiff
path: root/na
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-04-21 23:23:49 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-04-21 23:23:49 +0800
commit51899557c770b3dc9bd02579abf491745e607d92 (patch)
tree9681432a6db30a29f47aa1b21bbad777dfadd6c9 /na
parentab2c0a8ea472679c846e74ca40823091becc5bf0 (diff)
downloadlume-51899557c770b3dc9bd02579abf491745e607d92.tar.gz
lume-51899557c770b3dc9bd02579abf491745e607d92.tar.xz
feat: add boot self-tests for various kernel components
- Implemented a self-test runner in `selftest.cc` to invoke all test suites. - Added tests for atomic operations in `test_atomic.cc`, covering construction, store/load, exchange, arithmetic, and compare-and-swap. - Created tests for the Flattened Device Tree (FDT) in `test_fdt.cc`, verifying node access and compatibility queries. - Developed a comprehensive Physical Memory Manager (PMM) test suite in `test_pmm.cc`, including allocation, deallocation, reference counting, and boundary conditions. - Implemented Red-Black Tree tests in `test_rbtree.cc`, validating insertion, deletion, and iteration. - Added Slab Allocator tests in `test_slab.cc`, covering various allocation sizes, edge cases, and operator overloads. - Created Spinlock tests in `test_spinlock.cc`, ensuring proper locking behavior and interrupt state preservation. - Developed Virtual Memory Area (VMA) tests in `test_vma.cc`, validating region mapping, unmapping, and fault handling. - Added tests for Virtual Memory Management (VMM) in `test_vmm.cc`, verifying address translation and page alignment.
Diffstat (limited to 'na')
-rw-r--r--na/include/lume/selftest.h110
-rw-r--r--na/test/selftest.cc41
-rw-r--r--na/test/test_atomic.cc102
-rw-r--r--na/test/test_fdt.cc50
-rw-r--r--na/test/test_pmm.cc283
-rw-r--r--na/test/test_rbtree.cc119
-rw-r--r--na/test/test_slab.cc255
-rw-r--r--na/test/test_spinlock.cc135
-rw-r--r--na/test/test_vma.cc158
-rw-r--r--na/test/test_vmm.cc61
10 files changed, 1314 insertions, 0 deletions
diff --git a/na/include/lume/selftest.h b/na/include/lume/selftest.h
new file mode 100644
index 0000000..f5beffd
--- /dev/null
+++ b/na/include/lume/selftest.h
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#pragma once
+
+/*
+ * Boot Self-Test Framework
+ *
+ * Minimal assertion-based test harness that runs during kernel_main()
+ * after all subsystems are initialized. Each test is a void function
+ * that calls ST_ASSERT / ST_ASSERT_EQ macros. On failure, prints
+ * the failing expression and panics.
+ *
+ * Usage:
+ * void selftest_pmm(); // defined in test/test_pmm.cc
+ * void selftest_slab(); // defined in test/test_slab.cc
+ *
+ * All self-tests run only on BSP in single-core mode.
+ */
+
+#include <lume/types.h>
+#include <lume/panic.h>
+
+extern "C" void early_puts(const char* s);
+
+/* Print a decimal number (minimal, no printf available) */
+inline void st_print_num(uint64 n)
+{
+ if (n == 0) { early_puts("0"); return; }
+ char buf[21];
+ int i = 20;
+ buf[i] = '\0';
+ while (n > 0 && i > 0) {
+ buf[--i] = '0' + static_cast<char>(n % 10);
+ n /= 10;
+ }
+ early_puts(&buf[i]);
+}
+
+inline void st_print_hex(uint64 n)
+{
+ early_puts("0x");
+ char buf[17];
+ int i = 16;
+ buf[i] = '\0';
+ if (n == 0) { early_puts("0"); return; }
+ while (n > 0 && i > 0) {
+ int d = static_cast<int>(n & 0xF);
+ buf[--i] = (d < 10) ? ('0' + d) : ('a' + d - 10);
+ n >>= 4;
+ }
+ early_puts(&buf[i]);
+}
+
+#define ST_ASSERT(cond) \
+ do { \
+ if (!(cond)) { \
+ early_puts("[FAIL] "); \
+ early_puts(__FILE__); \
+ early_puts(":"); \
+ st_print_num(__LINE__); \
+ early_puts(": assertion failed: " #cond "\n"); \
+ kernel_panic("selftest failed", #cond); \
+ } \
+ } while (0)
+
+#define ST_ASSERT_EQ(a, b) \
+ do { \
+ auto _a = (a); auto _b = (b); \
+ if (_a != _b) { \
+ early_puts("[FAIL] "); \
+ early_puts(__FILE__); \
+ early_puts(":"); \
+ st_print_num(__LINE__); \
+ early_puts(": " #a " != " #b " ("); \
+ st_print_hex((uint64)(uintptr)(_a)); \
+ early_puts(" != "); \
+ st_print_hex((uint64)(uintptr)(_b)); \
+ early_puts(")\n"); \
+ kernel_panic("selftest failed", #a " != " #b); \
+ } \
+ } while (0)
+
+#define ST_ASSERT_NE(a, b) \
+ do { \
+ auto _a = (a); auto _b = (b); \
+ if (_a == _b) { \
+ early_puts("[FAIL] "); \
+ early_puts(__FILE__); \
+ early_puts(":"); \
+ st_print_num(__LINE__); \
+ early_puts(": " #a " == " #b " (both "); \
+ st_print_hex((uint64)(uintptr)(_a)); \
+ early_puts(")\n"); \
+ kernel_panic("selftest failed", #a " == " #b); \
+ } \
+ } while (0)
+
+/* Announce a test suite beginning/end */
+inline void st_begin(const char* name)
+{
+ early_puts("[test] ");
+ early_puts(name);
+ early_puts(" ... ");
+}
+
+inline void st_pass()
+{
+ early_puts("PASS\n");
+}
diff --git a/na/test/selftest.cc b/na/test/selftest.cc
new file mode 100644
index 0000000..a992ae2
--- /dev/null
+++ b/na/test/selftest.cc
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Boot Self-Test Runner
+ *
+ * Entry point that invokes all registered self-test suites.
+ * Called from kernel_main() after all subsystem initialization.
+ */
+
+#include <lume/selftest.h>
+
+/* Individual test suite entry points */
+void selftest_pmm();
+void selftest_slab();
+void selftest_spinlock();
+void selftest_rbtree();
+void selftest_vmm();
+void selftest_fdt();
+void selftest_vma();
+void selftest_atomic();
+
+void selftest_run_all()
+{
+ early_puts("\n========================================\n");
+ early_puts(" Boot Self-Test Suite\n");
+ early_puts("========================================\n");
+
+ selftest_spinlock();
+ selftest_pmm();
+ selftest_slab();
+ selftest_rbtree();
+ selftest_vmm();
+ selftest_vma();
+ selftest_atomic();
+ selftest_fdt();
+
+ early_puts("========================================\n");
+ early_puts(" All self-tests PASSED\n");
+ early_puts("========================================\n\n");
+}
diff --git a/na/test/test_atomic.cc b/na/test/test_atomic.cc
new file mode 100644
index 0000000..9851e7b
--- /dev/null
+++ b/na/test/test_atomic.cc
@@ -0,0 +1,102 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Boot Self-Test: lume::atomic<T>
+ *
+ * Tests:
+ * 1. Default construction (zero-initialized)
+ * 2. Explicit construction
+ * 3. store / load round-trip with different memory orders
+ * 4. exchange returns old value
+ * 5. fetch_add / fetch_sub arithmetic
+ * 6. compare_exchange success and failure paths
+ */
+
+#include <lume/selftest.h>
+#include <lume/atomic.h>
+
+void selftest_atomic() {
+ st_begin("atomic: default construction is zero");
+ {
+ lume::atomic<uint32> a;
+ ST_ASSERT_EQ(a.load(), 0U);
+ }
+ st_pass();
+
+ st_begin("atomic: explicit construction");
+ {
+ lume::atomic<uint64> a(42);
+ ST_ASSERT_EQ(a.load(), 42ULL);
+ }
+ st_pass();
+
+ st_begin("atomic: store/load round-trip");
+ {
+ lume::atomic<uint32> a;
+ a.store(0xDEAD, __ATOMIC_RELEASE);
+ ST_ASSERT_EQ(a.load(__ATOMIC_ACQUIRE), 0xDEADU);
+
+ a.store(0, __ATOMIC_SEQ_CST);
+ ST_ASSERT_EQ(a.load(__ATOMIC_SEQ_CST), 0U);
+ }
+ st_pass();
+
+ st_begin("atomic: exchange returns old value");
+ {
+ lume::atomic<uint32> a(10);
+ uint32 old = a.exchange(20);
+ ST_ASSERT_EQ(old, 10U);
+ ST_ASSERT_EQ(a.load(), 20U);
+ }
+ st_pass();
+
+ st_begin("atomic: fetch_add / fetch_sub");
+ {
+ lume::atomic<uint32> a(100);
+
+ uint32 old = a.fetch_add(5);
+ ST_ASSERT_EQ(old, 100U);
+ ST_ASSERT_EQ(a.load(), 105U);
+
+ old = a.fetch_sub(10);
+ ST_ASSERT_EQ(old, 105U);
+ ST_ASSERT_EQ(a.load(), 95U);
+
+ // Underflow wraps (unsigned)
+ lume::atomic<uint32> b(0);
+ old = b.fetch_sub(1);
+ ST_ASSERT_EQ(old, 0U);
+ ST_ASSERT_EQ(b.load(), 0xFFFFFFFFU);
+ }
+ st_pass();
+
+ st_begin("atomic: compare_exchange success and failure");
+ {
+ lume::atomic<uint32> a(42);
+
+ // Success path: expected matches, swap happens
+ uint32 expected = 42;
+ bool ok = a.compare_exchange(expected, 99);
+ ST_ASSERT(ok);
+ ST_ASSERT_EQ(a.load(), 99U);
+ ST_ASSERT_EQ(expected, 42U); // expected unchanged on success
+
+ // Failure path: expected doesn't match, expected gets updated
+ expected = 0; // wrong
+ ok = a.compare_exchange(expected, 200);
+ ST_ASSERT(!ok);
+ ST_ASSERT_EQ(a.load(), 99U); // value unchanged
+ ST_ASSERT_EQ(expected, 99U); // expected updated to actual value
+ }
+ st_pass();
+
+ st_begin("atomic: 64-bit fetch_add correctness");
+ {
+ lume::atomic<uint64> a(0xFFFFFFFF00000000ULL);
+ uint64 old = a.fetch_add(1);
+ ST_ASSERT_EQ(old, 0xFFFFFFFF00000000ULL);
+ ST_ASSERT_EQ(a.load(), 0xFFFFFFFF00000001ULL);
+ }
+ st_pass();
+}
diff --git a/na/test/test_fdt.cc b/na/test/test_fdt.cc
new file mode 100644
index 0000000..cf09029
--- /dev/null
+++ b/na/test/test_fdt.cc
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Boot Self-Test: FDT (Flattened Device Tree)
+ */
+
+#include <lume/selftest.h>
+#include <lume/fdt.h>
+
+// Manually declare what we need to avoid header conflicts with toolchain libc
+extern "C" int strcmp(const char *s1, const char *s2);
+
+void selftest_fdt() {
+ st_begin("fdt: tree root access");
+ {
+ DeviceNode* root = fdt_get_node_by_path("/");
+ ST_ASSERT(root != nullptr);
+ ST_ASSERT_EQ(strcmp(root->get_name(), ""), 0);
+ }
+ st_pass();
+
+ st_begin("fdt: find existing compatible node (UART)");
+ {
+ // QEMU virt RISC-V platform always has a ns16550a UART
+ DeviceNode* uart = fdt_find_compatible("ns16550a");
+ ST_ASSERT(uart != nullptr);
+
+ // Use get_prop_string which is convenience helper
+ const char* compat = uart->get_prop_string("compatible");
+ ST_ASSERT(compat != nullptr);
+
+ // Check raw property access
+ const FdtProperty* reg = uart->get_prop("reg");
+ ST_ASSERT(reg != nullptr);
+ ST_ASSERT(reg->value != nullptr);
+ ST_ASSERT(reg->len > 0);
+ }
+ st_pass();
+
+ st_begin("fdt: non-existent node/compatible queries");
+ {
+ DeviceNode* missing1 = fdt_get_node_by_path("/definitely_does_not_exist_node_12345");
+ ST_ASSERT(missing1 == nullptr);
+
+ DeviceNode* missing2 = fdt_find_compatible("invalid_compatible_string_xyz");
+ ST_ASSERT(missing2 == nullptr);
+ }
+ st_pass();
+}
diff --git a/na/test/test_pmm.cc b/na/test/test_pmm.cc
new file mode 100644
index 0000000..3263662
--- /dev/null
+++ b/na/test/test_pmm.cc
@@ -0,0 +1,283 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Boot Self-Test: Physical Memory Manager (PMM)
+ *
+ * Tests:
+ * 1. Single frame alloc/free round-trip
+ * 2. Frame address alignment (4KB)
+ * 3. No duplicate frames from consecutive allocs
+ * 4. Free then re-alloc returns the same frame (PCP cache)
+ * 5. Multi-frame (order > 0) alloc/free
+ * 6. Bulk alloc stress test (alloc many, free all)
+ * 7. Reference counting (incref/decref)
+ * 8. frame_to_pa / pa_to_frame round-trip
+ */
+
+#include <lume/selftest.h>
+#include <lume/pmm.h>
+#include <lume/addr.h>
+
+void selftest_pmm()
+{
+ st_begin("pmm: single frame alloc/free");
+ {
+ Frame* f = pmm_alloc_frame();
+ ST_ASSERT(f != nullptr);
+ ST_ASSERT(f->state == FrameState::Allocated);
+ ST_ASSERT(f->order == 0);
+
+ uint64 pa = frame_to_pa(f);
+ ST_ASSERT_EQ(pa % kPageSize, 0ULL); // 4KB aligned
+
+ uint64 va = frame_to_va(f);
+ ST_ASSERT_EQ(va, pa_to_va(pa));
+
+ pmm_free_frame(f);
+ }
+ st_pass();
+
+ st_begin("pmm: no duplicate frames");
+ {
+ Frame* f1 = pmm_alloc_frame();
+ Frame* f2 = pmm_alloc_frame();
+ ST_ASSERT(f1 != nullptr);
+ ST_ASSERT(f2 != nullptr);
+ ST_ASSERT_NE(f1, f2);
+ ST_ASSERT_NE(frame_to_pa(f1), frame_to_pa(f2));
+ pmm_free_frame(f2);
+ pmm_free_frame(f1);
+ }
+ st_pass();
+
+ st_begin("pmm: free then re-alloc (PCP)");
+ {
+ Frame* f1 = pmm_alloc_frame();
+ uint64 pa1 = frame_to_pa(f1);
+ pmm_free_frame(f1);
+
+ // PCP should return the most recently freed frame
+ Frame* f2 = pmm_alloc_frame();
+ uint64 pa2 = frame_to_pa(f2);
+ ST_ASSERT_EQ(pa1, pa2);
+ pmm_free_frame(f2);
+ }
+ st_pass();
+
+ st_begin("pmm: multi-frame alloc (order 2 = 4 pages)");
+ {
+ Frame* f = pmm_alloc_frames(2);
+ ST_ASSERT(f != nullptr);
+ ST_ASSERT(f->order == 2);
+
+ uint64 pa = frame_to_pa(f);
+ ST_ASSERT_EQ(pa % (kPageSize * 4), 0ULL); // Naturally aligned
+
+ pmm_free_frames(f, 2);
+ }
+ st_pass();
+
+ st_begin("pmm: bulk alloc/free stress (256 frames)");
+ {
+ constexpr int N = 256;
+ Frame* frames[N];
+
+ // Allocate all
+ for (int i = 0; i < N; i++) {
+ frames[i] = pmm_alloc_frame();
+ ST_ASSERT(frames[i] != nullptr);
+ }
+
+ // Verify no duplicates (spot check: consecutive pairs)
+ for (int i = 1; i < N; i++) {
+ ST_ASSERT_NE(frames[i], frames[i - 1]);
+ }
+
+ // Free all
+ for (int i = 0; i < N; i++) {
+ pmm_free_frame(frames[i]);
+ }
+ }
+ st_pass();
+
+ st_begin("pmm: frame_to_pa / pa_to_frame round-trip");
+ {
+ Frame* f = pmm_alloc_frame();
+ ST_ASSERT(f != nullptr);
+ uint64 pa = frame_to_pa(f);
+ Frame* f2 = pa_to_frame(pa);
+ ST_ASSERT_EQ(f, f2);
+ pmm_free_frame(f);
+ }
+ st_pass();
+
+ st_begin("pmm: refcount incref/decref");
+ {
+ Frame* f = pmm_alloc_frame();
+ ST_ASSERT(f != nullptr);
+ ST_ASSERT(f->refcount.load() == 1);
+
+ frame_incref(f);
+ ST_ASSERT(f->refcount.load() == 2);
+
+ frame_decref(f); // 2 -> 1, should NOT free
+ ST_ASSERT(f->refcount.load() == 1);
+ ST_ASSERT(f->state == FrameState::Allocated);
+
+ frame_decref(f); // 1 -> 0, should free to pool
+ // After decref to 0, frame is returned to pool.
+ // We can't safely check f->state because it might be reused.
+ }
+ st_pass();
+ st_begin("pmm: pmm_alloc_frames natural alignment (order-3)");
+ {
+ Frame* f = pmm_alloc_frames(3);
+ ST_ASSERT(f != nullptr);
+ uint64 pa = frame_to_pa(f);
+ ST_ASSERT_EQ(pa % (kPageSize * 8), 0ULL);
+ pmm_free_frames(f, 3);
+ }
+ st_pass();
+
+ st_begin("pmm: frame_decref to 0 and reuse (order-0)");
+ {
+ Frame* f = pmm_alloc_frame();
+ uint64 pa = frame_to_pa(f);
+ frame_decref(f); // drops 1 -> 0, freeing it
+
+ // Next alloc from PCP should give back the same frame
+ Frame* f2 = pmm_alloc_frame();
+ ST_ASSERT_EQ(frame_to_pa(f2), pa);
+ pmm_free_frame(f2);
+ }
+ st_pass();
+
+ st_begin("pmm: frame_decref to 0 on multi-page blocks (order-3)");
+ {
+ Frame* f = pmm_alloc_frames(3);
+ uint64 pa = frame_to_pa(f);
+ frame_decref(f); // Should internally route to pmm_free_frames(f, 3)
+
+ // Re-allocate order-3 and see if we get it back (buddy coalescing was respected)
+ Frame* f2 = pmm_alloc_frames(3);
+ ST_ASSERT_EQ(frame_to_pa(f2), pa);
+ pmm_free_frames(f2, 3);
+ }
+ st_pass();
+
+ st_begin("pmm: buddy split and block coalescing correctness");
+ {
+ // Force split: allocate order-1, free it, allocate two order-0
+ Frame* f_ord1 = pmm_alloc_frames(1);
+ uint64 pa1 = frame_to_pa(f_ord1);
+ pmm_free_frames(f_ord1, 1);
+
+ Frame* f0_a = pmm_alloc_frame();
+ Frame* f0_b = pmm_alloc_frame();
+
+ // Free them to trigger coalesce
+ pmm_free_frame(f0_a);
+ pmm_free_frame(f0_b);
+
+ // Allocate order-1 again to verify successful coalescing
+ Frame* f_ord1_again = pmm_alloc_frames(1);
+ ST_ASSERT_EQ(frame_to_pa(f_ord1_again), pa1);
+ pmm_free_frames(f_ord1_again, 1);
+ }
+ st_pass();
+
+ st_begin("pmm: mixed order cross allocation");
+ {
+ Frame* f0 = pmm_alloc_frame();
+ Frame* f2 = pmm_alloc_frames(2);
+ Frame* f0b = pmm_alloc_frame();
+
+ ST_ASSERT(f0 != nullptr);
+ ST_ASSERT(f2 != nullptr);
+ ST_ASSERT(f0b != nullptr);
+ ST_ASSERT_NE(frame_to_pa(f0), frame_to_pa(f2));
+
+ pmm_free_frame(f0);
+ pmm_free_frames(f2, 2);
+ pmm_free_frame(f0b);
+ }
+ st_pass();
+
+ st_begin("pmm: PCP drain/refill boundaries");
+ {
+ // kPcpHighWatermark = 64. Allocating and freeing 65 blocks triggers drain.
+ Frame* frames[65];
+ for (int i=0; i<65; i++) frames[i] = pmm_alloc_frame();
+ for (int i=0; i<65; i++) pmm_free_frame(frames[i]);
+
+ // The system shouldn't crash and PCP logic should handle this.
+ Frame* verify_f = pmm_alloc_frame();
+ ST_ASSERT(verify_f != nullptr);
+ pmm_free_frame(verify_f);
+ }
+ st_pass();
+
+ st_begin("pmm: OOM exhaustion stability");
+ {
+ Frame* head = nullptr;
+ // First allocate large chunks to be fast
+ while (true) {
+ Frame* f = pmm_alloc_frames(5);
+ if (!f) break;
+ uint64* ptr = reinterpret_cast<uint64*>(frame_to_va(f));
+ *ptr = reinterpret_cast<uint64>(head);
+ // encode order in the second word
+ *(ptr + 1) = 5;
+ head = f;
+ }
+ // Then allocate single frames until absolute zero
+ while (true) {
+ Frame* f = pmm_alloc_frame();
+ if (!f) break;
+ uint64* ptr = reinterpret_cast<uint64*>(frame_to_va(f));
+ *ptr = reinterpret_cast<uint64>(head);
+ *(ptr + 1) = 0;
+ head = f;
+ }
+
+ // We hit nullptr. System is completely exhausted.
+ ST_ASSERT(pmm_alloc_frame() == nullptr);
+
+ // Free everything
+ while (head) {
+ uint64* ptr = reinterpret_cast<uint64*>(frame_to_va(head));
+ Frame* nxt = reinterpret_cast<Frame*>(*ptr);
+ uint64 order = *(ptr + 1);
+ if (order == 5) pmm_free_frames(head, 5);
+ else pmm_free_frame(head);
+ head = nxt;
+ }
+ }
+ st_pass();
+
+ st_begin("pmm: out of bounds order allocation gracefully fails");
+ {
+ // kMaxOrder is 11, so passing 11 or higher should safely return nullptr.
+ Frame* f = pmm_alloc_frames(11);
+ ST_ASSERT(f == nullptr);
+ f = pmm_alloc_frames(255);
+ ST_ASSERT(f == nullptr);
+ }
+ st_pass();
+
+ st_begin("pmm: pmm_get_mem_base / pmm_get_mem_size sanity");
+ {
+ uint64 base = pmm_get_mem_base();
+ uint64 size = pmm_get_mem_size();
+
+ // QEMU virt platform: RAM starts at 0x80000000
+ ST_ASSERT(base >= 0x80000000ULL);
+ ST_ASSERT(size > 0);
+ // Size must be page-aligned
+ ST_ASSERT_EQ(size % kPageSize, 0ULL);
+ // Base must be page-aligned
+ ST_ASSERT_EQ(base % kPageSize, 0ULL);
+ }
+ st_pass();
+}
diff --git a/na/test/test_rbtree.cc b/na/test/test_rbtree.cc
new file mode 100644
index 0000000..27f049a
--- /dev/null
+++ b/na/test/test_rbtree.cc
@@ -0,0 +1,119 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#include <lume/selftest.h>
+#include <lume/rbtree.h>
+
+struct TestNode {
+ int key;
+ struct rb_node rb;
+};
+
+static bool node_less(struct rb_node *node, const struct rb_node *parent) {
+ TestNode *n = rb_entry(node, TestNode, rb);
+ const TestNode *p = reinterpret_cast<const TestNode*>(
+ reinterpret_cast<const char*>(parent) - __builtin_offsetof(TestNode, rb));
+ return n->key < p->key;
+}
+
+static int node_cmp(const void *key, const struct rb_node *node) {
+ int k = *static_cast<const int*>(key);
+ const TestNode *n = reinterpret_cast<const TestNode*>(
+ reinterpret_cast<const char*>(node) - __builtin_offsetof(TestNode, rb));
+ if (k < n->key) return -1;
+ if (k > n->key) return 1;
+ return 0;
+}
+
+void selftest_rbtree() {
+ st_begin("rbtree: basic insert, find, erase");
+ {
+ struct rb_root tree = RB_ROOT;
+ TestNode n1{10, {}}, n2{20, {}}, n3{5, {}};
+
+ rb_add(&n1.rb, &tree, node_less);
+ rb_add(&n2.rb, &tree, node_less);
+ rb_add(&n3.rb, &tree, node_less);
+
+ int search_key = 20;
+ struct rb_node *found = rb_find(&search_key, &tree, node_cmp);
+ ST_ASSERT(found == &n2.rb);
+
+ search_key = 99;
+ found = rb_find(&search_key, &tree, node_cmp);
+ ST_ASSERT(found == nullptr);
+
+ rb_erase(&n2.rb, &tree);
+ // Verify erased
+ search_key = 20;
+ found = rb_find(&search_key, &tree, node_cmp);
+ ST_ASSERT(found == nullptr);
+ }
+ st_pass();
+
+ st_begin("rbtree: ordered iteration");
+ {
+ struct rb_root tree = RB_ROOT;
+ TestNode nodes[5] = { {50, {}}, {10, {}}, {40, {}}, {20, {}}, {30, {}} };
+ for (int i=0; i<5; i++) {
+ rb_add(&nodes[i].rb, &tree, node_less);
+ }
+
+ int expected[] = {10, 20, 30, 40, 50};
+ int idx = 0;
+ struct rb_node *node = rb_first(&tree);
+ while (node) {
+ TestNode *n = rb_entry(node, TestNode, rb);
+ ST_ASSERT_EQ(n->key, expected[idx]);
+ idx++;
+ node = rb_next(node);
+ }
+ ST_ASSERT_EQ(idx, 5);
+ }
+ st_pass();
+
+ st_begin("rbtree: large scale insert/erase balance");
+ {
+ struct rb_root tree = RB_ROOT;
+ constexpr int N = 200;
+ static TestNode nodes[N]; // Static to avoid blowing up the BSP stack
+ for (int i=0; i<N; i++) {
+ nodes[i].key = (i * 17) % N; // Pseudo-random insertions
+ rb_add(&nodes[i].rb, &tree, node_less);
+ }
+
+ int c = 0;
+ for (struct rb_node *n = rb_first(&tree); n; n = rb_next(n)) c++;
+ ST_ASSERT_EQ(c, N);
+
+ for (int i=0; i<N; i+=2) {
+ rb_erase(&nodes[i].rb, &tree);
+ }
+ c = 0;
+ for (struct rb_node *n = rb_first(&tree); n; n = rb_next(n)) c++;
+ ST_ASSERT_EQ(c, N / 2);
+ }
+ st_pass();
+
+ st_begin("rbtree: empty tree operations");
+ {
+ struct rb_root tree = RB_ROOT;
+ ST_ASSERT(RB_EMPTY_ROOT(&tree));
+ ST_ASSERT(rb_first(&tree) == nullptr);
+ ST_ASSERT(rb_last(&tree) == nullptr);
+ }
+ st_pass();
+
+ st_begin("rbtree: rb_erase_init after effects");
+ {
+ struct rb_root tree = RB_ROOT;
+ TestNode n1{100, {}};
+ rb_add(&n1.rb, &tree, node_less);
+ ST_ASSERT(!RB_EMPTY_NODE(&n1.rb));
+
+ rb_erase_init(&n1.rb, &tree);
+ ST_ASSERT(RB_EMPTY_NODE(&n1.rb));
+ ST_ASSERT(RB_EMPTY_ROOT(&tree));
+ }
+ st_pass();
+}
diff --git a/na/test/test_slab.cc b/na/test/test_slab.cc
new file mode 100644
index 0000000..1035663
--- /dev/null
+++ b/na/test/test_slab.cc
@@ -0,0 +1,255 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Boot Self-Test: Slab Allocator (kmalloc/kfree)
+ *
+ * Tests:
+ * 1. Small allocation (8 bytes) and free
+ * 2. Multiple size classes
+ * 3. No aliasing between consecutive allocs
+ * 4. Alignment check (minimum 8-byte aligned)
+ * 5. Bulk alloc/free stress
+ * 6. operator new / operator delete
+ * 7. Large allocation (> 2048, falls back to PMM)
+ * 8. Zero-size and null-free edge cases
+ */
+
+#include <lume/selftest.h>
+#include <lume/new.h>
+#include <lume/pmm.h>
+#include <lume/addr.h>
+
+/* Forward declarations — defined in slab.cc */
+void* kmalloc(uint32 size);
+void kfree(void* ptr);
+
+void selftest_slab()
+{
+ st_begin("slab: small alloc (8 bytes)");
+ {
+ void* p = kmalloc(8);
+ ST_ASSERT(p != nullptr);
+ // Write and read back to verify the memory is usable
+ *static_cast<uint64*>(p) = 0xDEADBEEFCAFEBABEULL;
+ ST_ASSERT_EQ(*static_cast<uint64*>(p), 0xDEADBEEFCAFEBABEULL);
+ kfree(p);
+ }
+ st_pass();
+
+ st_begin("slab: multiple size classes");
+ {
+ uint32 sizes[] = {8, 16, 32, 64, 128, 256, 512, 1024, 2048};
+ void* ptrs[9];
+ for (int i = 0; i < 9; i++) {
+ ptrs[i] = kmalloc(sizes[i]);
+ ST_ASSERT(ptrs[i] != nullptr);
+ // Write a marker
+ *static_cast<uint8*>(ptrs[i]) = static_cast<uint8>(i);
+ }
+ // Verify markers (no cross-contamination)
+ for (int i = 0; i < 9; i++) {
+ ST_ASSERT_EQ(*static_cast<uint8*>(ptrs[i]), static_cast<uint8>(i));
+ }
+ for (int i = 0; i < 9; i++) {
+ kfree(ptrs[i]);
+ }
+ }
+ st_pass();
+
+ st_begin("slab: no aliasing");
+ {
+ void* a = kmalloc(32);
+ void* b = kmalloc(32);
+ ST_ASSERT(a != nullptr);
+ ST_ASSERT(b != nullptr);
+ ST_ASSERT_NE(a, b);
+ kfree(b);
+ kfree(a);
+ }
+ st_pass();
+
+ st_begin("slab: alignment (8-byte minimum)");
+ {
+ for (int i = 0; i < 16; i++) {
+ void* p = kmalloc(7); // odd size, should still be 8-aligned
+ ST_ASSERT(p != nullptr);
+ ST_ASSERT_EQ(reinterpret_cast<uint64>(p) % 8, 0ULL);
+ kfree(p);
+ }
+ }
+ st_pass();
+
+ st_begin("slab: bulk alloc/free stress (128 x 64B)");
+ {
+ constexpr int N = 128;
+ void* ptrs[N];
+ for (int i = 0; i < N; i++) {
+ ptrs[i] = kmalloc(64);
+ ST_ASSERT(ptrs[i] != nullptr);
+ }
+ // Free in reverse order
+ for (int i = N - 1; i >= 0; i--) {
+ kfree(ptrs[i]);
+ }
+ }
+ st_pass();
+
+ st_begin("slab: operator new/delete");
+ {
+ struct TestObj {
+ uint64 a;
+ uint64 b;
+ };
+ TestObj* obj = new TestObj{42, 99};
+ ST_ASSERT(obj != nullptr);
+ ST_ASSERT_EQ(obj->a, 42ULL);
+ ST_ASSERT_EQ(obj->b, 99ULL);
+ delete obj;
+ }
+ st_pass();
+
+ st_begin("slab: large alloc (4096 bytes, PMM fallback)");
+ {
+ void* p = kmalloc(4096);
+ ST_ASSERT(p != nullptr);
+ ST_ASSERT_EQ(reinterpret_cast<uint64>(p) % kPageSize, 0ULL);
+ kfree(p);
+ }
+ st_pass();
+
+ st_begin("slab: edge cases (zero size, null free)");
+ {
+ void* p = kmalloc(0);
+ ST_ASSERT(p == nullptr);
+ kfree(nullptr); // should not crash
+ }
+ st_pass();
+ st_begin("slab: LIFO freelist reallocation (same address)");
+ {
+ void* a = kmalloc(32);
+ kfree(a);
+ void* b = kmalloc(32);
+ ST_ASSERT_EQ(a, b);
+ kfree(b);
+ }
+ st_pass();
+
+ st_begin("slab: boundary conditions (2048 bytes size class)");
+ {
+ // 2048 bytes should still use slab (2 objects per page)
+ // We allocate 3 to guarantee at least two fall in the exact same slab page
+ void* p1 = kmalloc(2048);
+ void* p2 = kmalloc(2048);
+ void* p3 = kmalloc(2048);
+
+ auto diff = [](void* a, void* b) -> uint64 {
+ uint64 ua = reinterpret_cast<uint64>(a);
+ uint64 ub = reinterpret_cast<uint64>(b);
+ return ua > ub ? ua - ub : ub - ua;
+ };
+
+ // At least one adjacent pair MUST be exactly separated by 2048 bytes
+ bool found_2048 = (diff(p1, p2) == 2048ULL) || (diff(p2, p3) == 2048ULL);
+ ST_ASSERT(found_2048);
+
+ kfree(p1);
+ kfree(p2);
+ kfree(p3);
+ }
+ st_pass();
+
+ st_begin("slab: cross-page allocation (> 1 page equivalent objects)");
+ {
+ // Allocating more than one page worth of 32-byte objects (4096 / 32 = 128)
+ constexpr int NUM = 150;
+ void* ptrs[NUM];
+ for (int i=0; i<NUM; i++) {
+ ptrs[i] = kmalloc(32);
+ ST_ASSERT(ptrs[i] != nullptr);
+ }
+ for (int i=0; i<NUM; i++) {
+ kfree(ptrs[i]);
+ }
+ }
+ st_pass();
+
+ st_begin("slab: empty page reclamation (verifiable by frame availability)");
+ {
+ // Allocate 128-byte objects until we hit the start of a completely new page.
+ void* base_ptr = nullptr;
+ void* drain[64];
+ int drain_count = 0;
+
+ while (true) {
+ void* p = kmalloc(128);
+ // Because 128 divides 4096 evenly, a new page starts at an exact multiple of kPageSize.
+ if (reinterpret_cast<uint64>(p) % kPageSize == 0) {
+ base_ptr = p;
+ break;
+ }
+ drain[drain_count++] = p;
+ }
+
+ // base_ptr is the very first object in a fresh slab page!
+ // The page holds exactly 32 objects (4096 / 128 = 32). We already hold 1 (base_ptr).
+ void* ptrs[32];
+ ptrs[0] = base_ptr;
+ for (int i = 1; i < 32; i++) {
+ ptrs[i] = kmalloc(128);
+ }
+
+ // At this point, we completely own all 32 objects of this slab page.
+ uint64 page_pa = ensure_pa(reinterpret_cast<uint64>(base_ptr));
+ Frame* f = pa_to_frame(page_pa);
+
+ ST_ASSERT(f->state == FrameState::Slab);
+ ST_ASSERT(f->slab.obj_count == 32);
+
+ // Free all items in this isolated slab page
+ for (int i = 0; i < 32; i++) {
+ kfree(ptrs[i]);
+ }
+
+ // The frame MUST have been returned to the PMM (either global Free or Per-CPU Cached).
+ ST_ASSERT(f->state == FrameState::Free || f->state == FrameState::PcpCached);
+
+ // Clean up the drain array
+ for (int i = 0; i < drain_count; i++) {
+ kfree(drain[i]);
+ }
+ }
+ st_pass();
+
+ st_begin("slab: continuous operator new/delete memory leak loop");
+ {
+ struct BigObj { uint8 data[512]; };
+ BigObj* ptr = nullptr;
+ // 10K allocations to ensure no memory leak causes OOM
+ for (int i = 0; i < 10000; i++) {
+ ptr = new BigObj;
+ ST_ASSERT(ptr != nullptr);
+ delete ptr;
+ }
+ }
+ st_pass();
+
+ st_begin("slab: allocation pressure (mixed sizes)");
+ {
+ constexpr int MAX_ITEMS = 100;
+ void* ptrs[MAX_ITEMS];
+ uint32 sizes[MAX_ITEMS];
+
+ for (int i = 0; i < MAX_ITEMS; i++) {
+ sizes[i] = (i * 17) % 2000 + 8; // Pseudo-random sizes between 8 and 2008
+ ptrs[i] = kmalloc(sizes[i]);
+ ST_ASSERT(ptrs[i] != nullptr);
+ ST_ASSERT(reinterpret_cast<uintptr>(ptrs[i]) % 8 == 0);
+ }
+
+ for (int i = 0; i < MAX_ITEMS; i++) {
+ kfree(ptrs[i]);
+ }
+ }
+ st_pass();
+}
diff --git a/na/test/test_spinlock.cc b/na/test/test_spinlock.cc
new file mode 100644
index 0000000..be794a0
--- /dev/null
+++ b/na/test/test_spinlock.cc
@@ -0,0 +1,135 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Boot Self-Test: Spinlock
+ *
+ * Tests:
+ * 1. Basic acquire/release
+ * 2. LockGuard RAII acquire/release
+ * 3. Nested locking (two different locks)
+ * 4. Interrupt state preservation across lock/unlock
+ * 5. is_held_by_current_cpu correctness
+ *
+ * Note: We cannot test multi-core contention in BSP-only self-test.
+ * That requires AP participation (future Phase 2 test).
+ */
+
+#include <lume/selftest.h>
+#include <kernel/sync/spinlock.h>
+#include <kernel/sync/cpu_state.h>
+#include <arch/cpu.h>
+
+void selftest_spinlock()
+{
+ st_begin("spinlock: basic acquire/release");
+ {
+ Spinlock lock;
+ lock.init("test_basic");
+
+ ST_ASSERT(!lock.is_held_by_current_cpu());
+
+ lock.acquire();
+ ST_ASSERT(lock.is_held_by_current_cpu());
+
+ lock.release();
+ ST_ASSERT(!lock.is_held_by_current_cpu());
+ }
+ st_pass();
+
+ st_begin("spinlock: LockGuard RAII");
+ {
+ Spinlock lock;
+ lock.init("test_guard");
+
+ {
+ LockGuard guard(lock);
+ ST_ASSERT(lock.is_held_by_current_cpu());
+ }
+ // After scope exit, lock should be released
+ ST_ASSERT(!lock.is_held_by_current_cpu());
+ }
+ st_pass();
+
+ st_begin("spinlock: nested locking (two locks)");
+ {
+ Spinlock lock_a, lock_b;
+ lock_a.init("test_nest_a");
+ lock_b.init("test_nest_b");
+
+ lock_a.acquire();
+ ST_ASSERT(lock_a.is_held_by_current_cpu());
+
+ lock_b.acquire();
+ ST_ASSERT(lock_b.is_held_by_current_cpu());
+
+ lock_b.release();
+ ST_ASSERT(!lock_b.is_held_by_current_cpu());
+ ST_ASSERT(lock_a.is_held_by_current_cpu()); // A still held
+
+ lock_a.release();
+ ST_ASSERT(!lock_a.is_held_by_current_cpu());
+ }
+ st_pass();
+
+ st_begin("spinlock: interrupt state preservation");
+ {
+ Spinlock lock;
+ lock.init("test_intr");
+
+ // Interrupts are currently off (BSP init period), but let's
+ // verify the lock doesn't corrupt the state.
+ bool before = arch::cpu::intr_enabled();
+
+ lock.acquire();
+ // Inside lock, interrupts must be disabled
+ ST_ASSERT(!arch::cpu::intr_enabled());
+ lock.release();
+
+ // After release, interrupt state should be restored
+ ST_ASSERT_EQ(arch::cpu::intr_enabled(), before);
+ }
+ st_pass();
+ st_begin("spinlock: deep nesting tracking (> 2)");
+ {
+ Spinlock l1, l2, l3;
+ l1.init("l1");
+ l2.init("l2");
+ l3.init("l3");
+
+ bool before = arch::cpu::intr_enabled();
+
+ l1.acquire();
+ l2.acquire();
+ l3.acquire();
+
+ ST_ASSERT(!arch::cpu::intr_enabled());
+
+ l3.release();
+ ST_ASSERT(!arch::cpu::intr_enabled());
+
+ l2.release();
+ ST_ASSERT(!arch::cpu::intr_enabled());
+
+ l1.release();
+ ST_ASSERT_EQ(arch::cpu::intr_enabled(), before);
+ }
+ st_pass();
+
+ st_begin("spinlock: interrupt leak detection logic (lock_depth metadata)");
+ {
+ Spinlock l;
+ l.init("l");
+
+ auto& state = sync::g_cpu_sync_states[arch::cpu::id()];
+ int initial_depth = state.lock_depth;
+
+ l.acquire();
+ // The lock depth should strictly increase
+ ST_ASSERT_EQ(state.lock_depth, initial_depth + 1);
+ l.release();
+
+ ST_ASSERT_EQ(state.lock_depth, initial_depth);
+ }
+ st_pass();
+}
diff --git a/na/test/test_vma.cc b/na/test/test_vma.cc
new file mode 100644
index 0000000..d47fbd0
--- /dev/null
+++ b/na/test/test_vma.cc
@@ -0,0 +1,158 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+#include <lume/selftest.h>
+#include <lume/vmm.h>
+#include <lume/addr.h>
+#include <lume/errno.h>
+#include <arch/pmap.h>
+
+void selftest_vma() {
+ st_begin("vma: create and destroy VmSpace");
+ {
+ VmSpace* space = VmSpace::create();
+ ST_ASSERT(space != nullptr);
+ ST_ASSERT(space->root_pa != 0);
+
+ // Add a few regions to ensure destruction works
+ vmm_map_user(space, 0x1000, 0x1000, VM_READ);
+ vmm_map_user(space, 0x3000, 0x1000, VM_READ | VM_WRITE);
+
+ space->destroy();
+ }
+ st_pass();
+
+ st_begin("vma: map user region and handle demand paging");
+ {
+ VmSpace* space = VmSpace::create();
+
+ // Map 8KB (2 pages) at 0x400000 using MI-layer flags
+ int ret = vmm_map_user(space, 0x400000, 0x2000, VM_READ | VM_WRITE);
+ ST_ASSERT_EQ(ret, 0);
+
+ // Fault in the first page
+ ret = vmm_handle_page_fault(space, 0x400500, 0);
+ ST_ASSERT_EQ(ret, 0);
+
+ // Verify mapping in hardware page table
+ uint64 pa = 0;
+ bool mapped = pmap::lookup(space->root_pa, 0x400000, &pa);
+ ST_ASSERT(mapped);
+ ST_ASSERT_NE(pa, 0ULL);
+
+ // Fault in the second page
+ ret = vmm_handle_page_fault(space, 0x401FFF, 0);
+ ST_ASSERT_EQ(ret, 0);
+
+ // Should reject fault outside VMA with -EFAULT
+ ret = vmm_handle_page_fault(space, 0x402000, 0);
+ ST_ASSERT_EQ(ret, -EFAULT);
+
+ space->destroy();
+ }
+ st_pass();
+
+ st_begin("vma: unmap user region prevents page fault");
+ {
+ VmSpace* space = VmSpace::create();
+ vmm_map_user(space, 0x10000, 0x2000, VM_READ);
+
+ // Inside the region — should succeed
+ ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x10500, 0), 0);
+
+ // Unmap exactly
+ vmm_unmap_user(space, 0x10000, 0x2000);
+
+ // Should fail now with -EFAULT
+ ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x10500, 0), -EFAULT);
+
+ space->destroy();
+ }
+ st_pass();
+
+ st_begin("vma: duplicate page fault on same page returns 0");
+ {
+ VmSpace* space = VmSpace::create();
+ vmm_map_user(space, 0x200000, 0x1000, VM_READ | VM_WRITE);
+
+ // First fault allocates the physical frame
+ int ret = vmm_handle_page_fault(space, 0x200000, 0);
+ ST_ASSERT_EQ(ret, 0);
+
+ // Second fault on the same page should hit "already mapped" path
+ ret = vmm_handle_page_fault(space, 0x200100, 0);
+ ST_ASSERT_EQ(ret, 0);
+
+ // Verify both land on the same physical frame
+ uint64 pa1 = 0, pa2 = 0;
+ pmap::lookup(space->root_pa, 0x200000, &pa1);
+ pmap::lookup(space->root_pa, 0x200100, &pa2);
+ // Same page, so PA for both addresses should be the same base page
+ ST_ASSERT_EQ(pa1, pa2);
+
+ space->destroy();
+ }
+ st_pass();
+
+ st_begin("vma: vm_perm_to_pte flag translation correctness");
+ {
+ // Test each MI flag maps to correct hardware PTE bit
+ uint64 pte_r = pmap::vm_perm_to_pte(VM_READ);
+ ST_ASSERT(pte_r & pmap::PTE_R);
+ ST_ASSERT(!(pte_r & pmap::PTE_W));
+ ST_ASSERT(!(pte_r & pmap::PTE_X));
+ ST_ASSERT(!(pte_r & pmap::PTE_U));
+
+ uint64 pte_rw = pmap::vm_perm_to_pte(VM_READ | VM_WRITE);
+ ST_ASSERT(pte_rw & pmap::PTE_R);
+ ST_ASSERT(pte_rw & pmap::PTE_W);
+
+ uint64 pte_rx = pmap::vm_perm_to_pte(VM_READ | VM_EXEC);
+ ST_ASSERT(pte_rx & pmap::PTE_R);
+ ST_ASSERT(pte_rx & pmap::PTE_X);
+ ST_ASSERT(!(pte_rx & pmap::PTE_W));
+
+ uint64 pte_u = pmap::vm_perm_to_pte(VM_READ | VM_USER);
+ ST_ASSERT(pte_u & pmap::PTE_R);
+ ST_ASSERT(pte_u & pmap::PTE_U);
+
+ // All combined
+ uint64 pte_all = pmap::vm_perm_to_pte(VM_READ | VM_WRITE | VM_EXEC | VM_USER);
+ ST_ASSERT(pte_all & pmap::PTE_R);
+ ST_ASSERT(pte_all & pmap::PTE_W);
+ ST_ASSERT(pte_all & pmap::PTE_X);
+ ST_ASSERT(pte_all & pmap::PTE_U);
+ ST_ASSERT(pte_all & pmap::PTE_V); // Always valid
+ }
+ st_pass();
+
+ st_begin("vma: unmap subset only removes contained VMAs");
+ {
+ VmSpace* space = VmSpace::create();
+ // Map three separate regions
+ vmm_map_user(space, 0x50000, 0x1000, VM_READ);
+ vmm_map_user(space, 0x60000, 0x1000, VM_READ);
+ vmm_map_user(space, 0x70000, 0x1000, VM_READ);
+
+ // Unmap only the middle one (range covers 0x60000-0x61000)
+ vmm_unmap_user(space, 0x60000, 0x1000);
+
+ // First and third should still be reachable
+ ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x50000, 0), 0);
+ ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x70000, 0), 0);
+
+ // Middle should fail
+ ST_ASSERT_EQ(vmm_handle_page_fault(space, 0x60000, 0), -EFAULT);
+
+ space->destroy();
+ }
+ st_pass();
+
+ st_begin("vma: null space returns -EINVAL");
+ {
+ ST_ASSERT_EQ(vmm_map_user(nullptr, 0x1000, 0x1000, VM_READ), -EINVAL);
+ ST_ASSERT_EQ(vmm_unmap_user(nullptr, 0x1000, 0x1000), -EINVAL);
+ ST_ASSERT_EQ(vmm_handle_page_fault(nullptr, 0x1000, 0), -EINVAL);
+ }
+ st_pass();
+}
diff --git a/na/test/test_vmm.cc b/na/test/test_vmm.cc
new file mode 100644
index 0000000..cd76140
--- /dev/null
+++ b/na/test/test_vmm.cc
@@ -0,0 +1,61 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Boot Self-Test: VMM and Address Translation
+ */
+
+#include <lume/selftest.h>
+#include <lume/addr.h>
+#include <lume/vmm.h>
+#include <arch/config.h>
+
+/* Assuming pmap namespace is accessible or we can test basic structures */
+
+void selftest_vmm() {
+ st_begin("vmm: virtual/physical address translation");
+ {
+ uint64 pa = 0x80000000ULL;
+ uint64 va = pa_to_va(pa);
+ ST_ASSERT_EQ(va, 0xFFFFFFC080000000ULL);
+ ST_ASSERT_EQ(va_to_pa(va), pa);
+
+ uint64 pfn = pa_to_pfn(pa);
+ ST_ASSERT_EQ(pfn, 0x80000ULL);
+ ST_ASSERT_EQ(pfn_to_pa(pfn), pa);
+
+ ST_ASSERT(is_kernel_va(va));
+ ST_ASSERT(!is_kernel_va(pa));
+
+ ST_ASSERT_EQ(ensure_pa(va), pa);
+ ST_ASSERT_EQ(ensure_pa(pa), pa);
+
+ ST_ASSERT_EQ(ensure_va(va), va);
+ ST_ASSERT_EQ(ensure_va(pa), va);
+ }
+ st_pass();
+
+ st_begin("vmm: page alignment macros");
+ {
+ uint64 addr = 0x80000FFFULL;
+ ST_ASSERT_EQ(page_align_down(addr), 0x80000000ULL);
+ ST_ASSERT_EQ(page_align_up(addr), 0x80001000ULL);
+ ST_ASSERT_EQ(page_align_down(0x80000000ULL), 0x80000000ULL);
+ ST_ASSERT_EQ(page_align_up(0x80000000ULL), 0x80000000ULL);
+ }
+ st_pass();
+
+ st_begin("vmm: mapping kernel MMIO (driver API)");
+ {
+ // Map 4KB of MMIO at 0x10001000 (UART is at 0x10000000)
+ vmm_map_kernel_mmio(0x10001000, 0x1000);
+
+ // Verify it doesn't crash and address is reachable in higher half
+ uint64 va = pa_to_va(0x10001000);
+ ST_ASSERT(is_kernel_va(va));
+
+ // We can't safely dereference it here without knowing QEMU layout perfectly,
+ // but vmm_init should have mapped it.
+ }
+ st_pass();
+}