summaryrefslogtreecommitdiff
path: root/na/test
diff options
context:
space:
mode:
Diffstat (limited to 'na/test')
-rw-r--r--na/test/module.mk14
-rw-r--r--na/test/selftest.cc30
-rw-r--r--na/test/test_dcache.cc232
-rw-r--r--na/test/test_driver_model.cc209
-rw-r--r--na/test/test_exec.cc447
-rw-r--r--na/test/test_mutex.cc183
-rw-r--r--na/test/test_namei.cc173
-rw-r--r--na/test/test_page_cache.cc180
-rw-r--r--na/test/test_pid.cc106
-rw-r--r--na/test/test_pipe_devfs.cc420
-rw-r--r--na/test/test_pmm.cc6
-rw-r--r--na/test/test_reloc.cc46
-rw-r--r--na/test/test_signal.cc193
-rw-r--r--na/test/test_slab.cc4
-rw-r--r--na/test/test_smp.cc136
-rw-r--r--na/test/test_sys_fs_extra.cc238
-rw-r--r--na/test/test_sys_process.cc93
-rw-r--r--na/test/test_uaccess.cc18
-rw-r--r--na/test/test_vfs_io.cc345
-rw-r--r--na/test/test_vmm.cc15
-rw-r--r--na/test/test_waitqueue.cc2
21 files changed, 3022 insertions, 68 deletions
diff --git a/na/test/module.mk b/na/test/module.mk
index da6c065..47a693c 100644
--- a/na/test/module.mk
+++ b/na/test/module.mk
@@ -3,6 +3,8 @@ SRCS_CC += test/selftest.cc
SRCS_CC += test/test_pmm.cc
SRCS_CC += test/test_slab.cc
SRCS_CC += test/test_spinlock.cc
+SRCS_CC += test/test_mutex.cc
+SRCS_CC += test/test_driver_model.cc
SRCS_CC += test/test_rbtree.cc
SRCS_CC += test/test_vmm.cc
SRCS_CC += test/test_vma.cc
@@ -12,7 +14,17 @@ SRCS_CC += test/test_trap.cc
SRCS_CC += test/test_sched.cc
SRCS_CC += test/test_klog.cc
SRCS_CC += test/test_random.cc
-SRCS_CC += test/test_reloc.cc
SRCS_CC += test/test_waitqueue.cc
SRCS_CC += test/test_uaccess.cc
SRCS_CC += test/test_sys_mm.cc
+SRCS_CC += test/test_smp.cc
+SRCS_CC += test/test_dcache.cc
+SRCS_CC += test/test_namei.cc
+SRCS_CC += test/test_vfs_io.cc
+SRCS_CC += test/test_exec.cc
+SRCS_CC += test/test_pipe_devfs.cc
+SRCS_CC += test/test_sys_process.cc
+SRCS_CC += test/test_signal.cc
+SRCS_CC += test/test_pid.cc
+SRCS_CC += test/test_sys_fs_extra.cc
+SRCS_CC += test/test_page_cache.cc
diff --git a/na/test/selftest.cc b/na/test/selftest.cc
index 96962e2..e1bb2b4 100644
--- a/na/test/selftest.cc
+++ b/na/test/selftest.cc
@@ -14,6 +14,8 @@
void selftest_pmm();
void selftest_slab();
void selftest_spinlock();
+void selftest_mutex();
+void selftest_driver_model();
void selftest_rbtree();
void selftest_vmm();
void selftest_fdt();
@@ -23,10 +25,21 @@ void selftest_trap();
void selftest_sched();
void selftest_klog();
void selftest_random();
-void selftest_reloc();
void selftest_waitqueue();
void selftest_uaccess();
void selftest_sys_mm();
+void selftest_dcache();
+void selftest_namei();
+void selftest_vfs_io();
+void selftest_exec();
+void selftest_devfs();
+void selftest_pipe();
+void selftest_fcntl();
+void selftest_sys_process();
+void selftest_signal();
+void selftest_pid();
+void selftest_sys_fs_extra();
+void selftest_page_cache();
void selftest_run_all()
{
@@ -35,6 +48,8 @@ void selftest_run_all()
kprintf("========================================\n");
selftest_spinlock();
+ selftest_mutex();
+ selftest_driver_model();
selftest_pmm();
selftest_slab();
selftest_rbtree();
@@ -46,10 +61,21 @@ void selftest_run_all()
selftest_sched();
selftest_klog();
selftest_random();
- selftest_reloc();
selftest_waitqueue();
selftest_uaccess();
selftest_sys_mm();
+ selftest_dcache();
+ selftest_namei();
+ selftest_vfs_io();
+ selftest_devfs();
+ selftest_pipe();
+ selftest_fcntl();
+ selftest_sys_process();
+ selftest_signal();
+ selftest_pid();
+ selftest_sys_fs_extra();
+ selftest_page_cache();
+ selftest_exec();
kprintf("========================================\n");
kprintf(" All self-tests PASSED\n");
diff --git a/na/test/test_dcache.cc b/na/test/test_dcache.cc
new file mode 100644
index 0000000..9e0eec5
--- /dev/null
+++ b/na/test/test_dcache.cc
@@ -0,0 +1,232 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_dcache.cc — Self-tests for the dentry cache layer
+ */
+
+#include <lume/selftest.h>
+#include <lume/dcache.h>
+#include <lume/fs.h>
+#include <lume/mount.h>
+#include <lume/errno.h>
+#include <lume/slab.h>
+
+/* Root dentry/mount from VFS init */
+extern dentry* g_root_dentry;
+extern vfsmount* g_root_mnt;
+
+void selftest_dcache()
+{
+ /* ---- d_alloc: basic allocation under root ---- */
+ st_begin("dcache: d_alloc basic");
+ {
+ qstr name{};
+ name.name = "test_child";
+ name.len = 10;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* child = d_alloc(g_root_dentry, &name);
+ ST_ASSERT(child != nullptr);
+ ST_ASSERT_EQ(child->d_name.len, 10u);
+ ST_ASSERT(child->d_parent == g_root_dentry);
+ ST_ASSERT(child->d_inode == nullptr); /* not instantiated yet */
+ ST_ASSERT(child->d_sb == g_root_dentry->d_sb);
+
+ /* Name was copied into inline buffer */
+ bool match = true;
+ for (uint32 i = 0; i < name.len; i++) {
+ if (child->d_name.name[i] != name.name[i]) { match = false; break; }
+ }
+ ST_ASSERT(match);
+
+ d_drop(child);
+ dput(child);
+ }
+ st_pass();
+
+ /* ---- d_lookup: find existing child ---- */
+ st_begin("dcache: d_lookup finds child");
+ {
+ qstr name{};
+ name.name = "lookup_me";
+ name.len = 9;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* child = d_alloc(g_root_dentry, &name);
+ ST_ASSERT(child != nullptr);
+
+ /* d_lookup should find it */
+ dentry* found = d_lookup(g_root_dentry, &name);
+ ST_ASSERT(found != nullptr);
+ ST_ASSERT(found == child);
+ dput(found); /* drop d_lookup ref */
+
+ d_drop(child);
+ dput(child);
+ }
+ st_pass();
+
+ /* ---- d_lookup: miss returns nullptr ---- */
+ st_begin("dcache: d_lookup miss returns nullptr");
+ {
+ qstr name{};
+ name.name = "nonexistent_xyz";
+ name.len = 15;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* found = d_lookup(g_root_dentry, &name);
+ ST_ASSERT(found == nullptr);
+ }
+ st_pass();
+
+ /* ---- d_instantiate: bind inode to dentry ---- */
+ st_begin("dcache: d_instantiate binds inode");
+ {
+ qstr name{};
+ name.name = "has_inode";
+ name.len = 9;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* child = d_alloc(g_root_dentry, &name);
+ ST_ASSERT(child != nullptr);
+ ST_ASSERT(child->d_inode == nullptr);
+
+ /* Create a dummy inode */
+ inode* ip = new_inode(g_root_dentry->d_sb);
+ ST_ASSERT(ip != nullptr);
+ ip->i_mode = S_IFREG | 0644;
+
+ d_instantiate(child, ip);
+ ST_ASSERT(child->d_inode == ip);
+ ST_ASSERT(child->d_flags & DCACHE_REGULAR_TYPE);
+
+ d_drop(child);
+ dput(child);
+ }
+ st_pass();
+
+ /* ---- dget/dput: refcount ---- */
+ st_begin("dcache: dget/dput refcount");
+ {
+ qstr name{};
+ name.name = "reftest";
+ name.len = 7;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* child = d_alloc(g_root_dentry, &name);
+ ST_ASSERT(child != nullptr);
+ /* d_alloc sets refcount = 1 */
+ int rc1 = child->d_lockref.load();
+ ST_ASSERT(rc1 >= 1);
+
+ dget(child);
+ int rc2 = child->d_lockref.load();
+ ST_ASSERT_EQ(rc2, rc1 + 1);
+
+ dput(child);
+ int rc3 = child->d_lockref.load();
+ ST_ASSERT_EQ(rc3, rc1);
+
+ d_drop(child);
+ dput(child);
+ }
+ st_pass();
+
+ /* ---- d_drop: removes from parent children list ---- */
+ st_begin("dcache: d_drop removes from parent");
+ {
+ qstr name{};
+ name.name = "dropme";
+ name.len = 6;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* child = d_alloc(g_root_dentry, &name);
+ ST_ASSERT(child != nullptr);
+
+ /* Should be findable */
+ dentry* f1 = d_lookup(g_root_dentry, &name);
+ ST_ASSERT(f1 != nullptr);
+ dput(f1);
+
+ /* Drop it */
+ d_drop(child);
+
+ /* Should no longer be findable */
+ dentry* f2 = d_lookup(g_root_dentry, &name);
+ ST_ASSERT(f2 == nullptr);
+
+ dput(child);
+ }
+ st_pass();
+
+ /* ---- d_path: root returns "/" ---- */
+ st_begin("dcache: d_path root returns /");
+ {
+ path p{};
+ p.de = g_root_dentry;
+ p.mnt = g_root_mnt;
+
+ char buf[64];
+ char* result = d_path(&p, buf, sizeof(buf));
+ ST_ASSERT(result != nullptr);
+ ST_ASSERT(result[0] == '/');
+ ST_ASSERT(result[1] == '\0');
+ }
+ st_pass();
+
+ /* ---- d_path: child path ---- */
+ st_begin("dcache: d_path child returns /name");
+ {
+ qstr name{};
+ name.name = "pathtest";
+ name.len = 8;
+ name.hash = full_name_hash(name.name, name.len);
+
+ dentry* child = d_alloc(g_root_dentry, &name);
+ ST_ASSERT(child != nullptr);
+
+ path p{};
+ p.de = child;
+ p.mnt = g_root_mnt;
+
+ char buf[64];
+ char* result = d_path(&p, buf, sizeof(buf));
+ ST_ASSERT(result != nullptr);
+
+ /* Should be "/pathtest" */
+ const char* expected = "/pathtest";
+ bool eq = true;
+ for (int i = 0; expected[i]; i++) {
+ if (result[i] != expected[i]) { eq = false; break; }
+ }
+ ST_ASSERT(eq);
+
+ d_drop(child);
+ dput(child);
+ }
+ st_pass();
+
+ /* ---- d_alloc_anon: root-like dentry ---- */
+ st_begin("dcache: d_alloc_anon");
+ {
+ dentry* anon = d_alloc_anon(g_root_dentry->d_sb);
+ ST_ASSERT(anon != nullptr);
+ ST_ASSERT(anon->d_parent == anon); /* root: parent is self */
+ ST_ASSERT(anon->d_sb == g_root_dentry->d_sb);
+ dput(anon);
+ }
+ st_pass();
+
+ /* ---- full_name_hash: deterministic ---- */
+ st_begin("dcache: full_name_hash deterministic");
+ {
+ uint32 h1 = full_name_hash("hello", 5);
+ uint32 h2 = full_name_hash("hello", 5);
+ ST_ASSERT_EQ(h1, h2);
+
+ uint32 h3 = full_name_hash("world", 5);
+ ST_ASSERT_NE(h1, h3);
+ }
+ st_pass();
+}
diff --git a/na/test/test_driver_model.cc b/na/test/test_driver_model.cc
new file mode 100644
index 0000000..8f105b3
--- /dev/null
+++ b/na/test/test_driver_model.cc
@@ -0,0 +1,209 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_driver_model — exercises kref, kobject, and the bus/device/driver
+ * core. Uses a dummy bus + dummy driver + dummy device so the test is
+ * self-contained and doesn't depend on FDT/platform_device.
+ */
+
+#include <lume/selftest.h>
+#include <lume/kref.h>
+#include <lume/kobject.h>
+#include <lume/device.h>
+
+/* ------------------------------------------------------------------ */
+/* Test 1: kref get/put + release fires exactly once */
+/* ------------------------------------------------------------------ */
+
+namespace {
+
+struct krefable {
+ kref ref;
+ int release_count;
+};
+
+void krefable_release(kref* r)
+{
+ auto* k = container_of(r, krefable, ref);
+ k->release_count++;
+}
+
+void test_kref()
+{
+ st_begin("kref: init/get/put");
+
+ krefable k = {};
+ kref_init(&k.ref);
+
+ ST_ASSERT_EQ(kref_read(&k.ref), 1);
+
+ kref_get(&k.ref);
+ kref_get(&k.ref);
+ ST_ASSERT_EQ(kref_read(&k.ref), 3);
+
+ ST_ASSERT_EQ(kref_put(&k.ref, krefable_release), 0);
+ ST_ASSERT_EQ(kref_put(&k.ref, krefable_release), 0);
+ ST_ASSERT_EQ(k.release_count, 0);
+
+ ST_ASSERT_EQ(kref_put(&k.ref, krefable_release), 1);
+ ST_ASSERT_EQ(k.release_count, 1);
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Test 2: kobject parent/child + release on final put */
+/* ------------------------------------------------------------------ */
+
+int g_child_released = 0;
+int g_parent_released = 0;
+
+void child_release(kobject*) { g_child_released++; }
+void parent_release(kobject*) { g_parent_released++; }
+
+const kobj_type child_ktype = { .release = child_release };
+const kobj_type parent_ktype = { .release = parent_release };
+
+void test_kobject_hierarchy()
+{
+ st_begin("kobject: parent/child + ref counting");
+
+ g_child_released = 0;
+ g_parent_released = 0;
+
+ kobject parent;
+ kobject_init(&parent, &parent_ktype);
+
+ kobject child;
+ kobject_init(&child, &child_ktype);
+ kobject_add(&child, &parent, "child");
+
+ /* Adding child took a ref on parent. */
+ ST_ASSERT_EQ(kref_read(&parent.refcount), 2);
+ ST_ASSERT(child.parent == &parent);
+
+ /* Drop child's "added" reference — release should NOT fire because
+ * we (the test) still hold the original kobject_init ref. */
+ kobject_del(&child);
+ ST_ASSERT(child.parent == nullptr);
+ ST_ASSERT_EQ(kref_read(&parent.refcount), 1);
+ ST_ASSERT_EQ(g_child_released, 0);
+ ST_ASSERT_EQ(g_parent_released, 0);
+
+ /* Drop final refs. */
+ kobject_put(&child);
+ ST_ASSERT_EQ(g_child_released, 1);
+
+ kobject_put(&parent);
+ ST_ASSERT_EQ(g_parent_released, 1);
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Test 3: bus / device / driver bind via match + probe */
+/* ------------------------------------------------------------------ */
+
+struct dummy_dev { device dev; int probed; };
+struct dummy_drv { device_driver drv; const char* want; };
+
+int g_probes = 0;
+
+int dummy_match(device* dev, device_driver* drv)
+{
+ auto* dd = container_of(drv, dummy_drv, drv);
+ return (dev->init_name != nullptr &&
+ dd->want != nullptr &&
+ __builtin_strcmp(dev->init_name, dd->want) == 0) ? 1 : 0;
+}
+
+int dummy_probe(device* dev)
+{
+ auto* dd = container_of(dev, dummy_dev, dev);
+ dd->probed++;
+ g_probes++;
+ return 0;
+}
+
+bus_type dummy_bus = {
+ .name = "test_dummy",
+ .match = dummy_match,
+ .probe = nullptr, /* fall through to drv->probe */
+ .devices = {},
+ .drivers = {},
+ .lock = {},
+ .registered = false,
+};
+
+void test_bus_bind()
+{
+ st_begin("driver model: bus / driver / device bind");
+
+ g_probes = 0;
+ ST_ASSERT_EQ(bus_register(&dummy_bus), 0);
+
+ /* Register driver before any matching device exists. */
+ dummy_drv drv_a = {};
+ drv_a.drv.name = "drv_a";
+ drv_a.drv.bus = &dummy_bus;
+ drv_a.drv.probe = dummy_probe;
+ drv_a.want = "compat_a";
+ ST_ASSERT_EQ(driver_register(&drv_a.drv), 0);
+
+ /* Register a device that should bind to drv_a immediately. */
+ dummy_dev dev_a = {};
+ dev_a.dev.bus = &dummy_bus;
+ dev_a.dev.init_name = "compat_a";
+ ST_ASSERT_EQ(device_register(&dev_a.dev), 0);
+ ST_ASSERT_EQ(dev_a.probed, 1);
+ ST_ASSERT(dev_a.dev.driver == &drv_a.drv);
+
+ /* Register a device first, then a driver — should also bind. */
+ dummy_dev dev_b = {};
+ dev_b.dev.bus = &dummy_bus;
+ dev_b.dev.init_name = "compat_b";
+ ST_ASSERT_EQ(device_register(&dev_b.dev), 0);
+ ST_ASSERT_EQ(dev_b.probed, 0);
+
+ dummy_drv drv_b = {};
+ drv_b.drv.name = "drv_b";
+ drv_b.drv.bus = &dummy_bus;
+ drv_b.drv.probe = dummy_probe;
+ drv_b.want = "compat_b";
+ ST_ASSERT_EQ(driver_register(&drv_b.drv), 0);
+ ST_ASSERT_EQ(dev_b.probed, 1);
+ ST_ASSERT(dev_b.dev.driver == &drv_b.drv);
+
+ /* Non-matching device sits unbound. */
+ dummy_dev dev_c = {};
+ dev_c.dev.bus = &dummy_bus;
+ dev_c.dev.init_name = "compat_unknown";
+ ST_ASSERT_EQ(device_register(&dev_c.dev), 0);
+ ST_ASSERT_EQ(dev_c.probed, 0);
+ ST_ASSERT(dev_c.dev.driver == nullptr);
+
+ ST_ASSERT_EQ(g_probes, 2);
+
+ /* Cleanup. */
+ device_unregister(&dev_a.dev);
+ device_unregister(&dev_b.dev);
+ device_unregister(&dev_c.dev);
+ driver_unregister(&drv_a.drv);
+ driver_unregister(&drv_b.drv);
+
+ st_pass();
+}
+
+} // namespace
+
+/* ------------------------------------------------------------------ */
+/* Entry */
+/* ------------------------------------------------------------------ */
+
+void selftest_driver_model()
+{
+ test_kref();
+ test_kobject_hierarchy();
+ test_bus_bind();
+}
diff --git a/na/test/test_exec.cc b/na/test/test_exec.cc
new file mode 100644
index 0000000..bbfa3b6
--- /dev/null
+++ b/na/test/test_exec.cc
@@ -0,0 +1,447 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_exec.cc — Self-tests for execve / ELF loader
+ *
+ * Tests the ELF validation, binary loading, and user stack construction
+ * by creating synthetic ELF binaries in RamFS and invoking do_execve
+ * in a controlled environment.
+ *
+ * We cannot test the actual user-space transition (that requires
+ * sret to U-mode), but we CAN test everything up to and including
+ * the trapframe setup.
+ */
+
+#include <lume/selftest.h>
+#include <lume/elf.h>
+#include <lume/task.h>
+#include <lume/vmm.h>
+#include <lume/pmm.h>
+#include <lume/addr.h>
+#include <lume/errno.h>
+#include <lume/fdtable.h>
+#include <lume/fs.h>
+#include <lume/fs_struct.h>
+#include <lume/dcache.h>
+#include <lume/namei.h>
+#include <lume/file.h>
+#include <lume/mm_layout.h>
+#include <arch/trapframe.h>
+#include <arch/pmap.h>
+
+/* Extern: do_execve (kernel/exec.cc) */
+int do_execve(const char* pathname, const char* const* argv,
+ const char* const* envp);
+
+/* Extern: VFS file creation syscalls */
+int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* ================================================================== */
+/* Helper: build a minimal valid RISC-V ELF64 in a buffer */
+/* ================================================================== */
+
+struct TestElf {
+ Elf64_Ehdr ehdr;
+ Elf64_Phdr phdr;
+ uint8 code[16]; /* Tiny "program" — just a few NOPs */
+};
+
+/*
+ * Build a minimal static ELF64 for RISC-V.
+ * Single PT_LOAD segment mapping the tiny code at 0x10000.
+ * Entry point = 0x10000 + offsetof(code).
+ */
+static void build_minimal_elf(TestElf* elf)
+{
+ /* Zero everything */
+ for (uint64 i = 0; i < sizeof(*elf); i++)
+ reinterpret_cast<uint8*>(elf)[i] = 0;
+
+ /* ELF header */
+ Elf64_Ehdr* e = &elf->ehdr;
+ e->e_ident[EI_MAG0] = ELFMAG0;
+ e->e_ident[EI_MAG1] = ELFMAG1;
+ e->e_ident[EI_MAG2] = ELFMAG2;
+ e->e_ident[EI_MAG3] = ELFMAG3;
+ e->e_ident[EI_CLASS] = ELFCLASS64;
+ e->e_ident[EI_DATA] = ELFDATA2LSB;
+ e->e_ident[EI_VERSION] = 1;
+ e->e_type = ET_EXEC;
+ e->e_machine = EM_RISCV;
+ e->e_version = 1;
+ e->e_phoff = sizeof(Elf64_Ehdr);
+ e->e_phentsize = sizeof(Elf64_Phdr);
+ e->e_phnum = 1;
+ e->e_ehsize = sizeof(Elf64_Ehdr);
+
+ /* Code section offset within the file */
+ uint64 code_offset = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr);
+
+ /* Entry point = vaddr of code */
+ uint64 load_vaddr = 0x10000;
+ e->e_entry = load_vaddr + code_offset;
+
+ /* Program header — single PT_LOAD covering entire file */
+ Elf64_Phdr* p = &elf->phdr;
+ p->p_type = PT_LOAD;
+ p->p_flags = PF_R | PF_X;
+ p->p_offset = 0;
+ p->p_vaddr = load_vaddr;
+ p->p_paddr = load_vaddr;
+ p->p_filesz = sizeof(TestElf);
+ p->p_memsz = sizeof(TestElf);
+ p->p_align = 0x1000;
+
+ /* Code: RISC-V "unimp" (all zeros = illegal instruction, but good enough for testing) */
+ /* We just need the loader to succeed; we won't actually execute this. */
+ elf->code[0] = 0x13; /* nop = addi x0, x0, 0 */
+ elf->code[4] = 0x13;
+ elf->code[8] = 0x73; /* ebreak */
+}
+
+/* ================================================================== */
+/* Helper: write a buffer to a file in RamFS */
+/* ================================================================== */
+
+static int create_test_file(const char* path, const void* data, uint64 size)
+{
+ /* Open with O_CREAT | O_WRONLY | O_TRUNC */
+ int64 fd = sys_openat(
+ static_cast<uint64>(-100), /* AT_FDCWD */
+ reinterpret_cast<uint64>(path),
+ O_CREAT | O_WRONLY | O_TRUNC,
+ 0755, 0, 0);
+ if (fd < 0) return static_cast<int>(fd);
+
+ int64 written = sys_write(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(data),
+ size, 0, 0, 0);
+
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+
+ if (written < 0) return static_cast<int>(written);
+ if (static_cast<uint64>(written) != size) return -EIO;
+ return 0;
+}
+
+/* ================================================================== */
+/* Helper: ensure current task has VFS context for file ops */
+/* ================================================================== */
+
+struct ExecTestCtx {
+ TaskControlBlock* curr;
+ VmSpace* old_mm;
+ arch::TrapFrame fake_tf;
+ arch::TrapFrame* old_tf;
+ uint64 saved_satp;
+};
+
+static void exec_test_setup(ExecTestCtx* ctx)
+{
+ ctx->curr = current_task();
+
+ /* Save kernel SATP so we can restore it in teardown */
+ asm volatile("csrr %0, satp" : "=r"(ctx->saved_satp));
+
+ /* Save and replace mm with a fresh one */
+ ctx->old_mm = ctx->curr->mm;
+ ctx->curr->mm = VmSpace::create();
+
+ /* Set up a fake trapframe so do_execve can modify it */
+ for (uint64 i = 0; i < sizeof(ctx->fake_tf); i++)
+ reinterpret_cast<uint8*>(&ctx->fake_tf)[i] = 0;
+ ctx->old_tf = ctx->curr->trapframe;
+ ctx->curr->trapframe = &ctx->fake_tf;
+}
+
+static void exec_test_teardown(ExecTestCtx* ctx)
+{
+ /* Restore trapframe */
+ ctx->curr->trapframe = ctx->old_tf;
+
+ /* Destroy the test mm (may have been replaced by do_execve) */
+ VmSpace* test_mm = ctx->curr->mm;
+ ctx->curr->mm = ctx->old_mm;
+ if (test_mm && test_mm != ctx->old_mm) {
+ /* If do_execve succeeded, it activated new_mm's page table.
+ * Switch back to the kernel page table before destroying. */
+ asm volatile("csrw satp, %0" :: "r"(ctx->saved_satp));
+ asm volatile("sfence.vma");
+ test_mm->put();
+ }
+}
+
+/* ================================================================== */
+/* Test cases */
+/* ================================================================== */
+
+void selftest_exec()
+{
+ /* ---- 1a. Non-existent path under missing directory ---- */
+ st_begin("exec: missing path returns error");
+ {
+ const char* argv[] = { "/no_such_dir/no_file", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/no_such_dir/no_file", argv, envp);
+ ST_ASSERT(ret < 0); /* -ENOENT from filename_lookup */
+ }
+ st_pass();
+
+ /* ---- 1b. Directory is not executable ---- */
+ st_begin("exec: directory returns -EACCES");
+ {
+ /* "/" exists and is a directory */
+ const char* argv[] = { "/dev", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/dev", argv, envp);
+ ST_ASSERT_EQ(ret, -EACCES);
+ }
+ st_pass();
+
+ /* ---- 2. Bad ELF magic ---- */
+ st_begin("exec: bad ELF magic returns -ENOEXEC");
+ {
+ uint8 garbage[128];
+ for (int i = 0; i < 128; i++) garbage[i] = 0xAA;
+ int err = create_test_file("/test_bad_magic", garbage, 128);
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_bad_magic", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_bad_magic", argv, envp);
+ ST_ASSERT_EQ(ret, -ENOEXEC);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 3. Wrong ELF class (32-bit) ---- */
+ st_begin("exec: 32-bit ELF returns -ENOEXEC");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ elf.ehdr.e_ident[EI_CLASS] = 1; /* ELFCLASS32 */
+ int err = create_test_file("/test_elf32", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_elf32", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_elf32", argv, envp);
+ ST_ASSERT_EQ(ret, -ENOEXEC);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 4. Wrong endianness (big-endian) ---- */
+ st_begin("exec: big-endian ELF returns -ENOEXEC");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ elf.ehdr.e_ident[EI_DATA] = 2; /* ELFDATA2MSB */
+ int err = create_test_file("/test_be", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_be", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_be", argv, envp);
+ ST_ASSERT_EQ(ret, -ENOEXEC);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 5. Wrong machine (x86_64 instead of RISC-V) ---- */
+ st_begin("exec: wrong machine type returns -ENOEXEC");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ elf.ehdr.e_machine = 62; /* EM_X86_64 */
+ int err = create_test_file("/test_x86", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_x86", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_x86", argv, envp);
+ ST_ASSERT_EQ(ret, -ENOEXEC);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 6. Truncated ELF (too small for header) ---- */
+ st_begin("exec: truncated ELF returns -ENOEXEC");
+ {
+ /* Valid magic but file is too short for full header */
+ uint8 tiny[8] = { 0x7f, 'E', 'L', 'F', 2, 1, 1, 0 };
+ int err = create_test_file("/test_tiny", tiny, sizeof(tiny));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_tiny", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_tiny", argv, envp);
+ ST_ASSERT_EQ(ret, -ENOEXEC);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 7. Zero program headers ---- */
+ st_begin("exec: zero phnum returns -ENOEXEC");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ elf.ehdr.e_phnum = 0;
+ int err = create_test_file("/test_nophdr", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_nophdr", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_nophdr", argv, envp);
+ ST_ASSERT_EQ(ret, -ENOEXEC);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 8. Valid ELF: successful load and trapframe setup ---- */
+ st_begin("exec: valid ELF loads and sets up trapframe");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ int err = create_test_file("/test_valid", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_valid", nullptr };
+ const char* envp[] = { "HOME=/", nullptr };
+ int ret = do_execve("/test_valid", argv, envp);
+ ST_ASSERT_EQ(ret, 0);
+
+ /* Verify trapframe was set up correctly */
+ arch::TrapFrame* tf = &ctx.fake_tf;
+
+ /* Entry point should match what we set in the ELF */
+ uint64 code_offset = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr);
+ uint64 expected_entry = 0x10000 + code_offset;
+ ST_ASSERT_EQ(tf->epc, expected_entry);
+
+ /* Stack pointer should be in user space, below the stack top */
+ ST_ASSERT(tf->sp > 0);
+ ST_ASSERT(tf->sp < kUserStackTop);
+ ST_ASSERT(tf->sp >= kUserStackBase);
+
+ /* sstatus: SPP=0 (U-mode), SPIE=1 (interrupts enabled on sret) */
+ uint64 spp_bit = (1ULL << 8);
+ uint64 spie_bit = (1ULL << 5);
+ ST_ASSERT_EQ(tf->sstatus & spp_bit, 0ULL); /* Not S-mode */
+ ST_ASSERT_EQ(tf->sstatus & spie_bit, spie_bit); /* SPIE set */
+
+ /* General registers should be zeroed */
+ ST_ASSERT_EQ(tf->ra, 0ULL);
+ ST_ASSERT_EQ(tf->gp, 0ULL);
+ ST_ASSERT_EQ(tf->tp, 0ULL);
+ for (int i = 0; i < 7; i++)
+ ST_ASSERT_EQ(tf->t[i], 0ULL);
+ for (int i = 0; i < 12; i++)
+ ST_ASSERT_EQ(tf->s[i], 0ULL);
+ for (int i = 0; i < 8; i++)
+ ST_ASSERT_EQ(tf->a[i], 0ULL);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 9. Verify user stack layout (argc/argv) ---- */
+ st_begin("exec: user stack has correct argc/argv layout");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ int err = create_test_file("/test_stack", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ const char* argv[] = { "/test_stack", "arg1", nullptr };
+ const char* envp[] = { "A=B", nullptr };
+ int ret = do_execve("/test_stack", argv, envp);
+ ST_ASSERT_EQ(ret, 0);
+
+ /* The stack was set up in the new mm.
+ * Read argc from the top of the stack using pmap lookup. */
+ VmSpace* mm = ctx.curr->mm;
+ uint64 sp = ctx.fake_tf.sp;
+
+ /* Look up the physical page for sp */
+ uint64 pa = 0;
+ bool mapped = pmap::lookup(mm->root_pa, sp & ~(kPageSize - 1), &pa);
+ ST_ASSERT(mapped);
+
+ /* Read argc (first uint64 on stack) */
+ uint64 page_offset = sp & (kPageSize - 1);
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ uint64 argc = *reinterpret_cast<uint64*>(kva + page_offset);
+ ST_ASSERT_EQ(argc, 2ULL); /* "/test_stack" and "arg1" */
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+
+ /* ---- 10. Multiple consecutive execve replaces mm correctly ---- */
+ st_begin("exec: consecutive execve replaces address space");
+ {
+ TestElf elf;
+ build_minimal_elf(&elf);
+ int err = create_test_file("/test_reexec", &elf, sizeof(elf));
+ ST_ASSERT_EQ(err, 0);
+
+ ExecTestCtx ctx;
+ exec_test_setup(&ctx);
+
+ /* First exec */
+ const char* argv[] = { "/test_reexec", nullptr };
+ const char* envp[] = { nullptr };
+ int ret = do_execve("/test_reexec", argv, envp);
+ ST_ASSERT_EQ(ret, 0);
+
+ VmSpace* first_mm = ctx.curr->mm;
+ ST_ASSERT(first_mm != nullptr);
+
+ /* Second exec — should replace the first mm */
+ ret = do_execve("/test_reexec", argv, envp);
+ ST_ASSERT_EQ(ret, 0);
+
+ /* mm should be different (new address space) */
+ VmSpace* second_mm = ctx.curr->mm;
+ ST_ASSERT(second_mm != nullptr);
+ ST_ASSERT_NE(first_mm, second_mm);
+
+ exec_test_teardown(&ctx);
+ }
+ st_pass();
+}
diff --git a/na/test/test_mutex.cc b/na/test/test_mutex.cc
new file mode 100644
index 0000000..e86c984
--- /dev/null
+++ b/na/test/test_mutex.cc
@@ -0,0 +1,183 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_mutex — self-tests for the Mutex sleeping lock.
+ *
+ * Coverage:
+ * - basic lock/unlock single-thread
+ * - try_lock when free and when held
+ * - mutual exclusion across kthreads (no lost increments)
+ * - FIFO wake order (A acquires, B/C queue; release lets B in then C)
+ */
+
+#include <lume/selftest.h>
+#include <lume/mutex.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+#include <lume/kprintf.h>
+
+extern TaskControlBlock* kthread_create(const char* name,
+ void (*entry)(void*), void* arg,
+ uint8 priority);
+extern void task_exit(int code);
+
+/* ------------------------------------------------------------------ */
+/* Test 1: basic lock/unlock */
+/* ------------------------------------------------------------------ */
+
+static void test_basic()
+{
+ st_begin("mutex: basic lock/unlock");
+
+ Mutex m;
+ m.init("test_basic");
+
+ ST_ASSERT(!m.is_locked());
+ m.lock();
+ ST_ASSERT(m.is_locked());
+ ST_ASSERT(m.is_owned_by_current());
+ m.unlock();
+ ST_ASSERT(!m.is_locked());
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Test 2: try_lock */
+/* ------------------------------------------------------------------ */
+
+static void test_trylock()
+{
+ st_begin("mutex: try_lock");
+
+ Mutex m;
+ m.init("test_trylock");
+
+ ST_ASSERT(m.try_lock() == true);
+ ST_ASSERT(m.is_locked());
+
+ /* A second try_lock from the same task: since owner == current,
+ * we still return false (try_lock is non-recursive). */
+ ST_ASSERT(m.try_lock() == false);
+
+ m.unlock();
+ ST_ASSERT(!m.is_locked());
+ ST_ASSERT(m.try_lock() == true);
+ m.unlock();
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Test 3: mutual exclusion across kthreads */
+/* ------------------------------------------------------------------ */
+
+static Mutex g_mut;
+static volatile int32 g_counter = 0;
+static volatile int32 g_done = 0;
+static constexpr int32 kIters = 200;
+
+static void mutex_worker(void*)
+{
+ for (int i = 0; i < kIters; i++) {
+ g_mut.lock();
+ int32 v = g_counter;
+ /* Yield inside the critical section to expose missing mutex.
+ * If the mutex were broken we'd see lost increments. */
+ sched_yield();
+ g_counter = v + 1;
+ g_mut.unlock();
+ }
+ __sync_fetch_and_add(&g_done, 1);
+ task_exit(0);
+}
+
+static void test_mutual_exclusion()
+{
+ st_begin("mutex: mutual exclusion (2 workers)");
+
+ g_mut.init("test_mutex_mx");
+ g_counter = 0;
+ g_done = 0;
+
+ kthread_create("mut_w1", mutex_worker, nullptr, 0);
+ kthread_create("mut_w2", mutex_worker, nullptr, 0);
+
+ /* Spin-yield until both workers finish. */
+ while (g_done < 2) {
+ sched_yield();
+ }
+
+ ST_ASSERT_EQ(g_counter, 2 * kIters);
+ ST_ASSERT(!g_mut.is_locked());
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Test 4: FIFO wake order */
+/* ------------------------------------------------------------------ */
+
+static Mutex g_fifo_mut;
+static volatile int32 g_fifo_seq = 0; // monotonically increases
+static volatile int32 g_fifo_order[3]; // order each worker observed
+static volatile int32 g_fifo_done = 0;
+
+static void fifo_worker(void* arg)
+{
+ int slot = static_cast<int>(reinterpret_cast<uint64>(arg));
+ g_fifo_mut.lock();
+ g_fifo_order[slot] = __sync_fetch_and_add(&g_fifo_seq, 1);
+ g_fifo_mut.unlock();
+ __sync_fetch_and_add(&g_fifo_done, 1);
+ task_exit(0);
+}
+
+static void test_fifo_order()
+{
+ st_begin("mutex: FIFO wake order (3 waiters)");
+
+ g_fifo_mut.init("test_mutex_fifo");
+ g_fifo_seq = 0;
+ g_fifo_done = 0;
+ g_fifo_order[0] = g_fifo_order[1] = g_fifo_order[2] = -1;
+
+ /* Main grabs the lock first so workers will queue up behind. */
+ g_fifo_mut.lock();
+
+ /* Spawn workers in deterministic order. After each create() yield
+ * once so the new thread reaches mutex.lock() and enqueues itself
+ * before we spawn the next one. */
+ kthread_create("fifo_A", fifo_worker, reinterpret_cast<void*>(0), 0);
+ sched_yield();
+ kthread_create("fifo_B", fifo_worker, reinterpret_cast<void*>(1), 0);
+ sched_yield();
+ kthread_create("fifo_C", fifo_worker, reinterpret_cast<void*>(2), 0);
+ sched_yield();
+
+ /* Release: A should fire next, then B, then C. */
+ g_fifo_mut.unlock();
+
+ while (g_fifo_done < 3) {
+ sched_yield();
+ }
+
+ ST_ASSERT_EQ(g_fifo_order[0], 0);
+ ST_ASSERT_EQ(g_fifo_order[1], 1);
+ ST_ASSERT_EQ(g_fifo_order[2], 2);
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Entry */
+/* ------------------------------------------------------------------ */
+
+void selftest_mutex()
+{
+ test_basic();
+ test_trylock();
+ test_mutual_exclusion();
+ test_fifo_order();
+}
diff --git a/na/test/test_namei.cc b/na/test/test_namei.cc
new file mode 100644
index 0000000..8e4ecb0
--- /dev/null
+++ b/na/test/test_namei.cc
@@ -0,0 +1,173 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_namei.cc — Self-tests for pathname resolution (namei.cc)
+ */
+
+#include <lume/selftest.h>
+#include <lume/namei.h>
+#include <lume/dcache.h>
+#include <lume/fs.h>
+#include <lume/file.h>
+#include <lume/mount.h>
+#include <lume/task.h>
+#include <lume/errno.h>
+
+extern dentry* g_root_dentry;
+extern vfsmount* g_root_mnt;
+
+/* sys_mkdirat from kernel/sys_fs.cc */
+int64 sys_mkdirat(uint64, uint64, uint64, uint64, uint64, uint64);
+
+void selftest_namei()
+{
+ /* ---- lookup root "/" ---- */
+ st_begin("namei: lookup root /");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/", 0, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(p.de == g_root_dentry);
+ ST_ASSERT(p.de->d_inode != nullptr);
+ ST_ASSERT(S_ISDIR(p.de->d_inode->i_mode));
+ }
+ st_pass();
+
+ /* ---- lookup /dev (exists from boot) ---- */
+ st_begin("namei: lookup /dev");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev", 0, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(p.de != nullptr);
+ ST_ASSERT(p.de->d_inode != nullptr);
+ ST_ASSERT(S_ISDIR(p.de->d_inode->i_mode));
+ }
+ st_pass();
+
+ /* ---- lookup nonexistent path returns -ENOENT ---- */
+ st_begin("namei: lookup nonexistent returns -ENOENT");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/this_does_not_exist", 0, &p);
+ ST_ASSERT_EQ(err, -ENOENT);
+ }
+ st_pass();
+
+ /* ---- lookup /dev/console (chardev) ---- */
+ st_begin("namei: lookup /dev/console");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev/console", 0, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(p.de->d_inode != nullptr);
+ ST_ASSERT(S_ISCHR(p.de->d_inode->i_mode));
+ }
+ st_pass();
+
+ /* ---- lookup with LOOKUP_DIRECTORY on non-dir returns -ENOTDIR ---- */
+ st_begin("namei: LOOKUP_DIRECTORY on file returns -ENOTDIR");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev/console", LOOKUP_DIRECTORY, &p);
+ ST_ASSERT_EQ(err, -ENOTDIR);
+ }
+ st_pass();
+
+ /* ---- lookup with LOOKUP_DIRECTORY on dir succeeds ---- */
+ st_begin("namei: LOOKUP_DIRECTORY on dir succeeds");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev", LOOKUP_DIRECTORY, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(S_ISDIR(p.de->d_inode->i_mode));
+ }
+ st_pass();
+
+ /* ---- kern_path shortcut ---- */
+ st_begin("namei: kern_path works like filename_lookup");
+ {
+ path p{};
+ int err = kern_path("/dev", 0, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(p.de->d_inode != nullptr);
+ }
+ st_pass();
+
+ /* ---- dot path: /dev/. resolves to /dev ---- */
+ st_begin("namei: /dev/. resolves to /dev");
+ {
+ path p1{}, p2{};
+ int e1 = filename_lookup(AT_FDCWD, "/dev", 0, &p1);
+ int e2 = filename_lookup(AT_FDCWD, "/dev/.", 0, &p2);
+ ST_ASSERT_EQ(e1, 0);
+ ST_ASSERT_EQ(e2, 0);
+ ST_ASSERT(p1.de == p2.de);
+ }
+ st_pass();
+
+ /* ---- dotdot path: /dev/.. resolves to root ---- */
+ st_begin("namei: /dev/.. resolves to root");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev/..", 0, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(p.de == g_root_dentry);
+ }
+ st_pass();
+
+ /* ---- lookup into non-dir returns -ENOTDIR ---- */
+ st_begin("namei: path through non-dir returns -ENOTDIR");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev/console/child", 0, &p);
+ ST_ASSERT_EQ(err, -ENOTDIR);
+ }
+ st_pass();
+
+ /* ---- null/empty paths ---- */
+ st_begin("namei: null name returns -EINVAL");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, nullptr, 0, &p);
+ ST_ASSERT_EQ(err, -EINVAL);
+ }
+ st_pass();
+
+ /* ---- LOOKUP_PARENT: returns parent dentry ---- */
+ st_begin("namei: LOOKUP_PARENT returns parent");
+ {
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/dev/console", LOOKUP_PARENT, &p);
+ ST_ASSERT_EQ(err, 0);
+ /* Should return /dev, not /dev/console */
+ path dev_path{};
+ int e2 = filename_lookup(AT_FDCWD, "/dev", 0, &dev_path);
+ ST_ASSERT_EQ(e2, 0);
+ ST_ASSERT(p.de == dev_path.de);
+ }
+ st_pass();
+
+ /* ---- mkdir + lookup round-trip ---- */
+ st_begin("namei: mkdir then lookup succeeds");
+ {
+ /* Create /test_namei_dir via sys_mkdirat */
+ int64 ret = sys_mkdirat(
+ static_cast<uint64>(-100),
+ reinterpret_cast<uint64>("/test_namei_dir"),
+ 0755, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+
+ /* Now lookup should find it */
+ path p{};
+ int err = filename_lookup(AT_FDCWD, "/test_namei_dir", 0, &p);
+ ST_ASSERT_EQ(err, 0);
+ ST_ASSERT(p.de->d_inode != nullptr);
+ ST_ASSERT(S_ISDIR(p.de->d_inode->i_mode));
+ }
+ st_pass();
+
+ /* NOTE: trailing slash "/dev/" not tested — Phase 1 namei
+ * doesn't handle empty trailing components. */
+}
diff --git a/na/test/test_page_cache.cc b/na/test/test_page_cache.cc
new file mode 100644
index 0000000..ee0e0e2
--- /dev/null
+++ b/na/test/test_page_cache.cc
@@ -0,0 +1,180 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_page_cache.cc — Self-tests for the page cache hash table
+ *
+ * Tests: insert, get, remove, duplicate insert, invalidate,
+ * get_or_alloc, null args.
+ */
+
+#include <lume/selftest.h>
+#include <lume/page_cache.h>
+#include <lume/page.h>
+#include <lume/pmm.h>
+#include <lume/errno.h>
+
+void selftest_page_cache()
+{
+ /* Use sentinel pointers as fake "mapping" identifiers.
+ * These are never dereferenced — only used as hash keys. */
+ int mapping_a_tag = 0;
+ int mapping_b_tag = 0;
+ void* mapping_a = &mapping_a_tag;
+ void* mapping_b = &mapping_b_tag;
+
+ /* ---- insert and get ---- */
+ st_begin("page_cache: insert then get returns same page");
+ {
+ Page* page = pmm_alloc_page();
+ ST_ASSERT(page != nullptr);
+
+ int ret = page_cache_insert(mapping_a, 0, page);
+ ST_ASSERT_EQ(ret, 0);
+
+ Page* got = page_cache_get(mapping_a, 0);
+ ST_ASSERT_EQ(got, page);
+
+ /* put the reference from get */
+ page_cache_put(got);
+
+ /* Clean up */
+ page_cache_remove(mapping_a, 0);
+ pmm_free_page(page);
+ }
+ st_pass();
+
+ /* ---- get nonexistent returns nullptr ---- */
+ st_begin("page_cache: get nonexistent returns nullptr");
+ {
+ Page* got = page_cache_get(mapping_a, 99999);
+ ST_ASSERT(got == nullptr);
+ }
+ st_pass();
+
+ /* ---- duplicate insert returns -EEXIST ---- */
+ st_begin("page_cache: duplicate insert returns -EEXIST");
+ {
+ Page* page = pmm_alloc_page();
+ ST_ASSERT(page != nullptr);
+
+ int ret1 = page_cache_insert(mapping_a, 1, page);
+ ST_ASSERT_EQ(ret1, 0);
+
+ Page* page2 = pmm_alloc_page();
+ ST_ASSERT(page2 != nullptr);
+
+ int ret2 = page_cache_insert(mapping_a, 1, page2);
+ ST_ASSERT_EQ(ret2, -EEXIST);
+
+ /* Clean up */
+ page_cache_remove(mapping_a, 1);
+ pmm_free_page(page);
+ pmm_free_page(page2);
+ }
+ st_pass();
+
+ /* ---- remove returns page ---- */
+ st_begin("page_cache: remove returns page and deletes entry");
+ {
+ Page* page = pmm_alloc_page();
+ ST_ASSERT(page != nullptr);
+
+ page_cache_insert(mapping_a, 2, page);
+
+ Page* removed = page_cache_remove(mapping_a, 2);
+ ST_ASSERT_EQ(removed, page);
+
+ /* Should be gone now */
+ Page* got = page_cache_get(mapping_a, 2);
+ ST_ASSERT(got == nullptr);
+
+ pmm_free_page(page);
+ }
+ st_pass();
+
+ /* ---- remove nonexistent returns nullptr ---- */
+ st_begin("page_cache: remove nonexistent returns nullptr");
+ {
+ Page* got = page_cache_remove(mapping_a, 77777);
+ ST_ASSERT(got == nullptr);
+ }
+ st_pass();
+
+ /* ---- different mappings are independent ---- */
+ st_begin("page_cache: different mappings are independent");
+ {
+ Page* pa = pmm_alloc_page();
+ Page* pb = pmm_alloc_page();
+ ST_ASSERT(pa != nullptr);
+ ST_ASSERT(pb != nullptr);
+
+ page_cache_insert(mapping_a, 5, pa);
+ page_cache_insert(mapping_b, 5, pb);
+
+ Page* got_a = page_cache_get(mapping_a, 5);
+ Page* got_b = page_cache_get(mapping_b, 5);
+ ST_ASSERT_EQ(got_a, pa);
+ ST_ASSERT_EQ(got_b, pb);
+ ST_ASSERT_NE(got_a, got_b);
+
+ page_cache_put(got_a);
+ page_cache_put(got_b);
+ page_cache_remove(mapping_a, 5);
+ page_cache_remove(mapping_b, 5);
+ pmm_free_page(pa);
+ pmm_free_page(pb);
+ }
+ st_pass();
+
+ /* ---- invalidate removes all entries for a mapping ---- */
+ st_begin("page_cache: invalidate removes all entries for mapping");
+ {
+ Page* pages[4];
+ for (int i = 0; i < 4; i++) {
+ pages[i] = pmm_alloc_page();
+ ST_ASSERT(pages[i] != nullptr);
+ page_cache_insert(mapping_a, 10 + i, pages[i]);
+ }
+
+ /* Also insert one for mapping_b */
+ Page* pb = pmm_alloc_page();
+ page_cache_insert(mapping_b, 10, pb);
+
+ /* Invalidate mapping_a */
+ page_cache_invalidate(mapping_a);
+
+ /* All mapping_a entries should be gone */
+ for (int i = 0; i < 4; i++) {
+ Page* got = page_cache_get(mapping_a, 10 + i);
+ ST_ASSERT(got == nullptr);
+ pmm_free_page(pages[i]);
+ }
+
+ /* mapping_b should still be there */
+ Page* got_b = page_cache_get(mapping_b, 10);
+ ST_ASSERT_EQ(got_b, pb);
+ page_cache_put(got_b);
+ page_cache_remove(mapping_b, 10);
+ pmm_free_page(pb);
+ }
+ st_pass();
+
+ /* ---- insert null mapping returns -EINVAL ---- */
+ st_begin("page_cache: insert null mapping returns -EINVAL");
+ {
+ Page* page = pmm_alloc_page();
+ int ret = page_cache_insert(nullptr, 0, page);
+ ST_ASSERT_EQ(ret, -EINVAL);
+ pmm_free_page(page);
+ }
+ st_pass();
+
+ /* ---- insert null page returns -EINVAL ---- */
+ st_begin("page_cache: insert null page returns -EINVAL");
+ {
+ int ret = page_cache_insert(mapping_a, 0, nullptr);
+ ST_ASSERT_EQ(ret, -EINVAL);
+ }
+ st_pass();
+}
diff --git a/na/test/test_pid.cc b/na/test/test_pid.cc
new file mode 100644
index 0000000..e79eb1e
--- /dev/null
+++ b/na/test/test_pid.cc
@@ -0,0 +1,106 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_pid.cc — Self-tests for PID allocator
+ *
+ * Tests: alloc, free, re-use, exhaustion guard, reserved PIDs.
+ */
+
+#include <lume/selftest.h>
+#include <lume/pid.h>
+
+void selftest_pid()
+{
+ /* Create a local PidAllocator for isolated testing */
+ PidAllocator pa;
+ pa.init();
+
+ /* ---- PID 0 is reserved (idle/swapper) ---- */
+ st_begin("pid: PID 0 is reserved");
+ {
+ /* First alloc should return 1 (PID 0 reserved for idle) */
+ int32 first = pa.alloc();
+ ST_ASSERT(first >= 1);
+ }
+ st_pass();
+
+ /* ---- Sequential allocation returns unique PIDs ---- */
+ st_begin("pid: sequential allocation returns unique PIDs");
+ {
+ PidAllocator pa2;
+ pa2.init();
+
+ int32 p1 = pa2.alloc();
+ int32 p2 = pa2.alloc();
+ int32 p3 = pa2.alloc();
+ ST_ASSERT(p1 >= 1);
+ ST_ASSERT(p2 >= 1);
+ ST_ASSERT(p3 >= 1);
+ ST_ASSERT_NE(p1, p2);
+ ST_ASSERT_NE(p2, p3);
+ ST_ASSERT_NE(p1, p3);
+ }
+ st_pass();
+
+ /* ---- Freed PID can be reused ---- */
+ st_begin("pid: freed PID can be reused");
+ {
+ PidAllocator pa3;
+ pa3.init();
+
+ /* Allocate all PIDs from 2..N */
+ constexpr int kAllocCount = 60;
+ int32 pids[kAllocCount];
+ for (int i = 0; i < kAllocCount; i++) {
+ pids[i] = pa3.alloc();
+ ST_ASSERT(pids[i] >= 1);
+ }
+
+ /* Free one in the middle */
+ int32 freed_pid = pids[kAllocCount / 2];
+ pa3.free(freed_pid);
+
+ /* Allocate again — should eventually get the freed PID */
+ bool found = false;
+ for (int i = 0; i < kAllocCount; i++) {
+ int32 p = pa3.alloc();
+ if (p == freed_pid) { found = true; break; }
+ pa3.free(p);
+ }
+ ST_ASSERT(found);
+ }
+ st_pass();
+
+ /* ---- free invalid PID is safe ---- */
+ st_begin("pid: free invalid PID is no-op");
+ {
+ PidAllocator pa4;
+ pa4.init();
+
+ /* These should not crash */
+ pa4.free(-1);
+ pa4.free(kMaxPid);
+ pa4.free(kMaxPid + 100);
+
+ /* Allocator should still work — PID 0 still reserved */
+ int32 p = pa4.alloc();
+ ST_ASSERT(p >= 1);
+ }
+ st_pass();
+
+ /* ---- Monotonic allocation before wrap ---- */
+ st_begin("pid: monotonic allocation");
+ {
+ PidAllocator pa5;
+ pa5.init();
+
+ int32 prev = pa5.alloc();
+ for (int i = 0; i < 10; i++) {
+ int32 next = pa5.alloc();
+ ST_ASSERT(next > prev);
+ prev = next;
+ }
+ }
+ st_pass();
+}
diff --git a/na/test/test_pipe_devfs.cc b/na/test/test_pipe_devfs.cc
new file mode 100644
index 0000000..ed375ed
--- /dev/null
+++ b/na/test/test_pipe_devfs.cc
@@ -0,0 +1,420 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_pipe_devfs.cc — Self-tests for pipe, /dev/null, /dev/zero, fcntl
+ */
+
+#include <lume/selftest.h>
+#include <lume/errno.h>
+#include <lume/file.h>
+#include <lume/fs.h>
+#include <lume/fs_types.h>
+#include <lume/task.h>
+#include <lume/fdtable.h>
+#include <lume/pipe.h>
+
+/* Syscall entry points */
+int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_read(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_pipe2(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_fcntl(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_fstat_wrapper(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* ================================================================== */
+/* Helpers */
+/* ================================================================== */
+
+static int64 open_dev(const char* path, int flags)
+{
+ return sys_openat(
+ static_cast<uint64>(-100), /* AT_FDCWD */
+ reinterpret_cast<uint64>(path),
+ static_cast<uint64>(flags),
+ 0, 0, 0);
+}
+
+static int64 do_close(int64 fd)
+{
+ return sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+}
+
+static int64 do_read(int64 fd, void* buf, uint64 count)
+{
+ return sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), count, 0, 0, 0);
+}
+
+static int64 do_write(int64 fd, const void* buf, uint64 count)
+{
+ return sys_write(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), count, 0, 0, 0);
+}
+
+/* fcntl command constants — must match sys_fs.cc */
+inline constexpr int F_DUPFD = 0;
+inline constexpr int F_GETFD = 1;
+inline constexpr int F_SETFD = 2;
+inline constexpr int F_GETFL = 3;
+inline constexpr int F_SETFL = 4;
+inline constexpr int F_DUPFD_CLOEXEC = 1030;
+inline constexpr int FD_CLOEXEC = 1;
+
+/* ================================================================== */
+/* /dev/null tests */
+/* ================================================================== */
+
+void selftest_devfs()
+{
+ /* ---- /dev/null: read returns EOF ---- */
+ st_begin("devfs: /dev/null read returns 0 (EOF)");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ char buf[16];
+ int64 n = do_read(fd, buf, sizeof(buf));
+ ST_ASSERT_EQ(n, (int64)0);
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- /dev/null: write discards data ---- */
+ st_begin("devfs: /dev/null write discards data");
+ {
+ int64 fd = open_dev("/dev/null", O_WRONLY);
+ ST_ASSERT(fd >= 0);
+
+ const char* msg = "discarded";
+ int64 n = do_write(fd, msg, 9);
+ ST_ASSERT_EQ(n, (int64)9);
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- /dev/null: fstat reports S_IFCHR ---- */
+ st_begin("devfs: /dev/null fstat is S_IFCHR");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ kstat st{};
+ int64 ret = sys_fstat_wrapper(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(&st), 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+ ST_ASSERT(S_ISCHR(st.st_mode));
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- /dev/zero: read returns zeros ---- */
+ st_begin("devfs: /dev/zero read returns zeros");
+ {
+ int64 fd = open_dev("/dev/zero", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ char buf[32];
+ /* Pre-fill with non-zero */
+ for (int i = 0; i < 32; i++) buf[i] = 0xFF;
+
+ int64 n = do_read(fd, buf, 32);
+ ST_ASSERT_EQ(n, (int64)32);
+
+ bool all_zero = true;
+ for (int i = 0; i < 32; i++) {
+ if (buf[i] != 0) { all_zero = false; break; }
+ }
+ ST_ASSERT(all_zero);
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- /dev/zero: write discards data ---- */
+ st_begin("devfs: /dev/zero write discards data");
+ {
+ int64 fd = open_dev("/dev/zero", O_WRONLY);
+ ST_ASSERT(fd >= 0);
+
+ char buf[16] = {1, 2, 3};
+ int64 n = do_write(fd, buf, 16);
+ ST_ASSERT_EQ(n, (int64)16);
+ do_close(fd);
+ }
+ st_pass();
+}
+
+/* ================================================================== */
+/* Pipe tests */
+/* ================================================================== */
+
+void selftest_pipe()
+{
+ /* ---- pipe2: basic creation ---- */
+ st_begin("pipe: pipe2 creates two valid fds");
+ {
+ int fds[2] = {-1, -1};
+ int64 ret = sys_pipe2(
+ reinterpret_cast<uint64>(fds), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+ ST_ASSERT(fds[0] >= 0);
+ ST_ASSERT(fds[1] >= 0);
+ ST_ASSERT_NE(fds[0], fds[1]);
+
+ do_close(fds[0]);
+ do_close(fds[1]);
+ }
+ st_pass();
+
+ /* ---- pipe: write then read ---- */
+ st_begin("pipe: write then read returns correct data");
+ {
+ int fds[2];
+ sys_pipe2(reinterpret_cast<uint64>(fds), 0, 0, 0, 0, 0);
+
+ const char* msg = "pipe_test";
+ int64 w = do_write(fds[1], msg, 9);
+ ST_ASSERT_EQ(w, (int64)9);
+
+ char buf[16] = {};
+ int64 r = do_read(fds[0], buf, 16);
+ ST_ASSERT_EQ(r, (int64)9);
+
+ bool match = true;
+ for (int i = 0; i < 9; i++) {
+ if (buf[i] != msg[i]) { match = false; break; }
+ }
+ ST_ASSERT(match);
+
+ do_close(fds[0]);
+ do_close(fds[1]);
+ }
+ st_pass();
+
+ /* ---- pipe: read from closed write end returns EOF ---- */
+ st_begin("pipe: read after writer close returns EOF");
+ {
+ int fds[2];
+ sys_pipe2(reinterpret_cast<uint64>(fds), 0, 0, 0, 0, 0);
+
+ /* Write some data, then close write end */
+ const char* msg = "eof";
+ do_write(fds[1], msg, 3);
+ do_close(fds[1]);
+
+ /* Read the data */
+ char buf[16];
+ int64 r1 = do_read(fds[0], buf, 16);
+ ST_ASSERT_EQ(r1, (int64)3);
+
+ /* Next read should return 0 (EOF) */
+ int64 r2 = do_read(fds[0], buf, 16);
+ ST_ASSERT_EQ(r2, (int64)0);
+
+ do_close(fds[0]);
+ }
+ st_pass();
+
+ /* ---- pipe: write to closed read end returns -EPIPE ---- */
+ st_begin("pipe: write after reader close returns -EPIPE");
+ {
+ int fds[2];
+ sys_pipe2(reinterpret_cast<uint64>(fds), 0, 0, 0, 0, 0);
+
+ /* Close read end */
+ do_close(fds[0]);
+
+ /* Write should fail */
+ const char* msg = "broken";
+ int64 w = do_write(fds[1], msg, 6);
+ ST_ASSERT_EQ(w, (int64)-EPIPE);
+
+ do_close(fds[1]);
+ }
+ st_pass();
+
+ /* ---- pipe: multiple writes accumulate ---- */
+ st_begin("pipe: multiple writes accumulate in buffer");
+ {
+ int fds[2];
+ sys_pipe2(reinterpret_cast<uint64>(fds), 0, 0, 0, 0, 0);
+
+ do_write(fds[1], "AAA", 3);
+ do_write(fds[1], "BBB", 3);
+
+ char buf[16] = {};
+ int64 r = do_read(fds[0], buf, 16);
+ ST_ASSERT_EQ(r, (int64)6);
+ ST_ASSERT_EQ(buf[0], 'A');
+ ST_ASSERT_EQ(buf[3], 'B');
+
+ do_close(fds[0]);
+ do_close(fds[1]);
+ }
+ st_pass();
+
+ /* ---- pipe: read partial ---- */
+ st_begin("pipe: partial read leaves remainder");
+ {
+ int fds[2];
+ sys_pipe2(reinterpret_cast<uint64>(fds), 0, 0, 0, 0, 0);
+
+ do_write(fds[1], "ABCDEF", 6);
+
+ char buf[3] = {};
+ int64 r1 = do_read(fds[0], buf, 3);
+ ST_ASSERT_EQ(r1, (int64)3);
+ ST_ASSERT_EQ(buf[0], 'A');
+ ST_ASSERT_EQ(buf[2], 'C');
+
+ int64 r2 = do_read(fds[0], buf, 3);
+ ST_ASSERT_EQ(r2, (int64)3);
+ ST_ASSERT_EQ(buf[0], 'D');
+ ST_ASSERT_EQ(buf[2], 'F');
+
+ do_close(fds[0]);
+ do_close(fds[1]);
+ }
+ st_pass();
+}
+
+/* ================================================================== */
+/* fcntl tests */
+/* ================================================================== */
+
+void selftest_fcntl()
+{
+ /* ---- fcntl F_DUPFD ---- */
+ st_begin("fcntl: F_DUPFD duplicates fd");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 fd2 = sys_fcntl(static_cast<uint64>(fd), F_DUPFD, 0, 0, 0, 0);
+ ST_ASSERT(fd2 >= 0);
+ ST_ASSERT_NE(fd, fd2);
+
+ /* Both should be valid reads */
+ char buf[4];
+ int64 r = do_read(fd2, buf, 4);
+ ST_ASSERT_EQ(r, (int64)0); /* /dev/null → EOF */
+
+ do_close(fd);
+ do_close(fd2);
+ }
+ st_pass();
+
+ /* ---- fcntl F_DUPFD with min_fd ---- */
+ st_begin("fcntl: F_DUPFD respects min_fd");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 fd2 = sys_fcntl(static_cast<uint64>(fd), F_DUPFD, 100, 0, 0, 0);
+ ST_ASSERT(fd2 >= 100);
+
+ do_close(fd);
+ do_close(fd2);
+ }
+ st_pass();
+
+ /* ---- fcntl F_GETFD / F_SETFD ---- */
+ st_begin("fcntl: F_GETFD/F_SETFD cloexec");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ /* Initially no CLOEXEC */
+ int64 flags = sys_fcntl(static_cast<uint64>(fd), F_GETFD, 0, 0, 0, 0);
+ ST_ASSERT_EQ(flags, (int64)0);
+
+ /* Set CLOEXEC */
+ int64 ret = sys_fcntl(static_cast<uint64>(fd), F_SETFD, FD_CLOEXEC, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+
+ /* Verify it was set */
+ flags = sys_fcntl(static_cast<uint64>(fd), F_GETFD, 0, 0, 0, 0);
+ ST_ASSERT_EQ(flags, (int64)FD_CLOEXEC);
+
+ /* Clear it */
+ sys_fcntl(static_cast<uint64>(fd), F_SETFD, 0, 0, 0, 0);
+ flags = sys_fcntl(static_cast<uint64>(fd), F_GETFD, 0, 0, 0, 0);
+ ST_ASSERT_EQ(flags, (int64)0);
+
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- fcntl F_GETFL ---- */
+ st_begin("fcntl: F_GETFL returns open flags");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 fl = sys_fcntl(static_cast<uint64>(fd), F_GETFL, 0, 0, 0, 0);
+ /* O_RDONLY == 0, so result should have no write bits */
+ ST_ASSERT_EQ(fl & O_WRONLY, (int64)0);
+
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- fcntl F_SETFL O_NONBLOCK ---- */
+ st_begin("fcntl: F_SETFL toggles O_NONBLOCK");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ /* Set O_NONBLOCK */
+ sys_fcntl(static_cast<uint64>(fd), F_SETFL, O_NONBLOCK, 0, 0, 0);
+ int64 fl = sys_fcntl(static_cast<uint64>(fd), F_GETFL, 0, 0, 0, 0);
+ ST_ASSERT(fl & O_NONBLOCK);
+
+ /* Clear it */
+ sys_fcntl(static_cast<uint64>(fd), F_SETFL, 0, 0, 0, 0);
+ fl = sys_fcntl(static_cast<uint64>(fd), F_GETFL, 0, 0, 0, 0);
+ ST_ASSERT_EQ(fl & O_NONBLOCK, (int64)0);
+
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- fcntl F_DUPFD_CLOEXEC ---- */
+ st_begin("fcntl: F_DUPFD_CLOEXEC sets cloexec");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 fd2 = sys_fcntl(static_cast<uint64>(fd), F_DUPFD_CLOEXEC, 0, 0, 0, 0);
+ ST_ASSERT(fd2 >= 0);
+
+ int64 flags = sys_fcntl(static_cast<uint64>(fd2), F_GETFD, 0, 0, 0, 0);
+ ST_ASSERT_EQ(flags, (int64)FD_CLOEXEC);
+
+ do_close(fd);
+ do_close(fd2);
+ }
+ st_pass();
+
+ /* ---- fcntl bad fd ---- */
+ st_begin("fcntl: bad fd returns -EBADF");
+ {
+ int64 ret = sys_fcntl(999, F_GETFD, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EBADF);
+ }
+ st_pass();
+
+ /* ---- fcntl unknown cmd ---- */
+ st_begin("fcntl: unknown cmd returns -EINVAL");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 ret = sys_fcntl(static_cast<uint64>(fd), 9999, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EINVAL);
+ do_close(fd);
+ }
+ st_pass();
+}
diff --git a/na/test/test_pmm.cc b/na/test/test_pmm.cc
index 53b2b2a..3330866 100644
--- a/na/test/test_pmm.cc
+++ b/na/test/test_pmm.cc
@@ -25,7 +25,7 @@ void selftest_pmm()
{
Page* page = pmm_alloc_page();
ST_ASSERT(page != nullptr);
- ST_ASSERT(page->state == PageState::Allocated);
+ ST_ASSERT(page->type == PageType::Allocated);
ST_ASSERT(page->order == 0);
uint64 pa = page_to_pa(page).raw;
@@ -123,11 +123,11 @@ void selftest_pmm()
page_decref(page); // 2 -> 1, should NOT free
ST_ASSERT(page->refcount.load() == 1);
- ST_ASSERT(page->state == PageState::Allocated);
+ ST_ASSERT(page->type == PageType::Allocated);
page_decref(page); // 1 -> 0, should free to pool
// After decref to 0, page is returned to pool.
- // We can't safely check page->state because it might be reused.
+ // We can't safely check page->type because it might be reused.
}
st_pass();
st_begin("pmm: pmm_alloc_pages natural alignment (order-3)");
diff --git a/na/test/test_reloc.cc b/na/test/test_reloc.cc
deleted file mode 100644
index 54e7144..0000000
--- a/na/test/test_reloc.cc
+++ /dev/null
@@ -1,46 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-/*
- * Boot Self-Test: relocation engine
- */
-
-#include <lume/relocate.h>
-#include <lume/selftest.h>
-
-struct RelocBlob {
- RelocTableHeader hdr;
- RelocEntry entries[2];
-};
-
-void selftest_reloc()
-{
- st_begin("reloc: relative + abs64");
-
- alignas(8) uint8 buf[64] = {};
- uint64 base = reinterpret_cast<uint64>(buf);
- uint64 link_base = base - 0x1000;
-
- RelocBlob blob = {};
- blob.hdr.magic = kRelocTableMagic;
- blob.hdr.version = kRelocTableVersion;
- blob.hdr.arch = RELOC_ARCH_RISCV;
- blob.hdr.entry_size = sizeof(RelocEntry);
- blob.hdr.entry_count = 2;
-
- blob.entries[0].offset = 8;
- blob.entries[0].addend = 0x200;
- blob.entries[0].type = RELOC_RISCV_RELATIVE;
-
- blob.entries[1].offset = 16;
- blob.entries[1].addend = link_base + 0x1234;
- blob.entries[1].type = RELOC_RISCV_64;
-
- RelocStatus st = relocate_apply(base, sizeof(buf), link_base,
- &blob, sizeof(blob));
- ST_ASSERT_EQ(st, RELOC_OK);
- ST_ASSERT_EQ(*reinterpret_cast<uint64*>(base + 8), base + 0x200);
- ST_ASSERT_EQ(*reinterpret_cast<uint64*>(base + 16), base + 0x1234);
-
- st_pass();
-}
diff --git a/na/test/test_signal.cc b/na/test/test_signal.cc
new file mode 100644
index 0000000..360b0e6
--- /dev/null
+++ b/na/test/test_signal.cc
@@ -0,0 +1,193 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_signal.cc — Self-tests for signal infrastructure
+ *
+ * Tests the kernel-internal signal APIs directly (not via syscalls,
+ * since syscalls use copy_to_user which rejects kernel-space buffers).
+ */
+
+#include <lume/selftest.h>
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/signal_nr.h>
+#include <lume/task.h>
+#include <lume/errno.h>
+#include <lume/new.h>
+
+/* From signal.cc */
+extern int send_signal(int sig, kernel_siginfo* info, TaskControlBlock* task);
+
+/* Drain all queued sigqueue entries from a task's pending list */
+static void flush_pending(TaskControlBlock* task)
+{
+ task->pending.signal = sigset_t{};
+ list_node* head = &task->pending.list;
+ while (head->next != head) {
+ list_node* node = head->next;
+ head->next = node->next;
+ node->next->prev = head;
+ auto* q = list_entry<sigqueue, &sigqueue::list>(node);
+ delete q;
+ }
+}
+
+void selftest_signal()
+{
+ TaskControlBlock* curr = current_task();
+
+ /* Ensure sighand exists for this task */
+ if (!curr->sighand) {
+ curr->sighand = new sighand_struct;
+ curr->sighand->init();
+ }
+
+ /* Ensure pending list is initialized */
+ if (curr->pending.list.next == nullptr) {
+ curr->pending.list.next = &curr->pending.list;
+ curr->pending.list.prev = &curr->pending.list;
+ curr->pending.signal = sigset_t{};
+ }
+
+ /* ---- send_signal queues a signal ---- */
+ st_begin("signal: send_signal queues to pending");
+ {
+ curr->pending.signal = sigset_t{};
+
+ kernel_siginfo info{};
+ info.si_signo = SIGUSR1;
+ info.si_code = 0;
+
+ int ret = send_signal(SIGUSR1, &info, curr);
+ ST_ASSERT_EQ(ret, 0);
+ ST_ASSERT(curr->pending.signal.has(SIGUSR1));
+
+ flush_pending(curr);
+ }
+ st_pass();
+
+ /* ---- send_signal: invalid signal returns -EINVAL ---- */
+ st_begin("signal: send_signal invalid sig returns -EINVAL");
+ {
+ int ret = send_signal(0, nullptr, curr);
+ ST_ASSERT_EQ(ret, -EINVAL);
+
+ ret = send_signal(_NSIG + 1, nullptr, curr);
+ ST_ASSERT_EQ(ret, -EINVAL);
+ }
+ st_pass();
+
+ /* ---- send_signal: null task returns -ESRCH ---- */
+ st_begin("signal: send_signal null task returns -ESRCH");
+ {
+ int ret = send_signal(SIGUSR1, nullptr, nullptr);
+ ST_ASSERT_EQ(ret, -ESRCH);
+ }
+ st_pass();
+
+ /* ---- duplicate standard signal is coalesced ---- */
+ st_begin("signal: duplicate standard signal coalesced");
+ {
+ curr->pending.signal = sigset_t{};
+
+ send_signal(SIGUSR1, nullptr, curr);
+ ST_ASSERT(curr->pending.signal.has(SIGUSR1));
+
+ int ret = send_signal(SIGUSR1, nullptr, curr);
+ ST_ASSERT_EQ(ret, 0);
+
+ flush_pending(curr);
+ }
+ st_pass();
+
+ /* ---- sigset_t: add/del/has ---- */
+ st_begin("signal: sigset_t add/del/has");
+ {
+ sigset_t s{};
+ ST_ASSERT(!s.has(SIGUSR1));
+ s.add(SIGUSR1);
+ ST_ASSERT(s.has(SIGUSR1));
+ s.del(SIGUSR1);
+ ST_ASSERT(!s.has(SIGUSR1));
+ }
+ st_pass();
+
+ /* ---- sigset_t: bitwise operators ---- */
+ st_begin("signal: sigset_t bitwise OR and AND");
+ {
+ sigset_t a{};
+ a.add(SIGUSR1);
+ sigset_t b{};
+ b.add(SIGUSR2);
+
+ sigset_t c = a | b;
+ ST_ASSERT(c.has(SIGUSR1));
+ ST_ASSERT(c.has(SIGUSR2));
+
+ sigset_t d = c & a;
+ ST_ASSERT(d.has(SIGUSR1));
+ ST_ASSERT(!d.has(SIGUSR2));
+ }
+ st_pass();
+
+ /* ---- blocked mask: direct manipulation ---- */
+ st_begin("signal: blocked mask direct manipulation");
+ {
+ sigset_t saved = curr->blocked;
+ curr->blocked = sigset_t{};
+
+ curr->blocked.add(SIGUSR1);
+ curr->blocked.add(SIGUSR2);
+ ST_ASSERT(curr->blocked.has(SIGUSR1));
+ ST_ASSERT(curr->blocked.has(SIGUSR2));
+
+ curr->blocked.del(SIGUSR1);
+ ST_ASSERT(!curr->blocked.has(SIGUSR1));
+ ST_ASSERT(curr->blocked.has(SIGUSR2));
+
+ curr->blocked = saved;
+ }
+ st_pass();
+
+ /* ---- sighand action table ---- */
+ st_begin("signal: sighand action table defaults to SIG_DFL");
+ {
+ /* All handlers should default to SIG_DFL (0) */
+ kernel_sigaction* ka = &curr->sighand->action[SIGUSR1 - 1];
+ ST_ASSERT_EQ(ka->sa_handler, (uint64)SIG_DFL);
+
+ /* Set a handler and verify */
+ ka->sa_handler = 0x12345678ULL;
+ ST_ASSERT_EQ(ka->sa_handler, (uint64)0x12345678ULL);
+
+ /* Reset */
+ ka->sa_handler = SIG_DFL;
+ }
+ st_pass();
+
+ /* ---- sig_kernel_only rejects SIGKILL/SIGSTOP handlers ---- */
+ st_begin("signal: SIGKILL/SIGSTOP are kernel-only");
+ {
+ ST_ASSERT(sig_kernel_only(SIGKILL));
+ ST_ASSERT(sig_kernel_only(SIGSTOP));
+ ST_ASSERT(!sig_kernel_only(SIGUSR1));
+ ST_ASSERT(!sig_kernel_only(SIGINT));
+ }
+ st_pass();
+
+ /* ---- sig_default_action correctness ---- */
+ st_begin("signal: sig_default_action returns correct actions");
+ {
+ ST_ASSERT_EQ((int)sig_default_action(SIGCHLD), (int)SIG_ACT_IGN);
+ ST_ASSERT_EQ((int)sig_default_action(SIGKILL), (int)SIG_ACT_TERM);
+ ST_ASSERT_EQ((int)sig_default_action(SIGSTOP), (int)SIG_ACT_STOP);
+ ST_ASSERT_EQ((int)sig_default_action(SIGCONT), (int)SIG_ACT_CONT);
+ ST_ASSERT_EQ((int)sig_default_action(SIGSEGV), (int)SIG_ACT_CORE);
+ }
+ st_pass();
+
+ /* Clean up */
+ flush_pending(curr);
+ curr->blocked = sigset_t{};
+}
diff --git a/na/test/test_slab.cc b/na/test/test_slab.cc
index 2ba1a5d..8a448b4 100644
--- a/na/test/test_slab.cc
+++ b/na/test/test_slab.cc
@@ -203,7 +203,7 @@ void selftest_slab()
uint64 page_pa = va_to_pa(virt_addr(reinterpret_cast<uint64>(base_ptr))).raw;
Page* page = pa_to_page(phys_addr(page_pa));
- ST_ASSERT(page->state == PageState::Slab);
+ ST_ASSERT(page->type == PageType::Slab);
ST_ASSERT(page->slab.obj_count == 32);
// Free all items in this isolated slab page
@@ -212,7 +212,7 @@ void selftest_slab()
}
// The page MUST have been returned to the PMM (either global Free or Per-CPU Cached).
- ST_ASSERT(page->state == PageState::Free || page->state == PageState::PcpCached);
+ ST_ASSERT(page->type == PageType::Free || page->type == PageType::PcpCached);
// Clean up the drain array
for (int i = 0; i < drain_count; i++) {
diff --git a/na/test/test_smp.cc b/na/test/test_smp.cc
new file mode 100644
index 0000000..f934526
--- /dev/null
+++ b/na/test/test_smp.cc
@@ -0,0 +1,136 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_smp.cc — Post-SMP boot tests
+ *
+ * These tests run after all APs are online. They verify:
+ * 1. IPI delivery — BSP sends IPI to each AP, AP increments counter.
+ * 2. Task migration — a task moves from one runqueue to another.
+ * 3. TLB shootdown IPI — cross-CPU flush.
+ *
+ * Called from the init thread (PID 1) context, NOT from the pre-SMP
+ * self-test suite.
+ */
+
+#include <lume/selftest.h>
+#include <lume/smp.h>
+#include <lume/sched.h>
+#include <lume/atomic.h>
+#include <lume/config.h>
+#include <arch/cpu.h>
+
+/* Shared counter incremented by APs when they receive an IPI */
+static lume::atomic<uint32> g_ipi_ack_count{0};
+
+/* ------------------------------------------------------------------ */
+/* IPI delivery test */
+/* ------------------------------------------------------------------ */
+
+void selftest_ipi_delivery()
+{
+ st_begin("smp: IPI delivery to all APs");
+
+ /* Reset counter */
+ g_ipi_ack_count.store(0, __ATOMIC_RELAXED);
+
+ uint32 bsp = static_cast<uint32>(arch::cpu::id());
+ uint32 num_cpus = 4; /* TODO: derive from FDT */
+
+ /* Build mask of all APs */
+ uint64 ap_mask = 0;
+ uint32 expected = 0;
+ for (uint32 i = 0; i < num_cpus; i++) {
+ if (i == bsp) continue;
+ ap_mask |= (1ULL << i);
+ expected++;
+ }
+
+ /* Send reschedule IPI to all APs */
+ smp_send_ipi(ap_mask, IPI_RESCHEDULE);
+
+ /* Wait a short while for APs to process the interrupt.
+ * APs are in wfi (idle loop), so they'll wake immediately
+ * on interrupt delivery. We just need to give them time
+ * to run the handler. */
+ for (volatile int i = 0; i < 100000; i++) {
+ /* spin-wait */
+ }
+
+ /* We can't directly check that IPI was received because
+ * IPI_RESCHEDULE just sets need_resched — no ack.
+ * Instead verify no crash occurred and the IPI infrastructure
+ * didn't deadlock. This is a liveness test. */
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* IPI halt test (send to self, skip) */
+/* ------------------------------------------------------------------ */
+
+void selftest_ipi_self_skip()
+{
+ st_begin("smp: IPI reason bitmask accumulation");
+
+ uint32 cpu = static_cast<uint32>(arch::cpu::id());
+
+ /* Manually set multiple reasons atomically */
+ g_ipi_data[cpu].pending.store(0, __ATOMIC_RELAXED);
+ g_ipi_data[cpu].pending.fetch_or(IPI_RESCHEDULE, __ATOMIC_RELEASE);
+ g_ipi_data[cpu].pending.fetch_or(IPI_TLB_SHOOTDOWN, __ATOMIC_RELEASE);
+
+ uint32 val = g_ipi_data[cpu].pending.load(__ATOMIC_ACQUIRE);
+ ST_ASSERT_EQ(val, IPI_RESCHEDULE | IPI_TLB_SHOOTDOWN);
+
+ /* Clean up */
+ g_ipi_data[cpu].pending.store(0, __ATOMIC_RELAXED);
+
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* TLB shootdown IPI test */
+/* ------------------------------------------------------------------ */
+
+void selftest_tlb_shootdown()
+{
+ st_begin("smp: TLB shootdown IPI (cross-CPU flush)");
+
+ uint32 bsp = static_cast<uint32>(arch::cpu::id());
+ uint32 num_cpus = 4;
+
+ /* Build AP mask */
+ uint64 ap_mask = 0;
+ for (uint32 i = 0; i < num_cpus; i++) {
+ if (i == bsp) continue;
+ ap_mask |= (1ULL << i);
+ }
+
+ /* Set TLB range on each AP's IpiData */
+ for (uint32 i = 0; i < num_cpus; i++) {
+ if (i == bsp) continue;
+ g_ipi_data[i].tlb_start = 0;
+ g_ipi_data[i].tlb_end = 0; /* 0,0 = full flush */
+ }
+
+ /* Send TLB shootdown */
+ smp_send_ipi(ap_mask, IPI_TLB_SHOOTDOWN);
+
+ /* Wait for APs to process */
+ for (volatile int i = 0; i < 100000; i++) {}
+
+ /* If we got here without panic, TLB shootdown succeeded */
+ st_pass();
+}
+
+/* ------------------------------------------------------------------ */
+/* Public entry point */
+/* ------------------------------------------------------------------ */
+
+void selftest_smp()
+{
+ selftest_ipi_delivery();
+ selftest_ipi_self_skip();
+ selftest_tlb_shootdown();
+}
diff --git a/na/test/test_sys_fs_extra.cc b/na/test/test_sys_fs_extra.cc
new file mode 100644
index 0000000..705ce63
--- /dev/null
+++ b/na/test/test_sys_fs_extra.cc
@@ -0,0 +1,238 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_sys_fs_extra.cc — Self-tests for additional filesystem syscalls
+ *
+ * Tests: dup3, chdir/getcwd, writev, ioctl (stub), unlinkat (stub).
+ * Complements test_vfs_io.cc which covers the core read/write/open/close.
+ */
+
+#include <lume/selftest.h>
+#include <lume/errno.h>
+#include <lume/file.h>
+#include <lume/fs.h>
+#include <lume/fs_types.h>
+#include <lume/task.h>
+
+/* Syscall entry points */
+int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_read(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_dup3(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_chdir(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getcwd(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_writev(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_ioctl(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_unlinkat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_mkdirat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_lseek(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* ================================================================== */
+/* Helpers */
+/* ================================================================== */
+
+static int64 open_dev(const char* path, int flags)
+{
+ return sys_openat(
+ static_cast<uint64>(-100),
+ reinterpret_cast<uint64>(path),
+ static_cast<uint64>(flags), 0, 0, 0);
+}
+
+static int64 do_close(int64 fd)
+{
+ return sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+}
+
+static int64 do_read(int64 fd, void* buf, uint64 count)
+{
+ return sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), count, 0, 0, 0);
+}
+
+
+/* iovec must match sys_fs.cc definition */
+struct test_iovec {
+ void* iov_base;
+ uint64 iov_len;
+};
+
+/* ================================================================== */
+/* Tests */
+/* ================================================================== */
+
+void selftest_sys_fs_extra()
+{
+ /* ---- dup3: duplicate fd to specific target ---- */
+ st_begin("sys_fs: dup3 duplicates to specific fd");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ /* dup3 to fd 50 */
+ int64 fd2 = sys_dup3(static_cast<uint64>(fd), 50, 0, 0, 0, 0);
+ ST_ASSERT_EQ(fd2, (int64)50);
+
+ /* Both should work */
+ char buf[4];
+ int64 r = do_read(fd2, buf, 4);
+ ST_ASSERT_EQ(r, (int64)0); /* /dev/null → EOF */
+
+ do_close(fd);
+ do_close(fd2);
+ }
+ st_pass();
+
+ /* ---- dup3: same fd returns -EINVAL ---- */
+ st_begin("sys_fs: dup3 same fd returns -EINVAL");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 ret = sys_dup3(static_cast<uint64>(fd),
+ static_cast<uint64>(fd), 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EINVAL);
+
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- chdir to /dev, getcwd returns /dev ---- */
+ st_begin("sys_fs: chdir then getcwd returns new dir");
+ {
+ /* Save cwd */
+ char saved_cwd[64] = {};
+ sys_getcwd(reinterpret_cast<uint64>(saved_cwd), 64, 0, 0, 0, 0);
+
+ /* chdir to /dev */
+ int64 ret = sys_chdir(
+ reinterpret_cast<uint64>("/dev"), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+
+ /* getcwd should return /dev */
+ char buf[64] = {};
+ int64 len = sys_getcwd(
+ reinterpret_cast<uint64>(buf), 64, 0, 0, 0, 0);
+ ST_ASSERT(len > 0);
+ ST_ASSERT_EQ(buf[0], '/');
+ ST_ASSERT_EQ(buf[1], 'd');
+ ST_ASSERT_EQ(buf[2], 'e');
+ ST_ASSERT_EQ(buf[3], 'v');
+
+ /* Restore cwd to / */
+ sys_chdir(reinterpret_cast<uint64>("/"), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- chdir to nonexistent returns -ENOENT ---- */
+ st_begin("sys_fs: chdir nonexistent returns -ENOENT");
+ {
+ int64 ret = sys_chdir(
+ reinterpret_cast<uint64>("/no_such_dir"), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-ENOENT);
+ }
+ st_pass();
+
+ /* ---- chdir to file returns -ENOTDIR ---- */
+ st_begin("sys_fs: chdir to file returns -ENOTDIR");
+ {
+ int64 ret = sys_chdir(
+ reinterpret_cast<uint64>("/dev/null"), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-ENOTDIR);
+ }
+ st_pass();
+
+ /* ---- writev: scatter write to file ---- */
+ st_begin("sys_fs: writev scatter write");
+ {
+ /* Create a file, write with writev, read back */
+ int64 fd = sys_openat(
+ static_cast<uint64>(-100),
+ reinterpret_cast<uint64>("/writev_test"),
+ O_CREAT | O_RDWR, 0644, 0, 0);
+ ST_ASSERT(fd >= 0);
+
+ char a[] = "AAA";
+ char b[] = "BBB";
+ test_iovec iov[2];
+ iov[0].iov_base = a;
+ iov[0].iov_len = 3;
+ iov[1].iov_base = b;
+ iov[1].iov_len = 3;
+
+ int64 w = sys_writev(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(iov), 2, 0, 0, 0);
+ ST_ASSERT_EQ(w, (int64)6);
+
+ /* Seek back to start and read */
+ sys_lseek(static_cast<uint64>(fd), 0, 0 /* SEEK_SET */, 0, 0, 0);
+
+ char buf[8] = {};
+ int64 r = do_read(fd, buf, 8);
+ ST_ASSERT_EQ(r, (int64)6);
+ ST_ASSERT_EQ(buf[0], 'A');
+ ST_ASSERT_EQ(buf[3], 'B');
+
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- writev: bad fd returns -EBADF ---- */
+ st_begin("sys_fs: writev bad fd returns -EBADF");
+ {
+ test_iovec iov[1];
+ char dummy[] = "x";
+ iov[0].iov_base = dummy;
+ iov[0].iov_len = 1;
+
+ int64 ret = sys_writev(999,
+ reinterpret_cast<uint64>(iov), 1, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EBADF);
+ }
+ st_pass();
+
+ /* ---- ioctl stub returns -ENOTTY ---- */
+ st_begin("sys_fs: ioctl stub returns -ENOTTY");
+ {
+ int64 fd = open_dev("/dev/null", O_RDONLY);
+ ST_ASSERT(fd >= 0);
+
+ int64 ret = sys_ioctl(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-ENOTTY);
+
+ do_close(fd);
+ }
+ st_pass();
+
+ /* ---- unlinkat stub returns -ENOSYS ---- */
+ st_begin("sys_fs: unlinkat stub returns -ENOSYS");
+ {
+ int64 ret = sys_unlinkat(
+ static_cast<uint64>(-100),
+ reinterpret_cast<uint64>("/some_file"),
+ 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-ENOSYS);
+ }
+ st_pass();
+
+ /* ---- getcwd: null buf returns -EINVAL ---- */
+ st_begin("sys_fs: getcwd null buf returns -EINVAL");
+ {
+ int64 ret = sys_getcwd(0, 64, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EINVAL);
+ }
+ st_pass();
+
+ /* ---- getcwd: zero size returns -EINVAL ---- */
+ st_begin("sys_fs: getcwd zero size returns -EINVAL");
+ {
+ char buf[4];
+ int64 ret = sys_getcwd(
+ reinterpret_cast<uint64>(buf), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EINVAL);
+ }
+ st_pass();
+}
diff --git a/na/test/test_sys_process.cc b/na/test/test_sys_process.cc
new file mode 100644
index 0000000..686bed6
--- /dev/null
+++ b/na/test/test_sys_process.cc
@@ -0,0 +1,93 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_sys_process.cc — Self-tests for process-control syscalls
+ *
+ * Tests: getpid, gettid, getppid, getuid, getgid, set_tid_address,
+ * sched_yield, uname.
+ */
+
+#include <lume/selftest.h>
+#include <lume/errno.h>
+#include <lume/task.h>
+#include <lume/types.h>
+
+/* Syscall entry points */
+int64 sys_getpid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_gettid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getppid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getuid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getgid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_set_tid_address(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_sched_yield_wrapper(uint64, uint64, uint64, uint64, uint64, uint64);
+
+void selftest_sys_process()
+{
+ /* ---- getpid returns current tgid ---- */
+ st_begin("sys_process: getpid returns valid pid");
+ {
+ int64 pid = sys_getpid(0, 0, 0, 0, 0, 0);
+ /* Boot task pid is -1 but tgid may differ; just verify it returns */
+ TaskControlBlock* curr = current_task();
+ ST_ASSERT_EQ(pid, (int64)curr->tgid);
+ }
+ st_pass();
+
+ /* ---- gettid returns current pid (tid) ---- */
+ st_begin("sys_process: gettid returns current tid");
+ {
+ int64 tid = sys_gettid(0, 0, 0, 0, 0, 0);
+ TaskControlBlock* curr = current_task();
+ ST_ASSERT_EQ(tid, (int64)curr->pid);
+ }
+ st_pass();
+
+ /* ---- getppid returns parent tgid ---- */
+ st_begin("sys_process: getppid returns parent tgid");
+ {
+ int64 ppid = sys_getppid(0, 0, 0, 0, 0, 0);
+ TaskControlBlock* curr = current_task();
+ int64 expected = curr->parent ? (int64)curr->parent->tgid : 0;
+ ST_ASSERT_EQ(ppid, expected);
+ }
+ st_pass();
+
+ /* ---- getuid returns 0 (root) ---- */
+ st_begin("sys_process: getuid returns 0 (root)");
+ {
+ int64 uid = sys_getuid(0, 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(uid, (int64)0);
+ }
+ st_pass();
+
+ /* ---- getgid returns 0 (root) ---- */
+ st_begin("sys_process: getgid returns 0 (root)");
+ {
+ int64 gid = sys_getgid(0, 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(gid, (int64)0);
+ }
+ st_pass();
+
+ /* ---- set_tid_address stores pointer and returns tid ---- */
+ st_begin("sys_process: set_tid_address returns tid");
+ {
+ uint64 dummy_addr = 0x12345678ULL;
+ int64 tid = sys_set_tid_address(dummy_addr, 0, 0, 0, 0, 0);
+ TaskControlBlock* curr = current_task();
+ ST_ASSERT_EQ(tid, (int64)curr->pid);
+ ST_ASSERT_EQ(curr->clear_child_tid, dummy_addr);
+
+ /* Reset it to avoid side effects */
+ curr->clear_child_tid = 0;
+ }
+ st_pass();
+
+ /* ---- sched_yield returns 0 ---- */
+ st_begin("sys_process: sched_yield returns 0");
+ {
+ int64 ret = sys_sched_yield_wrapper(0, 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+ }
+}
+
diff --git a/na/test/test_uaccess.cc b/na/test/test_uaccess.cc
index d5795fc..c5a8c6c 100644
--- a/na/test/test_uaccess.cc
+++ b/na/test/test_uaccess.cc
@@ -7,13 +7,13 @@
#include <lume/vmm.h>
#include <lume/pmm.h>
+#include <lume/tlb.h>
+#include <lume/mm_layout.h>
#include <arch/pmap.h>
-constexpr uint64 kKernelVABase = 0xFFFFFFC000000000ULL;
-
void selftest_uaccess()
{
- /* To test valid user addresses (< kKernelVABase), we need a writable page mapped there.
+ /* To test valid user addresses (< kKernelVaBase), we need a writable page mapped there.
* We cannot use VmSpace::create() because it does not yet inherit kernel mappings,
* so switching to it hangs. We also cannot map it with the U bit because S-mode
* would need the SUM bit enabled.
@@ -33,7 +33,7 @@ void selftest_uaccess()
pmap::map(root_pa, mock_user_addr, paddr, pmap::PTE_R | pmap::PTE_W | pmap::PTE_V | pmap::PTE_A | pmap::PTE_D);
/* Flush TLB to see the new mapping */
- asm volatile("sfence.vma" : : : "memory");
+ tlb_shootdown_all();
st_begin("uaccess: copy_to_user valid range");
char kernel_buf[32] = "Hello User Space!";
@@ -67,12 +67,12 @@ void selftest_uaccess()
st_pass();
st_begin("uaccess: copy_to_user invalid range (crosses boundary)");
- ret = copy_to_user(kKernelVABase - 10, kernel_buf, 20);
+ ret = copy_to_user(kKernelVaBase - 10, kernel_buf, 20);
ST_ASSERT_EQ(ret, -EFAULT);
st_pass();
st_begin("uaccess: copy_to_user invalid range (entirely in kernel)");
- ret = copy_to_user(kKernelVABase + 0x1000, kernel_buf, 20);
+ ret = copy_to_user(kKernelVaBase + 0x1000, kernel_buf, 20);
ST_ASSERT_EQ(ret, -EFAULT);
st_pass();
@@ -82,15 +82,15 @@ void selftest_uaccess()
st_pass();
st_begin("uaccess: copy_from_user invalid ranges");
- ret = copy_from_user(read_buf, kKernelVABase - 5, 10);
+ ret = copy_from_user(read_buf, kKernelVaBase - 5, 10);
ST_ASSERT_EQ(ret, -EFAULT);
- ret = copy_from_user(read_buf, kKernelVABase, 10);
+ ret = copy_from_user(read_buf, kKernelVaBase, 10);
ST_ASSERT_EQ(ret, -EFAULT);
st_pass();
st_begin("uaccess: strncpy_from_user invalid range");
- uint64 invalid_addr = kKernelVABase + 0x1000;
+ uint64 invalid_addr = kKernelVaBase + 0x1000;
len = strncpy_from_user(str_buf, invalid_addr, 10);
ST_ASSERT_EQ(len, -EFAULT);
st_pass();
diff --git a/na/test/test_vfs_io.cc b/na/test/test_vfs_io.cc
new file mode 100644
index 0000000..3d3b125
--- /dev/null
+++ b/na/test/test_vfs_io.cc
@@ -0,0 +1,345 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * test_vfs_io.cc — Self-tests for VFS file I/O via syscalls
+ *
+ * Tests openat, read, write, close, lseek, dup, fstat, getdents64,
+ * getcwd, chdir, and mkdirat through the kernel syscall layer.
+ */
+
+#include <lume/selftest.h>
+#include <lume/errno.h>
+#include <lume/file.h>
+#include <lume/fs.h>
+#include <lume/fs_types.h>
+#include <lume/task.h>
+#include <lume/fdtable.h>
+
+/* Syscall entry points */
+int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_close(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_read(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_write(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_lseek(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_dup(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_dup3(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_fstat_wrapper(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getcwd(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_chdir(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_mkdirat(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getdents64(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* Helper: open a file, returns fd or negative error */
+static int64 open_file(const char* path, int flags, uint32 mode)
+{
+ return sys_openat(
+ static_cast<uint64>(-100), /* AT_FDCWD */
+ reinterpret_cast<uint64>(path),
+ static_cast<uint64>(flags),
+ static_cast<uint64>(mode), 0, 0);
+}
+
+void selftest_vfs_io()
+{
+ /* ---- openat O_CREAT creates a file ---- */
+ st_begin("vfs_io: openat O_CREAT creates file");
+ {
+ int64 fd = open_file("/vfs_test_file", O_CREAT | O_WRONLY, 0644);
+ ST_ASSERT(fd >= 0);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- openat existing file ---- */
+ st_begin("vfs_io: openat existing file succeeds");
+ {
+ int64 fd = open_file("/vfs_test_file", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- openat nonexistent without O_CREAT returns -ENOENT ---- */
+ st_begin("vfs_io: openat nonexistent returns -ENOENT");
+ {
+ int64 fd = open_file("/no_such_file_ever", O_RDONLY, 0);
+ ST_ASSERT_EQ(fd, (int64)-ENOENT);
+ }
+ st_pass();
+
+ /* ---- write then read ---- */
+ st_begin("vfs_io: write then read back data");
+ {
+ /* Write */
+ int64 fd = open_file("/vfs_rw_test", O_CREAT | O_WRONLY, 0644);
+ ST_ASSERT(fd >= 0);
+
+ const char* msg = "Hello VFS!";
+ int64 written = sys_write(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(msg),
+ 10, 0, 0, 0);
+ ST_ASSERT_EQ(written, (int64)10);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+
+ /* Read */
+ fd = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ char buf[32] = {};
+ int64 nread = sys_read(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf),
+ sizeof(buf), 0, 0, 0);
+ ST_ASSERT_EQ(nread, (int64)10);
+
+ bool match = true;
+ for (int i = 0; i < 10; i++) {
+ if (buf[i] != msg[i]) { match = false; break; }
+ }
+ ST_ASSERT(match);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- read past EOF returns 0 ---- */
+ st_begin("vfs_io: read past EOF returns 0");
+ {
+ int64 fd = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ char buf[64];
+ /* First read: 10 bytes */
+ int64 n1 = sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), 64, 0, 0, 0);
+ ST_ASSERT_EQ(n1, (int64)10);
+
+ /* Second read: should be 0 (EOF) */
+ int64 n2 = sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), 64, 0, 0, 0);
+ ST_ASSERT_EQ(n2, (int64)0);
+
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- O_TRUNC clears file content ---- */
+ st_begin("vfs_io: O_TRUNC clears file content");
+ {
+ int64 fd = open_file("/vfs_rw_test", O_WRONLY | O_TRUNC, 0);
+ ST_ASSERT(fd >= 0);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+
+ fd = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ char buf[16];
+ int64 n = sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), 16, 0, 0, 0);
+ ST_ASSERT_EQ(n, (int64)0); /* truncated = empty */
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- close invalid fd returns -EBADF ---- */
+ st_begin("vfs_io: close bad fd returns -EBADF");
+ {
+ int64 ret = sys_close(999, 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EBADF);
+ }
+ st_pass();
+
+ /* ---- read/write on bad fd returns -EBADF ---- */
+ st_begin("vfs_io: read/write bad fd returns -EBADF");
+ {
+ char buf[8];
+ int64 r = sys_read(999, reinterpret_cast<uint64>(buf), 8, 0, 0, 0);
+ ST_ASSERT_EQ(r, (int64)-EBADF);
+
+ int64 w = sys_write(999, reinterpret_cast<uint64>(buf), 8, 0, 0, 0);
+ ST_ASSERT_EQ(w, (int64)-EBADF);
+ }
+ st_pass();
+
+ /* ---- lseek SEEK_SET / SEEK_CUR / SEEK_END ---- */
+ st_begin("vfs_io: lseek SEEK_SET/CUR/END");
+ {
+ /* Write 20 bytes */
+ int64 fd = open_file("/vfs_lseek_test", O_CREAT | O_RDWR, 0644);
+ ST_ASSERT(fd >= 0);
+
+ char data[20];
+ for (int i = 0; i < 20; i++) data[i] = static_cast<char>('A' + i);
+ sys_write(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(data), 20, 0, 0, 0);
+
+ /* SEEK_SET to 5 */
+ int64 pos = sys_lseek(static_cast<uint64>(fd), 5, SEEK_SET, 0, 0, 0);
+ ST_ASSERT_EQ(pos, (int64)5);
+
+ /* Read 1 byte, should be 'F' */
+ char c = 0;
+ sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(&c), 1, 0, 0, 0);
+ ST_ASSERT_EQ(c, 'F');
+
+ /* SEEK_CUR +3 → pos 9 */
+ pos = sys_lseek(static_cast<uint64>(fd), 3, SEEK_CUR, 0, 0, 0);
+ ST_ASSERT_EQ(pos, (int64)9);
+
+ /* SEEK_END -1 → pos 19 */
+ pos = sys_lseek(static_cast<uint64>(fd), -1, SEEK_END, 0, 0, 0);
+ ST_ASSERT_EQ(pos, (int64)19);
+
+ sys_read(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(&c), 1, 0, 0, 0);
+ ST_ASSERT_EQ(c, 'T');
+
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- lseek invalid whence returns -EINVAL ---- */
+ st_begin("vfs_io: lseek invalid whence");
+ {
+ int64 fd = open_file("/vfs_lseek_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+ int64 ret = sys_lseek(static_cast<uint64>(fd), 0, 99, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)-EINVAL);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- dup: duplicate fd shares file position ---- */
+ st_begin("vfs_io: dup shares file position");
+ {
+ int64 fd1 = open_file("/vfs_lseek_test", O_RDONLY, 0);
+ ST_ASSERT(fd1 >= 0);
+
+ int64 fd2 = sys_dup(static_cast<uint64>(fd1), 0, 0, 0, 0, 0);
+ ST_ASSERT(fd2 >= 0);
+ ST_ASSERT_NE(fd1, fd2);
+
+ /* Both should work for reading */
+ char c = 0;
+ int64 n = sys_read(static_cast<uint64>(fd2),
+ reinterpret_cast<uint64>(&c), 1, 0, 0, 0);
+ ST_ASSERT_EQ(n, (int64)1);
+
+ sys_close(static_cast<uint64>(fd1), 0, 0, 0, 0, 0);
+ sys_close(static_cast<uint64>(fd2), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- fstat: basic stat ---- */
+ st_begin("vfs_io: fstat returns valid kstat");
+ {
+ int64 fd = open_file("/vfs_lseek_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ kstat st{};
+ int64 ret = sys_fstat_wrapper(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(&st), 0, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+ ST_ASSERT(S_ISREG(st.st_mode));
+ ST_ASSERT_EQ(st.st_size, (int64)20);
+
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- getcwd: returns "/" ---- */
+ st_begin("vfs_io: getcwd returns /");
+ {
+ char buf[64] = {};
+ int64 len = sys_getcwd(
+ reinterpret_cast<uint64>(buf), 64, 0, 0, 0, 0);
+ ST_ASSERT(len > 0);
+ ST_ASSERT_EQ(buf[0], '/');
+ }
+ st_pass();
+
+ /* ---- mkdirat: create directory ---- */
+ st_begin("vfs_io: mkdirat creates directory");
+ {
+ int64 ret = sys_mkdirat(
+ static_cast<uint64>(-100),
+ reinterpret_cast<uint64>("/vfs_io_dir"),
+ 0755, 0, 0, 0);
+ ST_ASSERT_EQ(ret, (int64)0);
+
+ /* Open it to confirm it's a directory */
+ int64 fd = open_file("/vfs_io_dir", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ kstat st{};
+ sys_fstat_wrapper(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(&st), 0, 0, 0, 0);
+ ST_ASSERT(S_ISDIR(st.st_mode));
+
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- getdents64: list root directory ---- */
+ st_begin("vfs_io: getdents64 lists root children");
+ {
+ int64 fd = open_file("/", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ char buf[512] = {};
+ int64 n = sys_getdents64(
+ static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf),
+ 512, 0, 0, 0);
+ ST_ASSERT(n > 0); /* At least "." and ".." */
+
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- write to O_RDONLY fd returns -EBADF ---- */
+ st_begin("vfs_io: write to O_RDONLY returns -EBADF");
+ {
+ int64 fd = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ char buf[] = "x";
+ int64 w = sys_write(static_cast<uint64>(fd),
+ reinterpret_cast<uint64>(buf), 1, 0, 0, 0);
+ ST_ASSERT_EQ(w, (int64)-EBADF);
+ sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+
+ /* ---- double close returns -EBADF ---- */
+ st_begin("vfs_io: double close returns -EBADF");
+ {
+ int64 fd = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd >= 0);
+
+ int64 r1 = sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(r1, (int64)0);
+
+ int64 r2 = sys_close(static_cast<uint64>(fd), 0, 0, 0, 0, 0);
+ ST_ASSERT_EQ(r2, (int64)-EBADF);
+ }
+ st_pass();
+
+ /* ---- fd reuse after close ---- */
+ st_begin("vfs_io: fd reuse after close");
+ {
+ int64 fd1 = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd1 >= 0);
+ sys_close(static_cast<uint64>(fd1), 0, 0, 0, 0, 0);
+
+ int64 fd2 = open_file("/vfs_rw_test", O_RDONLY, 0);
+ ST_ASSERT(fd2 >= 0);
+ /* fd2 should reuse the slot of fd1 */
+ ST_ASSERT(fd2 <= fd1);
+ sys_close(static_cast<uint64>(fd2), 0, 0, 0, 0, 0);
+ }
+ st_pass();
+}
diff --git a/na/test/test_vmm.cc b/na/test/test_vmm.cc
index 049570f..635e3f3 100644
--- a/na/test/test_vmm.cc
+++ b/na/test/test_vmm.cc
@@ -6,12 +6,17 @@
*/
#include <lume/selftest.h>
+#include <lume/klog.h>
+#include <lume/config.h>
#include <lume/addr.h>
#include <lume/vmm.h>
#include <lume/pmm.h>
#include <arch/config.h>
-/* Assuming pmap namespace is accessible or we can test basic structures */
+namespace {
+constexpr uint64 kUartBase = 0x10000000ULL;
+constexpr uint64 kPageOffsetMask = kPageSize - 1;
+}
void selftest_vmm() {
st_begin("vmm: virtual/physical address translation");
@@ -30,7 +35,7 @@ void selftest_vmm() {
st_begin("vmm: page alignment macros");
{
uint64 base = pmm_get_mem_base().raw;
- uint64 addr = base + 0xFFF;
+ uint64 addr = base + kPageOffsetMask;
ST_ASSERT_EQ(page_align_down(addr), base);
ST_ASSERT_EQ(page_align_up(addr), base + kPageSize);
ST_ASSERT_EQ(page_align_down(base), base);
@@ -40,8 +45,10 @@ void selftest_vmm() {
st_begin("vmm: mapping kernel MMIO (driver API)");
{
- uint64 mmio_pa = pmm_get_mem_base().raw;
- vmm_map_kernel_mmio(mmio_pa, 0x1000);
+ /* Use UART MMIO address (0x10000000 on RISC-V virt).
+ * Must NOT use pmm_get_mem_base() — that's PMP-protected firmware. */
+ uint64 mmio_pa = kUartBase;
+ vmm_map_kernel_mmio(mmio_pa, kPageSize);
// Verify it doesn't crash and address is reachable in higher half
uint64 va = pa_to_va(phys_addr(mmio_pa)).raw;
diff --git a/na/test/test_waitqueue.cc b/na/test/test_waitqueue.cc
index 1bf88bb..7f50d6b 100644
--- a/na/test/test_waitqueue.cc
+++ b/na/test/test_waitqueue.cc
@@ -59,7 +59,7 @@ void selftest_waitqueue()
kprintf("[test_wq] returned from yield 2\n");
ST_ASSERT(g_thread_woken == true);
-
+
st_pass();
st_begin("waitqueue: wake_all");