summaryrefslogtreecommitdiff
path: root/na/kernel
diff options
context:
space:
mode:
Diffstat (limited to 'na/kernel')
-rw-r--r--na/kernel/boot_params.cc45
-rw-r--r--na/kernel/exec.cc659
-rw-r--r--na/kernel/main.cc395
-rw-r--r--na/kernel/module.mk11
-rw-r--r--na/kernel/pgrp.cc148
-rw-r--r--na/kernel/pid.cc6
-rw-r--r--na/kernel/poll.cc250
-rw-r--r--na/kernel/ptrace.cc92
-rw-r--r--na/kernel/sched_class_o1.cc7
-rw-r--r--na/kernel/sched_core.cc306
-rw-r--r--na/kernel/signal.cc442
-rw-r--r--na/kernel/smp.cc112
-rw-r--r--na/kernel/sync/mutex.cc156
-rw-r--r--na/kernel/sync/spinlock.cc11
-rw-r--r--na/kernel/sys_fs.cc679
-rw-r--r--na/kernel/sys_misc.cc360
-rw-r--r--na/kernel/sys_mm.cc8
-rw-r--r--na/kernel/sys_process.cc86
-rw-r--r--na/kernel/sys_signal.cc351
-rw-r--r--na/kernel/syscall.cc115
-rw-r--r--na/kernel/task.cc481
-rw-r--r--na/kernel/timer.cc76
-rw-r--r--na/kernel/trap.cc118
-rw-r--r--na/kernel/uaccess.cc22
-rw-r--r--na/kernel/waitqueue.cc87
25 files changed, 4692 insertions, 331 deletions
diff --git a/na/kernel/boot_params.cc b/na/kernel/boot_params.cc
deleted file mode 100644
index e5d4ee5..0000000
--- a/na/kernel/boot_params.cc
+++ /dev/null
@@ -1,45 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* Copyright (C) 2026 Richard Qin */
-
-#include <lume/boot_params.h>
-
-static BootParams g_boot_params = {};
-
-static void boot_params_init_once()
-{
- if (g_boot_params.magic == kBootParamsMagic) {
- return;
- }
- g_boot_params = {};
- g_boot_params.magic = kBootParamsMagic;
- g_boot_params.version = kBootParamsVersion;
- g_boot_params.size = static_cast<uint32>(sizeof(BootParams));
-}
-
-void boot_params_init_fdt(uint64 fdt_paddr)
-{
- boot_params_init_once();
- g_boot_params.fdt_ptr = fdt_paddr;
- g_boot_params.seed_source = BOOT_SEED_NONE;
-}
-
-const BootParams* boot_params_get()
-{
- if (g_boot_params.magic != kBootParamsMagic) {
- return nullptr;
- }
- return &g_boot_params;
-}
-
-void boot_params_set_image_info(uint64 phys_base, uint64 image_size,
- uint64 reloc_offset, uint64 reloc_size,
- uint32 comp, uint32 image_flags)
-{
- boot_params_init_once();
- g_boot_params.kimage_phys_base = phys_base;
- g_boot_params.kimage_size = image_size;
- g_boot_params.kimage_reloc_offset = reloc_offset;
- g_boot_params.kimage_reloc_size = reloc_size;
- g_boot_params.kimage_comp = comp;
- g_boot_params.kimage_flags = image_flags;
-}
diff --git a/na/kernel/exec.cc b/na/kernel/exec.cc
new file mode 100644
index 0000000..616c0da
--- /dev/null
+++ b/na/kernel/exec.cc
@@ -0,0 +1,659 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * exec.cc — ELF64 binary loader and execve implementation
+ *
+ * Loads a static ELF64 executable into a fresh address space,
+ * constructs the user stack (argc/argv/envp/auxv), and redirects
+ * the current task's trapframe to begin executing at the ELF entry.
+ *
+ * Reference: Linux fs/binfmt_elf.c, fs/exec.c
+ */
+
+#include <lume/elf.h>
+#include <lume/types.h>
+#include <lume/task.h>
+#include <lume/vmm.h>
+#include <lume/addr.h>
+#include <lume/config.h>
+#include <lume/errno.h>
+#include <lume/pmm.h>
+#include <lume/kprintf.h>
+#include <lume/klog.h>
+#include <lume/file.h>
+#include <lume/fs.h>
+#include <lume/dcache.h>
+#include <lume/namei.h>
+#include <lume/fdtable.h>
+#include <lume/mm_layout.h>
+#include <lume/signal_types.h>
+#include <arch/pmap.h>
+#include <arch/trapframe.h>
+#include <arch/mmu.h>
+#include <arch/cpu.h>
+#include <arch/trap.h>
+
+/* Assembly entry points (trap_vector.S) */
+extern "C" void trap_vector();
+extern "C" [[noreturn]] void trap_dispatch(arch::TrapFrame*);
+extern "C" void userret(arch::TrapFrame* tf);
+
+/* ================================================================== */
+/* Constants */
+/* ================================================================== */
+
+/* User stack layout comes from mm_layout.h (MI layer).
+ * kUserStackTop, kUserStackBase, kUserStackSize are defined there,
+ * derived from kUserVaLimit so they automatically adapt to the
+ * page table mode (SV39/SV48). */
+
+/* ================================================================== */
+/* Helper: kernel_read — read from a file into a kernel buffer */
+/* ================================================================== */
+
+static int64 kernel_read(file* fp, void* buf, uint64 count, int64 offset)
+{
+ int64 pos = offset;
+ if (!fp->f_op || !fp->f_op->read) return -EINVAL;
+ return fp->f_op->read(fp, static_cast<char*>(buf), count, &pos);
+}
+
+/* ================================================================== */
+/* Helper: copy data to a user page via kernel direct map */
+/* ================================================================== */
+
+static void memcpy_kernel(void* dst, const void* src, uint64 n)
+{
+ auto* d = static_cast<uint8*>(dst);
+ auto* s = static_cast<const uint8*>(src);
+ for (uint64 i = 0; i < n; i++)
+ d[i] = s[i];
+}
+
+static void memset_kernel(void* dst, uint8 val, uint64 n)
+{
+ auto* d = static_cast<uint8*>(dst);
+ for (uint64 i = 0; i < n; i++)
+ d[i] = val;
+}
+
+static uint64 strlen_kernel(const char* s)
+{
+ uint64 n = 0;
+ while (s[n]) n++;
+ return n;
+}
+
+/* ================================================================== */
+/* Helper: allocate and map a page at a specific user VA */
+/* ================================================================== */
+
+static int map_page_at(VmSpace* mm, uint64 va, uint64 pte_perm)
+{
+ Page* pg = pmm_alloc_page();
+ if (!pg) return -ENOMEM;
+
+ uint64 pa = page_to_pa(pg).raw;
+
+ /* Zero the page */
+ void* kva = reinterpret_cast<void*>(pa_to_va(PhysAddr{pa}).raw);
+ memset_kernel(kva, 0, kPageSize);
+
+ int ret = pmap::map(mm->root_pa, va, pa, pte_perm);
+ if (ret != 0) {
+ pmm_free_page(pg);
+ return -ENOMEM;
+ }
+ page_inc_mapcount(pg);
+ return 0;
+}
+
+/* ================================================================== */
+/* load_elf_binary — parse ELF and map segments */
+/* ================================================================== */
+
+struct elf_load_info {
+ uint64 entry; /* ELF entry point (after base offset) */
+ uint64 phdr_addr; /* User VA where phdr was loaded */
+ uint16 phnum; /* Number of program headers */
+ uint16 phentsize; /* Size of each phdr */
+ uint64 brk_start; /* End of highest PT_LOAD segment (for brk) */
+ uint64 load_bias; /* PIE load bias (0 for ET_EXEC) */
+};
+
+static int load_elf_binary(file* fp, VmSpace* mm, elf_load_info* info)
+{
+ /* Read ELF header */
+ Elf64_Ehdr ehdr{};
+ int64 n = kernel_read(fp, &ehdr, sizeof(ehdr), 0);
+ if (n < static_cast<int64>(sizeof(ehdr))) {
+ printk(KERN_ERR "exec: failed to read ELF header (got %lld)\n", n);
+ return -ENOEXEC;
+ }
+
+ /* Validate magic */
+ if (ehdr.e_ident[EI_MAG0] != ELFMAG0 ||
+ ehdr.e_ident[EI_MAG1] != ELFMAG1 ||
+ ehdr.e_ident[EI_MAG2] != ELFMAG2 ||
+ ehdr.e_ident[EI_MAG3] != ELFMAG3) {
+ printk(KERN_ERR "exec: bad ELF magic\n");
+ return -ENOEXEC;
+ }
+
+ /* Validate class (64-bit) and endianness (little-endian) */
+ if (ehdr.e_ident[EI_CLASS] != ELFCLASS64) return -ENOEXEC;
+ if (ehdr.e_ident[EI_DATA] != ELFDATA2LSB) return -ENOEXEC;
+
+ /* Validate type and machine */
+ if (ehdr.e_type != ET_EXEC && ehdr.e_type != ET_DYN) return -ENOEXEC;
+ if (ehdr.e_machine != EM_RISCV) return -ENOEXEC;
+
+ if (ehdr.e_phnum == 0 || ehdr.e_phentsize < sizeof(Elf64_Phdr))
+ return -ENOEXEC;
+
+ /* Read program headers */
+ uint64 phdr_total = static_cast<uint64>(ehdr.e_phnum) * ehdr.e_phentsize;
+ if (phdr_total > kPageSize) return -ENOEXEC; /* Sanity limit */
+
+ /* Allocate kernel buffer for program headers */
+ uint8 phdr_buf[512]; /* Max ~8 phdrs * 56 bytes each */
+ if (phdr_total > sizeof(phdr_buf)) return -ENOEXEC;
+
+ n = kernel_read(fp, phdr_buf, phdr_total, static_cast<int64>(ehdr.e_phoff));
+ if (n < static_cast<int64>(phdr_total)) return -ENOEXEC;
+
+ info->phnum = ehdr.e_phnum;
+ info->phentsize = ehdr.e_phentsize;
+ info->phdr_addr = 0;
+ info->brk_start = 0;
+ info->load_bias = 0;
+
+ /*
+ * PIE (ET_DYN): segments have p_vaddr starting near 0.
+ * We must choose a load base and add it to all addresses.
+ * Linux uses ELF_ET_DYN_BASE (~2/3 of TASK_SIZE); we use 0x400000.
+ */
+ constexpr uint64 kPieLoadBase = 0x400000;
+ uint64 load_bias = (ehdr.e_type == ET_DYN) ? kPieLoadBase : 0;
+ info->load_bias = load_bias;
+ info->entry = ehdr.e_entry + load_bias;
+
+ /* Process each PT_LOAD segment */
+ for (uint16 i = 0; i < ehdr.e_phnum; i++) {
+ auto* phdr = reinterpret_cast<Elf64_Phdr*>(
+ phdr_buf + i * ehdr.e_phentsize);
+
+ if (phdr->p_type == PT_PHDR) {
+ /* Remember where program headers are mapped */
+ info->phdr_addr = phdr->p_vaddr + load_bias;
+ continue;
+ }
+
+ if (phdr->p_type != PT_LOAD) continue;
+ if (phdr->p_memsz == 0) continue;
+
+ /* Compute permissions */
+ uint64 perm = pmap::PTE_V | pmap::PTE_U;
+ if (phdr->p_flags & PF_R) perm |= pmap::PTE_R;
+ if (phdr->p_flags & PF_W) perm |= pmap::PTE_W;
+ if (phdr->p_flags & PF_X) perm |= pmap::PTE_X;
+
+ /* Page-align the segment boundaries (apply load bias for PIE) */
+ uint64 seg_start = page_align_down(phdr->p_vaddr + load_bias);
+ uint64 seg_end = page_align_up(phdr->p_vaddr + phdr->p_memsz + load_bias);
+
+ /* Create VMA for this segment */
+ uint64 vm_perm = VM_USER;
+ if (phdr->p_flags & PF_R) vm_perm |= VM_READ;
+ if (phdr->p_flags & PF_W) vm_perm |= VM_WRITE;
+ if (phdr->p_flags & PF_X) vm_perm |= VM_EXEC;
+
+ int err = vmm_map_user(mm, seg_start, seg_end - seg_start, vm_perm);
+ if (err) {
+ printk(KERN_ERR "exec: vmm_map_user failed for segment %d: %d\n",
+ i, err);
+ return err;
+ }
+
+ /* Map and fill pages */
+ for (uint64 va = seg_start; va < seg_end; va += kPageSize) {
+ err = map_page_at(mm, va, perm);
+ if (err) return err;
+
+ /* Determine how much file data goes into this page */
+ uint64 file_seg_start = phdr->p_vaddr + load_bias;
+ uint64 file_seg_end = phdr->p_vaddr + phdr->p_filesz + load_bias;
+
+ uint64 page_start = va;
+ uint64 page_end = va + kPageSize;
+
+ /* Overlap between [page_start, page_end) and [file_seg_start, file_seg_end) */
+ if (page_start < file_seg_end && page_end > file_seg_start) {
+ uint64 copy_start = (page_start > file_seg_start) ? page_start : file_seg_start;
+ uint64 copy_end = (page_end < file_seg_end) ? page_end : file_seg_end;
+ uint64 copy_len = copy_end - copy_start;
+
+ /* File offset for this data */
+ uint64 file_off = phdr->p_offset + (copy_start - phdr->p_vaddr - load_bias);
+
+ /* Get kernel VA of the mapped page */
+ uint64 pa;
+ if (!pmap::lookup(mm->root_pa, va, &pa)) return -EFAULT;
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+
+ /* Read file data into the page */
+ uint64 page_offset = copy_start - va;
+ n = kernel_read(fp, kva + page_offset, copy_len,
+ static_cast<int64>(file_off));
+ if (n < static_cast<int64>(copy_len)) {
+ printk(KERN_ERR "exec: short read at offset %llu\n", file_off);
+ return -EIO;
+ }
+ }
+ /* BSS region (memsz > filesz) is already zeroed by map_page_at */
+ }
+
+ /* Track highest loaded address for brk */
+ if (seg_end > info->brk_start)
+ info->brk_start = seg_end;
+
+ /* Detect which PT_LOAD contains the program header table.
+ * Linux: fs/binfmt_elf.c line ~1209 */
+ if (info->phdr_addr == 0 &&
+ phdr->p_offset <= ehdr.e_phoff &&
+ ehdr.e_phoff < phdr->p_offset + phdr->p_filesz) {
+ info->phdr_addr = ehdr.e_phoff - phdr->p_offset
+ + phdr->p_vaddr + load_bias;
+ }
+ }
+
+ /* phdr_addr should have been set by the PT_LOAD loop above
+ * or by a PT_PHDR entry. If still zero, something is wrong. */
+ if (info->phdr_addr == 0) {
+ printk(KERN_ERR "exec: unable to determine AT_PHDR address\n");
+ return -ENOEXEC;
+ }
+
+ return 0;
+}
+
+/* ================================================================== */
+/* setup_user_stack — build argc/argv/envp/auxv on the user stack */
+/* ================================================================== */
+
+static int setup_user_stack(VmSpace* mm, const char* const* argv,
+ const char* const* envp,
+ const elf_load_info* info,
+ uint64* out_sp)
+{
+ /* Create stack VMA */
+ uint64 vm_perm = VM_USER | VM_READ | VM_WRITE;
+ int err = vmm_map_user(mm, kUserStackBase, kUserStackSize, vm_perm);
+ if (err) return err;
+
+ /* We only need the top few pages for initial stack content.
+ * Map 2 pages at the top for argc/argv/envp/auxv. */
+ uint64 perm = pmap::PTE_V | pmap::PTE_U | pmap::PTE_R | pmap::PTE_W;
+ for (uint64 va = kUserStackTop - 2 * kPageSize; va < kUserStackTop; va += kPageSize) {
+ err = map_page_at(mm, va, perm);
+ if (err) return err;
+ }
+
+ /* Count arguments and environment */
+ int argc = 0;
+ int envc = 0;
+ if (argv) { while (argv[argc]) argc++; }
+ if (envp) { while (envp[envc]) envc++; }
+
+ /*
+ * Build the stack from top down using kernel VA access.
+ * Layout (top to bottom):
+ * 1. String data (argv strings, envp strings)
+ * 2. Padding (16-byte align)
+ * 3. auxv[]
+ * 4. NULL (end of envp)
+ * 5. envp[] pointers
+ * 6. NULL (end of argv)
+ * 7. argv[] pointers
+ * 8. argc
+ */
+
+ /* First pass: calculate total string space */
+ uint64 str_total = 0;
+ for (int i = 0; i < argc; i++)
+ str_total += strlen_kernel(argv[i]) + 1;
+ for (int i = 0; i < envc; i++)
+ str_total += strlen_kernel(envp[i]) + 1;
+
+ /* String area starts at top of stack */
+ uint64 str_pos = kUserStackTop - str_total;
+ str_pos &= ~15ULL; /* 16-byte align */
+
+ /* Copy strings and record user addresses */
+ uint64 argv_addrs[32]; /* Max 32 args */
+ uint64 envp_addrs[32];
+ if (argc > 32) argc = 32;
+ if (envc > 32) envc = 32;
+
+ uint64 cur = str_pos;
+ for (int i = 0; i < argc; i++) {
+ uint64 len = strlen_kernel(argv[i]) + 1;
+ argv_addrs[i] = cur;
+
+ /* Copy string bytes to user stack via kernel VA */
+ uint64 pa;
+ if (!pmap::lookup(mm->root_pa, page_align_down(cur), &pa))
+ return -EFAULT;
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ uint64 off = cur - page_align_down(cur);
+ memcpy_kernel(kva + off, argv[i], len);
+ cur += len;
+ }
+ for (int i = 0; i < envc; i++) {
+ uint64 len = strlen_kernel(envp[i]) + 1;
+ envp_addrs[i] = cur;
+
+ uint64 pa;
+ if (!pmap::lookup(mm->root_pa, page_align_down(cur), &pa))
+ return -EFAULT;
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ uint64 off = cur - page_align_down(cur);
+ memcpy_kernel(kva + off, envp[i], len);
+ cur += len;
+ }
+
+ /* Build auxv, envp[], argv[], argc below the strings */
+ /* Count entries: argc + argv_ptrs + NULL + envp_ptrs + NULL + auxv (6 pairs + terminator) */
+ int auxv_count = 8; /* AT_PHDR, AT_PHENT, AT_PHNUM, AT_PAGESZ, AT_BASE, AT_ENTRY, AT_RANDOM, AT_NULL */
+ uint64 table_size = sizeof(uint64) * (1 + argc + 1 + envc + 1 + auxv_count * 2);
+ uint64 table_start = str_pos - table_size;
+ table_start &= ~15ULL;
+
+ /* Write the table entries.
+ * We need to handle page boundaries, but for simplicity we assume
+ * the table fits in the already-mapped pages. */
+ uint64 sp = table_start;
+
+ /* Helper: write a uint64 to user stack */
+ auto write_u64 = [&](uint64 addr, uint64 val) -> int {
+ uint64 pa;
+ if (!pmap::lookup(mm->root_pa, page_align_down(addr), &pa))
+ return -EFAULT;
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ uint64 off = addr - page_align_down(addr);
+ *reinterpret_cast<uint64*>(kva + off) = val;
+ return 0;
+ };
+
+ /* argc */
+ err = write_u64(sp, static_cast<uint64>(argc));
+ if (err) return err;
+ sp += 8;
+
+ /* argv[] */
+ for (int i = 0; i < argc; i++) {
+ err = write_u64(sp, argv_addrs[i]);
+ if (err) return err;
+ sp += 8;
+ }
+ err = write_u64(sp, 0); /* NULL terminator */
+ if (err) return err;
+ sp += 8;
+
+ /* envp[] */
+ for (int i = 0; i < envc; i++) {
+ err = write_u64(sp, envp_addrs[i]);
+ if (err) return err;
+ sp += 8;
+ }
+ err = write_u64(sp, 0); /* NULL terminator */
+ if (err) return err;
+ sp += 8;
+
+ /* auxv[] */
+ auto write_auxv = [&](uint64 type, uint64 val) -> int {
+ int r = write_u64(sp, type);
+ if (r) return r;
+ sp += 8;
+ r = write_u64(sp, val);
+ if (r) return r;
+ sp += 8;
+ return 0;
+ };
+
+ write_auxv(AT_PHDR, info->phdr_addr);
+ write_auxv(AT_PHENT, info->phentsize);
+ write_auxv(AT_PHNUM, info->phnum);
+ write_auxv(AT_PAGESZ, kPageSize);
+ write_auxv(AT_BASE, info->load_bias);
+ write_auxv(AT_ENTRY, info->entry);
+ write_auxv(AT_RANDOM, str_pos - 16); /* Point to 16 bytes of zeros for now */
+ write_auxv(AT_NULL, 0);
+
+ *out_sp = table_start;
+ return 0;
+}
+
+/* ================================================================== */
+/* do_execve — main entry point */
+/* ================================================================== */
+
+int do_execve(const char* pathname, const char* const* argv,
+ const char* const* envp)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr) return -EINVAL;
+
+ /* 1. Look up the file */
+ path p{};
+ int err = filename_lookup(AT_FDCWD, pathname, 0, &p);
+ if (err) {
+ printk(KERN_ERR "exec: file not found: %s (err=%d)\n", pathname, err);
+ return err;
+ }
+
+ inode* ip = p.de->d_inode;
+ if (!ip || !S_ISREG(ip->i_mode)) {
+ printk(KERN_ERR "exec: not a regular file: %s\n", pathname);
+ return -EACCES;
+ }
+
+ /* Open a kernel file handle */
+ file* fp = alloc_empty_file(O_RDONLY);
+ if (!fp) {
+ return -ENOMEM;
+ }
+ fp->f_inode = ip;
+ fp->f_path = p;
+ fp->f_op = ip->i_fop;
+ fp->f_pos = 0;
+
+ if (fp->f_op && fp->f_op->open) {
+ err = fp->f_op->open(ip, fp);
+ if (err) {
+ fput(fp);
+ return err;
+ }
+ }
+
+ /* 2. Create new address space */
+ VmSpace* new_mm = VmSpace::create();
+ if (!new_mm) {
+ fput(fp);
+ return -ENOMEM;
+ }
+
+ /* 3. Load ELF segments */
+ elf_load_info info{};
+ err = load_elf_binary(fp, new_mm, &info);
+ if (err) {
+ printk(KERN_ERR "exec: load_elf_binary failed: %d\n", err);
+ new_mm->put();
+ fput(fp);
+ dput(p.de);
+ return err;
+ }
+
+ /* 4. Set up user stack */
+ uint64 user_sp = 0;
+ err = setup_user_stack(new_mm, argv, envp, &info, &user_sp);
+ if (err) {
+ printk(KERN_ERR "exec: setup_user_stack failed: %d\n", err);
+ new_mm->put();
+ fput(fp);
+ return err;
+ }
+
+ /* Set brk */
+ new_mm->brk_start = page_align_up(info.brk_start);
+ new_mm->brk_end = new_mm->brk_start;
+
+ /* Set mmap base — Linux: arch_pick_mmap_layout() sets mm->mmap_base
+ * to TASK_UNMAPPED_BASE = PAGE_ALIGN(TASK_SIZE / 3).
+ * This ensures mmap(NULL) returns addresses well above the ELF
+ * text/data segments. */
+ new_mm->mmap_base = page_align_up(kUserVaLimit / 3);
+
+ fput(fp);
+
+ /* =============================================================== */
+ /* Point of no return — replace the current process */
+ /* =============================================================== */
+
+ /* 5a. Replace address space */
+ VmSpace* old_mm = curr->mm;
+ curr->mm = new_mm;
+ pmap::activate(new_mm->root_pa);
+ if (old_mm) old_mm->put();
+
+ /* 5a'. Transition from kernel thread to user process.
+ * Linux: begin_new_exec() in fs/exec.c:
+ * current->flags &= ~PF_KTHREAD; (line 1173)
+ * tsk->exit_signal = SIGCHLD; (line 1021)
+ * Without this, a kernel_execve'd task stays marked as a kthread
+ * and would be auto-reaped (exit_signal=-1) instead of becoming
+ * a proper zombie for parent wait(). */
+ curr->flags &= ~TF_KTHREAD;
+ curr->exit_signal = SIGCHLD;
+
+ /* 5b. Reset signal handlers to SIG_DFL (POSIX requirement).
+ *
+ * Linux fs/exec.c:flush_signal_handlers() runs under sighand->siglock.
+ * Even though after fork our sighand is no longer shared with the
+ * parent (we copy in task_clone), Linux still locks here — and so
+ * must we, because the same sighand may be shared with CLONE_SIGHAND
+ * sibling threads of the execing task. Holding siglock here also
+ * keeps the per-task blocked mask reset atomic w.r.t. concurrent
+ * sigprocmask / signal delivery on this very task. */
+ if (curr->sighand) {
+ LockGuard guard(curr->sighand->siglock);
+ for (int i = 0; i < _NSIG; i++) {
+ if (curr->sighand->action[i].sa_handler != SIG_IGN)
+ curr->sighand->action[i].sa_handler = SIG_DFL;
+ curr->sighand->action[i].sa_flags = 0;
+ curr->sighand->action[i].sa_mask.clear();
+ }
+ curr->blocked.clear();
+ } else {
+ curr->blocked.clear();
+ }
+
+ /* 5c. Close O_CLOEXEC file descriptors */
+ /* TODO: implement close-on-exec */
+
+ /* 6. Set up trapframe to enter user space */
+ arch::TrapFrame* tf = curr->trapframe;
+ if (!tf) {
+ printk(KERN_ERR "exec: no trapframe!\n");
+ task_kill_current(SIGKILL);
+ return -EINVAL; /* not reached */
+ }
+
+ /* Initialize trapframe for user-mode return.
+ * This is the MI equivalent of Linux's start_thread() + kernel
+ * metadata setup. All arch-specific details (sstatus, FP state,
+ * kernel_satp, kernel_sp, trap handler address, etc.) are handled
+ * by the MD layer. MI exec never touches arch registers. */
+ arch::trapframe_init_user(tf, info.entry, user_sp);
+
+ pr_debug_s(KLOG_SUBSYS_EXEC,
+ "exec: pid %d entry=%llx sp=%llx phdr=%llx\n",
+ curr->pid, info.entry, user_sp, info.phdr_addr);
+
+ /* Verify loaded data at entry point — debug-level, ring buffer only. */
+ {
+ uint64 pa;
+ if (pmap::lookup(curr->mm->root_pa, page_align_down(info.entry), &pa)) {
+ uint8* kva = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ uint64 off = info.entry - page_align_down(info.entry);
+ pr_debug_s(KLOG_SUBSYS_EXEC,
+ "exec: entry bytes @%llx: %02x %02x %02x %02x %02x %02x %02x %02x\n",
+ info.entry,
+ kva[off+0], kva[off+1], kva[off+2], kva[off+3],
+ kva[off+4], kva[off+5], kva[off+6], kva[off+7]);
+ }
+ for (uint64 va = 0x10000; va < 0x14000; va += kPageSize) {
+ if (pmap::lookup(curr->mm->root_pa, va, &pa)) {
+ uint8* kva2 = reinterpret_cast<uint8*>(pa_to_va(PhysAddr{pa}).raw);
+ pr_debug_s(KLOG_SUBSYS_EXEC,
+ "exec: page %llx: %02x%02x%02x%02x %02x%02x%02x%02x\n",
+ va, kva2[0], kva2[1], kva2[2], kva2[3],
+ kva2[4], kva2[5], kva2[6], kva2[7]);
+ } else {
+ pr_debug_s(KLOG_SUBSYS_EXEC,
+ "exec: page %llx: NOT MAPPED\n", va);
+ }
+ }
+ }
+
+ /* Flush instruction cache after loading executable code into memory.
+ * Without this, the I-cache may contain stale data for the pages
+ * we just loaded, causing the CPU to execute garbage. */
+ arch::mmu::flush_icache();
+
+ return 0; /* Will return to user mode via trap return */
+}
+
+/* ================================================================== */
+/* sys_execve_wrapper — syscall entry point */
+/* ================================================================== */
+
+int64 sys_execve_wrapper(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ const char* pathname = reinterpret_cast<const char*>(a0);
+ const char* const* argv = reinterpret_cast<const char* const*>(a1);
+ const char* const* envp = reinterpret_cast<const char* const*>(a2);
+
+ return do_execve(pathname, argv, envp);
+}
+
+/* ================================================================== */
+/* kernel_execve — call execve from kernel context (e.g. init) */
+/* ================================================================== */
+
+int kernel_execve(const char* pathname, const char* const* argv,
+ const char* const* envp)
+{
+ TaskControlBlock* curr = current_task();
+
+ int ret = do_execve(pathname, argv, envp);
+ if (ret != 0)
+ return ret;
+
+ /*
+ * do_execve succeeded: the trapframe is set up with entry point,
+ * user stack, and sstatus (SPP=0 for U-mode). We must NOT return
+ * normally — that would go back through the kthread return path
+ * and jump to address 0. Instead, jump directly to user mode via
+ * the same userret() path that fork_child_entry uses.
+ *
+ * Switch stvec from kernelvec to trap_vector so that traps from
+ * U-mode are handled by the user trap entry path (which loads
+ * kernel context from the trapframe via sscratch).
+ */
+ arch::trap::set_vector(reinterpret_cast<uint64>(trap_vector));
+ userret(curr->trapframe);
+ __builtin_unreachable();
+}
diff --git a/na/kernel/main.cc b/na/kernel/main.cc
index f28d078..db5843c 100644
--- a/na/kernel/main.cc
+++ b/na/kernel/main.cc
@@ -10,26 +10,50 @@
* BSP (cpu_id == 0) executes the full subsystem initialization DAG.
* APs spin on ap_boot_sync, then perform per-CPU init only.
*
+ * Boot DAG invariants (docs/specs/linux_abi_mm_plan.md §Boot Sequence):
+ * - trap_init() MUST precede any allocator (exception safety net)
+ * - sched_init() MUST precede timer_init() (software before hardware)
+ * - PID 1 (init) is spawned before entering idle loop
+ *
* Reference: docs/specs/global_init.md
*/
+#include <arch/config.h>
#include <arch/cpu.h>
+#include <arch/extable.h>
+#include <arch/sbi.h>
+#include <lume/addr.h>
#include <lume/atomic.h>
-#include <lume/boot_params.h>
#include <lume/console.h>
#include <lume/driver.h>
#include <lume/fdt.h>
+#include <lume/fdtable.h>
+#include <lume/fs_struct.h>
+#include <lume/fs_types.h>
#include <lume/irq.h>
+#include <lume/kernel_map.h>
#include <lume/klog.h>
#include <lume/kprintf.h>
#include <lume/memblock.h>
+#include <lume/mount.h>
+#include <lume/page_cache.h>
#include <lume/pmm.h>
#include <lume/random.h>
#include <lume/sched.h>
+#include <lume/smp.h>
+#include <lume/task.h>
+#include <lume/pid.h>
+#include <lume/pgrp.h>
+#include <lume/signal.h>
+#include <lume/signal_types.h>
#include <lume/trap.h>
#include <lume/types.h>
-#include <arch/config.h>
-#include <arch/extable.h>
+
+// Declared by entry.S: the hart ID that won BSP election
+extern "C" uint32 bsp_hart_id;
+
+// Linker symbol: kernel entry point (physical start address)
+extern "C" char _start[];
// ============================================================
// Forward declarations for subsystem init functions.
@@ -42,11 +66,20 @@ void console_init(); // 1. UART ready; kprintf/panic can output
void fdt_init(uint64 fdt_paddr); // 2. Parse FDT, build device node table
void pmm_init(); // 3. Buddy allocator ready
void slab_init(); // 4. kmem_cache ready; operator new usable
-void vmm_init(); // 5. Fine-grained kernel page table; retire early_pgdir
-void task_init(); // 7. PID allocator + global task list
-void timer_init(); // 8. Configure timer interrupt (scheduler tick)
-void sched_init(); // 9. Create BSP Idle Task, init ready queues
-void syscall_init(); // 10. Register syscall jump table
+void vmm_init(); // 5. Fine-grained kernel page table; retire early_pgdir
+void task_init(); // 6. PID allocator + global task list
+void pgrp_init(); // 6.5. Process group hash table
+void sched_init(); // 7. Create BSP Idle Task, init ready queues
+void timer_init(); // 8. Configure timer interrupt (scheduler tick)
+void syscall_init(); // 9. Register syscall jump table
+void super_init(); // 10. VFS superblock registry
+void ramfs_init(); // 11. RamFS filesystem registration
+void vfs_init(); // 12. Mount root filesystem
+void cdev_init(); // 13. Character device registry
+void console_cdev_init(); // 14. Register console/null/zero cdevs
+void devfs_init(); // 15. Create /dev and populate nodes
+void tty_init(); // 15.5. TTY subsystem (console TTY + UART RX IRQ)
+void populate_rootfs(); // 15.6. Unpack embedded initramfs CPIO into ramfs
// Boot self-test runner
void selftest_run_all();
@@ -56,6 +89,102 @@ void vmm_init_ap(); // Switch to BSP-built kernel page table
void timer_init_ap(); // Start this CPU's timer interrupt
void sched_init_ap(); // Init per-CPU ready queue, create Idle Task
+// kthread_create from task.cc
+extern TaskControlBlock *kthread_create(const char *name, void (*entry)(void *),
+ void *arg, uint8 priority);
+
+// ============================================================
+// PID 1 — init thread
+// ============================================================
+static void init_thread_func(void *) {
+ printk(KERN_INFO "[init] PID 1 started\n");
+
+ /* Run post-SMP tests — APs are online by now */
+ extern void selftest_smp();
+ kprintf("\n========================================\n");
+ kprintf(" Post-SMP Test Suite\n");
+ kprintf("========================================\n");
+ selftest_smp();
+ kprintf("========================================\n");
+ kprintf(" Post-SMP tests PASSED\n");
+ kprintf("========================================\n\n");
+
+ /*
+ * Set up init's own VFS context (like Linux kernel_init).
+ * kthread_create does NOT inherit files/fs, so init creates its own.
+ */
+ TaskControlBlock *self = current_task();
+
+ if (!self->files) {
+ self->files = alloc_files_struct();
+ if (!self->files) {
+ printk(KERN_ERR "[init] failed to allocate files_struct\n");
+ goto out;
+ }
+ }
+ if (!self->fs) {
+ self->fs = alloc_fs_struct();
+ if (!self->fs) {
+ printk(KERN_ERR "[init] failed to allocate fs_struct\n");
+ goto out;
+ }
+ /* Point to the global root filesystem */
+ self->fs->root.de = g_root_dentry;
+ self->fs->root.mnt = g_root_mnt;
+ self->fs->pwd = self->fs->root;
+ }
+
+ /* Open /dev/console → fd 0 (stdin), dup to fd 1, 2 */
+ {
+ extern int64 sys_openat(uint64, uint64, uint64, uint64, uint64, uint64);
+ extern int64 sys_dup(uint64, uint64, uint64, uint64, uint64, uint64);
+
+ int64 fd0 =
+ sys_openat(static_cast<uint64>(-100), /* AT_FDCWD */
+ reinterpret_cast<uint64>("/dev/console"), O_RDWR, 0, 0, 0);
+ if (fd0 < 0) {
+ printk(KERN_ERR "[init] open /dev/console failed: %d\n",
+ static_cast<int>(fd0));
+ } else {
+ sys_dup(static_cast<uint64>(fd0), 0, 0, 0, 0, 0); /* fd 1 */
+ sys_dup(static_cast<uint64>(fd0), 0, 0, 0, 0, 0); /* fd 2 */
+ printk(KERN_INFO "[init] fd 0/1/2 -> /dev/console\n");
+ }
+ }
+
+ /*
+ * Try to exec /sbin/init (like Linux kernel_init).
+ * If not found, fall through to idle.
+ */
+ {
+ extern int kernel_execve(const char *, const char *const *,
+ const char *const *);
+
+ const char *init_paths[] = {"/sbin/init", "/etc/init", "/bin/init",
+ "/bin/sh", nullptr};
+ const char *argv[] = {"/sbin/init", nullptr};
+ const char *envp[] = {"HOME=/", "PATH=/sbin:/bin", nullptr};
+
+ for (int i = 0; init_paths[i]; i++) {
+ argv[0] = init_paths[i];
+ int ret = kernel_execve(init_paths[i], argv, envp);
+ if (ret == 0) {
+ /* Success — do_execve modified our trapframe.
+ * When this kthread returns through the trap return path,
+ * it will jump to user space. */
+ return;
+ }
+ printk(KERN_INFO "[init] exec %s failed: %d\n", init_paths[i], ret);
+ }
+
+ printk(KERN_ERR "[init] No init found! Kernel panic.\n");
+ }
+
+out:
+ extern void task_exit(int code);
+ task_exit(0);
+}
+
// ============================================================
// Second-stage synchronization barrier (C++ layer)
// AP cores spin here until BSP finishes all global init.
@@ -67,61 +196,46 @@ static lume::atomic<uint32> ap_boot_sync;
// ============================================================
extern "C" [[noreturn]] void kernel_main(uint64 cpu_id, uint64 fdt_paddr) {
- if (cpu_id == 0) {
+ if (cpu_id == bsp_hart_id) {
// ========================================================
// BSP: Absolute single-core period — no locks needed.
// APs are spinning on ap_boot_sync.
// ========================================================
- boot_params_init_fdt(fdt_paddr);
- const BootParams* bp = boot_params_get();
- uint64 fdt_pa = bp ? bp->fdt_ptr : fdt_paddr;
+ uint64 fdt_pa = fdt_paddr;
+
+ // ---- Phase 1: Console + Exception Safety Net ----
- // Early console setup: discover UART via FDT before ANY output.
+ // 1. Early console setup: discover UART via FDT before ANY output.
console_early_init(fdt_pa);
klog_init();
printk("\n");
printk(KERN_INFO "LumeOS Kernel v0.1\n");
- printk(KERN_INFO "boot: BSP online, cpu_id=0\n");
+ printk(KERN_INFO "boot: BSP online, cpu_id=%llu\n", cpu_id);
printk(KERN_INFO "boot: higher-half kernel active\n");
- BOOT_STAGE(0, "firmware handoff");
- BOOT_STAGE(1, "bootloader handoff");
- BOOT_STAGE(2, "arch early asm complete");
- if (bp) {
- if (bp->kimage_flags & BOOT_IMAGE_DECOMP_FAILED) {
- BOOT_STAGE(3, "decompress failed");
- } else if (bp->kimage_flags & BOOT_IMAGE_COMPRESSED) {
- BOOT_STAGE(3, "decompress ok");
- } else {
- BOOT_STAGE(3, "decompress skipped");
- }
-
- if (bp->kimage_flags & BOOT_IMAGE_RELOC_FAILED) {
- BOOT_STAGE(3, "relocate failed");
- } else if (bp->kimage_flags & BOOT_IMAGE_RELOCATED) {
- BOOT_STAGE(3, "relocate ok");
- } else {
- BOOT_STAGE(3, "relocate skipped");
- }
-
- printk(KERN_INFO "boot: kimage phys=0x%llx size=%llu reloc=%llu\n",
- bp->kimage_phys_base,
- static_cast<unsigned long long>(bp->kimage_size),
- static_cast<unsigned long long>(bp->kimage_reloc_size));
- } else {
- BOOT_STAGE(3, "decompress/relocate unknown");
- }
- BOOT_STAGE(4, "early c init");
+ /* kernel_map filled by arch/riscv/mm/init.cc::setup_vm before MMU
+ * enable; relocate_kernel has already applied .rela.dyn. */
+ printk(KERN_INFO "boot: kimage phys=0x%llx virt=0x%llx size=%llu\n",
+ static_cast<unsigned long long>(kernel_map.phys_addr),
+ static_cast<unsigned long long>(kernel_map.virt_addr),
+ static_cast<unsigned long long>(kernel_map.size));
// --- Subsystem init DAG (strict top-down order) ---
- // 1. FDT parsing — make device tree available
+ // 2. Trap handler — install exception vector FIRST.
+ // Any fault from here on gets a proper panic backtrace
+ // instead of a silent hang. (Linux: trap_init is in start_kernel
+ // right after setup_arch.)
+ trap_init();
+ printk(KERN_INFO "init: trap_init done\n");
+
+ // 3. FDT parsing — make device tree available
fdt_init(fdt_pa);
printk(KERN_INFO "init: fdt_init done\n");
- // 2. KASLR — seed PRNG and randomise VA layout.
+ // 4. KASLR — seed PRNG and randomise VA layout.
// MUST happen before vmm_init() builds the kernel page table.
{
uint64 kaslr_seed = 0;
@@ -133,52 +247,122 @@ extern "C" [[noreturn]] void kernel_main(uint64 cpu_id, uint64 fdt_paddr) {
// Sort exception table before setting RO permissions in vmm_init()
extable_init();
- // 3. Memblock allocator (Early boot PA allocator)
+ // ---- Phase 2: Memory Subsystem (strict cascade) ----
+
+ // 5. Memblock allocator (Early boot PA allocator)
memblock_init(fdt_pa);
printk(KERN_INFO "init: memblock_init done\n");
- // 4. Virtual memory manager — builds page table with KASLR'd bases
+ // 6. Virtual memory manager — builds page table with KASLR'd bases
vmm_init();
printk(KERN_INFO "init: vmm_init done\n");
- // 5. Physical memory manager (Buddy)
+ // 7. Physical memory manager (Buddy)
pmm_init();
printk(KERN_INFO "init: pmm_init done\n");
- // 6. Slab allocator — operator new usable after this
+ // 8. Slab allocator — operator new usable after this
slab_init();
printk(KERN_INFO "init: slab_init done\n");
- // 7. FDT Phase 2 — build DeviceNode object tree (needs Slab)
+ // 9. Page cache (minimal hash table, stable interface for file-backed mmap)
+ page_cache_init();
+
+ // 10. FDT Phase 2 — build DeviceNode object tree (needs Slab)
fdt_unflatten();
printk(KERN_INFO "init: fdt_unflatten done\n");
- // 6. Trap handler — set trap vector
- trap_init();
- printk(KERN_INFO "init: trap_init done\n");
+ // ---- Phase 3: Task & Scheduler (software state before hardware) ----
- // 6.5 Task management (PID allocator + global task list)
+ // 11. Task management (PID allocator + global task list)
task_init();
+ printk(KERN_INFO "init: task_init done\n");
- // 7. Hardware Drivers Discovery (Interrupt Controllers, etc)
+ pgrp_init();
+
+ sched_init();
+ printk(KERN_INFO "init: sched_init done\n");
+
+ // ---- Phase 3.5: VFS (needs slab + sched) ----
+ super_init();
+ ramfs_init();
+ vfs_init();
+ printk(KERN_INFO "init: vfs_init done (ramfs root mounted)\n");
+
+ // ---- Phase 3.55: Unpack initramfs into root ----
+ populate_rootfs();
+
+ // ---- Phase 3.6: Device nodes ----
+ cdev_init();
+ console_cdev_init();
+ devfs_init();
+ printk(KERN_INFO "init: devfs done\n");
+
+ // ---- Phase 4: Hardware Drivers & Interrupts ----
+
+ // 13. Hardware Drivers Discovery (Interrupt Controllers, etc)
driver_probe_all();
printk(KERN_INFO "init: driver_probe_all done\n");
- // 8. Timer interrupt (scheduler tick source)
+ // 13.5. TTY subsystem — needs PLIC (driver_probe_all) for UART RX IRQ
+ tty_init();
+ printk(KERN_INFO "init: tty_init done\n");
+
+ // 14. Timer interrupt (scheduler tick source)
+ // Safe now: scheduler is fully initialized.
timer_init();
printk(KERN_INFO "init: timer_init done\n");
- // 9. Scheduler — create Idle Task 0, init ready queues
- sched_init();
- printk(KERN_INFO "init: sched_init done\n");
-
- // 10. Syscall jump table
+ // 15. Syscall jump table
syscall_init();
+ printk(KERN_INFO "init: syscall_init done\n");
+
+ // 16. SMP / IPI infrastructure
+ smp_init();
+ arch::cpu::software_intr_on(); // Enable S-mode software interrupts for IPI
+ arch::cpu::external_intr_on(); // Enable S-mode external interrupts for PLIC (UART)
- // 10. Boot self-tests — verify subsystem correctness
+ // ---- Phase 5: Self-test & Launch ----
+
+ // 17. Boot self-tests — verify subsystem correctness.
+ // Tests that create kthreads must wait() for them (freeing PIDs).
selftest_run_all();
- // kernel_panic("Manual Panic Test", "triggered by developer"); // VERIFIED
- // OK
+
+ // 18. Spawn PID 1 (init) — the first persistent task.
+ // Like Linux's rest_init() → user_mode_thread(kernel_init).
+ // All test PIDs have been freed, so PID 1 is available.
+ TaskControlBlock *init_task =
+ kthread_create("init", init_thread_func, nullptr, 0);
+ if (init_task) {
+ init_task->flags |= TF_INIT;
+
+ /* Init is session and process-group leader: sid = pgid = pid.
+ * Allocate signal_struct AND sighand_struct eagerly so every
+ * descendant (created via task_clone) inherits a real identity
+ * and a real lock object — signal sending must take siglock and
+ * cannot tolerate a null sighand. */
+ init_task->signal = alloc_signal_struct(init_task->pid, init_task);
+ if (!init_task->signal) {
+ printk(KERN_ERR "boot: FAILED to allocate init signal_struct\n");
+ }
+ init_task->sighand = alloc_sighand();
+ if (!init_task->sighand) {
+ printk(KERN_ERR "boot: FAILED to allocate init sighand\n");
+ }
+
+ /* Publish g_init_task so reparenting paths can find the foster
+ * parent when other tasks exit before init takes over user space. */
+ extern TaskControlBlock* g_init_task;
+ g_init_task = init_task;
+
+ printk(KERN_INFO "boot: init thread created (pid=%d sid=%d pgid=%d)\n",
+ init_task->pid,
+ init_task->signal ? init_task->signal->sid : -1,
+ init_task->signal ? init_task->signal->pgid : -1);
+ } else {
+ printk(KERN_ERR "boot: FAILED to create init thread!\n");
+ }
+
BOOT_STAGE(5, "core kernel init");
printk(KERN_INFO "boot: BSP init complete, releasing APs\n");
@@ -187,9 +371,37 @@ extern "C" [[noreturn]] void kernel_main(uint64 cpu_id, uint64 fdt_paddr) {
// guarantees all global data is visible to APs.
ap_boot_sync.store(1, __ATOMIC_RELEASE);
+ // Start AP harts via SBI HSM extension.
+ // OpenSBI v1.0+ parks non-boot harts in STOPPED state.
+ // They must be explicitly started with sbi_hart_start().
+ //
+ // kernel_map.phys_addr was filled by setup_vm in arch/riscv/mm/init.cc
+ // before MMU enable — it's the runtime PA at which the kernel was
+ // loaded. APs start at the same PA as the BSP.
+ {
+ uint64 start_pa = kernel_map.phys_addr;
+ if (start_pa == 0) {
+ /* Defensive fallback if setup_vm hasn't filled the field. */
+ start_pa =
+ boot_va_to_pa(virt_addr(reinterpret_cast<uint64>(_start))).raw;
+ }
+ uint32 num_harts = 4; // TODO: read from FDT /cpus
+ for (uint32 h = 0; h < num_harts; h++) {
+ if (h == static_cast<uint32>(cpu_id))
+ continue; // skip BSP
+ auto ret = arch::sbi::sbi_hart_start(h, start_pa, fdt_paddr);
+ if (ret.error == arch::sbi::SBI_SUCCESS) {
+ printk(KERN_INFO "smp: started hart %u at PA 0x%llx\n", h, start_pa);
+ } else {
+ printk(KERN_ERR "smp: failed to start hart %u (err=%ld)\n", h,
+ ret.error);
+ }
+ }
+ }
+
// BSP enables interrupts — first timer tick will preempt
- // into the scheduler loop.
- // arch::cpu::intr_on();
+ // into the scheduler loop, which will pick up PID 1.
+ arch::cpu::intr_on();
printk(KERN_INFO "boot: BSP entering idle loop\n");
@@ -197,22 +409,59 @@ extern "C" [[noreturn]] void kernel_main(uint64 cpu_id, uint64 fdt_paddr) {
// ========================================================
// AP: spin until BSP finishes global init
// ========================================================
+ // DEBUG (commented out — uncomment for SMP debugging):
+ // arch::sbi::sbi_ecall(1, 0, '1'); // AP entered kernel_main
+
while (ap_boot_sync.load(__ATOMIC_ACQUIRE) == 0)
;
+ // arch::sbi::sbi_ecall(1, 0, '2'); // AP passed ap_boot_sync
+
+ // --- Per-CPU init (must happen before any printk!) ---
+ // early_pgdir doesn't map UART MMIO, so we MUST switch to
+ // BSP's kernel page table before any console output.
+ vmm_init_ap(); // Switch to BSP-built kernel page table
+
+ // arch::sbi::sbi_ecall(1, 0, '3'); // vmm_init_ap done
+
+ trap_init_ap(); // Install stvec on this CPU
+
+ // arch::sbi::sbi_ecall(1, 0, '4'); // trap_init_ap done
+
printk(KERN_INFO "boot: AP online, cpu_id=%llu\n", cpu_id);
- // --- Per-CPU init only (no global data mutation) ---
- // vmm_init_ap();
- // arch_irq_chip_init_ap();
- // timer_init_ap();
- // sched_init_ap();
+ sched_init_ap(); // Init per-CPU RunQueue, create Idle Task
+
+ // arch::sbi::sbi_ecall(1, 0, '5'); // sched_init_ap done
+
+ timer_init_ap(); // Start this CPU's timer interrupt
+ arch::cpu::software_intr_on(); // Enable S-mode software interrupts for IPI
+ arch::cpu::external_intr_on(); // Enable S-mode external interrupts for PLIC (UART)
- // AP enables interrupts — becomes Idle Task on this CPU
- // arch::cpu::intr_on();
+ // AP is now a fully functional scheduler CPU.
+ // Enable interrupts — first timer tick preempts into idle loop.
+ arch::cpu::intr_on();
+
+ printk(KERN_INFO "boot: AP cpu_id=%llu fully initialized\n", cpu_id);
+ }
+
+ // All CPUs reach here.
+ // The "current task" is the boot stub (bsp_task or ap_task, pid=-1).
+ // We must hand off to the scheduler's idle task by calling schedule().
+ // Without this, the BSP runs as bsp_task (O1 class, timeslice=20 ticks)
+ // instead of the idle task (which yields immediately when any task is
+ // runnable). That delays PID 1 by up to 200ms and can stall entirely
+ // if no timer tick fires before the first WFI.
+ {
+ TaskControlBlock *self = current_task();
+ if (self) {
+ self->state = TaskState::Dead; // Boot stub is done; don't re-enqueue
+ }
+ schedule(); // Switch to idle task (or init if it's already runnable)
}
- // All CPUs: idle loop (wfi until interrupt)
+ // Should not reach here — schedule() switches away permanently.
+ // But just in case, spin with WFI as a safety net.
while (true) {
arch::cpu::halt_until_interrupt();
}
diff --git a/na/kernel/module.mk b/na/kernel/module.mk
index 662f57e..754a14c 100644
--- a/na/kernel/module.mk
+++ b/na/kernel/module.mk
@@ -1,17 +1,26 @@
# kernel/module.mk
SRCS_CC += kernel/main.cc
-SRCS_CC += kernel/boot_params.cc
SRCS_CC += kernel/sync/spinlock.cc
+SRCS_CC += kernel/sync/mutex.cc
SRCS_CC += kernel/trap.cc
SRCS_CC += kernel/irq.cc
SRCS_CC += kernel/sched_core.cc
SRCS_CC += kernel/sched_class_o1.cc
SRCS_CC += kernel/timer.cc
SRCS_CC += kernel/pid.cc
+SRCS_CC += kernel/pgrp.cc
SRCS_CC += kernel/task.cc
SRCS_CC += kernel/syscall.cc
SRCS_CC += kernel/sys_process.cc
SRCS_CC += kernel/sys_mm.cc
SRCS_CC += kernel/sys_info.cc
+SRCS_CC += kernel/sys_fs.cc
SRCS_CC += kernel/uaccess.cc
SRCS_CC += kernel/waitqueue.cc
+SRCS_CC += kernel/signal.cc
+SRCS_CC += kernel/sys_signal.cc
+SRCS_CC += kernel/sys_misc.cc
+SRCS_CC += kernel/exec.cc
+SRCS_CC += kernel/poll.cc
+SRCS_CC += kernel/ptrace.cc
+SRCS_CC += kernel/smp.cc
diff --git a/na/kernel/pgrp.cc b/na/kernel/pgrp.cc
new file mode 100644
index 0000000..e5b382c
--- /dev/null
+++ b/na/kernel/pgrp.cc
@@ -0,0 +1,148 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Process Group / Session Implementation
+ *
+ * Hash table of signal_struct objects keyed by pgid. Lifecycle of
+ * signal_struct mirrors Linux: one per thread group, refcounted across
+ * CLONE_THREAD siblings, freed when the last task in the group exits.
+ *
+ * Reference: Linux kernel/pid.c, kernel/exit.c
+ */
+
+#include <lume/pgrp.h>
+#include <lume/signal_types.h>
+#include <lume/task.h>
+#include <lume/new.h>
+#include <lume/klog.h>
+
+/* ================================================================== */
+/* Hash table state */
+/* ================================================================== */
+
+list_node g_pgrp_hash[kPgrpHashSize];
+Spinlock g_pgrp_lock;
+
+void pgrp_init()
+{
+ for (auto& bucket : g_pgrp_hash) bucket.init();
+ g_pgrp_lock.init("pgrp");
+}
+
+/* ================================================================== */
+/* Internal helpers (must be called with g_pgrp_lock held) */
+/* ================================================================== */
+
+static void pgrp_attach_locked(signal_struct* sig, int32 pgid)
+{
+ sig->pgid = pgid;
+ list_node* bucket = &g_pgrp_hash[pgrp_hash_idx(pgid)];
+ sig->pgrp_link.prev = bucket->prev;
+ sig->pgrp_link.next = bucket;
+ bucket->prev->next = &sig->pgrp_link;
+ bucket->prev = &sig->pgrp_link;
+}
+
+static void pgrp_detach_locked(signal_struct* sig)
+{
+ /* Unlink only if currently on a list — init() makes prev/next point
+ * to self, which we treat as "not linked". */
+ if (sig->pgrp_link.next == &sig->pgrp_link) return;
+ sig->pgrp_link.prev->next = sig->pgrp_link.next;
+ sig->pgrp_link.next->prev = sig->pgrp_link.prev;
+ sig->pgrp_link.init();
+}
+
+/* ================================================================== */
+/* signal_struct allocation / refcount */
+/* ================================================================== */
+
+signal_struct* alloc_signal_struct(int32 leader_pid, TaskControlBlock* leader)
+{
+ auto* sig = new signal_struct;
+ if (!sig) return nullptr;
+ sig->init(leader_pid, leader);
+
+ LockGuard guard(g_pgrp_lock);
+ pgrp_attach_locked(sig, leader_pid);
+ return sig;
+}
+
+signal_struct* fork_signal_struct(signal_struct* parent,
+ TaskControlBlock* child_leader)
+{
+ if (!parent) return nullptr;
+
+ auto* sig = new signal_struct;
+ if (!sig) return nullptr;
+
+ /* Copy identity from parent, then override leader and attach. */
+ sig->lock.init("signal");
+ sig->refcount = 1;
+ sig->leader = child_leader;
+ sig->tty_old_pgrp = 0;
+ sig->pgrp_link.init();
+
+ {
+ LockGuard pg(parent->lock);
+ sig->pgid = parent->pgid;
+ sig->sid = parent->sid;
+ sig->tty = parent->tty;
+ }
+
+ LockGuard guard(g_pgrp_lock);
+ pgrp_attach_locked(sig, sig->pgid);
+ return sig;
+}
+
+void get_signal_struct(signal_struct* sig)
+{
+ if (!sig) return;
+ LockGuard guard(sig->lock);
+ sig->refcount++;
+}
+
+void put_signal_struct(signal_struct* sig)
+{
+ if (!sig) return;
+
+ uint32 remaining;
+ {
+ LockGuard guard(sig->lock);
+ remaining = --sig->refcount;
+ }
+ if (remaining != 0) return;
+
+ /* Last reference — detach from pgrp hash and free. */
+ {
+ LockGuard guard(g_pgrp_lock);
+ pgrp_detach_locked(sig);
+ }
+ delete sig;
+}
+
+/* ================================================================== */
+/* Pgrp membership change */
+/* ================================================================== */
+
+void change_pgid(signal_struct* sig, int32 new_pgid)
+{
+ if (!sig) return;
+ LockGuard guard(g_pgrp_lock);
+ if (sig->pgid == new_pgid) return;
+ pgrp_detach_locked(sig);
+ pgrp_attach_locked(sig, new_pgid);
+}
+
+int pgrp_member_count(int32 pgid)
+{
+ LockGuard guard(g_pgrp_lock);
+ int n = 0;
+ list_node* bucket = &g_pgrp_hash[pgrp_hash_idx(pgid)];
+ for (list_node* node = bucket->next; node != bucket; node = node->next) {
+ auto* sig = list_entry<signal_struct, &signal_struct::pgrp_link>(node);
+ if (sig->pgid == pgid) n++;
+ }
+ return n;
+}
diff --git a/na/kernel/pid.cc b/na/kernel/pid.cc
index 81b9494..04b846b 100644
--- a/na/kernel/pid.cc
+++ b/na/kernel/pid.cc
@@ -12,8 +12,10 @@ void PidAllocator::init()
{
for (int i = 0; i < kBitmapWords; i++)
bitmap_[i] = 0;
- next_hint_ = 2;
- bitmap_[0] |= 0x3ULL; /* Reserve PID 0 (idle) and 1 (init) */
+ next_hint_ = 1;
+ bitmap_[0] |= 0x1ULL; /* Reserve PID 0 (idle/swapper) only.
+ * PID 1 (init) is allocated normally — the first
+ * kthread_create() call gets it, matching Linux. */
}
int32 PidAllocator::alloc()
diff --git a/na/kernel/poll.cc b/na/kernel/poll.cc
new file mode 100644
index 0000000..3674491
--- /dev/null
+++ b/na/kernel/poll.cc
@@ -0,0 +1,250 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * sys_ppoll — POSIX ppoll() implementation.
+ *
+ * Linux fs/select.c:do_sys_poll() pattern, simplified for our needs:
+ * - Two-phase loop: "register-then-check". First pass calls every
+ * f_op->poll() with a real poll_table so drivers can register their
+ * wait queues; if any fd is already ready we return immediately.
+ * Otherwise schedule() until a wq fires (or timeout/signal), then
+ * re-check with poll_table=nullptr (probe-only).
+ *
+ * Limitations vs. Linux:
+ * - Only a single wait queue per ppoll call is tracked (the first one
+ * a driver hands to poll_wait). Multi-fd polls work, but only the
+ * first wq's wake actually re-runs the loop; other fds becoming
+ * ready won't be noticed until the timer fires or another wake.
+ * BusyBox interactive shell polls one fd (stdin), so this matches.
+ *
+ * Reference: Linux fs/select.c
+ */
+
+#include <lume/poll.h>
+#include <lume/file.h>
+#include <lume/fdtable.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+#include <lume/timer.h>
+#include <lume/waitqueue.h>
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/uaccess.h>
+#include <lume/errno.h>
+#include <lume/klog.h>
+#include <lume/new.h>
+
+/* ================================================================== */
+/* poll_wait — called by file_operations->poll() implementations */
+/* ================================================================== */
+
+void poll_wait(file* /*fp*/, WaitQueue* wq, poll_table* pt)
+{
+ /* Probe-only pass: pt is null, drivers skip registration. */
+ if (!pt || !wq) return;
+ /* Single-wq design: keep the first one we see. A more complete
+ * implementation would chain entries onto a list. */
+ if (!pt->wq) pt->wq = wq;
+}
+
+/* ================================================================== */
+/* Linux-compatible timespec */
+/* ================================================================== */
+
+struct linux_timespec_pp {
+ int64 tv_sec;
+ int64 tv_nsec;
+};
+
+/*
+ * Convert a timespec into a deadline in jiffies (g_jiffies clock).
+ * Returns 0 if the input is null (= infinite wait), or a deadline
+ * jiffy value otherwise. Caller treats 0 deadline as "no timeout".
+ */
+static uint64 timespec_to_deadline(const linux_timespec_pp* ts)
+{
+ if (!ts) return 0;
+ /* g_jiffies advances at 100 Hz (HZ=100) — 1 jiffy = 10 ms */
+ uint64 ticks = static_cast<uint64>(ts->tv_sec) * 100ULL;
+ ticks += ts->tv_nsec / 10'000'000; /* 10 ms granularity */
+ /* Round up sub-jiffy nsec so a tiny timeout doesn't truncate to 0. */
+ if (ts->tv_nsec % 10'000'000 != 0) ticks++;
+ return g_jiffies + ticks;
+}
+
+/* ================================================================== */
+/* do_poll — one scan over all fds */
+/* ================================================================== */
+
+/*
+ * For each fd: look up the file, call its poll() callback. If `pt` is
+ * non-null, drivers register their wait queues onto it. Returns the
+ * number of fds that have any non-zero revents.
+ */
+static int do_poll_scan(pollfd* fds, uint32 nfds, poll_table* pt)
+{
+ TaskControlBlock* c = current_task();
+ files_struct* files = c ? c->files : nullptr;
+ if (!files) return 0;
+ int ready = 0;
+
+ for (uint32 i = 0; i < nfds; i++) {
+ if (fds[i].fd < 0) {
+ fds[i].revents = 0;
+ continue;
+ }
+ file* fp = fcheck(files, fds[i].fd);
+ if (!fp) {
+ fds[i].revents = POLLNVAL;
+ ready++;
+ continue;
+ }
+ uint32 mask = 0;
+ if (fp->f_op && fp->f_op->poll) {
+ mask = fp->f_op->poll(fp, pt);
+ } else {
+ /* No poll handler — treat as always readable/writable so
+ * a missing implementation doesn't permanently block sh. */
+ mask = POLLIN | POLLOUT;
+ }
+ /* Filter: revents always include error/hup; userland event bits
+ * only set if requested in events. */
+ uint32 always = POLLERR | POLLHUP | POLLNVAL;
+ uint32 want = static_cast<uint32>(fds[i].events) | always;
+ fds[i].revents = static_cast<int16>(mask & want);
+ if (fds[i].revents) ready++;
+ }
+ return ready;
+}
+
+/* ================================================================== */
+/* sys_ppoll */
+/* ================================================================== */
+
+/*
+ * a0 = struct pollfd __user *ufds
+ * a1 = nfds_t nfds
+ * a2 = const struct __kernel_timespec __user *tsp (nullable = infinite)
+ * a3 = const sigset_t __user *sigmask (nullable = no override)
+ * a4 = size_t sigsetsize
+ */
+int64 sys_ppoll(uint64 a0, uint64 a1, uint64 a2, uint64 a3,
+ uint64 a4, uint64 /*a5*/)
+{
+ uint64 ufds = a0;
+ uint32 nfds = static_cast<uint32>(a1);
+ uint64 utsp = a2;
+ uint64 usigmask = a3;
+ uint64 sigsetsize = a4;
+
+ if (nfds > 1024) return -EINVAL; /* Linux RLIMIT_NOFILE-ish cap */
+ if (usigmask && sigsetsize != sizeof(sigset_t)) return -EINVAL;
+
+ /* Copy timespec from user. Null pointer means "wait forever". */
+ bool have_timeout = (utsp != 0);
+ linux_timespec_pp ts{};
+ if (have_timeout) {
+ if (copy_from_user(&ts, utsp, sizeof(ts)) < 0) return -EFAULT;
+ if (ts.tv_sec < 0 || ts.tv_nsec < 0 || ts.tv_nsec >= 1'000'000'000)
+ return -EINVAL;
+ }
+ uint64 deadline = have_timeout ? timespec_to_deadline(&ts) : 0;
+
+ /* Apply temporary sigmask if requested. Linux semantics: the new
+ * mask is in effect for the duration of the call; on return we
+ * restore the old mask. If a signal interrupts us mid-wait, the
+ * original mask is restored before returning -EINTR. */
+ TaskControlBlock* curr = current_task();
+ sigset_t saved_mask{};
+ bool mask_swapped = false;
+ if (usigmask && curr && curr->sighand) {
+ sigset_t newmask{};
+ if (copy_from_user(&newmask, usigmask, sizeof(newmask)) < 0)
+ return -EFAULT;
+ newmask.del(SIGKILL);
+ newmask.del(SIGSTOP);
+ LockGuard guard(curr->sighand->siglock);
+ saved_mask = curr->blocked;
+ curr->blocked = newmask;
+ mask_swapped = true;
+ }
+
+ /* Copy pollfd array from user. Allocate a kernel buffer; for the
+ * common nfds<=8 case this is one slab object. */
+ pollfd* kfds = nullptr;
+ uint64 fds_bytes = static_cast<uint64>(nfds) * sizeof(pollfd);
+ int64 ret = 0;
+
+ if (nfds > 0) {
+ kfds = static_cast<pollfd*>(operator new[](fds_bytes));
+ if (!kfds) { ret = -ENOMEM; goto out_restore_mask; }
+ if (copy_from_user(kfds, ufds, fds_bytes) < 0) {
+ ret = -EFAULT; goto out_free;
+ }
+ }
+
+ /* The main poll loop. */
+ {
+ poll_table pt{};
+ pt.wq = nullptr;
+ int n_ready = do_poll_scan(kfds, nfds, &pt);
+
+ while (n_ready == 0) {
+ /* Timeout==0 means non-blocking probe. */
+ if (have_timeout && deadline <= g_jiffies) break;
+
+ /* No registered wait queue? Nothing to sleep on. This
+ * happens when no fd has a poll handler that calls
+ * poll_wait — fall back to a short reschedule loop so we
+ * don't burn the CPU. */
+ if (!pt.wq) {
+ schedule();
+ if (signal_pending(curr)) { ret = -EINTR; goto out_free; }
+ pt.wq = nullptr;
+ n_ready = do_poll_scan(kfds, nfds, &pt);
+ continue;
+ }
+
+ /* Standard prepare-check-schedule pattern. */
+ pt.wq->prepare_to_wait(curr, TaskState::Sleeping);
+
+ /* Re-probe with pt=nullptr (no extra registration) so we
+ * see any state change that arrived between the previous
+ * scan and prepare_to_wait. */
+ n_ready = do_poll_scan(kfds, nfds, nullptr);
+ if (n_ready > 0) {
+ pt.wq->finish_wait(curr);
+ break;
+ }
+ if (signal_pending(curr)) {
+ pt.wq->finish_wait(curr);
+ ret = -EINTR; goto out_free;
+ }
+
+ schedule();
+ pt.wq->finish_wait(curr);
+
+ /* Reset the registration and rescan — drivers may register
+ * fresh wait queues this pass. */
+ pt.wq = nullptr;
+ n_ready = do_poll_scan(kfds, nfds, &pt);
+ }
+ ret = n_ready;
+ }
+
+ /* Copy revents back to user (events bits stay as user wrote them). */
+ if (nfds > 0) {
+ if (copy_to_user(ufds, kfds, fds_bytes) < 0) ret = -EFAULT;
+ }
+
+out_free:
+ if (kfds) operator delete[](kfds);
+
+out_restore_mask:
+ if (mask_swapped && curr && curr->sighand) {
+ LockGuard guard(curr->sighand->siglock);
+ curr->blocked = saved_mask;
+ }
+ return ret;
+}
diff --git a/na/kernel/ptrace.cc b/na/kernel/ptrace.cc
new file mode 100644
index 0000000..3280c07
--- /dev/null
+++ b/na/kernel/ptrace.cc
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * PtRegs ↔ TrapFrame conversion
+ *
+ * This is the ONLY file that knows both the internal TrapFrame layout
+ * and the Linux-ABI PtRegs layout. When TrapFrame is restructured
+ * (e.g., to embed pt_regs directly), only this file needs to change.
+ */
+
+#include <lume/ptrace.h>
+#include <arch/trapframe.h>
+
+void trapframe_to_pt_regs(PtRegs* dst, const arch::TrapFrame* src,
+ uint64 scause, uint64 stval)
+{
+ dst->epc = src->epc;
+ dst->ra = src->ra;
+ dst->sp = src->sp;
+ dst->gp = src->gp;
+ dst->tp = src->tp;
+ dst->t0 = src->t[0];
+ dst->t1 = src->t[1];
+ dst->t2 = src->t[2];
+ dst->s0 = src->s[0];
+ dst->s1 = src->s[1];
+ dst->a0 = src->a[0];
+ dst->a1 = src->a[1];
+ dst->a2 = src->a[2];
+ dst->a3 = src->a[3];
+ dst->a4 = src->a[4];
+ dst->a5 = src->a[5];
+ dst->a6 = src->a[6];
+ dst->a7 = src->a[7];
+ dst->s2 = src->s[2];
+ dst->s3 = src->s[3];
+ dst->s4 = src->s[4];
+ dst->s5 = src->s[5];
+ dst->s6 = src->s[6];
+ dst->s7 = src->s[7];
+ dst->s8 = src->s[8];
+ dst->s9 = src->s[9];
+ dst->s10 = src->s[10];
+ dst->s11 = src->s[11];
+ dst->t3 = src->t[3];
+ dst->t4 = src->t[4];
+ dst->t5 = src->t[5];
+ dst->t6 = src->t[6];
+ dst->status = src->sstatus;
+ dst->badaddr = stval;
+ dst->cause = scause;
+ dst->orig_a0 = src->a[0]; /* Caller should set this before syscall dispatch */
+}
+
+void pt_regs_to_trapframe(arch::TrapFrame* dst, const PtRegs* src)
+{
+ dst->epc = src->epc;
+ dst->ra = src->ra;
+ dst->sp = src->sp;
+ dst->gp = src->gp;
+ dst->tp = src->tp;
+ dst->t[0] = src->t0;
+ dst->t[1] = src->t1;
+ dst->t[2] = src->t2;
+ dst->s[0] = src->s0;
+ dst->s[1] = src->s1;
+ dst->a[0] = src->a0;
+ dst->a[1] = src->a1;
+ dst->a[2] = src->a2;
+ dst->a[3] = src->a3;
+ dst->a[4] = src->a4;
+ dst->a[5] = src->a5;
+ dst->a[6] = src->a6;
+ dst->a[7] = src->a7;
+ dst->s[2] = src->s2;
+ dst->s[3] = src->s3;
+ dst->s[4] = src->s4;
+ dst->s[5] = src->s5;
+ dst->s[6] = src->s6;
+ dst->s[7] = src->s7;
+ dst->s[8] = src->s8;
+ dst->s[9] = src->s9;
+ dst->s[10] = src->s10;
+ dst->s[11] = src->s11;
+ dst->t[3] = src->t3;
+ dst->t[4] = src->t4;
+ dst->t[5] = src->t5;
+ dst->t[6] = src->t6;
+ dst->sstatus = src->status;
+ /* badaddr and cause are CSR values, not written back to TrapFrame */
+}
diff --git a/na/kernel/sched_class_o1.cc b/na/kernel/sched_class_o1.cc
index a8ba5df..28c693c 100644
--- a/na/kernel/sched_class_o1.cc
+++ b/na/kernel/sched_class_o1.cc
@@ -44,8 +44,13 @@ int32 priority_to_timeslice(uint8 priority)
static void o1_task_tick(RunQueue* rq, TaskControlBlock* curr)
{
- if (curr == rq->idle())
+ if (curr == rq->idle()) {
+ /* Idle task: if anything is runnable, yield immediately */
+ if (rq->nr_running() > 0) {
+ rq->set_need_resched(true);
+ }
return;
+ }
curr->time_slice--;
if (curr->time_slice <= 0) {
diff --git a/na/kernel/sched_core.cc b/na/kernel/sched_core.cc
index 85248ef..c0e068e 100644
--- a/na/kernel/sched_core.cc
+++ b/na/kernel/sched_core.cc
@@ -13,11 +13,15 @@
#include <lume/sched_class.h>
#include <lume/task.h>
#include <lume/pmm.h>
+#include <lume/vmm.h>
#include <lume/addr.h>
#include <lume/new.h>
#include <lume/kprintf.h>
#include <lume/klog.h>
+#include <lume/smp.h>
#include <arch/cpu.h>
+#include <arch/mmu.h>
+#include <arch/trap.h>
#include <arch/context.h>
#include <kernel/sched_internal.h>
@@ -37,11 +41,15 @@ static const SchedClass* g_sched_class_head = &g_sched_class_o1;
static void idle_thread_func(void* /*arg*/)
{
while (true) {
+ if (g_rq[arch::cpu::id()].need_resched()) {
+ schedule();
+ }
arch::cpu::intr_on();
arch::cpu::halt_until_interrupt();
}
}
+
/* ========================================================================
* Idle class (always returns idle task)
* ======================================================================== */
@@ -108,6 +116,9 @@ static const char* idle_name_for_cpu(uint32 cpu)
* Kernel thread entry — MI-layer function called by MD trampoline
* ======================================================================== */
+/* Forward declaration — C++ linkage (task.cc) */
+void task_exit(int code);
+
extern "C" void kthread_entry(void (*entry)(void*), void* arg)
{
RunQueue* rq = this_rq();
@@ -117,74 +128,38 @@ extern "C" void kthread_entry(void (*entry)(void*), void* arg)
entry(arg);
- kprintf("[sched] kthread exited\n");
- while (true) {
- arch::cpu::halt_until_interrupt();
- }
+ /* Entry function returned — clean exit (release PID, stack, TCB) */
+ task_exit(0);
+ __builtin_unreachable();
}
/* ========================================================================
* fork_child_entry — called from ret_from_fork assembly
* ======================================================================== */
extern "C" void userret(arch::TrapFrame* tf);
+extern "C" void trap_vector();
extern "C" void fork_child_entry(arch::TrapFrame* tf)
{
RunQueue* rq = this_rq();
rq->lock().release();
- /* Do NOT enable interrupts here! userret expects them disabled. */
-
- /* Jump to user space */
+ /* Set stvec to trap_vector for user-mode traps, then return to user */
+ arch::trap::set_vector(reinterpret_cast<uint64>(trap_vector));
userret(tf);
__builtin_unreachable();
}
/* ========================================================================
- * Helper: create a TCB with a kernel stack
+ * alloc_task — canonical TCB allocator (defined in task.cc)
+ *
+ * Single point of initialization for TaskControlBlock. Ensures all
+ * fields are properly zeroed when the struct gains new members.
* ======================================================================== */
-static TaskControlBlock* create_tcb(const char* name, uint8 priority,
- uint8 flags, uint8 cpu)
-{
- auto* task = new TaskControlBlock;
- if (!task) return nullptr;
-
- task->context = {};
-
- Page* stack_page = pmm_alloc_pages(1);
- if (!stack_page) {
- delete task;
- return nullptr;
- }
- task->kstack_pa = page_to_pa(stack_page).raw;
- task->kstack_va = pa_to_va(phys_addr(task->kstack_pa)).raw;
-
- *reinterpret_cast<uint64*>(task->kstack_va) = kStackCanary;
-
- task->name = name;
- task->priority = priority;
- task->time_slice = priority_to_timeslice(priority);
- task->state = TaskState::Runnable;
- task->cpu_id = cpu;
- task->flags = flags;
- task->exit_code = 0;
- task->parent = nullptr;
- task->mm = nullptr;
- task->trapframe = nullptr;
-
- task->run_link.init();
- task->all_link.init();
- task->children.init();
- task->sibling.init();
- task->wait_chld.init("wait_chld");
-
- task->sched_class = &g_sched_class_o1;
- task->sched_entity = &task->run_link;
-
- return task;
-}
+extern TaskControlBlock* alloc_task(const char* name, uint8 priority,
+ uint8 flags, uint8 cpu);
/* ========================================================================
* Public API: current_task()
@@ -212,6 +187,10 @@ void sched_enqueue(TaskControlBlock* task)
}
task->sched_class->enqueue_task(rq, task);
+
+ if (rq->current() == rq->idle() || task->priority < rq->current()->priority) {
+ rq->set_need_resched(true);
+ }
}
void sched_dequeue(TaskControlBlock* task)
@@ -262,6 +241,52 @@ void schedule()
rq->set_current(next);
rq->clear_need_resched();
+ /* Switch address space (satp) if the new task has a different mm.
+ * Kernel threads (mm == nullptr) borrow the previous task's mm,
+ * so no satp switch is needed — this matches Linux's lazy TLB behavior. */
+ {
+ VmSpace* prev_mm = prev ? prev->mm : nullptr;
+ VmSpace* next_mm = next->mm;
+ uint32 cpu = static_cast<uint32>(arch::cpu::id());
+
+ if (next_mm && next_mm != prev_mm) {
+ /* Switching TO a user process with a different address space.
+ * Linux: switch_mm_irqs_off() in arch/riscv/mm/context.c
+ * sets satp and flushes TLB. */
+ arch::mmu::set_page_table(next_mm->root_pa);
+ arch::mmu::flush_tlb_all();
+ next_mm->cpu_mask.fetch_or(1ULL << cpu, __ATOMIC_RELAXED);
+
+ /* Critical: update TrapFrame's kernel_satp so that trap_vector.S
+ * restores the correct page table on next trap entry.
+ * Without this, a process migrated to a new CPU would have its
+ * trap entry use a stale satp from when trapframe_init_user()
+ * was called, pointing to whatever page table was active then
+ * on the ORIGINAL CPU. */
+ if (next->trapframe) {
+ next->trapframe->kernel_satp = arch::mmu::read_satp();
+ }
+ }
+
+ if (prev_mm && prev_mm != next_mm) {
+ /* Leaving prev's address space — clear our bit */
+ prev_mm->cpu_mask.fetch_and(~(1ULL << cpu), __ATOMIC_RELAXED);
+ }
+ }
+
+ /* FPU lazy save/restore.
+ * Save prev's FP registers only if they were modified (FS=Dirty).
+ * Restore next's FP state for user tasks; disable FPU for kthreads. */
+ if (arch::fpu_state_dirty(arch::fpu_get_fs())) {
+ fpu_save(&prev->fpstate);
+ }
+ if (next->mm != nullptr) {
+ arch::fpu_set_fs(arch::kSstatusFS_Clean);
+ fpu_restore(&next->fpstate);
+ } else {
+ arch::fpu_set_fs(arch::kSstatusFS_Off);
+ }
+
arch::context_switch(&prev->context, &next->context);
rq->lock().release();
@@ -274,24 +299,37 @@ void schedule()
void sched_tick()
{
RunQueue* rq = this_rq();
- LockGuard guard(rq->lock());
-
- TaskControlBlock* curr = rq->current();
- if (!curr) return;
+ {
+ LockGuard guard(rq->lock());
+ TaskControlBlock* curr = rq->current();
+ if (!curr) return;
+ curr->sched_class->task_tick(rq, curr);
+ }
- curr->sched_class->task_tick(rq, curr);
+ /* Periodic load balancing — runs without holding rq lock */
+ sched_balance();
}
/* ========================================================================
- * sched_check_preempt() — called before returning to user mode
+ * sched_check_preempt() — called from trap return path
+ *
+ * Only preempts if:
+ * 1. need_resched flag is set (a higher-priority task is ready)
+ * 2. preempt_count == 0 (no spinlocks held, safe to context switch)
* ======================================================================== */
void sched_check_preempt()
{
RunQueue* rq = this_rq();
- if (rq->need_resched()) {
- schedule();
+ if (!rq->need_resched()) return;
+
+ TaskControlBlock* curr = current_task();
+ if (curr && curr->preempt_count > 0) {
+ /* Cannot preempt — caller is in a critical section.
+ * The preemption will fire when the last lock is released. */
+ return;
}
+ schedule();
}
/* ========================================================================
@@ -328,32 +366,37 @@ void sched_init()
g_sched_class_head = &g_sched_class_o1;
- TaskControlBlock* idle0 = create_tcb(idle_name_for_cpu(0), kIdlePriority,
- TF_KTHREAD | TF_IDLE, 0);
- if (!idle0) kernel_panic("sched_init: failed to create idle/0");
+ uint64 bsp_cpu = arch::cpu::id();
+ uint8 bsp_cpu8 = static_cast<uint8>(bsp_cpu);
+
+ TaskControlBlock* idle0 = alloc_task(idle_name_for_cpu(static_cast<uint32>(bsp_cpu)),
+ kIdlePriority,
+ TF_KTHREAD | TF_IDLE, bsp_cpu8);
+ if (!idle0) kernel_panic("sched_init: failed to create idle/bsp");
idle0->pid = 0;
idle0->tgid = 0;
idle0->sched_class = &g_sched_class_idle;
idle0->sched_entity = nullptr;
- uint64 stack_top = idle0->kstack_va + kKernelStackSize;
+ uint64 stack_top = idle0->kstack_va + kKernelStackSize
+ - sizeof(arch::TrapFrame);
idle0->context = {};
idle0->context.ra = reinterpret_cast<uint64>(kthread_entry_trampoline);
idle0->context.sp = stack_top;
idle0->context.s[0] = reinterpret_cast<uint64>(idle_thread_func);
idle0->context.s[1] = 0;
- g_rq[0].set_idle(idle0);
+ g_rq[bsp_cpu].set_idle(idle0);
- TaskControlBlock* bsp_task = create_tcb("kernel_main", 0, TF_KTHREAD, 0);
+ TaskControlBlock* bsp_task = alloc_task("kernel_main", 0, TF_KTHREAD, bsp_cpu8);
if (!bsp_task) kernel_panic("sched_init: failed to create bsp_task");
bsp_task->pid = -1;
bsp_task->tgid = -1;
bsp_task->state = TaskState::Running;
- g_rq[0].set_current(bsp_task);
+ g_rq[bsp_cpu].set_current(bsp_task);
}
/* ========================================================================
@@ -366,7 +409,7 @@ void sched_init_ap()
const char* name = idle_name_for_cpu(static_cast<uint32>(cpu));
- TaskControlBlock* idle = create_tcb(name, kIdlePriority,
+ TaskControlBlock* idle = alloc_task(name, kIdlePriority,
TF_KTHREAD | TF_IDLE,
static_cast<uint8>(cpu));
if (!idle) kernel_panic("sched_init_ap: failed to create idle");
@@ -376,7 +419,8 @@ void sched_init_ap()
idle->sched_class = &g_sched_class_idle;
idle->sched_entity = nullptr;
- uint64 stack_top = idle->kstack_va + kKernelStackSize;
+ uint64 stack_top = idle->kstack_va + kKernelStackSize
+ - sizeof(arch::TrapFrame);
idle->context = {};
idle->context.ra = reinterpret_cast<uint64>(kthread_entry_trampoline);
idle->context.sp = stack_top;
@@ -385,7 +429,7 @@ void sched_init_ap()
g_rq[cpu].set_idle(idle);
- TaskControlBlock* ap_task = create_tcb("ap_boot", 0, TF_KTHREAD,
+ TaskControlBlock* ap_task = alloc_task("ap_boot", 0, TF_KTHREAD,
static_cast<uint8>(cpu));
if (!ap_task) kernel_panic("sched_init_ap: failed to create ap_task");
@@ -395,3 +439,133 @@ void sched_init_ap()
g_rq[cpu].set_current(ap_task);
}
+
+/* ========================================================================
+ * sched_nr_running() — runnable task count for a CPU
+ * ======================================================================== */
+
+uint32 sched_nr_running(uint32 cpu_id)
+{
+ if (cpu_id >= kMaxCpus) return 0;
+ return g_rq[cpu_id].o1().nr_running();
+}
+
+/* ========================================================================
+ * sched_migrate_task() — move a task between CPUs
+ *
+ * Locks are acquired in cpu_id order to prevent ABBA deadlock.
+ * The task must be Runnable (in a runqueue) and not currently running.
+ * ======================================================================== */
+
+int sched_migrate_task(TaskControlBlock* task, uint8 target_cpu)
+{
+ if (!task || target_cpu >= kMaxCpus) return -22; /* -EINVAL */
+
+ uint8 src_cpu = task->cpu_id;
+ if (src_cpu == target_cpu) return 0; /* already there */
+
+ /* Cannot migrate idle or currently-running tasks */
+ if (task->flags & TF_IDLE) return -22;
+
+ RunQueue* src_rq = rq_for_cpu(src_cpu);
+ RunQueue* dst_rq = rq_for_cpu(target_cpu);
+
+ /* Ordered lock acquisition: always lock lower cpu_id first */
+ if (src_cpu < target_cpu) {
+ src_rq->lock().acquire();
+ dst_rq->lock().acquire();
+ } else {
+ dst_rq->lock().acquire();
+ src_rq->lock().acquire();
+ }
+
+ int ret = -22;
+
+ /* Double-check: task must not be current on either CPU */
+ if (task != src_rq->current() && task->state == TaskState::Runnable) {
+ /* Dequeue from source */
+ task->sched_class->dequeue_task(src_rq, task);
+
+ /* Update cpu_id and enqueue on destination */
+ task->cpu_id = target_cpu;
+ task->sched_class->enqueue_task(dst_rq, task);
+
+ /* Signal target CPU to reschedule */
+ dst_rq->set_need_resched(true);
+ ret = 0;
+ }
+
+ /* Release in reverse order */
+ if (src_cpu < target_cpu) {
+ dst_rq->lock().release();
+ src_rq->lock().release();
+ } else {
+ src_rq->lock().release();
+ dst_rq->lock().release();
+ }
+
+ /* If migration succeeded, send reschedule IPI to target */
+ if (ret == 0) {
+ smp_send_reschedule(target_cpu);
+ }
+
+ return ret;
+}
+
+/* ========================================================================
+ * sched_balance() — periodic load balancer (pull model)
+ *
+ * Called from sched_tick() context. Looks at all other CPUs and if any
+ * has 2+ more runnable tasks than the local CPU, pulls one task.
+ * This is a simplified Linux-style load_balance().
+ * ======================================================================== */
+
+void sched_balance()
+{
+ uint32 this_cpu = static_cast<uint32>(arch::cpu::id());
+ uint32 local_load = g_rq[this_cpu].o1().nr_running();
+
+ /* Only balance if we're idle or lightly loaded */
+ if (local_load > 1) return;
+
+ uint32 busiest_cpu = this_cpu;
+ uint32 busiest_load = local_load;
+
+ /* Find the busiest CPU */
+ for (uint32 cpu = 0; cpu < kMaxCpus; cpu++) {
+ if (cpu == this_cpu) continue;
+ uint32 load = g_rq[cpu].o1().nr_running();
+ if (load > busiest_load) {
+ busiest_load = load;
+ busiest_cpu = cpu;
+ }
+ }
+
+ /* Only pull if imbalance is >= 2 */
+ if (busiest_cpu == this_cpu || busiest_load < local_load + 2) return;
+
+ /* Try to pull one task from the busiest CPU */
+ RunQueue* busy_rq = rq_for_cpu(busiest_cpu);
+
+ /* Lock ordering: lower cpu_id first */
+ RunQueue* first = (this_cpu < busiest_cpu)
+ ? &g_rq[this_cpu] : busy_rq;
+ RunQueue* second = (this_cpu < busiest_cpu)
+ ? busy_rq : &g_rq[this_cpu];
+
+ first->lock().acquire();
+ second->lock().acquire();
+
+ /* Re-check after acquiring locks */
+ TaskControlBlock* victim = busy_rq->o1().pick_next();
+ if (victim && victim != busy_rq->current()
+ && !(victim->flags & TF_IDLE)) {
+ victim->sched_class->dequeue_task(busy_rq, victim);
+ victim->cpu_id = static_cast<uint8>(this_cpu);
+ victim->sched_class->enqueue_task(&g_rq[this_cpu], victim);
+ g_rq[this_cpu].set_need_resched(true);
+ }
+
+ second->lock().release();
+ first->lock().release();
+}
diff --git a/na/kernel/signal.cc b/na/kernel/signal.cc
new file mode 100644
index 0000000..53d26da
--- /dev/null
+++ b/na/kernel/signal.cc
@@ -0,0 +1,442 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Signal Delivery — Queue, Dequeue, Default Actions, and do_signal
+ *
+ * Implements the core signal infrastructure:
+ * - send_signal(): enqueue a signal to a task's pending set
+ * - force_sig_fault(): fault-induced signal (trap handler API)
+ * - do_signal(): called on return-to-user, delivers pending signals
+ * - next_signal(): dequeue the highest-priority deliverable signal
+ *
+ * Signal frame construction (arch-specific) is in arch/riscv/signal.cc.
+ *
+ * Reference: Linux kernel/signal.c
+ */
+
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/signal_nr.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+#include <lume/wait.h>
+#include <lume/console.h>
+#include <lume/kprintf.h>
+#include <lume/klog.h>
+#include <lume/new.h>
+#include <lume/errno.h>
+#include <arch/trapframe.h>
+
+/* Forward declaration: arch-specific signal frame setup */
+int setup_signal_frame(arch::TrapFrame* tf, int signo,
+ kernel_sigaction* ka, kernel_siginfo* info);
+
+/* ================================================================== */
+/* sighand_struct lifecycle (refcount-shared, Linux copy_sighand) */
+/* ================================================================== */
+
+sighand_struct* alloc_sighand()
+{
+ auto* sh = new sighand_struct;
+ if (sh) sh->init();
+ return sh;
+}
+
+sighand_struct* copy_sighand(sighand_struct* parent)
+{
+ /* Linux: kernel/fork.c:copy_sighand() when !CLONE_SIGHAND copies
+ * the action table verbatim so the child inherits dispositions
+ * without sharing future modifications. */
+ if (!parent) return alloc_sighand();
+ auto* sh = new sighand_struct;
+ if (!sh) return nullptr;
+ sh->init();
+ LockGuard guard(parent->siglock);
+ for (int i = 0; i < _NSIG; i++) sh->action[i] = parent->action[i];
+ return sh;
+}
+
+void get_sighand(sighand_struct* sh)
+{
+ if (!sh) return;
+ LockGuard guard(sh->siglock);
+ sh->refcount++;
+}
+
+void put_sighand(sighand_struct* sh)
+{
+ if (!sh) return;
+ uint32 remaining;
+ {
+ LockGuard guard(sh->siglock);
+ remaining = --sh->refcount;
+ }
+ if (remaining == 0) delete sh;
+}
+
+/* ================================================================== */
+/* next_signal — find lowest-numbered deliverable signal */
+/* ================================================================== */
+
+/* Manual count-trailing-zeros (no libgcc __ctzdi2 dependency) */
+static inline int sig_ctz64(uint64 x)
+{
+ if (x == 0) return 64;
+ int n = 0;
+ if ((x & 0xFFFFFFFF) == 0) { n += 32; x >>= 32; }
+ if ((x & 0x0000FFFF) == 0) { n += 16; x >>= 16; }
+ if ((x & 0x000000FF) == 0) { n += 8; x >>= 8; }
+ if ((x & 0x0000000F) == 0) { n += 4; x >>= 4; }
+ if ((x & 0x00000003) == 0) { n += 2; x >>= 2; }
+ if ((x & 0x00000001) == 0) { n += 1; }
+ return n;
+}
+
+static int next_signal(sigpending* pending, const sigset_t* mask)
+{
+ /* Standard signals: check bitmap & ~blocked */
+ for (int i = 0; i < _NSIG_WORDS; i++) {
+ uint64 bits = pending->signal.sig[i] & ~mask->sig[i];
+ if (bits != 0) {
+ int sig = i * 64 + sig_ctz64(bits) + 1;
+ return sig;
+ }
+ }
+ return 0;
+}
+
+/* ================================================================== */
+/* dequeue_signal — remove a signal from the pending set */
+/* ================================================================== */
+
+/*
+ * Dequeue the next deliverable signal. MUST be called with
+ * task->sighand->siglock held. Mirrors Linux's __dequeue_signal()
+ * (kernel/signal.c) which requires the same lock.
+ */
+static int dequeue_signal_locked(TaskControlBlock* task, kernel_siginfo* info)
+{
+ int sig = next_signal(&task->pending, &task->blocked);
+ if (sig == 0) return 0;
+
+ /* Clear the bitmap bit */
+ task->pending.signal.del(sig);
+
+ /* Search the queue for a matching sigqueue entry */
+ list_node* node = task->pending.list.next;
+ while (node != &task->pending.list) {
+ auto* q = list_entry<sigqueue, &sigqueue::list>(node);
+ if (q->info.si_signo == sig) {
+ /* Remove from queue */
+ node->prev->next = node->next;
+ node->next->prev = node->prev;
+ if (info) *info = q->info;
+ delete q;
+ return sig;
+ }
+ node = node->next;
+ }
+
+ /* No queued entry — fill in minimal info */
+ if (info) {
+ info->si_signo = sig;
+ info->si_errno = 0;
+ info->si_code = 0; /* SI_USER */
+ }
+ return sig;
+}
+
+/* ================================================================== */
+/* send_signal — enqueue a signal to a task */
+/* ================================================================== */
+
+/*
+ * Drop a queued sigqueue entry with the given signo from `task`.
+ *
+ * MUST be called with task->sighand->siglock held — Linux's
+ * flush_sigqueue_mask() has the same precondition (kernel/signal.c).
+ */
+static bool discard_pending_locked(TaskControlBlock* task, int signo)
+{
+ bool any = false;
+ list_node* node = task->pending.list.next;
+ while (node != &task->pending.list) {
+ list_node* nxt = node->next;
+ auto* q = list_entry<sigqueue, &sigqueue::list>(node);
+ if (q->info.si_signo == signo) {
+ node->prev->next = node->next;
+ node->next->prev = node->prev;
+ delete q;
+ any = true;
+ }
+ node = nxt;
+ }
+ task->pending.signal.del(signo);
+ return any;
+}
+
+static inline bool is_stop_sig(int sig)
+{
+ return sig == SIGSTOP || sig == SIGTSTP ||
+ sig == SIGTTIN || sig == SIGTTOU;
+}
+
+/*
+ * send_signal — queue a signal to a task.
+ *
+ * Acquires task->sighand->siglock for the full update (bitmap, queue,
+ * stop/cont side-effects, scheduler wake). Mirrors Linux's
+ * __send_signal_locked() which also requires this lock (see
+ * kernel/signal.c:1048 lockdep_assert_held).
+ *
+ * Returns 0 on success or a negative errno.
+ */
+int send_signal(int sig, kernel_siginfo* info, TaskControlBlock* task)
+{
+ if (sig < 1 || sig > _NSIG) return -EINVAL;
+ if (!task) return -ESRCH;
+ if (!task->sighand) return -ESRCH; /* eager-alloc invariant; tasks
+ * without sighand can't receive
+ * signals (idle / early kthreads) */
+
+ LockGuard guard(task->sighand->siglock);
+
+ /*
+ * Linux's prepare_signal(): a stop signal cancels any pending
+ * SIGCONT and vice versa, so the most recent decision wins.
+ */
+ if (is_stop_sig(sig)) {
+ discard_pending_locked(task, SIGCONT);
+ } else if (sig == SIGCONT) {
+ discard_pending_locked(task, SIGSTOP);
+ discard_pending_locked(task, SIGTSTP);
+ discard_pending_locked(task, SIGTTIN);
+ discard_pending_locked(task, SIGTTOU);
+
+ /* SIGCONT's resume effect happens atomically with delivery. */
+ if (task->state == TaskState::Stopped) {
+ task->state = TaskState::Runnable;
+ task->notify_continued = true;
+ task->stopped_code = 0;
+ sched_enqueue(task);
+ if (task->parent) task->parent->wait_chld.wake_all();
+ }
+ }
+
+ /* Already pending? For standard signals, just mark the bitmap */
+ if (sig < SIGRTMIN && task->pending.signal.has(sig))
+ return 0;
+
+ /* Enqueue a sigqueue entry */
+ auto* q = new sigqueue;
+ if (!q) {
+ /* Can't allocate — just set the bitmap (signal still delivered) */
+ task->pending.signal.add(sig);
+ return 0;
+ }
+
+ q->list.init();
+ if (info) {
+ q->info = *info;
+ } else {
+ q->info.si_signo = sig;
+ q->info.si_errno = 0;
+ q->info.si_code = 0; /* SI_USER */
+ }
+ q->info.si_signo = sig; /* ensure consistent */
+
+ /* Add to tail of queue */
+ list_node* head = &task->pending.list;
+ q->list.prev = head->prev;
+ q->list.next = head;
+ head->prev->next = &q->list;
+ head->prev = &q->list;
+
+ /* Set bitmap */
+ task->pending.signal.add(sig);
+
+ /* Wake the task if it's sleeping in an interruptible wait. */
+ if (task->state == TaskState::Sleeping) {
+ task->state = TaskState::Runnable;
+ sched_enqueue(task);
+ }
+
+ /* SIGKILL must run even if the target is Stopped. */
+ if (sig == SIGKILL && task->state == TaskState::Stopped) {
+ task->state = TaskState::Runnable;
+ task->stopped_code = 0;
+ sched_enqueue(task);
+ }
+
+ return 0;
+}
+
+/* ================================================================== */
+/* force_sig_fault — trap handler API */
+/* ================================================================== */
+
+void force_sig_fault(int signo, int code, uint64 addr)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr) return;
+
+ /* Kernel threads cannot handle signals — kill immediately */
+ if (curr->flags & TF_KTHREAD) {
+ pr_err("signal: kernel thread %s received signal %d\n",
+ curr->name ? curr->name : "(noname)", signo);
+ task_kill_current(signo);
+ return;
+ }
+
+ /* Sighand must already exist for user-mode tasks (allocated at
+ * fork / kernel_execve time). If it's somehow missing, the only
+ * safe action is to terminate. */
+ if (!curr->sighand) {
+ pr_err("signal: pid %d has no sighand, killing\n", curr->pid);
+ task_kill_current(signo);
+ return;
+ }
+
+ kernel_siginfo info{};
+ info.si_signo = signo;
+ info.si_code = code;
+ info._sifields._sigfault._addr = addr;
+
+ /* Force: bypass blocked mask for fatal signals (lock to keep
+ * blocked-mask updates consistent with sigprocmask). */
+ {
+ LockGuard guard(curr->sighand->siglock);
+ curr->blocked.del(signo);
+ }
+
+ send_signal(signo, &info, curr);
+}
+
+/* ================================================================== */
+/* signal_pending — check if unblocked signals are pending */
+/* ================================================================== */
+
+bool signal_pending(TaskControlBlock* task)
+{
+ /* Advisory check (matches Linux's recalc_sigpending() usage):
+ * callers re-validate under siglock before acting. The bitmap is
+ * a uint64 word; a torn read can only produce false-positives or
+ * false-negatives transiently, never a corrupt pointer. */
+ if (!task) return false;
+ sigset_t unblocked = task->pending.signal & ~task->blocked;
+ return !unblocked.is_empty();
+}
+
+/* ================================================================== */
+/* do_signal — called before returning to user mode */
+/* ================================================================== */
+
+void do_signal(arch::TrapFrame* tf)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr) return;
+
+ /* Kernel threads never deliver signals via frames */
+ if (curr->flags & TF_KTHREAD) return;
+
+ /* No sighand means no signal infrastructure for this task */
+ if (!curr->sighand) return;
+
+ while (true) {
+ kernel_siginfo info{};
+ kernel_sigaction ka_copy;
+ int sig;
+
+ /*
+ * Take siglock for dequeue + action snapshot. Action processing
+ * (default actions like SIG_ACT_TERM, or constructing a signal
+ * frame for a user handler) is done OUTSIDE the lock — it can
+ * touch user memory and even call schedule(). This matches
+ * Linux's get_signal() loop in kernel/signal.c: it takes
+ * siglock around dequeue_signal+ka_copy, drops it before
+ * dispatching, and re-acquires next iteration.
+ */
+ {
+ LockGuard guard(curr->sighand->siglock);
+ sig = dequeue_signal_locked(curr, &info);
+ if (sig == 0) break;
+ ka_copy = curr->sighand->action[sig - 1];
+ }
+ kernel_sigaction* ka = &ka_copy;
+
+ /* SIG_IGN: discard */
+ if (ka->sa_handler == SIG_IGN && !sig_kernel_only(sig))
+ continue;
+
+ /* SIG_DFL: apply default action */
+ if (ka->sa_handler == SIG_DFL) {
+ SigDefaultAction act = sig_default_action(sig);
+ switch (act) {
+ case SIG_ACT_IGN:
+ continue;
+ case SIG_ACT_TERM:
+ case SIG_ACT_CORE:
+ pr_debug_s(KLOG_SUBSYS_SIGNAL,
+ "signal: pid %d killed by signal %d\n",
+ curr->pid, sig);
+ task_kill_current(sig);
+ return; /* never reached */
+ case SIG_ACT_STOP: {
+ /*
+ * Enter Stopped state. Record the encoded wstatus so
+ * the parent's wait4(WUNTRACED) can observe and report.
+ * The schedule() below does NOT re-enqueue Stopped
+ * tasks — when SIGCONT eventually arrives, send_signal
+ * flips us back to Runnable and re-enqueues.
+ */
+ curr->stopped_code = W_STOPCODE(sig);
+ curr->state = TaskState::Stopped;
+ if (curr->parent)
+ curr->parent->wait_chld.wake_all();
+ pr_debug_s(KLOG_SUBSYS_SIGNAL,
+ "signal: pid %d stopped by sig %d\n",
+ curr->pid, sig);
+ schedule();
+ /*
+ * Resumed (either by SIGCONT or SIGKILL). Loop back to
+ * deliver any remaining pending signals before returning
+ * to user mode.
+ */
+ continue;
+ }
+ case SIG_ACT_CONT:
+ /* SIGCONT's resume side-effect already happened in
+ * send_signal(). The only thing left at handler time
+ * is to consume the signal (no-op for SIG_DFL). */
+ continue;
+ }
+ }
+
+ /* User handler: construct signal frame */
+ pr_debug_s(KLOG_SUBSYS_SIGNAL,
+ "signal: pid %d delivering sig %d handler=%llx\n",
+ curr->pid, sig, reinterpret_cast<uint64>(ka->sa_handler));
+ int ret = setup_signal_frame(tf, sig, ka, &info);
+ if (ret < 0) {
+ pr_err("signal: failed to set up frame for sig %d\n", sig);
+ task_kill_current(sig);
+ return;
+ }
+
+ /* Post-delivery bookkeeping under siglock — both the per-task
+ * blocked mask and the shared action table (SA_RESETHAND) are
+ * touched, so we must hold the same lock the senders use. */
+ {
+ LockGuard guard(curr->sighand->siglock);
+ if (!(ka->sa_flags & SA_NODEFER)) curr->blocked.add(sig);
+ curr->blocked = curr->blocked | ka->sa_mask;
+ if (ka->sa_flags & SA_RESETHAND) {
+ curr->sighand->action[sig - 1].sa_handler = SIG_DFL;
+ }
+ }
+
+ /* Only deliver one signal per return-to-user */
+ break;
+ }
+}
diff --git a/na/kernel/smp.cc b/na/kernel/smp.cc
new file mode 100644
index 0000000..1a79ac3
--- /dev/null
+++ b/na/kernel/smp.cc
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * smp.cc — SMP IPI implementation
+ *
+ * Sending side: atomically set reason bits in target CPU's IpiData,
+ * then invoke arch-specific IPI delivery (SBI ecall on RISC-V).
+ *
+ * Receiving side: smp_handle_ipi() is called from the trap handler
+ * when an S-mode software interrupt arrives. It atomically consumes
+ * all pending reason bits and dispatches to the appropriate handler.
+ *
+ * Reference: docs/specs/linux_abi_mm_plan.md §SMP §2.2
+ */
+
+#include <lume/smp.h>
+#include <lume/klog.h>
+#include <lume/sched.h>
+#include <arch/cpu.h>
+#include <arch/mmu.h>
+#include <arch/sbi.h>
+
+/* Per-CPU IPI data — BSS zero-initialized */
+IpiData g_ipi_data[kMaxCpus];
+
+/* ------------------------------------------------------------------ */
+/* smp_init */
+/* ------------------------------------------------------------------ */
+
+void smp_init()
+{
+ for (uint32 i = 0; i < kMaxCpus; i++) {
+ g_ipi_data[i].pending.store(0, __ATOMIC_RELAXED);
+ g_ipi_data[i].tlb_start = 0;
+ g_ipi_data[i].tlb_end = 0;
+ }
+
+ printk(KERN_INFO "smp: IPI framework initialized (%u CPUs max)\n",
+ static_cast<unsigned>(kMaxCpus));
+}
+
+/* ------------------------------------------------------------------ */
+/* smp_send_ipi — send IPI to target CPUs */
+/* ------------------------------------------------------------------ */
+
+void smp_send_ipi(uint64 cpu_mask, uint32 reason)
+{
+ if (cpu_mask == 0) return;
+
+ /* Set pending bits on each target CPU's IpiData */
+ for (uint32 cpu = 0; cpu < kMaxCpus; cpu++) {
+ if (cpu_mask & (1ULL << cpu)) {
+ g_ipi_data[cpu].pending.fetch_or(reason, __ATOMIC_RELEASE);
+ }
+ }
+
+ /* Deliver hardware interrupt via SBI.
+ * RISC-V SBI IPI extension sends S-mode software interrupt
+ * to the specified harts. hart_mask maps 1:1 to cpu_mask
+ * (assuming hartid == cpu_id, which holds for QEMU virt). */
+ arch::sbi::sbi_send_ipi(cpu_mask, 0);
+}
+
+/* ------------------------------------------------------------------ */
+/* smp_handle_ipi — called from trap handler on software interrupt */
+/* ------------------------------------------------------------------ */
+
+void smp_handle_ipi()
+{
+ uint32 cpu = static_cast<uint32>(arch::cpu::id());
+
+ /* Clear the S-mode software interrupt pending bit (sip.SSIP).
+ * On RISC-V, we must clear this in software, otherwise the
+ * interrupt will keep firing. */
+ arch::cpu::clear_sip_ssip();
+
+ /* Atomically consume all pending reasons */
+ uint32 reasons = g_ipi_data[cpu].pending.exchange(0, __ATOMIC_ACQUIRE);
+
+ if (reasons == 0) return; /* Spurious */
+
+ /* Dispatch each reason */
+
+ if (reasons & IPI_RESCHEDULE) {
+ /* Just need to return from interrupt — schedule() will be
+ * called on the return-from-trap path if need_resched is set.
+ * For now, set the flag by yielding on next opportunity. */
+ /* TODO: set need_resched flag when preemption tracking is added */
+ }
+
+ if (reasons & IPI_TLB_SHOOTDOWN) {
+ /* Execute local TLB flush with the parameters the sender stored */
+ uint64 start = g_ipi_data[cpu].tlb_start;
+ uint64 end = g_ipi_data[cpu].tlb_end;
+ if (start == 0 && end == 0) {
+ arch::mmu::flush_tlb_all();
+ } else {
+ for (uint64 va = start; va < end; va += kPageSize) {
+ arch::mmu::flush_tlb_page(va);
+ }
+ }
+ }
+
+ if (reasons & IPI_HALT) {
+ printk(KERN_INFO "smp: CPU %u halting by IPI\n", cpu);
+ arch::cpu::intr_off();
+ while (true) {
+ arch::cpu::halt_until_interrupt();
+ }
+ }
+}
diff --git a/na/kernel/sync/mutex.cc b/na/kernel/sync/mutex.cc
new file mode 100644
index 0000000..c4d3e9d
--- /dev/null
+++ b/na/kernel/sync/mutex.cc
@@ -0,0 +1,156 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Mutex — Linux-style sleeping mutex (no PI).
+ *
+ * Implementation notes (mirroring kernel/locking/mutex.c, simplified):
+ *
+ * - Fast path (uncontended): wait_lock_ -> see owner_==null -> claim -> unlock.
+ * - Slow path (contended): enqueue MutexWaiter on stack, sleep, race for
+ * ownership after each wake. unlock() wakes only the head waiter.
+ * - No optimistic spin, no handoff anti-starvation, no interruptible wait.
+ * These belong to follow-up work (see mutex.md §5).
+ *
+ * Reference: docs/specs/mutex.md
+ */
+
+#include <lume/mutex.h>
+#include <lume/task.h>
+#include <lume/sched.h>
+#include <lume/klog.h>
+#include <arch/cpu.h>
+
+namespace {
+
+struct MutexWaiter {
+ list_node link;
+ TaskControlBlock* task;
+};
+
+} // namespace
+
+/* ------------------------------------------------------------------ */
+/* init */
+/* ------------------------------------------------------------------ */
+
+void Mutex::init(const char* name)
+{
+ owner_ = nullptr;
+ name_ = name;
+ wait_lock_.init(name != nullptr ? name : "mutex");
+ wait_list_.init();
+}
+
+/* ------------------------------------------------------------------ */
+/* try_lock — non-blocking, safe from any context */
+/* ------------------------------------------------------------------ */
+
+bool Mutex::try_lock()
+{
+ LockGuard g(wait_lock_);
+ if (owner_ != nullptr) {
+ return false;
+ }
+ owner_ = current_task();
+ return true;
+}
+
+/* ------------------------------------------------------------------ */
+/* lock — blocking, must be called in schedulable context */
+/* ------------------------------------------------------------------ */
+
+void Mutex::lock()
+{
+ TaskControlBlock* curr = current_task();
+
+ if (curr == nullptr) {
+ kernel_panic("Mutex::lock: no current task");
+ }
+ if (curr->preempt_count > 0) {
+ kernel_panic("Mutex::lock: preemption disabled (holding spinlock?)");
+ }
+
+ wait_lock_.acquire();
+
+ if (owner_ == nullptr) {
+ owner_ = curr;
+ wait_lock_.release();
+ return;
+ }
+
+ if (owner_ == curr) {
+ wait_lock_.release();
+ kernel_panic("Mutex::lock: recursive acquire by same task");
+ }
+
+ /* Slow path: enqueue ourselves and sleep until we are head + lock free. */
+ MutexWaiter waiter;
+ waiter.link.init();
+ waiter.task = curr;
+ list_add_tail(&waiter.link, &wait_list_);
+
+ for (;;) {
+ /* Set state inside wait_lock_ so unlock-side sees a coherent view:
+ * either we are on the list AND Sleeping (will be woken), or we
+ * already took ownership above (not in this loop). */
+ curr->state = TaskState::Sleeping;
+ wait_lock_.release();
+
+ schedule();
+
+ wait_lock_.acquire();
+
+ if (owner_ == nullptr && wait_list_.next == &waiter.link) {
+ owner_ = curr;
+ list_del_init(&waiter.link);
+ wait_lock_.release();
+ return;
+ }
+ /* Spurious wake / lost race — keep waiting. */
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* unlock */
+/* ------------------------------------------------------------------ */
+
+void Mutex::unlock()
+{
+ TaskControlBlock* curr = current_task();
+
+ wait_lock_.acquire();
+
+ if (owner_ != curr) {
+ wait_lock_.release();
+ kernel_panic("Mutex::unlock: not the owner");
+ }
+
+ owner_ = nullptr;
+
+ if (!wait_list_.is_empty()) {
+ list_node* first = wait_list_.next;
+ MutexWaiter* w = list_entry<MutexWaiter, &MutexWaiter::link>(first);
+ TaskControlBlock* victim = w->task;
+
+ /* Race-tolerant wake: only enqueue if still sleeping. If the
+ * waiter is already Runnable (e.g. it was already woken by a
+ * timer-driven schedule reentry and didn't yet retry the lock),
+ * sched_enqueue would double-link it. */
+ if (victim->state == TaskState::Sleeping) {
+ victim->state = TaskState::Runnable;
+ sched_enqueue(victim);
+ }
+ }
+
+ wait_lock_.release();
+}
+
+/* ------------------------------------------------------------------ */
+/* Debug accessors */
+/* ------------------------------------------------------------------ */
+
+bool Mutex::is_owned_by_current() const
+{
+ return owner_ == current_task();
+}
diff --git a/na/kernel/sync/spinlock.cc b/na/kernel/sync/spinlock.cc
index bca4684..f649fe7 100644
--- a/na/kernel/sync/spinlock.cc
+++ b/na/kernel/sync/spinlock.cc
@@ -17,6 +17,7 @@
#include "cpu_state.h"
#include <arch/cpu.h>
#include <lume/klog.h>
+#include <lume/task.h>
// ============================================================
// Per-CPU sync state (BSS zero-initialized)
@@ -88,6 +89,10 @@ void Spinlock::acquire() {
// Step 4: record owner
cpu_id_ = arch::cpu::id();
+
+ // Step 5: bump preempt_count — prevents schedule() from preempting
+ TaskControlBlock* curr = current_task();
+ if (curr) curr->preempt_count++;
}
void Spinlock::release() {
@@ -102,7 +107,11 @@ void Spinlock::release() {
// Step 3: release the lock with release semantics
locked_.store(0, __ATOMIC_RELEASE);
- // Step 4: restore interrupt state if this was the last lock
+ // Step 4: drop preempt_count
+ TaskControlBlock* curr = current_task();
+ if (curr) curr->preempt_count--;
+
+ // Step 5: restore interrupt state if this was the last lock
sync::pop_intr();
}
diff --git a/na/kernel/sys_fs.cc b/na/kernel/sys_fs.cc
new file mode 100644
index 0000000..1d26fdd
--- /dev/null
+++ b/na/kernel/sys_fs.cc
@@ -0,0 +1,679 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * sys_fs.cc — Filesystem-related syscall handlers
+ *
+ * Implements: openat, close, read, write, lseek, fstat, getdents64,
+ * dup, dup3, getcwd, chdir, mkdirat, unlinkat
+ */
+
+#include <lume/syscall.h>
+#include <lume/syscall_nr.h>
+#include <lume/errno.h>
+#include <lume/console.h>
+#include <lume/task.h>
+#include <lume/fs.h>
+#include <lume/file.h>
+#include <lume/dcache.h>
+#include <lume/namei.h>
+#include <lume/fdtable.h>
+#include <lume/fs_struct.h>
+#include <lume/mount.h>
+#include <lume/fs_types.h>
+#include <lume/klog.h>
+
+/* ================================================================== */
+/* Helpers */
+/* ================================================================== */
+
+static files_struct* current_files()
+{
+ TaskControlBlock* t = current_task();
+ return t ? t->files : nullptr;
+}
+
+static fs_struct* current_fs()
+{
+ TaskControlBlock* t = current_task();
+ return t ? t->fs : nullptr;
+}
+
+/* ================================================================== */
+/* sys_openat(dirfd, pathname, flags, mode) */
+/* ================================================================== */
+
+int64 sys_openat(uint64 a0, uint64 a1, uint64 a2,
+ uint64 a3, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int dirfd = static_cast<int>(a0);
+ const char* pathname = reinterpret_cast<const char*>(a1);
+ int flags = static_cast<int>(a2);
+ uint32 mode = static_cast<uint32>(a3);
+
+ files_struct* files = current_files();
+ if (!files) return -EMFILE;
+
+
+ /* Pathname lookup */
+ path p{};
+ int err = filename_lookup(dirfd, pathname, 0, &p);
+
+ if (err == -ENOENT && (flags & O_CREAT)) {
+ /* Create: look up the parent directory */
+ /* Find last '/' to split parent path / filename */
+ const char* last_slash = nullptr;
+ for (const char* s = pathname; *s; s++) {
+ if (*s == '/') last_slash = s;
+ }
+
+ if (!last_slash || last_slash == pathname) {
+ /* File in root or current directory */
+ path parent{};
+ if (last_slash == pathname) {
+ /* Absolute: parent is root */
+ parent.de = g_root_dentry;
+ parent.mnt = g_root_mnt;
+ } else {
+ /* Relative: parent is cwd */
+ fs_struct* fs = current_fs();
+ if (!fs) return -ENOENT;
+ parent = fs->pwd;
+ }
+
+ const char* name = last_slash ? last_slash + 1 : pathname;
+ inode* dir_ip = parent.de->d_inode;
+ if (!dir_ip || !dir_ip->i_op || !dir_ip->i_op->create) {
+ return -EACCES;
+ }
+
+ /* Create the inode via the directory's i_op->create */
+ qstr q{};
+ q.name = name;
+ q.len = 0;
+ for (const char* s = name; *s; s++) q.len++;
+ dentry* new_de = d_alloc(parent.de, &q);
+ if (!new_de) return -ENOMEM;
+
+
+ err = dir_ip->i_op->create(dir_ip, new_de, mode & ~S_IFMT, false);
+ if (err) {
+ /* TODO: free new_de */
+ return err;
+ }
+
+ p.de = new_de;
+ p.mnt = parent.mnt;
+ } else {
+ /* TODO: multi-component parent lookup */
+ return -ENOENT;
+ }
+ } else if (err) {
+ return err;
+ }
+
+ /* Allocate fd */
+ int fd = alloc_fd(files, 0);
+ if (fd < 0) {
+ return fd;
+ }
+
+ /* Allocate file object */
+ file* fp = alloc_empty_file(flags);
+ if (!fp) {
+ /* TODO: release fd */
+ return -ENFILE;
+ }
+
+ fp->f_inode = p.de->d_inode;
+ fp->f_path = p;
+ fp->f_op = p.de->d_inode->i_fop;
+ fp->f_pos = 0;
+
+ /* Chardev: look up cdev by i_rdev and replace f_op */
+ if (S_ISCHR(fp->f_inode->i_mode)) {
+ extern int devfs_chrdev_open(inode* ip, file* fp);
+ err = devfs_chrdev_open(fp->f_inode, fp);
+ if (err) {
+ fput(fp);
+ return err;
+ }
+ } else if (fp->f_op && fp->f_op->open) {
+ err = fp->f_op->open(fp->f_inode, fp);
+ if (err) {
+ fput(fp);
+ return err;
+ }
+ }
+
+ /* O_TRUNC for regular files */
+ if ((flags & O_TRUNC) && S_ISREG(fp->f_inode->i_mode)) {
+ fp->f_inode->i_size = 0;
+ }
+
+ fd_install(files, fd, fp);
+ return fd;
+}
+
+/* ================================================================== */
+/* sys_close(fd) */
+/* ================================================================== */
+
+int64 sys_close(uint64 a0, uint64 /*a1*/, uint64 /*a2*/,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+ return close_fd(files, fd);
+}
+
+/* ================================================================== */
+/* sys_read(fd, buf, count) */
+/* ================================================================== */
+
+int64 sys_read(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ char* buf = reinterpret_cast<char*>(a1);
+ uint64 count = a2;
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+
+ if (!(fp->f_mode & FMODE_READ)) return -EBADF;
+
+ return vfs_read(fp, buf, count, &fp->f_pos);
+}
+
+/* ================================================================== */
+/* sys_write(fd, buf, count) */
+/* ================================================================== */
+
+int64 sys_write(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ const char* buf = reinterpret_cast<const char*>(a1);
+ uint64 count = a2;
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+
+ if (!(fp->f_mode & FMODE_WRITE)) return -EBADF;
+
+ return vfs_write(fp, buf, count, &fp->f_pos);
+}
+
+/* ================================================================== */
+/* sys_lseek(fd, offset, whence) */
+/* ================================================================== */
+
+int64 sys_lseek(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ int64 offset = static_cast<int64>(a1);
+ int whence = static_cast<int>(a2);
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+
+ return vfs_llseek(fp, offset, whence);
+}
+
+/* ================================================================== */
+/* sys_fstat(fd, statbuf) */
+/* ================================================================== */
+
+int64 sys_fstat_wrapper(uint64 a0, uint64 a1, uint64 /*a2*/,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ auto* statbuf = reinterpret_cast<kstat*>(a1);
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+
+ return vfs_fstat(fp, statbuf);
+}
+
+/* ================================================================== */
+/* sys_getdents64(fd, dirp, count) */
+/* ================================================================== */
+
+struct linux_dirent64 {
+ uint64 d_ino;
+ int64 d_off;
+ uint16 d_reclen;
+ uint8 d_type;
+ char d_name[];
+};
+
+struct getdents_ctx {
+ dir_context ctx;
+ char* buf;
+ int count;
+ int written;
+ int error;
+};
+
+static bool filldir64(dir_context* ctx, const char* name, int namelen,
+ int64 offset, uint64 ino, unsigned int d_type)
+{
+ auto* gctx = container_of(ctx, getdents_ctx, ctx);
+
+ /* Calculate record length (rounded up to 8 bytes) */
+ int reclen = static_cast<int>(
+ (sizeof(linux_dirent64) + namelen + 1 + 7) & ~7ULL);
+
+ if (gctx->written + reclen > gctx->count) {
+ return false; /* Buffer full */
+ }
+
+ auto* dirent = reinterpret_cast<linux_dirent64*>(
+ gctx->buf + gctx->written);
+
+ dirent->d_ino = ino;
+ dirent->d_off = offset + 1;
+ dirent->d_reclen = static_cast<uint16>(reclen);
+ dirent->d_type = static_cast<uint8>(d_type);
+
+ /* Copy name */
+ for (int i = 0; i < namelen; i++) {
+ dirent->d_name[i] = name[i];
+ }
+ dirent->d_name[namelen] = '\0';
+
+ gctx->written += reclen;
+ return true;
+}
+
+int64 sys_getdents64(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ char* buf = reinterpret_cast<char*>(a1);
+ int count = static_cast<int>(a2);
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+
+ if (!fp->f_op || !fp->f_op->iterate_shared) {
+ return -ENOTDIR;
+ }
+
+ getdents_ctx gctx{};
+ gctx.ctx.actor = filldir64;
+ gctx.ctx.pos = fp->f_pos;
+ gctx.buf = buf;
+ gctx.count = count;
+ gctx.written = 0;
+ gctx.error = 0;
+
+ fp->f_op->iterate_shared(fp, &gctx.ctx);
+ fp->f_pos = gctx.ctx.pos;
+
+ return gctx.written;
+}
+
+/* ================================================================== */
+/* sys_dup(oldfd) */
+/* ================================================================== */
+
+int64 sys_dup(uint64 a0, uint64 /*a1*/, uint64 /*a2*/,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int oldfd = static_cast<int>(a0);
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, oldfd);
+ if (!fp) return -EBADF;
+
+ int newfd = alloc_fd(files, 0);
+ if (newfd < 0) return newfd;
+
+ get_file(fp);
+ fd_install(files, newfd, fp);
+ return newfd;
+}
+
+/* ================================================================== */
+/* sys_dup3(oldfd, newfd, flags) */
+/* ================================================================== */
+
+int64 sys_dup3(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int oldfd = static_cast<int>(a0);
+ int newfd = static_cast<int>(a1);
+ int flags = static_cast<int>(a2);
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ return do_dup2(files, oldfd, newfd, flags);
+}
+
+/* ================================================================== */
+/* sys_getcwd(buf, size) */
+/* ================================================================== */
+
+int64 sys_getcwd(uint64 a0, uint64 a1, uint64 /*a2*/,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ char* buf = reinterpret_cast<char*>(a0);
+ uint64 size = a1;
+
+ if (!buf || size == 0) return -EINVAL;
+
+ fs_struct* fs = current_fs();
+ if (!fs) return -ENOENT;
+
+ char tmp[256];
+ char* result = d_path(&fs->pwd, tmp, sizeof(tmp));
+ if (!result) return -ERANGE;
+
+ /* Calculate length */
+ int len = 0;
+ for (const char* s = result; *s; s++) len++;
+ len++; /* Include NUL */
+
+ if (static_cast<uint64>(len) > size) return -ERANGE;
+
+ for (int i = 0; i < len; i++) {
+ buf[i] = result[i];
+ }
+
+ return len;
+}
+
+/* ================================================================== */
+/* sys_chdir(pathname) */
+/* ================================================================== */
+
+int64 sys_chdir(uint64 a0, uint64 /*a1*/, uint64 /*a2*/,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ const char* pathname = reinterpret_cast<const char*>(a0);
+
+ path p{};
+ int err = filename_lookup(AT_FDCWD, pathname, LOOKUP_DIRECTORY, &p);
+ if (err) return err;
+
+ if (!S_ISDIR(p.de->d_inode->i_mode)) {
+ path_put(&p);
+ return -ENOTDIR;
+ }
+
+ fs_struct* fs = current_fs();
+ if (!fs) {
+ path_put(&p);
+ return -ENOENT;
+ }
+
+ set_fs_pwd(fs, &p);
+ path_put(&p);
+ return 0;
+}
+
+/* ================================================================== */
+/* sys_mkdirat(dirfd, pathname, mode) */
+/* ================================================================== */
+
+int64 sys_mkdirat(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int dirfd = static_cast<int>(a0);
+ const char* pathname = reinterpret_cast<const char*>(a1);
+ uint32 mode = static_cast<uint32>(a2);
+
+ /* Walk to the parent directory */
+ /* For simplicity, support only "single component" paths for now */
+ path parent{};
+ int err;
+
+ if (pathname[0] == '/') {
+ parent.de = g_root_dentry;
+ parent.mnt = g_root_mnt;
+ pathname++; /* skip leading / */
+ } else {
+ fs_struct* fs = current_fs();
+ if (!fs) return -ENOENT;
+ parent = fs->pwd;
+ (void)dirfd; /* TODO: respect dirfd for AT_FDCWD check */
+ }
+
+ inode* dir_ip = parent.de->d_inode;
+ if (!dir_ip || !dir_ip->i_op || !dir_ip->i_op->mkdir) {
+ return -EPERM;
+ }
+
+ qstr q{};
+ q.name = pathname;
+ q.len = 0;
+ for (const char* s = pathname; *s; s++) q.len++;
+ dentry* new_de = d_alloc(parent.de, &q);
+ if (!new_de) return -ENOMEM;
+
+ err = dir_ip->i_op->mkdir(dir_ip, new_de, mode | S_IFDIR);
+ if (err) {
+ return err;
+ }
+
+ return 0;
+}
+
+/* ================================================================== */
+/* sys_unlinkat(dirfd, pathname, flags) */
+/* ================================================================== */
+
+int64 sys_unlinkat(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ (void)a0; (void)a1; (void)a2;
+ /* TODO: implement unlink/rmdir */
+ return -ENOSYS;
+}
+
+/* ================================================================== */
+/* sys_writev(fd, iov, iovcnt) */
+/* ================================================================== */
+
+struct iovec {
+ void* iov_base;
+ uint64 iov_len;
+};
+
+int64 sys_writev(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ const iovec* iov = reinterpret_cast<const iovec*>(a1);
+ int iovcnt = static_cast<int>(a2);
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+ if (!(fp->f_mode & FMODE_WRITE)) return -EBADF;
+
+ int64 total = 0;
+ for (int i = 0; i < iovcnt; i++) {
+ if (iov[i].iov_len == 0) continue;
+ int64 ret = vfs_write(fp, static_cast<const char*>(iov[i].iov_base),
+ iov[i].iov_len, &fp->f_pos);
+ if (ret < 0) {
+ return total > 0 ? total : ret;
+ }
+ total += ret;
+ if (static_cast<uint64>(ret) < iov[i].iov_len) break;
+ }
+ return total;
+}
+
+/* ================================================================== */
+/* sys_ioctl — dispatch to file_operations */
+/* ================================================================== */
+
+int64 sys_ioctl(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ unsigned int cmd = static_cast<unsigned int>(a1);
+ uint64 arg = a2;
+
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->files) return -EBADF;
+
+ file* fp = fcheck(curr->files, fd);
+ if (!fp) return -EBADF;
+
+ if (fp->f_op && fp->f_op->unlocked_ioctl)
+ return fp->f_op->unlocked_ioctl(fp, cmd, arg);
+
+ return -ENOTTY;
+}
+
+/* ================================================================== */
+/* sys_pipe2(fds, flags) */
+/* ================================================================== */
+
+#include <lume/pipe.h>
+
+int64 sys_pipe2(uint64 a0, uint64 a1, uint64 /*a2*/,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int* user_fds = reinterpret_cast<int*>(a0);
+ int flags = static_cast<int>(a1);
+
+ files_struct* files = current_files();
+ if (!files) return -EMFILE;
+
+ int fds[2];
+ int err = do_pipe2(files, fds, flags);
+ if (err) return err;
+
+ user_fds[0] = fds[0];
+ user_fds[1] = fds[1];
+ return 0;
+}
+
+/* ================================================================== */
+/* sys_fcntl(fd, cmd, arg) */
+/* ================================================================== */
+
+/* fcntl commands — Linux ABI values */
+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;
+
+/* Bitmap helpers (duplicated from fd.cc — TODO: move to shared header) */
+static inline bool fcntl_bit_test(const uint64* bmap, int bit) {
+ return (bmap[bit / 64] >> (bit % 64)) & 1;
+}
+static inline void fcntl_bit_set(uint64* bmap, int bit) {
+ bmap[bit / 64] |= (1ULL << (bit % 64));
+}
+static inline void fcntl_bit_clear(uint64* bmap, int bit) {
+ bmap[bit / 64] &= ~(1ULL << (bit % 64));
+}
+
+int64 sys_fcntl(uint64 a0, uint64 a1, uint64 a2,
+ uint64 /*a3*/, uint64 /*a4*/, uint64 /*a5*/)
+{
+ int fd = static_cast<int>(a0);
+ int cmd = static_cast<int>(a1);
+ uint64 arg = a2;
+
+ files_struct* files = current_files();
+ if (!files) return -EBADF;
+
+ file* fp = fcheck(files, fd);
+ if (!fp) return -EBADF;
+
+ switch (cmd) {
+ case F_DUPFD:
+ case F_DUPFD_CLOEXEC: {
+ int new_fd = alloc_fd(files, static_cast<unsigned int>(arg));
+ if (new_fd < 0) return new_fd;
+ get_file(fp);
+ fd_install(files, new_fd, fp);
+ if (cmd == F_DUPFD_CLOEXEC) {
+ LockGuard guard(files->file_lock);
+ fcntl_bit_set(files->close_on_exec_init, new_fd);
+ }
+ return new_fd;
+ }
+
+ case F_GETFD: {
+ LockGuard guard(files->file_lock);
+ return fcntl_bit_test(files->close_on_exec_init, fd) ? FD_CLOEXEC : 0;
+ }
+
+ case F_SETFD: {
+ LockGuard guard(files->file_lock);
+ if (arg & FD_CLOEXEC)
+ fcntl_bit_set(files->close_on_exec_init, fd);
+ else
+ fcntl_bit_clear(files->close_on_exec_init, fd);
+ return 0;
+ }
+
+ case F_GETFL:
+ return fp->f_flags;
+
+ case F_SETFL:
+ /* Only allow changing O_APPEND, O_NONBLOCK, O_ASYNC */
+ fp->f_flags = (fp->f_flags & ~(O_APPEND | O_NONBLOCK)) |
+ (static_cast<unsigned int>(arg) & (O_APPEND | O_NONBLOCK));
+ return 0;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+/* ================================================================== */
+/* sys_socket (SYS_socket = 198) & sys_sendto (SYS_sendto = 206) */
+/* ================================================================== */
+
+int64 sys_socket(uint64, uint64, uint64, uint64, uint64, uint64)
+{
+ /*
+ * COMPATIBILITY STUB:
+ * LumeOS currently lacks a networking stack and Unix Domain Sockets (AF_UNIX).
+ * We return -EAFNOSUPPORT (Address family not supported by protocol) as expected
+ * by Linux applications (like BusyBox syslogd connecting to /dev/log).
+ */
+ return -EAFNOSUPPORT;
+}
+
+int64 sys_sendto(uint64, uint64, uint64, uint64, uint64, uint64)
+{
+ /* COMPATIBILITY STUB */
+ return -EAFNOSUPPORT;
+}
diff --git a/na/kernel/sys_misc.cc b/na/kernel/sys_misc.cc
new file mode 100644
index 0000000..48eb47b
--- /dev/null
+++ b/na/kernel/sys_misc.cc
@@ -0,0 +1,360 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * Miscellaneous System Calls — busybox bootstrap set
+ *
+ * Each syscall follows the Linux kernel semantics exactly.
+ * Reference: Linux kernel/sys.c, kernel/signal.c, kernel/time/posix-timers.c
+ */
+
+#include <lume/types.h>
+#include <lume/task.h>
+#include <lume/errno.h>
+#include <lume/kprintf.h>
+#include <lume/klog.h>
+#include <lume/pid.h>
+#include <lume/pgrp.h>
+#include <lume/signal_types.h>
+#include <lume/tty.h>
+#include <lume/uaccess.h>
+#include <arch/cpu.h>
+
+/* ======================================================================
+ * sys_reboot — Linux kernel/reboot.c
+ *
+ * Validates magic numbers and performs the requested action.
+ * For now, we only support LINUX_REBOOT_CMD_CAD_ON/OFF (ignore)
+ * and reject everything else with -EPERM (no CAP_SYS_BOOT).
+ * ====================================================================== */
+
+static constexpr uint32 LINUX_REBOOT_MAGIC1 = 0xfee1dead;
+static constexpr uint32 LINUX_REBOOT_MAGIC2 = 672274793; /* 0x28121969 */
+static constexpr uint32 LINUX_REBOOT_MAGIC2A = 85072278;
+static constexpr uint32 LINUX_REBOOT_MAGIC2B = 369367448;
+static constexpr uint32 LINUX_REBOOT_MAGIC2C = 537993216;
+
+int64 sys_reboot(uint64 magic1, uint64 magic2, uint64 cmd,
+ uint64, uint64, uint64)
+{
+ /* Linux: kernel/reboot.c — validate magic numbers first */
+ if (static_cast<uint32>(magic1) != LINUX_REBOOT_MAGIC1)
+ return -EINVAL;
+ if (static_cast<uint32>(magic2) != LINUX_REBOOT_MAGIC2 &&
+ static_cast<uint32>(magic2) != LINUX_REBOOT_MAGIC2A &&
+ static_cast<uint32>(magic2) != LINUX_REBOOT_MAGIC2B &&
+ static_cast<uint32>(magic2) != LINUX_REBOOT_MAGIC2C)
+ return -EINVAL;
+
+ /* We don't have CAP_SYS_BOOT yet — reject with -EPERM.
+ * This matches Linux behavior for unprivileged callers.
+ * Busybox handles this gracefully and continues. */
+ (void)cmd;
+ printk(KERN_INFO "reboot: cmd=%llu (not implemented, returning -EPERM)\n", cmd);
+ return -EPERM;
+}
+
+/* ======================================================================
+ * sys_prctl — Linux kernel/sys.c
+ *
+ * Process control. Most options are optional for early boot.
+ * We implement the minimum set busybox needs.
+ * ====================================================================== */
+
+/* prctl option constants (from <linux/prctl.h>) */
+static constexpr int PR_SET_NAME = 15;
+static constexpr int PR_GET_NAME = 16;
+static constexpr int PR_SET_PDEATHSIG = 1;
+static constexpr int PR_GET_PDEATHSIG = 2;
+
+int64 sys_prctl(uint64 option, uint64 arg2, uint64 arg3,
+ uint64 arg4, uint64 arg5, uint64)
+{
+ switch (static_cast<int>(option)) {
+ case PR_SET_PDEATHSIG:
+ /* Linux: set signal delivered when parent dies.
+ * Silently accept — we don't track pdeath_signal yet. */
+ return 0;
+ case PR_GET_PDEATHSIG: {
+ /* Linux: return current pdeath_signal (0 = none) */
+ int32 zero = 0;
+ if (copy_to_user(arg2, &zero, sizeof(zero)) != 0)
+ return -EFAULT;
+ return 0;
+ }
+ case PR_SET_NAME: {
+ /* Linux: set comm (task name), max 16 chars */
+ /* TODO: copy_from_user the string */
+ return 0;
+ }
+ case PR_GET_NAME: {
+ /* Linux: get comm (task name) */
+ TaskControlBlock* curr = current_task();
+ if (!curr) return -ESRCH;
+ const char* name = curr->name ? curr->name : "";
+ uint64 len = 0;
+ while (name[len] && len < 15) len++;
+ char buf[16] = {};
+ for (uint64 i = 0; i < len; i++) buf[i] = name[i];
+ if (copy_to_user(arg2, buf, 16) != 0)
+ return -EFAULT;
+ return 0;
+ }
+ default:
+ /* Unknown prctl — Linux returns -EINVAL for unrecognized options */
+ return -EINVAL;
+ }
+}
+
+/* ======================================================================
+ * Session / Process-Group syscalls
+ *
+ * All four operate on the caller's signal_struct (or a target task's).
+ * Identity lives in signal_struct, so these become straightforward reads
+ * and pgrp-hash updates. We mirror Linux kernel/sys.c semantics closely.
+ * ====================================================================== */
+
+static TaskControlBlock* find_task_by_pid(int32 pid)
+{
+ extern list_node g_all_tasks;
+ extern Spinlock g_task_list_lock;
+ LockGuard guard(g_task_list_lock);
+ list_node* node = g_all_tasks.next;
+ while (node != &g_all_tasks) {
+ auto* task = list_entry<TaskControlBlock, &TaskControlBlock::all_link>(node);
+ if (task->pid == pid) return task;
+ node = node->next;
+ }
+ return nullptr;
+}
+
+/*
+ * sys_setsid — Linux kernel/sys.c:ksys_setsid()
+ *
+ * Caller becomes leader of a new session+pgrp. Refused if a pgrp with
+ * id == caller pid already exists with members other than the caller —
+ * that's Linux's "process group id already exists" check, which prevents
+ * collisions when a non-leader tries to setsid.
+ */
+int64 sys_setsid(uint64, uint64, uint64, uint64, uint64, uint64)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ESRCH;
+
+ /* Detect collision: another pgrp already uses our pid as pgid. */
+ bool conflict = false;
+ for_each_pgrp_member(curr->pid, [&](signal_struct* sig) {
+ if (sig != curr->signal) conflict = true;
+ });
+ if (conflict) return -EPERM;
+
+ /* Move to new pgrp (creates one if missing), update sid, drop ctty. */
+ change_pgid(curr->signal, curr->pid);
+
+ /* Detach the controlling terminal. If this task is the session
+ * leader that owns the tty (tty->session == caller's old sid),
+ * also clear the tty's session/pgrp so the next session-leader
+ * that opens the device can claim it. Without this, tty->session
+ * permanently sticks to the abandoned sid and every subsequent
+ * tty_acquire_ctty() bails with "owned by another session" —
+ * which is exactly what kept BusyBox's child sh from picking the
+ * console back up after init's setsid().
+ *
+ * Linux's behavior is similar in spirit (disassociate_ctty when
+ * the leader exits), but it triggers from exit, not setsid. Our
+ * simpler "clear-on-give-up" matches the way our acquisition path
+ * is written. */
+ tty_struct* old_tty = nullptr;
+ int32 old_sid = 0;
+ {
+ LockGuard guard(curr->signal->lock);
+ old_tty = curr->signal->tty;
+ old_sid = curr->signal->sid;
+ curr->signal->sid = curr->pid;
+ curr->signal->tty = nullptr;
+ curr->signal->tty_old_pgrp = 0;
+ }
+ if (old_tty) {
+ LockGuard guard(old_tty->lock);
+ if (old_tty->session == old_sid) {
+ old_tty->session = 0;
+ old_tty->pgrp = 0;
+ }
+ }
+ return curr->pid;
+}
+
+/*
+ * sys_setpgid — Linux kernel/sys.c:ksys_setpgid()
+ *
+ * Move a process between groups within the same session. Allowed:
+ * - caller targeting self or a child
+ * - target is not a session leader
+ * - target stays in caller's session
+ * - target hasn't exec()'d (we don't track did_exec yet, so skip)
+ * - new pgid is either == target's pid (new group) or names a pgrp
+ * that already exists in the same session
+ */
+int64 sys_setpgid(uint64 pid, uint64 pgid, uint64, uint64, uint64, uint64)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ESRCH;
+
+ int32 target_pid = static_cast<int32>(pid);
+ int32 target_pgid = static_cast<int32>(pgid);
+
+ if (target_pid == 0) target_pid = curr->pid;
+ if (target_pgid == 0) target_pgid = target_pid;
+ if (target_pgid < 0) return -EINVAL;
+
+ TaskControlBlock* target = (target_pid == curr->pid)
+ ? curr
+ : find_task_by_pid(target_pid);
+ if (!target || !target->signal) return -ESRCH;
+
+ /* Target must be caller or a child of caller. */
+ if (target != curr && target->parent != curr) return -ESRCH;
+
+ /* Same session as caller. */
+ if (target->signal->sid != curr->signal->sid) return -EPERM;
+
+ /* Cannot move a session leader into a different pgrp. */
+ if (target->signal->sid == target->pid) return -EPERM;
+
+ /* If target_pgid != target_pid, the pgrp must already exist in
+ * the same session. pgrp existence == any signal_struct with
+ * pgid == target_pgid AND sid == caller's sid. */
+ if (target_pgid != target->pid) {
+ bool found = false;
+ for_each_pgrp_member(target_pgid, [&](signal_struct* sig) {
+ if (sig->sid == curr->signal->sid) found = true;
+ });
+ if (!found) return -EPERM;
+ }
+
+ change_pgid(target->signal, target_pgid);
+ return 0;
+}
+
+/*
+ * sys_getpgid — return process group ID of `pid` (0 = self).
+ */
+int64 sys_getpgid(uint64 pid, uint64, uint64, uint64, uint64, uint64)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ESRCH;
+
+ if (pid == 0) return curr->signal->pgid;
+
+ TaskControlBlock* target = find_task_by_pid(static_cast<int32>(pid));
+ if (!target || !target->signal) return -ESRCH;
+ return target->signal->pgid;
+}
+
+/*
+ * sys_getsid — return session ID of `pid` (0 = self).
+ */
+int64 sys_getsid(uint64 pid, uint64, uint64, uint64, uint64, uint64)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->signal) return -ESRCH;
+
+ if (pid == 0) return curr->signal->sid;
+
+ TaskControlBlock* target = find_task_by_pid(static_cast<int32>(pid));
+ if (!target || !target->signal) return -ESRCH;
+
+ /* Linux: -EPERM if target's session differs from caller's and we're
+ * in a different namespace. We have one namespace, so always allow. */
+ return target->signal->sid;
+}
+
+/* ======================================================================
+ * sys_geteuid / sys_getegid — Linux kernel/sys.c
+ *
+ * Return effective UID/GID. We run everything as root (0).
+ * ====================================================================== */
+
+int64 sys_geteuid(uint64, uint64, uint64, uint64, uint64, uint64)
+{
+ /* Linux: return current_euid(). We are always root. */
+ return 0;
+}
+
+int64 sys_getegid(uint64, uint64, uint64, uint64, uint64, uint64)
+{
+ /* Linux: return current_egid(). We are always root. */
+ return 0;
+}
+
+/* ======================================================================
+ * sys_sigaltstack — Linux kernel/signal.c
+ *
+ * Set/get alternate signal stack. For now, accept and ignore
+ * (busybox doesn't critically need it for shell operation).
+ * ====================================================================== */
+
+int64 sys_sigaltstack(uint64 uss_ptr, uint64 uoss_ptr,
+ uint64, uint64, uint64, uint64)
+{
+ /* Linux: if uoss != NULL, return current alt stack info.
+ * We don't have alt stacks yet — report SS_DISABLE. */
+ if (uoss_ptr != 0) {
+ /* stack_t { void* ss_sp; int ss_flags; size_t ss_size; }
+ * On RISC-V LP64: 8 + 4 + padding + 8 = 24 bytes typically,
+ * but the ABI packs it as {uint64, int32, pad, uint64}. */
+ struct {
+ uint64 ss_sp;
+ int32 ss_flags;
+ int32 _pad;
+ uint64 ss_size;
+ } oss = { 0, 2 /* SS_DISABLE */, 0, 0 };
+ if (copy_to_user(uoss_ptr, &oss, sizeof(oss)) != 0)
+ return -EFAULT;
+ }
+
+ /* uss: silently accept (we'll need it for proper signal handling later) */
+ (void)uss_ptr;
+ return 0;
+}
+
+/* ======================================================================
+ * sys_clock_gettime — Linux kernel/time/posix-timers.c
+ *
+ * Get the current time for a given clock.
+ * We use the RISC-V cycle counter as a monotonic source.
+ * ====================================================================== */
+
+static constexpr int CLOCK_REALTIME = 0;
+static constexpr int CLOCK_MONOTONIC = 1;
+
+int64 sys_clock_gettime(uint64 clk_id, uint64 tp_ptr,
+ uint64, uint64, uint64, uint64)
+{
+ if (tp_ptr == 0) return -EFAULT;
+
+ /* Read RISC-V time CSR (mtime mapped to SBI) */
+ uint64 ticks = arch::cpu::read_time();
+
+ /* Convert to seconds + nanoseconds.
+ * QEMU virt timebase-frequency is 10MHz. */
+ constexpr uint64 kTimebaseHz = 10'000'000ULL;
+ uint64 sec = ticks / kTimebaseHz;
+ uint64 nsec = (ticks % kTimebaseHz) * (1'000'000'000ULL / kTimebaseHz);
+
+ struct timespec64 {
+ int64 tv_sec;
+ int64 tv_nsec;
+ };
+
+ timespec64 ts;
+ ts.tv_sec = static_cast<int64>(sec);
+ ts.tv_nsec = static_cast<int64>(nsec);
+
+ (void)clk_id; /* We treat all clocks as monotonic for now */
+
+ if (copy_to_user(tp_ptr, &ts, sizeof(ts)) != 0)
+ return -EFAULT;
+
+ return 0;
+}
diff --git a/na/kernel/sys_mm.cc b/na/kernel/sys_mm.cc
index ff03e96..bfa067c 100644
--- a/na/kernel/sys_mm.cc
+++ b/na/kernel/sys_mm.cc
@@ -15,6 +15,7 @@
#include <lume/errno.h>
#include <lume/mman.h>
#include <lume/config.h>
+#include <lume/klog.h>
/* Page alignment helpers */
@@ -78,6 +79,9 @@ int64 sys_brk(uint64 new_brk, uint64, uint64, uint64, uint64, uint64)
int64 sys_mmap(uint64 addr, uint64 length, uint64 prot,
uint64 flags, uint64 fd, uint64 offset)
{
+ printk("[mmap] addr=%llx len=%llx prot=%llx flags=%llx fd=%lld off=%llx\n",
+ addr, length, prot, flags, static_cast<int64>(fd), offset);
+
if (length == 0) return -EINVAL;
length = page_align_up(length);
if (offset % kPageSize != 0) return -EINVAL;
@@ -89,8 +93,10 @@ int64 sys_mmap(uint64 addr, uint64 length, uint64 prot,
if (!mm) return -ENOMEM;
/* Phase 2: only anonymous private mappings */
- if (!(flags & MAP_ANONYMOUS))
+ if (!(flags & MAP_ANONYMOUS)) {
+ printk("[mmap] REJECTED: file-backed mmap not implemented\n");
return -ENOSYS; /* File-backed mmap is Phase 3 */
+ }
if (flags & MAP_SHARED)
return -ENOSYS; /* Shared mappings are Phase 3 */
diff --git a/na/kernel/sys_process.cc b/na/kernel/sys_process.cc
index 1759a81..438ad1b 100644
--- a/na/kernel/sys_process.cc
+++ b/na/kernel/sys_process.cc
@@ -11,6 +11,7 @@
* Reference: docs/specs/syscall.md §7.1
*/
+#include "lume/klog.h"
#include <lume/types.h>
#include <lume/task.h>
#include <lume/sched.h>
@@ -20,8 +21,10 @@
/* External: task lifecycle (kernel/task.cc) */
extern void task_exit(int code);
-extern void task_destroy(TaskControlBlock* task);
-extern int32 task_wait(int32* exit_code);
+extern int32 do_wait4(int32 wait_pid, int options, int32* exit_code);
+extern int32 task_clone(arch::TrapFrame* tf, uint64 clone_flags,
+ uint64 child_stack, uint64 parent_tidptr,
+ uint64 child_tidptr, uint64 tls);
extern int32 task_fork(arch::TrapFrame* tf);
/* ======================================================================
@@ -75,14 +78,10 @@ int64 sys_sched_yield_wrapper(uint64, uint64, uint64, uint64, uint64, uint64)
int64 sys_clone_wrapper(uint64 flags, uint64 child_stack,
uint64 ptid, uint64 ctid, uint64 tls, uint64)
{
- /* Phase A: Simple fork() support.
- * Ignore flags/stack for now and just clone the current task. */
- (void)flags; (void)child_stack; (void)ptid; (void)ctid; (void)tls;
-
arch::TrapFrame* tf = current_task()->trapframe;
if (!tf) return -EINVAL;
- return task_fork(tf);
+ return task_clone(tf, flags, child_stack, ptid, ctid, tls);
}
/* ========================================================================
@@ -92,23 +91,16 @@ int64 sys_clone_wrapper(uint64 flags, uint64 child_stack,
int64 sys_wait4_wrapper(uint64 pid, uint64 wstatus_ptr,
uint64 options, uint64, uint64, uint64)
{
- /* Phase A: Basic wait() support.
- * TODO: Implement pid-specific wait and WNOHANG in task_wait. */
- (void)pid; (void)options;
-
int32 exit_code = 0;
- int32 child_pid = task_wait(&exit_code);
+ int32 child_pid = do_wait4(static_cast<int32>(pid),
+ static_cast<int>(options),
+ &exit_code);
if (child_pid < 0) return child_pid;
+ /* exit_code is already in Linux wstatus format */
if (wstatus_ptr != 0) {
- int wstatus = 0;
- if (exit_code & kExitCodeSignalFlag) {
- wstatus = exit_code & 0x7F;
- } else {
- wstatus = W_EXITCODE(exit_code);
- }
- if (copy_to_user(wstatus_ptr, &wstatus, sizeof(int)) < 0)
+ if (copy_to_user(wstatus_ptr, &exit_code, sizeof(int)) < 0)
return -EFAULT;
}
@@ -122,9 +114,9 @@ int64 sys_wait4_wrapper(uint64 pid, uint64 wstatus_ptr,
int64 sys_set_tid_address(uint64 tidptr, uint64, uint64,
uint64, uint64, uint64)
{
- /* Phase 2: store the clear_child_tid pointer (needed for futex wakeup).
- * For now just return current tid. */
- (void)tidptr;
+ /* Store clear_child_tid pointer — on exit, kernel writes 0 here
+ * and does futex_wake (enables pthread_join). */
+ current_task()->clear_child_tid = tidptr;
return current_task()->pid;
}
@@ -141,3 +133,53 @@ int64 sys_getgid(uint64, uint64, uint64, uint64, uint64, uint64)
{
return 0; /* root */
}
+
+/* ======================================================================
+ * sys_nanosleep (SYS_nanosleep = 101)
+ * ====================================================================== */
+#include <lume/wait.h>
+#include <lume/kprintf.h>
+#include <lume/timer.h>
+
+struct linux_timespec {
+ int64 tv_sec;
+ int64 tv_nsec;
+};
+
+static void nanosleep_wakeup(TimerList* t) {
+ TaskControlBlock* task = static_cast<TaskControlBlock*>(t->data);
+ task->state = TaskState::Runnable;
+ sched_enqueue(task);
+}
+
+int64 sys_nanosleep(uint64 req_ptr, uint64 rem_ptr, uint64, uint64, uint64, uint64) {
+ if (req_ptr == 0) return -EINVAL;
+
+ linux_timespec req;
+ if (copy_from_user(&req, req_ptr, sizeof(req)) != 0) {
+ return -EFAULT;
+ }
+
+ uint64 target = *(volatile uint64*)&g_jiffies + (req.tv_sec * 100) + (req.tv_nsec / 10000000);
+
+ TaskControlBlock* curr = current_task();
+ curr->state = TaskState::Sleeping;
+
+ TimerList t;
+ t.init(nanosleep_wakeup);
+ t.expires = target;
+ t.data = curr;
+
+ add_timer(&t);
+
+ if (*(volatile uint64*)&g_jiffies < target) {
+ schedule();
+ } else {
+ curr->state = TaskState::Running;
+ }
+
+
+ del_timer(&t);
+
+ return 0;
+}
diff --git a/na/kernel/sys_signal.cc b/na/kernel/sys_signal.cc
new file mode 100644
index 0000000..8b459d5
--- /dev/null
+++ b/na/kernel/sys_signal.cc
@@ -0,0 +1,351 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright (C) 2026 Richard Qin */
+
+/*
+ * sys_signal.cc — Signal-related system calls
+ *
+ * Implements:
+ * sys_kill (SYS_kill = 129)
+ * sys_rt_sigaction (SYS_rt_sigaction = 134)
+ * sys_rt_sigprocmask (SYS_rt_sigprocmask = 135)
+ * sys_rt_sigreturn (SYS_rt_sigreturn = 139)
+ *
+ * Reference: Linux kernel/signal.c, arch/riscv/kernel/signal.c
+ */
+
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/signal_nr.h>
+#include <lume/task.h>
+#include <lume/errno.h>
+#include <lume/uaccess.h>
+#include <lume/kprintf.h>
+#include <lume/klog.h>
+#include <lume/new.h>
+#include <lume/pgrp.h>
+
+/* From signal.cc */
+extern int send_signal(int sig, kernel_siginfo* info, TaskControlBlock* task);
+
+/* From task.cc */
+extern list_node g_all_tasks;
+extern Spinlock g_task_list_lock;
+
+/* ================================================================== */
+/* Helper: find task by PID */
+/* ================================================================== */
+
+static TaskControlBlock* find_task_by_pid(int32 pid)
+{
+ LockGuard guard(g_task_list_lock);
+ list_node* node = g_all_tasks.next;
+ while (node != &g_all_tasks) {
+ auto* task = list_entry<TaskControlBlock, &TaskControlBlock::all_link>(node);
+ if (task->pid == pid) return task;
+ node = node->next;
+ }
+ return nullptr;
+}
+
+/* ================================================================== */
+/* sys_kill — send a signal to a process */
+/* ================================================================== */
+
+/*
+ * Deliver `sig` to the thread-group leader represented by `sig_st`.
+ * sighand is allocated eagerly at fork/exec time, so we just trust
+ * that the leader has one; send_signal returns -ESRCH if not.
+ */
+static int deliver_to_group(signal_struct* sig_st, int sig,
+ kernel_siginfo* info)
+{
+ if (!sig_st || !sig_st->leader) return 0;
+ if (send_signal(sig, info, sig_st->leader) == 0) return 1;
+ return 0;
+}
+
+int64 sys_kill(uint64 a0, uint64 a1, uint64, uint64, uint64, uint64)
+{
+ int32 pid = static_cast<int32>(a0);
+ int sig = static_cast<int>(a1);
+
+ if (sig < 0 || sig > _NSIG) return -EINVAL;
+
+ TaskControlBlock* curr = current_task();
+ if (!curr) return -ESRCH;
+
+ kernel_siginfo info{};
+ info.si_signo = sig;
+ info.si_code = 0; /* SI_USER */
+ info._sifields._kill._pid = curr->pid;
+ info._sifields._kill._uid = 0;
+
+ /* pid > 0: deliver to that thread group's leader. */
+ if (pid > 0) {
+ TaskControlBlock* target = find_task_by_pid(pid);
+ if (!target) return -ESRCH;
+ /* Group signal: prefer the thread-group leader if available. */
+ if (target->signal && target->signal->leader)
+ target = target->signal->leader;
+ if (sig == 0) return 0;
+ return send_signal(sig, &info, target);
+ }
+
+ /* pid == 0: every member of caller's process group. */
+ if (pid == 0) {
+ if (!curr->signal) return -ESRCH;
+ if (sig == 0) return 0;
+ int delivered = 0;
+ int32 target_pgid = curr->signal->pgid;
+ for_each_pgrp_member(target_pgid, [&](signal_struct* s) {
+ delivered += deliver_to_group(s, sig, &info);
+ });
+ return delivered > 0 ? 0 : -ESRCH;
+ }
+
+ /* pid == -1: every process the caller may signal, except init.
+ * We approximate "every" with "every thread-group leader on every
+ * pgrp bucket". This is rare in practice (BusyBox doesn't use it
+ * during normal shell operation). */
+ if (pid == -1) {
+ if (sig == 0) return 0;
+ int delivered = 0;
+ LockGuard guard(g_pgrp_lock);
+ for (auto& bucket : g_pgrp_hash) {
+ list_node* node = bucket.next;
+ while (node != &bucket) {
+ list_node* next = node->next;
+ auto* s = list_entry<signal_struct,
+ &signal_struct::pgrp_link>(node);
+ if (s->leader && s->leader->pid != 1)
+ delivered += deliver_to_group(s, sig, &info);
+ node = next;
+ }
+ }
+ return delivered > 0 ? 0 : -ESRCH;
+ }
+
+ /* pid < -1: every member of pgrp |pid|. */
+ int32 target_pgid = -pid;
+ if (sig == 0) {
+ bool any = false;
+ for_each_pgrp_member(target_pgid, [&](signal_struct*) { any = true; });
+ return any ? 0 : -ESRCH;
+ }
+ int delivered = 0;
+ for_each_pgrp_member(target_pgid, [&](signal_struct* s) {
+ delivered += deliver_to_group(s, sig, &info);
+ });
+ return delivered > 0 ? 0 : -ESRCH;
+}
+
+/* ================================================================== */
+/* sys_tkill / sys_tgkill — send a signal to a specific thread */
+/* ================================================================== */
+
+static int64 do_tkill(int32 tgid, int32 tid, int sig)
+{
+ if (sig < 0 || sig > _NSIG) return -EINVAL;
+ if (tid <= 0) return -EINVAL;
+ TaskControlBlock* target = find_task_by_pid(tid);
+ if (!target) return -ESRCH;
+ /* tgid == -1 from tkill means "any tgid"; otherwise enforce match. */
+ if (tgid != -1 && target->tgid != tgid) return -ESRCH;
+
+ if (sig == 0) return 0;
+
+ kernel_siginfo info{};
+ info.si_signo = sig;
+ info.si_code = 0; /* SI_TKILL would be -6; userland rarely checks */
+ info._sifields._kill._pid = current_task() ? current_task()->pid : 0;
+ info._sifields._kill._uid = 0;
+ return send_signal(sig, &info, target);
+}
+
+int64 sys_tkill(uint64 a0, uint64 a1, uint64, uint64, uint64, uint64)
+{
+ return do_tkill(-1,
+ static_cast<int32>(a0),
+ static_cast<int>(a1));
+}
+
+int64 sys_tgkill(uint64 a0, uint64 a1, uint64 a2, uint64, uint64, uint64)
+{
+ return do_tkill(static_cast<int32>(a0),
+ static_cast<int32>(a1),
+ static_cast<int>(a2));
+}
+
+/* ================================================================== */
+/* sys_rt_sigaction — get/set signal disposition */
+/* ================================================================== */
+
+/*
+ * User-space sigaction struct layout (RISC-V LP64):
+ * uint64 sa_handler;
+ * uint64 sa_flags;
+ * uint64 sa_restorer; (SA_RESTORER)
+ * sigset_t sa_mask; (8 bytes on LP64)
+ */
+struct user_sigaction {
+ uint64 sa_handler;
+ uint64 sa_flags;
+ uint64 sa_restorer;
+ sigset_t sa_mask;
+};
+
+int64 sys_rt_sigaction(uint64 a0, uint64 a1, uint64 a2, uint64 a3,
+ uint64, uint64)
+{
+ int sig = static_cast<int>(a0);
+ uint64 act_uaddr = a1; /* const struct sigaction __user * */
+ uint64 oact_uaddr = a2; /* struct sigaction __user * */
+ uint64 sigsetsize = a3;
+
+ if (sig < 1 || sig > _NSIG) return -EINVAL;
+ if (sig_kernel_only(sig)) return -EINVAL;
+ if (sigsetsize != sizeof(sigset_t)) return -EINVAL;
+
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->sighand) return -EINVAL;
+
+ /* All reads/writes of the action table happen under siglock —
+ * Linux's do_sigaction() also takes the same lock (kernel/signal.c). */
+ user_sigaction old{};
+ user_sigaction act{};
+ bool have_new = (act_uaddr != 0);
+
+ if (have_new) {
+ int err = copy_from_user(&act, act_uaddr, sizeof(act));
+ if (err < 0) return -EFAULT;
+ }
+
+ {
+ LockGuard guard(curr->sighand->siglock);
+ kernel_sigaction* ksa = &curr->sighand->action[sig - 1];
+ old.sa_handler = ksa->sa_handler;
+ old.sa_flags = ksa->sa_flags;
+ old.sa_restorer = ksa->sa_restorer;
+ old.sa_mask = ksa->sa_mask;
+
+ if (have_new) {
+ ksa->sa_handler = act.sa_handler;
+ ksa->sa_flags = act.sa_flags;
+ ksa->sa_restorer = act.sa_restorer;
+ ksa->sa_mask = act.sa_mask;
+ ksa->sa_mask.del(SIGKILL);
+ ksa->sa_mask.del(SIGSTOP);
+ }
+ }
+
+ if (oact_uaddr != 0) {
+ int err = copy_to_user(oact_uaddr, &old, sizeof(old));
+ if (err < 0) return -EFAULT;
+ }
+ return 0;
+}
+
+/* ================================================================== */
+/* sys_rt_sigprocmask — change signal blocked mask */
+/* ================================================================== */
+
+int64 sys_rt_sigprocmask(uint64 a0, uint64 a1, uint64 a2, uint64 a3,
+ uint64, uint64)
+{
+ int how = static_cast<int>(a0);
+ uint64 set_uaddr = a1;
+ uint64 oset_uaddr = a2;
+ uint64 sigsetsize = a3;
+
+ if (sigsetsize != sizeof(sigset_t)) return -EINVAL;
+
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->sighand) return -EINVAL;
+
+ sigset_t newset{};
+ bool have_new = (set_uaddr != 0);
+ if (have_new) {
+ int err = copy_from_user(&newset, set_uaddr, sizeof(sigset_t));
+ if (err < 0) return -EFAULT;
+ newset.del(SIGKILL);
+ newset.del(SIGSTOP);
+ }
+
+ sigset_t old{};
+ {
+ /* Linux: kernel/signal.c:do_sigprocmask() also holds siglock
+ * across read+update so the snapshot is atomic vs. concurrent
+ * signal delivery. */
+ LockGuard guard(curr->sighand->siglock);
+ old = curr->blocked;
+ if (have_new) {
+ switch (how) {
+ case SIG_BLOCK:
+ curr->blocked = curr->blocked | newset;
+ break;
+ case SIG_UNBLOCK:
+ curr->blocked = curr->blocked & ~newset;
+ break;
+ case SIG_SETMASK:
+ curr->blocked = newset;
+ break;
+ default:
+ return -EINVAL;
+ }
+ }
+ }
+
+ if (oset_uaddr != 0) {
+ int err = copy_to_user(oset_uaddr, &old, sizeof(sigset_t));
+ if (err < 0) return -EFAULT;
+ }
+ return 0;
+}
+
+/* ================================================================== */
+/* sys_rt_sigreturn — restore context after signal handler */
+/* ================================================================== */
+
+/* Arch-specific: implemented in arch/riscv/signal.cc */
+extern int64 arch_sigreturn(arch::TrapFrame* tf);
+
+int64 sys_rt_sigreturn(uint64, uint64, uint64, uint64, uint64, uint64)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr || !curr->trapframe) return -EINVAL;
+
+ return arch_sigreturn(curr->trapframe);
+}
+
+/* ================================================================== */
+/* sys_rt_sigtimedwait (SYS_rt_sigtimedwait = 137) */
+/* ================================================================== */
+#include <lume/timer.h>
+#include <lume/sched.h>
+
+struct linux_timespec {
+ int64 tv_sec;
+ int64 tv_nsec;
+};
+
+int64 sys_rt_sigtimedwait(uint64 /* set_ptr */, uint64 /* info_ptr */, uint64 timeout_ptr, uint64 /* sigsetsize */, uint64, uint64)
+{
+ /*
+ * COMPATIBILITY STUB:
+ * LumeOS lacks full real-time signal queues and timed waiting mechanism.
+ * When BusyBox init runs out of children to reap, it calls this to wait
+ * for incoming signals (like SIGCHLD) with a timeout.
+ * We simulate the timeout by yielding the CPU, then return -EAGAIN (Try again)
+ * which tells user space that the timeout expired without receiving a signal.
+ */
+ if (timeout_ptr != 0) {
+ linux_timespec req;
+ if (copy_from_user(&req, timeout_ptr, sizeof(req)) == 0) {
+ uint64 target = g_jiffies + (req.tv_sec * 100) + (req.tv_nsec / 10000000);
+ while (g_jiffies < target) {
+ schedule();
+ }
+ }
+ }
+ return -EAGAIN;
+}
diff --git a/na/kernel/syscall.cc b/na/kernel/syscall.cc
index 4dfbb5d..a5b80ec 100644
--- a/na/kernel/syscall.cc
+++ b/na/kernel/syscall.cc
@@ -15,6 +15,8 @@
#include <lume/errno.h>
#include <lume/kprintf.h>
#include <lume/klog.h>
+#include <lume/console.h>
+#include <arch/cpu.h>
/* ======================================================================
* Jump table
@@ -30,12 +32,23 @@ static SyscallHandler sys_call_table[kNrSyscalls];
int64 syscall_dispatch(uint64 nr, uint64 args[6])
{
- if (nr >= static_cast<uint64>(kNrSyscalls))
+ if (nr >= static_cast<uint64>(kNrSyscalls)) {
+ kprintf("[syscall] nr=%llu out of range\n", nr);
return -ENOSYS;
+ }
+
+ /* Per-syscall entry trace. KLOG_DEBUG keeps it out of the console
+ * by default; enable via klog_set_level(KLOG_DEBUG) or read the
+ * ring buffer to investigate. Subsys mask gates this further. */
+ pr_debug_s(KLOG_SUBSYS_SYSCALL,
+ "syscall: nr=%llu a0=%llx a1=%llx a2=%llx a3=%llx\n",
+ nr, args[0], args[1], args[2], args[3]);
SyscallHandler handler = sys_call_table[nr];
- if (!handler)
+ if (!handler) {
+ kprintf("[syscall] unimplemented nr=%llu\n", nr);
return -ENOSYS;
+ }
return handler(args[0], args[1], args[2],
args[3], args[4], args[5]);
@@ -57,6 +70,7 @@ int64 sys_wait4_wrapper(uint64, uint64, uint64, uint64, uint64, uint64);
int64 sys_set_tid_address(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_nanosleep(uint64, uint64, uint64, uint64, uint64, uint64);
/* sys_mm.cc */
int64 sys_brk(uint64, uint64, uint64, uint64, uint64, uint64);
@@ -67,6 +81,52 @@ int64 sys_mprotect(uint64, uint64, uint64, uint64, uint64, uint64);
/* sys_info.cc */
int64 sys_uname(uint64, uint64, uint64, uint64, uint64, uint64);
+/* sys_fs.cc */
+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_fstat_wrapper(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getdents64(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_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_unlinkat(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_ppoll(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_socket(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_sendto(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* sys_signal.cc */
+int64 sys_kill(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_tkill(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_tgkill(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_rt_sigaction(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_rt_sigprocmask(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_rt_sigreturn(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_rt_sigtimedwait(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* exec.cc */
+int64 sys_execve_wrapper(uint64, uint64, uint64, uint64, uint64, uint64);
+
+/* sys_misc.cc */
+int64 sys_reboot(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_prctl(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_setsid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getsid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_setpgid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getpgid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_geteuid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_getegid(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_sigaltstack(uint64, uint64, uint64, uint64, uint64, uint64);
+int64 sys_clock_gettime(uint64, uint64, uint64, uint64, uint64, uint64);
+
/* ======================================================================
* syscall_init — register all handlers
* ====================================================================== */
@@ -95,5 +155,54 @@ void syscall_init()
sys_call_table[SYS_mprotect] = sys_mprotect;
sys_call_table[SYS_uname] = sys_uname;
- printk(KERN_INFO "init: syscall_init done (16 handlers registered)\n");
+ /* Phase 3: filesystem */
+ sys_call_table[SYS_openat] = sys_openat;
+ sys_call_table[SYS_close] = sys_close;
+ sys_call_table[SYS_read] = sys_read;
+ sys_call_table[SYS_write] = sys_write;
+ sys_call_table[SYS_lseek] = sys_lseek;
+ sys_call_table[SYS_fstat] = sys_fstat_wrapper;
+ sys_call_table[SYS_getdents64] = sys_getdents64;
+ sys_call_table[SYS_dup] = sys_dup;
+ sys_call_table[SYS_dup3] = sys_dup3;
+ sys_call_table[SYS_getcwd] = sys_getcwd;
+ sys_call_table[SYS_chdir] = sys_chdir;
+ sys_call_table[SYS_mkdirat] = sys_mkdirat;
+ sys_call_table[SYS_unlinkat] = sys_unlinkat;
+ sys_call_table[SYS_writev] = sys_writev;
+ sys_call_table[SYS_ioctl] = sys_ioctl;
+ sys_call_table[SYS_ppoll] = sys_ppoll;
+
+ /* Phase 4: signals */
+ sys_call_table[SYS_kill] = sys_kill;
+ sys_call_table[SYS_tkill] = sys_tkill;
+ sys_call_table[SYS_tgkill] = sys_tgkill;
+ sys_call_table[SYS_rt_sigaction] = sys_rt_sigaction;
+ sys_call_table[SYS_rt_sigprocmask] = sys_rt_sigprocmask;
+ sys_call_table[SYS_rt_sigreturn] = sys_rt_sigreturn;
+ sys_call_table[SYS_rt_sigtimedwait] = sys_rt_sigtimedwait;
+
+ /* Phase 5: exec */
+ sys_call_table[SYS_execve] = sys_execve_wrapper;
+
+ /* Phase 6: pipe + fcntl */
+ sys_call_table[SYS_pipe2] = sys_pipe2;
+ sys_call_table[SYS_fcntl] = sys_fcntl;
+
+ /* Phase 7: busybox bootstrap */
+ sys_call_table[SYS_reboot] = sys_reboot;
+ sys_call_table[SYS_prctl] = sys_prctl;
+ sys_call_table[SYS_setsid] = sys_setsid;
+ sys_call_table[SYS_getsid] = sys_getsid;
+ sys_call_table[SYS_setpgid] = sys_setpgid;
+ sys_call_table[SYS_getpgid] = sys_getpgid;
+ sys_call_table[SYS_geteuid] = sys_geteuid;
+ sys_call_table[SYS_getegid] = sys_getegid;
+ sys_call_table[SYS_sigaltstack] = sys_sigaltstack;
+ sys_call_table[SYS_clock_gettime] = sys_clock_gettime;
+ sys_call_table[SYS_nanosleep] = sys_nanosleep;
+ sys_call_table[SYS_socket] = sys_socket;
+ sys_call_table[SYS_sendto] = sys_sendto;
+
+ printk(KERN_INFO "init: syscall_init done (%d handlers registered)\n", 46);
}
diff --git a/na/kernel/task.cc b/na/kernel/task.cc
index 9f09d62..9f9b043 100644
--- a/na/kernel/task.cc
+++ b/na/kernel/task.cc
@@ -20,8 +20,18 @@
#include <lume/vmm.h>
#include <lume/errno.h>
#include <lume/pid.h>
+#include <lume/wait.h>
+#include <lume/clone.h>
+#include <lume/signal_nr.h>
#include <arch/cpu.h>
#include <arch/context.h>
+#include <lume/uaccess.h>
+#include <lume/fs_struct.h>
+#include <lume/fdtable.h>
+#include <lume/signal.h>
+#include <lume/signal_types.h>
+#include <lume/pgrp.h>
+#include <arch/pmap.h>
#include <kernel/sync/spinlock.h>
/* ======================================================================
@@ -57,11 +67,16 @@ void task_init()
}
/* ======================================================================
- * Internal: allocate TCB + kernel stack
+ * alloc_task — allocate TCB + kernel stack
+ *
+ * This is the SINGLE canonical TCB initializer. Both kthread_create
+ * (task.cc) and sched_init (sched_core.cc) must use this function.
+ * NEVER duplicate this logic — adding fields to TaskControlBlock
+ * requires updating only this function.
* ====================================================================== */
-static TaskControlBlock* alloc_task(const char* name, uint8 priority,
- uint8 flags)
+TaskControlBlock* alloc_task(const char* name, uint8 priority,
+ uint8 flags, uint8 cpu)
{
auto* task = new TaskControlBlock;
if (!task) return nullptr;
@@ -80,23 +95,45 @@ static TaskControlBlock* alloc_task(const char* name, uint8 priority,
/* Write stack canary at bottom */
*reinterpret_cast<uint64*>(task->kstack_va) = kStackCanary;
+ /* Place TrapFrame at the top of the kernel stack (like Linux pt_regs).
+ * ALL tasks — kernel threads and user processes — have a TrapFrame
+ * here. kthread_create sets the initial SP below the TrapFrame so
+ * the kthread function never touches this reserved area.
+ * kernel_execve / do_execve can later fill it in for user-mode return. */
+ uint64 tf_addr = task->kstack_va + kKernelStackSize
+ - sizeof(arch::TrapFrame);
+ task->trapframe = reinterpret_cast<arch::TrapFrame*>(tf_addr);
+ __builtin_memset(task->trapframe, 0, sizeof(arch::TrapFrame));
+
/* Basic fields */
task->name = name;
task->priority = priority;
task->time_slice = priority_to_timeslice(priority);
task->state = TaskState::Runnable;
- task->cpu_id = static_cast<uint8>(arch::cpu::id());
+ task->cpu_id = cpu;
task->flags = flags;
task->exit_code = 0;
+ task->exit_signal = 0;
+ task->preempt_count = 0;
+ task->clear_child_tid = 0;
+ task->stopped_code = 0;
+ task->notify_continued = false;
task->parent = nullptr;
task->mm = nullptr;
- task->trapframe = nullptr;
+ task->fpstate = {};
+ task->files = nullptr;
+ task->fs = nullptr;
+ task->pending.init();
+ task->blocked.clear();
+ task->sighand = nullptr;
+ task->signal = nullptr;
/* Init all list nodes */
task->run_link.init();
task->all_link.init();
task->children.init();
task->sibling.init();
+ task->lock.init("task");
task->wait_chld.init("wait_chld");
/* Default sched class */
@@ -143,7 +180,8 @@ TaskControlBlock* kthread_create(const char* name,
void* arg,
uint8 priority)
{
- TaskControlBlock* task = alloc_task(name, priority, TF_KTHREAD);
+ TaskControlBlock* task = alloc_task(name, priority, TF_KTHREAD,
+ static_cast<uint8>(arch::cpu::id()));
if (!task) return nullptr;
/* Allocate PID */
@@ -156,19 +194,30 @@ TaskControlBlock* kthread_create(const char* name,
}
task->tgid = task->pid;
- /* Set up context for kthread_entry_trampoline */
- uint64 stack_top = task->kstack_va + kKernelStackSize;
+ /* Kernel threads: exit_signal = -1 → auto-reap on exit.
+ * Linux: kernel_thread() passes (flags & ~CSIGNAL) → exit_signal = 0,
+ * but kthreads' parent (kthreadd) reaps them. We have no kthreadd,
+ * so we use -1 which makes exit_notify() call release_task() directly,
+ * identical to Linux's CLONE_THREAD autoreap path. */
+ task->exit_signal = -1;
+
+ /* Set up context for kthread_entry_trampoline.
+ * SP starts BELOW the TrapFrame reserved at the stack top.
+ * This ensures the kthread function never clobbers the TrapFrame area. */
+ uint64 stack_top = task->kstack_va + kKernelStackSize
+ - sizeof(arch::TrapFrame);
task->context = {};
task->context.ra = reinterpret_cast<uint64>(kthread_entry_trampoline);
task->context.sp = stack_top;
task->context.s[0] = reinterpret_cast<uint64>(entry);
task->context.s[1] = reinterpret_cast<uint64>(arg);
- /* Parent is kernel_task (PID 0) */
- task->parent = g_kernel_task;
- if (g_kernel_task) {
- LockGuard guard(g_task_list_lock);
- list_node* head = &g_kernel_task->children;
+ /* Parent is kernel_task (PID 0) or current caller */
+ TaskControlBlock* parent = g_kernel_task ? g_kernel_task : current_task();
+ task->parent = parent;
+ if (parent) {
+ LockGuard guard(parent->lock);
+ list_node* head = &parent->children;
task->sibling.prev = head->prev;
task->sibling.next = head;
head->prev->next = &task->sibling;
@@ -191,18 +240,32 @@ void task_exit(int code)
TaskControlBlock* curr = current_task();
if (!curr) kernel_panic("task_exit: no current task");
- curr->exit_code = code;
+ /* Linux ABI: normal exit encodes status as (code & 0xFF) << 8 */
+ curr->exit_code = (code & 0xFF) << 8;
- /* Release user address space if we're the last reference */
- if (curr->mm) {
- curr->mm->destroy();
+ /* CLONE_CHILD_CLEARTID / set_tid_address: write 0 to user tidptr.
+ * This is how pthread_join detects thread termination.
+ * TODO: futex_wake(clear_child_tid, 1) after writing 0. */
+ if (curr->clear_child_tid != 0 && curr->mm != nullptr) {
+ int32 zero = 0;
+ copy_to_user(curr->clear_child_tid, &zero, sizeof(zero));
+ curr->clear_child_tid = 0;
+ }
+
+ /* Release user address space (refcount-safe).
+ * MUST switch to kernel page table FIRST — otherwise we're running
+ * on a freed page table after put() deallocates it. */
+ if (curr->mm != nullptr) {
+ extern uint64 g_kernel_pgtbl;
+ pmap::activate(g_kernel_pgtbl);
+ curr->mm->put();
curr->mm = nullptr;
}
/* Reparent children to init (or kernel_task if no init yet) */
TaskControlBlock* foster = g_init_task ? g_init_task : g_kernel_task;
if (foster && !curr->children.is_empty()) {
- LockGuard guard(g_task_list_lock);
+ LockGuard guard(curr->lock);
list_node* child_node = curr->children.next;
while (child_node != &curr->children) {
@@ -226,7 +289,46 @@ void task_exit(int code)
curr->children.init();
}
- /* Transition to Zombie — TCB + kstack preserved for parent wait() */
+
+ /* ---- exit_notify: Linux kernel/exit.c:exit_notify() ----
+ * Determine whether this task should be auto-reaped or become zombie.
+ *
+ * Linux logic (simplified):
+ * - exit_signal == -1 (kthreads, sub-threads): autoreap = true
+ * - exit_signal >= 1 (user processes): notify parent, become zombie
+ *
+ * If autoreap, we call release_task() equivalent inline:
+ * free PID, remove from task list, then deschedule to Dead. */
+ bool autoreap = (curr->exit_signal <= 0);
+
+ if (autoreap) {
+ /* Auto-reap: release resources immediately (no zombie state).
+ * Linux: exit_notify() → tsk->exit_state = EXIT_DEAD → release_task() */
+ task_list_remove(curr);
+ g_pid_alloc.free(curr->pid);
+
+ /* Remove from parent's children list */
+ curr->sibling.prev->next = curr->sibling.next;
+ curr->sibling.next->prev = curr->sibling.prev;
+ curr->sibling.init();
+
+ /* Drop signal_struct ref: last task in the thread group also
+ * detaches us from the pgrp hash. */
+ if (curr->signal) {
+ put_signal_struct(curr->signal);
+ curr->signal = nullptr;
+ }
+ if (curr->sighand) {
+ put_sighand(curr->sighand);
+ curr->sighand = nullptr;
+ }
+
+ curr->state = TaskState::Dead;
+ schedule();
+ __builtin_unreachable();
+ }
+
+ /* User process path: become Zombie for parent wait() */
curr->state = TaskState::Zombie;
/* Wake parent->wait_chld queue */
@@ -242,8 +344,55 @@ void task_exit(int code)
void task_kill_current(int sig)
{
- int code = (sig & 0x7F) | kExitCodeSignalFlag;
- task_exit(code);
+ TaskControlBlock* curr = current_task();
+ if (!curr) kernel_panic("task_kill_current: no current task");
+
+ /* Linux ABI: signal-killed encodes status as (signo & 0x7F) */
+ curr->exit_code = sig & 0x7F;
+
+ /* Release user address space (refcount-safe).
+ * Switch to kernel page table first (same as task_exit). */
+ if (curr->mm) {
+ extern uint64 g_kernel_pgtbl;
+ pmap::activate(g_kernel_pgtbl);
+ curr->mm->put();
+ curr->mm = nullptr;
+ }
+
+ /* Reparent children (same as task_exit) */
+ TaskControlBlock* foster = g_init_task ? g_init_task : g_kernel_task;
+ if (foster && !curr->children.is_empty()) {
+ LockGuard guard(curr->lock);
+
+ list_node* child_node = curr->children.next;
+ while (child_node != &curr->children) {
+ list_node* next_sib = child_node->next;
+ auto* child = list_entry<TaskControlBlock,
+ &TaskControlBlock::sibling>(child_node);
+ child->parent = foster;
+
+ child_node->prev->next = child_node->next;
+ child_node->next->prev = child_node->prev;
+
+ list_node* fhead = &foster->children;
+ child_node->prev = fhead->prev;
+ child_node->next = fhead;
+ fhead->prev->next = child_node;
+ fhead->prev = child_node;
+
+ child_node = next_sib;
+ }
+ curr->children.init();
+ }
+
+ curr->state = TaskState::Zombie;
+
+ if (curr->parent) {
+ curr->parent->wait_chld.wake_all();
+ }
+
+ schedule();
+ __builtin_unreachable();
}
/* ======================================================================
@@ -263,6 +412,19 @@ void task_destroy(TaskControlBlock* task)
/* Free PID */
g_pid_alloc.free(task->pid);
+ /* Drop signal_struct + sighand refs. For zombies these are delayed
+ * until reap (here) so a waitpid() walker can still inspect
+ * pgid/sid; Linux defers __exit_signal/__cleanup_sighand to
+ * release_task() for the same reason. */
+ if (task->signal) {
+ put_signal_struct(task->signal);
+ task->signal = nullptr;
+ }
+ if (task->sighand) {
+ put_sighand(task->sighand);
+ task->sighand = nullptr;
+ }
+
/* Free kernel stack */
if (task->kstack_pa != 0) {
pmm_free_pages(pa_to_page(phys_addr(task->kstack_pa)), 1);
@@ -273,61 +435,155 @@ void task_destroy(TaskControlBlock* task)
}
/* ======================================================================
- * task_wait — sleep until a child exits, then reclaim it
- * Returns PID of exited child, or -ECHILD if no children exist.
+ * do_wait4 — wait for a child to exit (Linux-compatible)
+ *
+ * @wait_pid: >0 = specific child PID
+ * -1 = any child
+ * 0 = any child in same process group (TODO)
+ * <-1 = any child in specific process group (TODO)
+ * @options: WNOHANG = return immediately if no zombie
+ * @exit_code: output parameter (already in Linux wstatus format)
+ *
+ * Returns: PID of exited child, -ECHILD, or -EAGAIN (WNOHANG).
* ====================================================================== */
-int32 task_wait(int32* exit_code)
+int32 do_wait4(int32 wait_pid, int options, int32* exit_code)
{
TaskControlBlock* curr = current_task();
if (!curr) return -EINVAL;
while (true) {
TaskControlBlock* zombie = nullptr;
+ TaskControlBlock* report = nullptr; /* Stopped/Continued report */
+ int32 report_status = 0;
+ bool has_children = false;
+
+ /*
+ * Two-phase wait: put ourselves on the wait queue BEFORE
+ * checking the condition. This closes the race window where
+ * a child exits between our check and schedule().
+ *
+ * If a child exits and calls wake_all() while we're checking,
+ * our state will be flipped back to Runnable, and schedule()
+ * will simply return immediately.
+ */
+ curr->wait_chld.prepare_to_wait(curr, TaskState::Sleeping);
+
+ /* Scan children */
+ curr->lock.acquire();
- /* Look for a Zombie child */
- g_task_list_lock.acquire();
if (curr->children.is_empty()) {
- g_task_list_lock.release();
+ curr->lock.release();
+ curr->wait_chld.finish_wait(curr);
return -ECHILD;
}
+ has_children = true;
list_node* node = curr->children.next;
while (node != &curr->children) {
auto* child = list_entry<TaskControlBlock, &TaskControlBlock::sibling>(node);
+ bool matches = (wait_pid == -1) || (wait_pid == child->pid);
+ if (!matches) {
+ node = node->next;
+ continue;
+ }
+
+ /* Zombie reaping has priority — it frees resources. */
if (child->state == TaskState::Zombie) {
zombie = child;
break;
}
+
+ /* WUNTRACED: report Stopped child once per stop event. */
+ if ((options & WUNTRACED) && child->stopped_code != 0) {
+ report = child;
+ report_status = child->stopped_code;
+ break;
+ }
+
+ /* WCONTINUED: report a one-shot SIGCONT resume. */
+ if ((options & WCONTINUED) && child->notify_continued) {
+ report = child;
+ report_status = W_CONTINUED();
+ break;
+ }
+
node = node->next;
}
- g_task_list_lock.release();
+ curr->lock.release();
if (zombie) {
+ curr->wait_chld.finish_wait(curr);
int32 pid = zombie->pid;
- if (exit_code) {
- /* In a real kernel, we copy_to_user */
- *exit_code = zombie->exit_code;
- }
+ if (exit_code) *exit_code = zombie->exit_code;
task_destroy(zombie);
return pid;
}
- /* Sleep if no child has exited yet */
- curr->wait_chld.sleep(&g_task_list_lock);
+ if (report) {
+ /* Status-only report: clear the one-shot flag, leave the
+ * child alive. WNOWAIT (peek) would skip the clear, but
+ * we don't yet honour it. */
+ curr->wait_chld.finish_wait(curr);
+ int32 pid = report->pid;
+ if (exit_code) *exit_code = report_status;
+ if (W_CONTINUED() == report_status) {
+ report->notify_continued = false;
+ } else {
+ report->stopped_code = 0;
+ }
+ return pid;
+ }
+
+ /* No event to report */
+ if (!has_children) {
+ curr->wait_chld.finish_wait(curr);
+ return -ECHILD;
+ }
+ if (options & WNOHANG) {
+ curr->wait_chld.finish_wait(curr);
+ return 0;
+ }
+
+ /* Condition still false — deschedule.
+ * We are already on the wait queue and marked Sleeping. */
+ schedule();
+
+ /* Woken up — finish_wait removes us from queue, restores Runnable */
+ curr->wait_chld.finish_wait(curr);
+
+ /* Loop back to re-check the condition */
}
}
+/* Legacy wrapper for internal callers */
+int32 task_wait(int32* exit_code)
+{
+ return do_wait4(-1, 0, exit_code);
+}
+
/* ======================================================================
- * task_fork — clone current task
+ * task_clone — clone current task with Linux-compatible flags
+ *
+ * @tf: trap frame to copy to child
+ * @clone_flags: CLONE_VM | CLONE_THREAD | CSIGNAL etc.
+ * @child_stack: if non-zero, set as child's user SP (for pthread)
+ * @parent_tidptr: write child tid here in parent (CLONE_PARENT_SETTID)
+ * @child_tidptr: write child tid here in child (CLONE_CHILD_SETTID)
+ * @tls: TLS pointer (CLONE_SETTLS → tp register)
+ *
+ * Returns: child PID in parent, 0 in child, or negative errno.
* ====================================================================== */
-int32 task_fork(arch::TrapFrame* tf)
+int32 task_clone(arch::TrapFrame* tf, uint64 clone_flags,
+ uint64 child_stack, uint64 parent_tidptr,
+ uint64 child_tidptr, uint64 tls)
{
TaskControlBlock* curr = current_task();
if (!curr) return -EINVAL;
- TaskControlBlock* child = alloc_task("forked", curr->priority, 0);
+ TaskControlBlock* child = alloc_task("cloned", curr->priority, 0,
+ static_cast<uint8>(arch::cpu::id()));
if (!child) return -ENOMEM;
/* Allocate PID */
@@ -337,16 +593,35 @@ int32 task_fork(arch::TrapFrame* tf)
delete child;
return -ENOMEM;
}
- child->tgid = child->pid;
- /* Copy VmSpace */
- if (curr->mm) {
- child->mm = vmm_clone(curr->mm);
- if (!child->mm) {
- g_pid_alloc.free(child->pid);
- pmm_free_pages(pa_to_page(phys_addr(child->kstack_pa)), 1);
- delete child;
- return -ENOMEM;
+ /* exit_signal + tgid: from clone_flags.
+ * Linux: fork.c line 2418/2424 */
+ if (clone_flags & CLONE_THREAD) {
+ child->tgid = curr->tgid;
+ child->exit_signal = -1; /* Linux: sub-threads auto-reap */
+ } else {
+ child->tgid = child->pid;
+ child->exit_signal = static_cast<int32>(clone_flags & 0xFF); /* CSIGNAL */
+ if (child->exit_signal == 0) {
+ child->exit_signal = SIGCHLD; /* default for fork() */
+ }
+ }
+
+ /* Address space: CLONE_VM shares, otherwise deep copy */
+ if (clone_flags & CLONE_VM) {
+ if (curr->mm) {
+ curr->mm->incref();
+ child->mm = curr->mm;
+ }
+ } else {
+ if (curr->mm) {
+ child->mm = vmm_clone(curr->mm);
+ if (!child->mm) {
+ g_pid_alloc.free(child->pid);
+ pmm_free_pages(pa_to_page(phys_addr(child->kstack_pa)), 1);
+ delete child;
+ return -ENOMEM;
+ }
}
}
@@ -355,20 +630,94 @@ int32 task_fork(arch::TrapFrame* tf)
child->trapframe = reinterpret_cast<arch::TrapFrame*>(tf_addr);
*child->trapframe = *tf;
- /* Child returns 0 from fork */
+ /* Set Kernel SP */
+ child->trapframe->kernel_sp = reinterpret_cast<uint64>(child->trapframe);
+ child->trapframe->kernel_cpu_id = child->cpu_id;
+
+ /* Child returns 0 from fork/clone */
child->trapframe->a[0] = 0;
+ /* If caller specified a child stack, use it (pthread_create) */
+ if (child_stack != 0) {
+ child->trapframe->sp = child_stack;
+ }
+
+ /* CLONE_SETTLS: set thread pointer register */
+ if (clone_flags & CLONE_SETTLS) {
+ child->trapframe->tp = tls;
+ }
+
/* Set up context for ret_from_fork */
- uint64 stack_top = tf_addr; // Context goes below TrapFrame conceptually, but we just set SP
+ uint64 stack_top = tf_addr;
child->context = {};
child->context.ra = reinterpret_cast<uint64>(ret_from_fork);
child->context.sp = stack_top;
- child->context.s[0] = tf_addr; /* Passed to a0 in ret_from_fork */
+ child->context.s[0] = tf_addr;
+
+ /* ---- Copy fs_struct (Linux: copy_fs in fork.c) ----
+ * CLONE_FS: share the same fs_struct (increment refcount).
+ * Otherwise: allocate a new fs_struct copying root/pwd. */
+ if (curr->fs != nullptr) {
+ if (clone_flags & CLONE_FS) {
+ curr->fs->count.fetch_add(1, __ATOMIC_RELAXED);
+ child->fs = curr->fs;
+ } else {
+ child->fs = copy_fs_struct(curr->fs);
+ }
+ }
+
+ /* ---- Copy files_struct (Linux: copy_files in fork.c) ----
+ * CLONE_FILES: share the same fd table.
+ * Otherwise: duplicate the fd table. */
+ if (curr->files != nullptr) {
+ if (clone_flags & CLONE_FILES) {
+ curr->files->count.fetch_add(1, __ATOMIC_RELAXED);
+ child->files = curr->files;
+ } else {
+ child->files = dup_fd(curr->files);
+ }
+ }
+
+ /* ---- Copy sighand (Linux kernel/fork.c:copy_sighand) ----
+ * CLONE_SIGHAND: share signal handlers (incref parent's).
+ * Otherwise: deep copy of the action table; child gets its own
+ * sighand_struct (and its own siglock). Both branches MUST yield
+ * a non-null sighand — every user task needs one so signal paths
+ * can take the siglock without lazy-allocation races. */
+ if (clone_flags & CLONE_SIGHAND) {
+ if (!curr->sighand) {
+ /* CLONE_SIGHAND requires the parent to have a sighand. */
+ return -EINVAL;
+ }
+ get_sighand(curr->sighand);
+ child->sighand = curr->sighand;
+ } else {
+ child->sighand = copy_sighand(curr->sighand);
+ if (!child->sighand) {
+ /* OOM — same best-effort policy as fs/files/signal above.
+ * A separate cleanup-on-OOM pass is on the to-do list. */
+ }
+ }
+
+ /* ---- Copy signal_struct (Linux: copy_signal in fork.c) ----
+ * CLONE_THREAD shares the parent's signal_struct (incref).
+ * Otherwise allocate a new one that inherits pgid/sid/tty from the
+ * parent so the child stays in the same process group and session —
+ * this is what makes fork() participate in the existing job-control
+ * identity without explicit setpgid() calls. */
+ if (curr->signal != nullptr) {
+ if (clone_flags & CLONE_THREAD) {
+ get_signal_struct(curr->signal);
+ child->signal = curr->signal;
+ } else {
+ child->signal = fork_signal_struct(curr->signal, child);
+ }
+ }
/* Add to task tree */
child->parent = curr;
{
- LockGuard guard(g_task_list_lock);
+ LockGuard guard(curr->lock);
list_node* head = &curr->children;
child->sibling.prev = head->prev;
child->sibling.next = head;
@@ -380,9 +729,41 @@ int32 task_fork(arch::TrapFrame* tf)
task_list_add(child);
sched_enqueue(child);
+ /* CLONE_PARENT_SETTID: write child tid to parent's user address */
+ if ((clone_flags & CLONE_PARENT_SETTID) && parent_tidptr != 0) {
+ copy_to_user(parent_tidptr, &child->pid, sizeof(child->pid));
+ }
+
+ /* CLONE_CHILD_SETTID: write child tid into child's user address.
+ * Note: this runs in parent context, but the address is in the
+ * child's mm. For CLONE_VM (threads), parent and child share mm
+ * so this works directly. For fork, the page was just CoW-copied. */
+ if ((clone_flags & CLONE_CHILD_SETTID) && child_tidptr != 0) {
+ if (clone_flags & CLONE_VM) {
+ /* Same address space — write directly */
+ copy_to_user(child_tidptr, &child->pid, sizeof(child->pid));
+ }
+ /* For non-CLONE_VM fork, the child's mm is separate.
+ * The write would need to go into the child's pagetable.
+ * Linux handles this in the child's first schedule via
+ * a special flag. For now, threads (CLONE_VM) are the
+ * primary consumer. */
+ }
+
+ /* CLONE_CHILD_CLEARTID: kernel writes 0 to child_tidptr on exit */
+ if (clone_flags & CLONE_CHILD_CLEARTID) {
+ child->clear_child_tid = child_tidptr;
+ }
+
return child->pid;
}
+/* Legacy wrapper — plain fork() semantics */
+int32 task_fork(arch::TrapFrame* tf)
+{
+ return task_clone(tf, SIGCHLD, 0, 0, 0, 0);
+}
+
/* ======================================================================
* task_check_stack_canary
diff --git a/na/kernel/timer.cc b/na/kernel/timer.cc
index 01e8f9d..3cf03cf 100644
--- a/na/kernel/timer.cc
+++ b/na/kernel/timer.cc
@@ -7,10 +7,72 @@
#include <arch/cpu.h>
#include <arch/sbi.h>
+#include <kernel/sync/spinlock.h>
+
uint64 g_jiffies = 0;
static uint64 g_timer_freq = 10000000;
static uint64 g_tick_interval = 0;
+static list_node g_timer_list;
+static Spinlock g_timer_lock;
+
+
+void add_timer(TimerList* timer) {
+ LockGuard guard(g_timer_lock);
+
+ list_node* pos;
+ list_for_each(pos, &g_timer_list) {
+ TimerList* t = list_entry<TimerList, &TimerList::entry>(pos);
+ if (t->expires > timer->expires) {
+ break;
+ }
+ }
+
+ timer->entry.prev = pos->prev;
+ timer->entry.next = pos;
+ pos->prev->next = &timer->entry;
+ pos->prev = &timer->entry;
+}
+
+int del_timer(TimerList* timer) {
+ LockGuard guard(g_timer_lock);
+ if (timer->entry.next != nullptr && timer->entry.next != &timer->entry) {
+ timer->entry.prev->next = timer->entry.next;
+ timer->entry.next->prev = timer->entry.prev;
+ timer->entry.init();
+ return 1;
+ }
+ return 0;
+}
+
+int mod_timer(TimerList* timer, uint64 expires) {
+ int ret = del_timer(timer);
+ timer->expires = expires;
+ add_timer(timer);
+ return ret;
+}
+
+static void run_timers() {
+ g_timer_lock.acquire();
+
+ while (!g_timer_list.is_empty()) {
+ TimerList* t = list_entry<TimerList, &TimerList::entry>(g_timer_list.next);
+ if (t->expires > g_jiffies) {
+ break;
+ }
+
+ t->entry.prev->next = t->entry.next;
+ t->entry.next->prev = t->entry.prev;
+ t->entry.init();
+
+ g_timer_lock.release();
+ t->function(t);
+ g_timer_lock.acquire();
+ }
+
+ g_timer_lock.release();
+}
+
static void timer_set_next() {
arch::sbi::set_timer(arch::cpu::read_time() + g_tick_interval);
}
@@ -24,21 +86,33 @@ void timer_init() {
// Default 100 Hz (10ms per tick)
g_tick_interval = g_timer_freq / 100;
+ g_timer_lock.init("timer_list");
+ g_timer_list.init();
+
timer_set_next();
+
arch::cpu::timer_intr_on();
}
+
void timer_init_ap() {
timer_set_next();
arch::cpu::timer_intr_on();
}
+extern "C" uint32 bsp_hart_id;
+
void timer_tick() {
- g_jiffies++;
+ if (arch::cpu::id() == bsp_hart_id) {
+ g_jiffies++;
+ run_timers();
+ }
+
timer_set_next();
sched_tick();
}
+
uint64 timer_get_freq() {
return g_timer_freq;
}
diff --git a/na/kernel/trap.cc b/na/kernel/trap.cc
index 738b2db..2116d91 100644
--- a/na/kernel/trap.cc
+++ b/na/kernel/trap.cc
@@ -15,6 +15,7 @@
* Reference: docs/specs/trap.md §4
*/
+#include <arch/cpu.h>
#include <lume/trap.h>
#include <lume/irq.h>
#include <lume/console.h>
@@ -26,8 +27,11 @@
#include <lume/syscall.h>
#include <lume/task.h>
#include <lume/vmm.h>
+#include <lume/signal.h>
#include <lume/signal_nr.h>
+#include <lume/smp.h>
#include <arch/trapframe.h> /* TrapFrame: cross-layer data contract */
+#include <arch/extable.h> /* search_extable: kernel fault recovery */
/* ========================================================================
* trap_handle() — MI-layer dispatcher for traps
@@ -39,22 +43,18 @@ static TrapStats g_trap_stats = {};
static uint32 trap_cause_index(TrapCause cause)
{
- switch (cause) {
- case TrapCause::PageFaultLoad: return 0;
- case TrapCause::PageFaultStore: return 1;
- case TrapCause::PageFaultExec: return 2;
- case TrapCause::Syscall: return 3;
- case TrapCause::IllegalInst: return 4;
- case TrapCause::Breakpoint: return 5;
- case TrapCause::AccessFault: return 6;
- case TrapCause::AlignFault: return 7;
- case TrapCause::IrqTimer: return 8;
- case TrapCause::IrqSoftware: return 9;
- case TrapCause::IrqExternal: return 10;
- case TrapCause::Unknown:
- default:
- return 11;
+ uint32 raw = static_cast<uint32>(cause);
+
+ /* Exceptions 0-7 map directly to indices 0-7 */
+ if (raw <= 7) {
+ return raw;
+ }
+ /* IRQs 16-18 map to indices 8-10 */
+ if (raw >= 16 && raw <= 18) {
+ return raw - 16 + 8;
}
+ /* Unknown / out-of-range */
+ return 11;
}
static void trap_record(const TrapInfo& info)
@@ -100,10 +100,33 @@ void trap_handle(arch::TrapFrame* tf, const TrapInfo& info)
{
trap_record(info);
+ /* ---- Kernel exception handling ----
+ * Interrupts are always allowed through. For exceptions:
+ * - Page faults: handled in the switch below (extable fixup path)
+ * - Other exceptions: try extable, then panic.
+ *
+ * This replaces the old blanket kernel_panic, enabling
+ * copy_from_user/copy_to_user to safely fault on user pages. */
if (info.origin == TrapOrigin::FromKernel &&
info.cls != TrapClass::Interrupt) {
- kprintf("[trap] kernel exception\n");
- kernel_panic("kernel exception");
+
+ /* Page faults are dispatched in the switch — let them through */
+ bool is_page_fault =
+ (info.cause == TrapCause::PageFaultLoad ||
+ info.cause == TrapCause::PageFaultStore ||
+ info.cause == TrapCause::PageFaultExec);
+
+ if (!is_page_fault) {
+ /* Non-page-fault kernel exception: try extable fixup */
+ uint64 fixup = search_extable(info.epc);
+ if (fixup != 0) {
+ tf->epc = fixup;
+ return;
+ }
+ kprintf("[trap] fatal kernel exception: cause=%u epc=0x%llx val=0x%llx\n",
+ static_cast<unsigned>(info.cause), info.epc, info.val);
+ kernel_panic("unrecoverable kernel exception");
+ }
}
switch (info.cause) {
@@ -120,17 +143,43 @@ void trap_handle(arch::TrapFrame* tf, const TrapInfo& info)
int ret = vmm_handle_page_fault(curr->mm, info.val,
vm_fault_cause_from_trap(info.cause));
if (ret == -EFAULT) {
- task_kill_current(SIGSEGV);
+ kprintf("[trap] user SIGSEGV (MAPERR) epc=0x%llx addr=0x%llx\n",
+ info.epc, info.val);
+ force_sig_fault(SIGSEGV, 1 /* SEGV_MAPERR */, info.val);
} else if (ret == -ENOMEM) {
- task_kill_current(SIGKILL);
+ kprintf("[trap] user SIGKILL (OOM) epc=0x%llx addr=0x%llx\n",
+ info.epc, info.val);
+ force_sig_fault(SIGKILL, 0, info.val);
} else if (ret < 0) {
- task_kill_current(SIGSEGV);
+ kprintf("[trap] user SIGSEGV (ACCERR) epc=0x%llx addr=0x%llx\n",
+ info.epc, info.val);
+ force_sig_fault(SIGSEGV, 2 /* SEGV_ACCERR */, info.val);
}
break;
}
- kprintf("[trap] kernel page fault\n");
- kernel_panic("unhandled page fault");
+ /* Kernel page fault — try exception table fixup.
+ * This is the path used by copy_from_user/copy_to_user when
+ * accessing unmapped or faulting user addresses from S-mode. */
+ {
+ uint64 fixup = search_extable(info.epc);
+ if (fixup != 0) {
+ tf->epc = fixup;
+ break;
+ }
+ }
+ kprintf("[trap] kernel page fault: epc=0x%llx addr=0x%llx\n",
+ info.epc, info.val);
+ {
+ TaskControlBlock* curr = current_task();
+ if (curr) {
+ kprintf("[trap] pid=%d state=%d\n",
+ curr->pid, static_cast<int>(curr->state));
+ kprintf("[trap] tf->epc=%llx tf->ra=%llx tf->sp=%llx\n",
+ tf->epc, tf->ra, tf->sp);
+ }
+ }
+ kernel_panic("unhandled kernel page fault (no extable entry)");
break;
/* ---- System Call ---- */
@@ -155,14 +204,20 @@ void trap_handle(arch::TrapFrame* tf, const TrapInfo& info)
}
case TrapCause::IrqSoftware:
- /* IPI handling — Phase 2 stub */
+ /* S-mode software interrupt = IPI from another hart */
+ smp_handle_ipi();
break;
/* ---- Fatal exceptions ---- */
case TrapCause::IllegalInst:
- kprintf("[trap] illegal instruction\n");
- /* Phase 2: panic. Phase 3: task_kill(SIGILL) */
- kernel_panic("illegal instruction in user mode");
+ if (info.origin == TrapOrigin::FromUser) {
+ kprintf("[trap] user illegal instruction at epc=0x%llx stval=0x%llx\n",
+ info.epc, info.val);
+ force_sig_fault(SIGILL, 1 /* ILL_ILLOPC */, info.epc);
+ } else {
+ kprintf("[trap] illegal instruction at epc=0x%llx\n", info.epc);
+ kernel_panic("illegal instruction in kernel mode");
+ }
break;
case TrapCause::AccessFault:
@@ -187,8 +242,17 @@ void trap_handle(arch::TrapFrame* tf, const TrapInfo& info)
break;
}
- /* Check if rescheduling is needed before returning to user mode */
+ /* Process pending signals before returning to user mode */
if (info.origin == TrapOrigin::FromUser) {
+ do_signal(tf);
+ }
+
+ /* Preemption check on return from any interrupt.
+ * This is the kernel's equivalent of Linux's retint_kernel path:
+ * if a timer tick (or IPI) set need_resched, we must call schedule()
+ * before returning — otherwise the idle loop never yields to
+ * runnable tasks like PID 1. */
+ if (info.cls == TrapClass::Interrupt) {
sched_check_preempt();
}
}
diff --git a/na/kernel/uaccess.cc b/na/kernel/uaccess.cc
index 4a5b963..14c5fb7 100644
--- a/na/kernel/uaccess.cc
+++ b/na/kernel/uaccess.cc
@@ -15,25 +15,11 @@
#include <lume/task.h>
#include <lume/errno.h>
#include <lume/config.h>
-
-/* Kernel VA base — addresses at or above this are kernel space */
-inline constexpr uint64 kKernelVABase = 0xFFFFFFC000000000ULL;
+#include <lume/mm_layout.h>
extern "C" int __uaccess_memcpy(void* dst, const void* src, uint64 len);
extern "C" int64 __uaccess_strncpy(char* dst, const char* src, uint64 max_len);
-/* Validate that [addr, addr+len) is entirely in user space */
-static bool is_user_range(uint64 addr, uint64 len)
-{
- if (addr >= kKernelVABase)
- return false;
- if (addr + len < addr) /* overflow */
- return false;
- if (addr + len > kKernelVABase)
- return false;
- return true;
-}
-
int copy_from_user(void* kernel_dst, uint64 user_src, uint64 len)
{
if (len == 0) return 0;
@@ -56,9 +42,9 @@ int64 strncpy_from_user(char* kernel_dst, uint64 user_src, uint64 max_len)
/* We must also check that the string does not cross into kernel space.
This is safely handled by the assembly routine which will fault if it hits
- an unmapped page. However, to strictly respect kKernelVABase, we should
- restrict max_len to the distance to kKernelVABase. */
- uint64 max_allowed = kKernelVABase - user_src;
+ an unmapped page. However, to strictly respect kKernelVaBase, we should
+ restrict max_len to the distance to kKernelVaBase. */
+ uint64 max_allowed = kKernelVaBase - user_src;
if (max_len > max_allowed) {
max_len = max_allowed;
}
diff --git a/na/kernel/waitqueue.cc b/na/kernel/waitqueue.cc
index 9ee3f46..9689417 100644
--- a/na/kernel/waitqueue.cc
+++ b/na/kernel/waitqueue.cc
@@ -4,11 +4,18 @@
/*
* WaitQueue — blocking synchronization implementation
*
- * sleep(): current task → Sleeping, link into queue, schedule()
- * wake_one(): dequeue head task → Runnable, sched_enqueue()
+ * Two-phase wait protocol:
+ *
+ * prepare_to_wait(): task → Sleeping, link into queue
+ * [caller checks condition — if still false, calls schedule()]
+ * finish_wait(): remove from queue, restore Runnable
+ *
+ * Legacy sleep() wraps the two-phase pattern for simple callers.
+ *
+ * wake_one()/wake_all(): dequeue waiters → Runnable, sched_enqueue()
*
* The internal spinlock protects the wait list only.
- * schedule() is called AFTER releasing the spinlock to avoid
+ * schedule() is called AFTER releasing all spinlocks to avoid
* holding a lock across a context switch.
*/
@@ -18,40 +25,80 @@
#include <lume/kprintf.h>
#include <arch/cpu.h>
-void WaitQueue::sleep(Spinlock* external_lock)
-{
- TaskControlBlock* curr = current_task();
- if (!curr) {
- if (external_lock) external_lock->release();
- return;
- }
+/* ------------------------------------------------------------------ */
+/* Two-phase wait API */
+/* ------------------------------------------------------------------ */
- {
- LockGuard guard(lock_);
-
- /* Mark task as sleeping */
- curr->state = TaskState::Sleeping;
+void WaitQueue::prepare_to_wait(TaskControlBlock* task, TaskState new_state)
+{
+ LockGuard guard(lock_);
- /* Remove from RunQueue (sched_dequeue acquires its own lock) */
- /* We do NOT call sched_dequeue here because schedule() handles
- * the put_prev_task logic when state != Running. */
+ /* Set state BEFORE linking — this is the key to preventing lost wakeups.
+ * If wake_one fires between now and schedule(), it will set state back
+ * to Runnable, and schedule() will see Runnable and not block. */
+ task->state = new_state;
+ /* Only add to queue if not already linked (idempotent) */
+ if (task->run_link.next == &task->run_link) {
/* Link into wait queue tail */
- list_node* node = &curr->run_link;
+ list_node* node = &task->run_link;
node->prev = head_.prev;
node->next = &head_;
head_.prev->next = node;
head_.prev = node;
}
+}
+
+void WaitQueue::finish_wait(TaskControlBlock* task)
+{
+ LockGuard guard(lock_);
+
+ /* Remove from wait queue if still linked */
+ if (task->run_link.next != &task->run_link) {
+ list_node* node = &task->run_link;
+ node->prev->next = node->next;
+ node->next->prev = node->prev;
+ node->init();
+ }
+
+ /* Ensure task is Runnable (it may have been woken by wake_one/wake_all
+ * already, or it may never have slept if the condition was true). */
+ if (task->state == TaskState::Sleeping) {
+ task->state = TaskState::Runnable;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Legacy convenience API */
+/* ------------------------------------------------------------------ */
+void WaitQueue::sleep(Spinlock* external_lock)
+{
+ TaskControlBlock* curr = current_task();
+ if (!curr) {
+ if (external_lock) external_lock->release();
+ return;
+ }
+
+ /* Phase 1: add to queue, mark Sleeping */
+ prepare_to_wait(curr, TaskState::Sleeping);
+
+ /* Release external lock AFTER we are on the queue (critical ordering) */
if (external_lock) {
external_lock->release();
}
- /* Context switch away — will return here when woken */
+ /* Phase 2: context switch away — will return here when woken */
schedule();
+
+ /* Phase 3: clean up */
+ finish_wait(curr);
}
+/* ------------------------------------------------------------------ */
+/* Wakeup */
+/* ------------------------------------------------------------------ */
+
bool WaitQueue::wake_one()
{
LockGuard guard(lock_);