From 51899557c770b3dc9bd02579abf491745e607d92 Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Tue, 21 Apr 2026 23:23:49 +0800 Subject: 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. --- na/include/lume/selftest.h | 110 ++++++++++++++++++ na/test/selftest.cc | 41 +++++++ na/test/test_atomic.cc | 102 ++++++++++++++++ na/test/test_fdt.cc | 50 ++++++++ na/test/test_pmm.cc | 283 +++++++++++++++++++++++++++++++++++++++++++++ na/test/test_rbtree.cc | 119 +++++++++++++++++++ na/test/test_slab.cc | 255 ++++++++++++++++++++++++++++++++++++++++ na/test/test_spinlock.cc | 135 +++++++++++++++++++++ na/test/test_vma.cc | 158 +++++++++++++++++++++++++ na/test/test_vmm.cc | 61 ++++++++++ 10 files changed, 1314 insertions(+) create mode 100644 na/include/lume/selftest.h create mode 100644 na/test/selftest.cc create mode 100644 na/test/test_atomic.cc create mode 100644 na/test/test_fdt.cc create mode 100644 na/test/test_pmm.cc create mode 100644 na/test/test_rbtree.cc create mode 100644 na/test/test_slab.cc create mode 100644 na/test/test_spinlock.cc create mode 100644 na/test/test_vma.cc create mode 100644 na/test/test_vmm.cc (limited to 'na') 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 +#include + +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(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(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 + +/* 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 + * + * 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 +#include + +void selftest_atomic() { + st_begin("atomic: default construction is zero"); + { + lume::atomic a; + ST_ASSERT_EQ(a.load(), 0U); + } + st_pass(); + + st_begin("atomic: explicit construction"); + { + lume::atomic a(42); + ST_ASSERT_EQ(a.load(), 42ULL); + } + st_pass(); + + st_begin("atomic: store/load round-trip"); + { + lume::atomic 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 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 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 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 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 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 +#include + +// 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 +#include +#include + +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(frame_to_va(f)); + *ptr = reinterpret_cast(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(frame_to_va(f)); + *ptr = reinterpret_cast(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(frame_to_va(head)); + Frame* nxt = reinterpret_cast(*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 +#include + +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( + reinterpret_cast(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(key); + const TestNode *n = reinterpret_cast( + reinterpret_cast(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 2048, falls back to PMM) + * 8. Zero-size and null-free edge cases + */ + +#include +#include +#include +#include + +/* 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(p) = 0xDEADBEEFCAFEBABEULL; + ST_ASSERT_EQ(*static_cast(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(ptrs[i]) = static_cast(i); + } + // Verify markers (no cross-contamination) + for (int i = 0; i < 9; i++) { + ST_ASSERT_EQ(*static_cast(ptrs[i]), static_cast(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(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(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(a); + uint64 ub = reinterpret_cast(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(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(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(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 +#include +#include +#include + +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 +#include +#include +#include +#include + +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 +#include +#include +#include + +/* 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(); +} -- cgit v1.2.3