diff options
| author | Richard Qin <richard.qin.x@gmail.com> | 2026-07-10 11:31:46 +0800 |
|---|---|---|
| committer | Richard Qin <richard.qin.x@gmail.com> | 2026-07-10 11:31:46 +0800 |
| commit | e90b72ec3082588b296956730bd2930cf8d5d671 (patch) | |
| tree | 36ff6916af9b4176ea5a9662219d11e721f0b691 /kernel | |
| parent | 1767d390fbf3d0c65389de46caf0b6d43ef3b6ad (diff) | |
| download | lume-e90b72ec3082588b296956730bd2930cf8d5d671.tar.gz lume-e90b72ec3082588b296956730bd2930cf8d5d671.tar.xz | |
refactor: promote new architecture to repository root
Archive the deprecated implementation under lume-legacy, make the new kernel the default root-level build, and update repository entry points without changing kernel behavior.
Diffstat (limited to 'kernel')
| -rw-r--r-- | kernel/exec.cc | 659 | ||||
| -rw-r--r-- | kernel/irq.cc | 106 | ||||
| -rw-r--r-- | kernel/main.cc | 468 | ||||
| -rw-r--r-- | kernel/module.mk | 26 | ||||
| -rw-r--r-- | kernel/pgrp.cc | 148 | ||||
| -rw-r--r-- | kernel/pid.cc | 63 | ||||
| -rw-r--r-- | kernel/poll.cc | 250 | ||||
| -rw-r--r-- | kernel/ptrace.cc | 92 | ||||
| -rw-r--r-- | kernel/sched_class_o1.cc | 81 | ||||
| -rw-r--r-- | kernel/sched_core.cc | 571 | ||||
| -rw-r--r-- | kernel/sched_internal.h | 115 | ||||
| -rw-r--r-- | kernel/signal.cc | 442 | ||||
| -rw-r--r-- | kernel/smp.cc | 112 | ||||
| -rw-r--r-- | kernel/sync/cpu_state.h | 35 | ||||
| -rw-r--r-- | kernel/sync/mutex.cc | 156 | ||||
| -rw-r--r-- | kernel/sync/spinlock.cc | 120 | ||||
| -rw-r--r-- | kernel/sync/spinlock.h | 61 | ||||
| -rw-r--r-- | kernel/sys_fs.cc | 679 | ||||
| -rw-r--r-- | kernel/sys_info.cc | 49 | ||||
| -rw-r--r-- | kernel/sys_misc.cc | 360 | ||||
| -rw-r--r-- | kernel/sys_mm.cc | 178 | ||||
| -rw-r--r-- | kernel/sys_process.cc | 185 | ||||
| -rw-r--r-- | kernel/sys_signal.cc | 351 | ||||
| -rw-r--r-- | kernel/syscall.cc | 208 | ||||
| -rw-r--r-- | kernel/task.cc | 780 | ||||
| -rw-r--r-- | kernel/timer.cc | 118 | ||||
| -rw-r--r-- | kernel/trap.cc | 258 | ||||
| -rw-r--r-- | kernel/uaccess.cc | 53 | ||||
| -rw-r--r-- | kernel/waitqueue.cc | 144 |
29 files changed, 6868 insertions, 0 deletions
diff --git a/kernel/exec.cc b/kernel/exec.cc new file mode 100644 index 0000000..616c0da --- /dev/null +++ b/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/kernel/irq.cc b/kernel/irq.cc new file mode 100644 index 0000000..4de468a --- /dev/null +++ b/kernel/irq.cc @@ -0,0 +1,106 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * IRQ Manager Implementation + * + * Lock-free dispatch path for interrupt context safety. + * Registration/unregistration is protected by atomic publish. + * + * Reference: docs/specs/trap.md §3.3 + */ + +#include <lume/console.h> +#include <lume/errno.h> +#include <lume/irq.h> + +/* Global singleton */ +IrqManager g_irq_manager; + +void IrqManager::init() { + for (uint32 i = 0; i < kMaxIrqs; i++) { + entries_[i].handler_ptr.store(0, __ATOMIC_RELAXED); + entries_[i].arg = nullptr; + } +} + +int IrqManager::register_handler(uint32 irq, IrqHandler handler, void *arg) { + if (irq >= kMaxIrqs) + return -EINVAL; + + if (!handler) + return -EINVAL; + + /* Check if already registered (CAS from 0 to handler) */ + uint64 expected = 0; + uint64 desired = reinterpret_cast<uint64>(handler); + + /* Set arg first (before publishing handler) so dispatch sees valid arg */ + entries_[irq].arg = arg; + + if (!entries_[irq].handler_ptr.compare_exchange(expected, desired, + __ATOMIC_ACQ_REL)) { + /* Already registered by someone else */ + return -EBUSY; + } + + return 0; +} + +void IrqManager::unregister_handler(uint32 irq) { + if (irq >= kMaxIrqs) + return; + + /* Clear handler with release semantics */ + entries_[irq].handler_ptr.store(0, __ATOMIC_RELEASE); + entries_[irq].arg = nullptr; +} + +void IrqManager::dispatch(uint32 irq) { + if (irq >= kMaxIrqs) + return; + + /* Load handler with acquire semantics — pairs with register's release */ + uint64 h = entries_[irq].handler_ptr.load(__ATOMIC_ACQUIRE); + if (h == 0) { + /* Spurious or unregistered IRQ — silently ignore */ + return; + } + + auto handler = reinterpret_cast<IrqHandler>(h); + handler(irq, entries_[irq].arg); +} + +bool IrqManager::has_handler(uint32 irq) const { + if (irq >= kMaxIrqs) + return false; + return entries_[irq].handler_ptr.load(__ATOMIC_ACQUIRE) != 0; +} + +void IrqManager::set_chip(const IrqChip* chip) { + chip_ = chip; +} + +uint32 IrqManager::claim() const { + return chip_ ? chip_->claim() : 0; +} + +void IrqManager::complete(uint32 irq) const { + if (chip_) chip_->complete(irq); +} + +void IrqManager::init_ap() const { + if (chip_ && chip_->init_ap) chip_->init_ap(); +} + +void IrqManager::enable(uint32 irq) const { + if (chip_ && chip_->enable) chip_->enable(irq); +} + +void IrqManager::disable(uint32 irq) const { + if (chip_ && chip_->disable) chip_->disable(irq); +} + +void IrqManager::set_priority(uint32 irq, uint32 priority) const { + if (chip_ && chip_->set_priority) chip_->set_priority(irq, priority); +} diff --git a/kernel/main.cc b/kernel/main.cc new file mode 100644 index 0000000..db5843c --- /dev/null +++ b/kernel/main.cc @@ -0,0 +1,468 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * LumeOS Kernel Main Entry — Global Initialization Sequence + * + * All CPUs enter kernel_main() after the assembly bootstrap (entry.S) + * has enabled MMU, cleared BSS, and called early_cpp_init(). + * + * 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/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> + +// 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. +// Each is implemented by its owning module. On failure they +// call kernel_panic() directly — no error return. +// ============================================================ + +// BSP global init (strict order) +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(); // 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(); + +// AP per-CPU init +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. +// ============================================================ +static lume::atomic<uint32> ap_boot_sync; + +// ============================================================ +// kernel_main — C++ entry point for all CPUs +// ============================================================ +extern "C" [[noreturn]] void kernel_main(uint64 cpu_id, uint64 fdt_paddr) { + + if (cpu_id == bsp_hart_id) { + // ======================================================== + // BSP: Absolute single-core period — no locks needed. + // APs are spinning on ap_boot_sync. + // ======================================================== + + uint64 fdt_pa = fdt_paddr; + + // ---- Phase 1: Console + Exception Safety Net ---- + + // 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=%llu\n", cpu_id); + printk(KERN_INFO "boot: higher-half kernel active\n"); + + /* 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) --- + + // 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"); + + // 4. KASLR — seed PRNG and randomise VA layout. + // MUST happen before vmm_init() builds the kernel page table. + { + uint64 kaslr_seed = 0; + fdt_early_get_kaslr_seed(&kaslr_seed); + random_early_init(kaslr_seed); + arch::kaslr_init(); + } + + // Sort exception table before setting RO permissions in vmm_init() + extable_init(); + + // ---- 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"); + + // 6. Virtual memory manager — builds page table with KASLR'd bases + vmm_init(); + printk(KERN_INFO "init: vmm_init done\n"); + + // 7. Physical memory manager (Buddy) + pmm_init(); + printk(KERN_INFO "init: pmm_init done\n"); + + // 8. Slab allocator — operator new usable after this + slab_init(); + printk(KERN_INFO "init: slab_init done\n"); + + // 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"); + + // ---- Phase 3: Task & Scheduler (software state before hardware) ---- + + // 11. Task management (PID allocator + global task list) + task_init(); + printk(KERN_INFO "init: task_init done\n"); + + 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"); + + // 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"); + + // 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) + + // ---- 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(); + + // 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"); + + // Release APs with RELEASE semantics: + // 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, which will pick up PID 1. + arch::cpu::intr_on(); + + printk(KERN_INFO "boot: BSP entering idle loop\n"); + + } else { + // ======================================================== + // 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); + + 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 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) + } + + // 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/kernel/module.mk b/kernel/module.mk new file mode 100644 index 0000000..754a14c --- /dev/null +++ b/kernel/module.mk @@ -0,0 +1,26 @@ +# kernel/module.mk +SRCS_CC += kernel/main.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/kernel/pgrp.cc b/kernel/pgrp.cc new file mode 100644 index 0000000..e5b382c --- /dev/null +++ b/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/kernel/pid.cc b/kernel/pid.cc new file mode 100644 index 0000000..04b846b --- /dev/null +++ b/kernel/pid.cc @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * PID Allocator — Bitmap-based O(1) block search + * Reference: docs/specs/task.md §3.2 + */ + +#include <lume/pid.h> + +void PidAllocator::init() +{ + for (int i = 0; i < kBitmapWords; i++) + bitmap_[i] = 0; + 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() +{ + LockGuard guard(lock_); + + int32 word_start = next_hint_ / 64; + + for (int32 passes = 0; passes < kBitmapWords; passes++) { + int32 w = (word_start + passes) % kBitmapWords; + uint64 free_bits = ~bitmap_[w]; + if (free_bits == 0) + continue; + + int bit = 0; + uint64 v = free_bits; + while ((v & 1ULL) == 0) { v >>= 1; bit++; } + int32 pid = w * 64 + bit; + + if (pid >= kMaxPid) + continue; + + bitmap_[w] |= (1ULL << bit); + next_hint_ = pid + 1; + if (next_hint_ >= kMaxPid) + next_hint_ = 2; + + return pid; + } + + return -1; +} + +void PidAllocator::free(int32 pid) +{ + if (pid < 0 || pid >= kMaxPid) + return; + + LockGuard guard(lock_); + int32 w = pid / 64; + int32 bit = pid % 64; + bitmap_[w] &= ~(1ULL << bit); +} + +PidAllocator g_pid_alloc; diff --git a/kernel/poll.cc b/kernel/poll.cc new file mode 100644 index 0000000..3674491 --- /dev/null +++ b/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/kernel/ptrace.cc b/kernel/ptrace.cc new file mode 100644 index 0000000..3280c07 --- /dev/null +++ b/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/kernel/sched_class_o1.cc b/kernel/sched_class_o1.cc new file mode 100644 index 0000000..28c693c --- /dev/null +++ b/kernel/sched_class_o1.cc @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * O(1) Priority Bitmap Scheduling Class + */ + +#include <lume/sched.h> +#include <lume/sched_class.h> +#include <lume/task.h> +#include <lume/bitops.h> +#include <kernel/sched_internal.h> + +static void o1_init_rq(RunQueue* rq) +{ + rq->o1().init(); +} + +static void o1_enqueue_task(RunQueue* rq, TaskControlBlock* task) +{ + rq->o1().enqueue(task); + rq->inc_running(); +} + +static void o1_dequeue_task(RunQueue* rq, TaskControlBlock* task) +{ + rq->o1().dequeue(task); + rq->dec_running(); +} + +static TaskControlBlock* o1_pick_next_task(RunQueue* rq) +{ + return rq->o1().pick_next(); +} + +static void o1_put_prev_task(RunQueue* /*rq*/, TaskControlBlock* /*task*/) {} + +int32 priority_to_timeslice(uint8 priority) +{ + int32 ts = 20 - static_cast<int32>(priority) * 19 / 31; + if (ts < 1) ts = 1; + return ts; +} + +static void o1_task_tick(RunQueue* rq, TaskControlBlock* curr) +{ + 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) { + curr->time_slice = priority_to_timeslice(curr->priority); + rq->set_need_resched(true); + } +} + +static void o1_yield_task(RunQueue* rq, TaskControlBlock* curr) +{ + if (curr == rq->idle()) + return; + + curr->time_slice = 0; + rq->set_need_resched(true); +} + +const SchedClass g_sched_class_o1 = { + .name = "o1", + .next = &g_sched_class_idle, + .init_rq = o1_init_rq, + .enqueue_task = o1_enqueue_task, + .dequeue_task = o1_dequeue_task, + .pick_next_task = o1_pick_next_task, + .put_prev_task = o1_put_prev_task, + .task_tick = o1_task_tick, + .yield_task = o1_yield_task, +}; diff --git a/kernel/sched_core.cc b/kernel/sched_core.cc new file mode 100644 index 0000000..c0e068e --- /dev/null +++ b/kernel/sched_core.cc @@ -0,0 +1,571 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * Scheduler Core + * + * The core walks sched_class chain to pick the next task. Each class + * owns its own runqueue data and policy. This file owns the per-CPU + * runqueue array and the scheduling state machine. + */ + +#include <lume/sched.h> +#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> + +/* Assembly trampoline symbol (MD layer) */ +extern "C" void kthread_entry_trampoline(); + +/* Per-CPU runqueue array */ +static RunQueue g_rq[kMaxCpus]; + +/* sched_class chain head (highest priority first) */ +static const SchedClass* g_sched_class_head = &g_sched_class_o1; + +/* ======================================================================== + * Idle thread + * ======================================================================== */ + +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) + * ======================================================================== */ + +static void idle_init_rq(RunQueue* /*rq*/) {} +static void idle_enqueue(RunQueue* /*rq*/, TaskControlBlock* /*task*/) {} +static void idle_dequeue(RunQueue* /*rq*/, TaskControlBlock* /*task*/) {} +static TaskControlBlock* idle_pick_next(RunQueue* rq) { return rq->idle(); } +static void idle_put_prev(RunQueue* /*rq*/, TaskControlBlock* /*task*/) {} +static void idle_task_tick(RunQueue* /*rq*/, TaskControlBlock* /*curr*/) {} +static void idle_yield(RunQueue* /*rq*/, TaskControlBlock* /*curr*/) {} + +const SchedClass g_sched_class_idle = { + .name = "idle", + .next = nullptr, + .init_rq = idle_init_rq, + .enqueue_task = idle_enqueue, + .dequeue_task = idle_dequeue, + .pick_next_task = idle_pick_next, + .put_prev_task = idle_put_prev, + .task_tick = idle_task_tick, + .yield_task = idle_yield, +}; + +/* ======================================================================== + * Internal helpers + * ======================================================================== */ + +static RunQueue* rq_for_cpu(uint32 cpu_id) +{ + return &g_rq[cpu_id]; +} + +static RunQueue* this_rq() +{ + return rq_for_cpu(arch::cpu::id()); +} + +static TaskControlBlock* pick_next_task(RunQueue* rq) +{ + for (const SchedClass* c = g_sched_class_head; c; c = c->next) { + TaskControlBlock* t = c->pick_next_task(rq); + if (t) { + return t; + } + } + return rq->idle(); +} + +static const char* idle_name_for_cpu(uint32 cpu) +{ + static char names[kMaxCpus][16]; + if (cpu >= kMaxCpus) { + kernel_panic("sched: cpu_id out of range"); + } + + if (names[cpu][0] == '\0') { + ksnprintf(names[cpu], sizeof(names[cpu]), "idle/%u", cpu); + } + return names[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(); + rq->lock().release(); + + arch::cpu::intr_on(); + + entry(arg); + + /* 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(); + + /* 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(); +} + +/* ======================================================================== + * 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. + * ======================================================================== */ + +extern TaskControlBlock* alloc_task(const char* name, uint8 priority, + uint8 flags, uint8 cpu); + +/* ======================================================================== + * Public API: current_task() + * ======================================================================== */ + +TaskControlBlock* current_task() +{ + return this_rq()->current(); +} + +/* ======================================================================== + * Public API: sched_enqueue / sched_dequeue + * ======================================================================== */ + +void sched_enqueue(TaskControlBlock* task) +{ + RunQueue* rq = rq_for_cpu(task->cpu_id); + LockGuard guard(rq->lock()); + + if (!task->sched_class) { + task->sched_class = &g_sched_class_o1; + } + if (!task->sched_entity) { + task->sched_entity = &task->run_link; + } + + 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) +{ + RunQueue* rq = rq_for_cpu(task->cpu_id); + LockGuard guard(rq->lock()); + + if (!task->sched_class) { + task->sched_class = &g_sched_class_o1; + } + task->sched_class->dequeue_task(rq, task); +} + +/* ======================================================================== + * schedule() — main scheduling entry point + * ======================================================================== */ + +void schedule() +{ + RunQueue* rq = this_rq(); + rq->lock().acquire(); + + TaskControlBlock* prev = rq->current(); + + if (prev && prev->state == TaskState::Running) { + prev->state = TaskState::Runnable; + prev->sched_class->enqueue_task(rq, prev); + } else if (prev) { + prev->sched_class->put_prev_task(rq, prev); + } + + TaskControlBlock* next = pick_next_task(rq); + + if (prev == next) { + /* We picked the same task. It must be Running again. */ + next->state = TaskState::Running; + next->sched_class->dequeue_task(rq, next); + rq->clear_need_resched(); + rq->lock().release(); + return; + } + + if (next != rq->idle()) { + next->sched_class->dequeue_task(rq, next); + } + + next->state = TaskState::Running; + 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(); +} + +/* ======================================================================== + * sched_tick() — called from timer interrupt (MI trap layer) + * ======================================================================== */ + +void sched_tick() +{ + RunQueue* rq = this_rq(); + { + LockGuard guard(rq->lock()); + TaskControlBlock* curr = rq->current(); + if (!curr) return; + curr->sched_class->task_tick(rq, curr); + } + + /* Periodic load balancing — runs without holding rq lock */ + sched_balance(); +} + +/* ======================================================================== + * 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()) 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(); +} + +/* ======================================================================== + * sched_yield() — voluntary yield + * ======================================================================== */ + +void sched_yield() +{ + RunQueue* rq = this_rq(); + { + LockGuard guard(rq->lock()); + TaskControlBlock* curr = rq->current(); + if (curr && curr != rq->idle()) { + curr->sched_class->yield_task(rq, curr); + rq->set_need_resched(true); + } + } + schedule(); +} + +/* ======================================================================== + * sched_init() — BSP initialization + * ======================================================================== */ + +void sched_init() +{ + for (int i = 0; i < kMaxCpus; i++) { + RunQueue* rq = &g_rq[i]; + rq->init(static_cast<uint32>(i)); + + g_sched_class_o1.init_rq(rq); + g_sched_class_idle.init_rq(rq); + } + + g_sched_class_head = &g_sched_class_o1; + + 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 + - 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[bsp_cpu].set_idle(idle0); + + 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[bsp_cpu].set_current(bsp_task); +} + +/* ======================================================================== + * sched_init_ap() — AP per-CPU initialization + * ======================================================================== */ + +void sched_init_ap() +{ + uint64 cpu = arch::cpu::id(); + + const char* name = idle_name_for_cpu(static_cast<uint32>(cpu)); + + 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"); + + idle->pid = 0; + idle->tgid = 0; + idle->sched_class = &g_sched_class_idle; + idle->sched_entity = nullptr; + + 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; + idle->context.s[0] = reinterpret_cast<uint64>(idle_thread_func); + idle->context.s[1] = 0; + + g_rq[cpu].set_idle(idle); + + 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"); + + ap_task->pid = -1; + ap_task->tgid = -1; + ap_task->state = TaskState::Running; + + 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/kernel/sched_internal.h b/kernel/sched_internal.h new file mode 100644 index 0000000..514b613 --- /dev/null +++ b/kernel/sched_internal.h @@ -0,0 +1,115 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +#pragma once + +#include <lume/types.h> +#include <lume/list.h> +#include <lume/sched.h> +#include <lume/config.h> +#include <lume/bitops.h> +#include <lume/task.h> +#include <kernel/sync/spinlock.h> + +class O1RunQueue { +public: + void init() { + bitmap_ = 0; + nr_running_ = 0; + for (int p = 0; p < kNumPriorities; p++) { + queues_[p].init(); + } + } + + void enqueue(TaskControlBlock* task) { + int prio = task->priority; + + list_node* head = &queues_[prio]; + task->run_link.prev = head->prev; + task->run_link.next = head; + head->prev->next = &task->run_link; + head->prev = &task->run_link; + + bitmap_ |= (1U << prio); + nr_running_++; + + task->state = TaskState::Runnable; + } + + void dequeue(TaskControlBlock* task) { + int prio = task->priority; + + task->run_link.prev->next = task->run_link.next; + task->run_link.next->prev = task->run_link.prev; + task->run_link.init(); + + if (queues_[prio].is_empty()) { + bitmap_ &= ~(1U << prio); + } + + if (nr_running_ > 0) { + nr_running_--; + } + } + + TaskControlBlock* pick_next() const { + if (bitmap_ == 0) { + return nullptr; + } + + int prio = lume::ctz32(bitmap_); + list_node* node = queues_[prio].next; + return list_entry<TaskControlBlock, &TaskControlBlock::run_link>(node); + } + + uint32 nr_running() const { return nr_running_; } + +private: + uint32 bitmap_ = 0; + list_node queues_[kNumPriorities]; + uint32 nr_running_ = 0; +}; + +class RunQueue { +public: + void init(uint32 cpu_id) { + lock_.init("RunQueue"); + cpu_id_ = cpu_id; + nr_running_ = 0; + need_resched_ = false; + current_ = nullptr; + idle_ = nullptr; + } + + Spinlock& lock() { return lock_; } + + uint32 cpu_id() const { return cpu_id_; } + + uint32 nr_running() const { return nr_running_; } + void inc_running() { nr_running_++; } + void dec_running() { if (nr_running_ > 0) nr_running_--; } + + bool need_resched() const { return need_resched_; } + void set_need_resched(bool v) { need_resched_ = v; } + void clear_need_resched() { need_resched_ = false; } + + TaskControlBlock* current() const { return current_; } + void set_current(TaskControlBlock* task) { current_ = task; } + + TaskControlBlock* idle() const { return idle_; } + void set_idle(TaskControlBlock* task) { idle_ = task; } + + O1RunQueue& o1() { return o1_; } + const O1RunQueue& o1() const { return o1_; } + +private: + Spinlock lock_; + uint32 cpu_id_ = 0; + uint32 nr_running_ = 0; + bool need_resched_ = false; + + TaskControlBlock* current_ = nullptr; + TaskControlBlock* idle_ = nullptr; + + O1RunQueue o1_; +}; diff --git a/kernel/signal.cc b/kernel/signal.cc new file mode 100644 index 0000000..53d26da --- /dev/null +++ b/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/kernel/smp.cc b/kernel/smp.cc new file mode 100644 index 0000000..1a79ac3 --- /dev/null +++ b/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/kernel/sync/cpu_state.h b/kernel/sync/cpu_state.h new file mode 100644 index 0000000..1f7dde0 --- /dev/null +++ b/kernel/sync/cpu_state.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +#pragma once + +/* + * Per-CPU Interrupt Nesting Tracker + * + * Tracks spinlock nesting depth on each CPU to ensure interrupts + * are only restored when the last spinlock is released. + * + * BSS-segment global array; zero-initialized by entry.S BSS clear. + * Reference: docs/specs/spinlock.md §3.1 + */ + +#include <lume/types.h> +#include <lume/config.h> + +namespace sync { + +struct CpuSyncState { + int lock_depth; // Number of spinlocks held by this CPU + bool intr_was_on; // Were interrupts enabled before the first lock? +}; + +inline constexpr uint64 kInvalidCpuId = ~0ULL; + +// BSS-segment global array, zero-initialized by entry.S +extern CpuSyncState g_cpu_sync_states[kMaxCpus]; + +// Internal API — called only by Spinlock::acquire/release +void push_intr(); +void pop_intr(); + +} // namespace sync diff --git a/kernel/sync/mutex.cc b/kernel/sync/mutex.cc new file mode 100644 index 0000000..c4d3e9d --- /dev/null +++ b/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/kernel/sync/spinlock.cc b/kernel/sync/spinlock.cc new file mode 100644 index 0000000..f649fe7 --- /dev/null +++ b/kernel/sync/spinlock.cc @@ -0,0 +1,120 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * Spinlock Implementation + * + * TTAS (Test-and-Test-and-Set) spinlock with: + * - Acquire: __ATOMIC_ACQUIRE memory ordering + * - Release: __ATOMIC_RELEASE memory ordering + * - Automatic interrupt disable/enable via push_intr/pop_intr + * - Dead-lock and invalid-release detection via kernel_panic + * + * Reference: docs/specs/spinlock.md + */ + +#include "spinlock.h" +#include "cpu_state.h" +#include <arch/cpu.h> +#include <lume/klog.h> +#include <lume/task.h> + +// ============================================================ +// Per-CPU sync state (BSS zero-initialized) +// ============================================================ +namespace sync { + +CpuSyncState g_cpu_sync_states[kMaxCpus]; + +void push_intr() { + bool was_on = arch::cpu::intr_enabled(); + arch::cpu::intr_off(); + + auto& state = g_cpu_sync_states[arch::cpu::id()]; + if (state.lock_depth == 0) { + state.intr_was_on = was_on; + } + state.lock_depth++; +} + +void pop_intr() { + auto& state = g_cpu_sync_states[arch::cpu::id()]; + state.lock_depth--; + + if (state.lock_depth == 0 && state.intr_was_on) { + arch::cpu::intr_on(); + } +} + +} // namespace sync + +// ============================================================ +// Spinlock methods +// ============================================================ + +void Spinlock::init(const char* name) { + name_ = name; + locked_.store(0, __ATOMIC_RELAXED); + cpu_id_ = sync::kInvalidCpuId; +} + +void Spinlock::acquire() { +#ifdef DEBUG + // Optional: detect interrupt-state leakage + if (arch::cpu::intr_enabled()) { + auto& state = sync::g_cpu_sync_states[arch::cpu::id()]; + if (state.lock_depth > 0) { + kernel_panic("spinlock: interrupts enabled while holding lock"); + } + } +#endif + + // Step 1: disable interrupts, track nesting depth + sync::push_intr(); + + // Step 2: dead-lock detection (same CPU re-acquiring) + if (is_held_by_current_cpu()) { + kernel_panic("spinlock: double lock", name_); + } + + // Step 3: TTAS spin loop + while (true) { + // Outer: spin on local cache copy (no bus traffic) + while (locked_.load(__ATOMIC_RELAXED)) + ; + // Inner: attempt atomic exchange with acquire semantics + if (locked_.exchange(1, __ATOMIC_ACQUIRE) == 0) + break; + } + + // 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() { + // Step 1: verify caller is the holder + if (!is_held_by_current_cpu()) { + kernel_panic("spinlock: invalid release", name_); + } + + // Step 2: clear owner + cpu_id_ = sync::kInvalidCpuId; + + // Step 3: release the lock with release semantics + locked_.store(0, __ATOMIC_RELEASE); + + // 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(); +} + +bool Spinlock::is_held_by_current_cpu() const { + return cpu_id_ == arch::cpu::id(); +} diff --git a/kernel/sync/spinlock.h b/kernel/sync/spinlock.h new file mode 100644 index 0000000..a1369d1 --- /dev/null +++ b/kernel/sync/spinlock.h @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +#pragma once + +/* + * Spinlock & LockGuard + * + * Provides mutual exclusion for SMP kernels using TTAS + * (Test-and-Test-and-Set) with acquire/release memory ordering. + * Interrupt disable/enable is managed automatically via push_intr/pop_intr. + * + * Reference: docs/specs/spinlock.md + */ + +#include <lume/atomic.h> +#include <lume/types.h> +#include "cpu_state.h" + +class Spinlock { +public: + // Default: BSS zero is safe. Call init() to give the lock a name. + Spinlock() = default; + + // Explicit initialization (for global objects or Placement New) + void init(const char* name); + + // Non-copyable, non-movable (lock identity is its address) + Spinlock(const Spinlock&) = delete; + Spinlock& operator=(const Spinlock&) = delete; + Spinlock(Spinlock&&) = delete; + Spinlock& operator=(Spinlock&&) = delete; + + void acquire(); + void release(); + bool is_held_by_current_cpu() const; + +private: + lume::atomic<uint32> locked_; // 0 = free, 1 = held + const char* name_ = "uninit"; // Lock name (for panic messages) + uint64 cpu_id_ = sync::kInvalidCpuId; // Holder CPU ID +}; + +class LockGuard { +public: + explicit LockGuard(Spinlock& lock) : lock_(lock) { + lock_.acquire(); + } + + ~LockGuard() { + lock_.release(); + } + + LockGuard(const LockGuard&) = delete; + LockGuard& operator=(const LockGuard&) = delete; + LockGuard(LockGuard&&) = delete; + LockGuard& operator=(LockGuard&&) = delete; + +private: + Spinlock& lock_; +}; diff --git a/kernel/sys_fs.cc b/kernel/sys_fs.cc new file mode 100644 index 0000000..1d26fdd --- /dev/null +++ b/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/kernel/sys_info.cc b/kernel/sys_info.cc new file mode 100644 index 0000000..79baab0 --- /dev/null +++ b/kernel/sys_info.cc @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * System Information Calls + * + * Implements: uname + * + * Reference: docs/specs/syscall.md §7.3 + */ + +#include <lume/types.h> +#include <lume/errno.h> +#include <lume/uaccess.h> + +/* Linux-compatible utsname structure (65-byte fields) */ +struct utsname { + char sysname[65]; + char nodename[65]; + char release[65]; + char version[65]; + char machine[65]; + char domainname[65]; +}; + +/* Simple safe string copy */ +static void safe_strncpy(char* dst, const char* src, uint64 n) +{ + uint64 i = 0; + for (; i < n - 1 && src[i] != '\0'; i++) + dst[i] = src[i]; + dst[i] = '\0'; +} + +int64 sys_uname(uint64 buf_ptr, uint64, uint64, uint64, uint64, uint64) +{ + utsname info = {}; + safe_strncpy(info.sysname, "LumeOS", sizeof(info.sysname)); + safe_strncpy(info.nodename, "lume", sizeof(info.nodename)); + safe_strncpy(info.release, "0.1.0", sizeof(info.release)); + safe_strncpy(info.version, "Phase 2", sizeof(info.version)); + safe_strncpy(info.machine, "riscv64", sizeof(info.machine)); + safe_strncpy(info.domainname, "(none)", sizeof(info.domainname)); + + if (copy_to_user(buf_ptr, &info, sizeof(info)) < 0) + return -EFAULT; + + return 0; +} diff --git a/kernel/sys_misc.cc b/kernel/sys_misc.cc new file mode 100644 index 0000000..48eb47b --- /dev/null +++ b/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/kernel/sys_mm.cc b/kernel/sys_mm.cc new file mode 100644 index 0000000..bfa067c --- /dev/null +++ b/kernel/sys_mm.cc @@ -0,0 +1,178 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * Memory Management System Calls + * + * Implements: brk, mmap (anonymous only), munmap, mprotect. + * + * Reference: docs/specs/syscall.md §7.2 + */ + +#include <lume/types.h> +#include <lume/task.h> +#include <lume/vmm.h> +#include <lume/errno.h> +#include <lume/mman.h> +#include <lume/config.h> +#include <lume/klog.h> + + +/* Page alignment helpers */ +static inline uint64 page_align_up(uint64 x) +{ + return (x + kPageSize - 1) & ~(kPageSize - 1); +} + +static inline uint64 page_align_down(uint64 x) +{ + return x & ~(kPageSize - 1); +} + +/* ====================================================================== + * sys_brk + * ====================================================================== */ + +int64 sys_brk(uint64 new_brk, uint64, uint64, uint64, uint64, uint64) +{ + VmSpace* mm = current_task()->mm; + if (!mm) + return -ENOMEM; /* kernel threads have no user address space */ + + if (new_brk == 0) { + return static_cast<int64>(mm->brk_end); + } + + if (mm->brk_start == 0) { + uint64 init_brk = page_align_up(new_brk); + mm->brk_start = init_brk; + mm->brk_end = init_brk; + } + + if (new_brk < mm->brk_start) { + return -EINVAL; + } + + uint64 new_end = page_align_up(new_brk); + if (new_end > mm->brk_end) { + uint64 grow = new_end - mm->brk_end; + int ret = vmm_map_user(mm, mm->brk_end, grow, VM_READ | VM_WRITE); + if (ret < 0) { + return ret; + } + } else if (new_end < mm->brk_end) { + uint64 shrink = mm->brk_end - new_end; + int ret = vmm_unmap_user(mm, new_end, shrink); + if (ret < 0) { + return ret; + } + } + + mm->brk_end = new_end; + return static_cast<int64>(new_brk); +} + +/* ====================================================================== + * sys_mmap + * ====================================================================== */ + +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; + + if ((flags & MAP_PRIVATE) && (flags & MAP_SHARED)) return -EINVAL; + if (!(flags & MAP_PRIVATE) && !(flags & MAP_SHARED)) return -EINVAL; + + VmSpace* mm = current_task()->mm; + if (!mm) return -ENOMEM; + + /* Phase 2: only anonymous private mappings */ + 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 */ + + (void)fd; + + /* Convert POSIX prot → MI permission flags */ + uint64 vm_perm = 0; + if (prot & PROT_READ) vm_perm |= VM_READ; + if (prot & PROT_WRITE) vm_perm |= VM_WRITE; + if (prot & PROT_EXEC) vm_perm |= VM_EXEC; + vm_perm |= VM_USER; + + uint64 va = addr; + if (flags & MAP_FIXED) { + if (va % kPageSize != 0) return -EINVAL; + } else if (va != 0) { + va = page_align_down(va); + } + + if (va == 0) { + va = mm_find_free_va(mm, length, kPageSize); + if (!va) return -ENOMEM; + } + + if (flags & MAP_FIXED) { + int ret = vmm_unmap_user(mm, va, length); + if (ret < 0) return ret; + } + + int ret = vmm_map_user(mm, va, length, vm_perm); + if (ret < 0 && !(flags & MAP_FIXED) && addr != 0) { + uint64 fallback = mm_find_free_va(mm, length, kPageSize); + if (!fallback) return ret; + ret = vmm_map_user(mm, fallback, length, vm_perm); + if (ret < 0) return ret; + va = fallback; + } else if (ret < 0) { + return ret; + } + + return static_cast<int64>(va); +} + +/* ====================================================================== + * sys_munmap + * ====================================================================== */ + +int64 sys_munmap(uint64 addr, uint64 length, uint64, uint64, uint64, uint64) +{ + if (length == 0) return -EINVAL; + if (addr % kPageSize != 0) return -EINVAL; + length = page_align_up(length); + + VmSpace* mm = current_task()->mm; + if (!mm) return -EINVAL; + + return vmm_unmap_user(mm, addr, length); +} + +/* ====================================================================== + * sys_mprotect — Phase 2 stub + * ====================================================================== */ + +int64 sys_mprotect(uint64 addr, uint64 len, uint64 prot, + uint64, uint64, uint64) +{ + if (len == 0) return -EINVAL; + if (addr % kPageSize != 0) return -EINVAL; + + VmSpace* mm = current_task()->mm; + if (!mm) return -ENOMEM; + + uint64 vm_perm = 0; + if (prot & PROT_READ) vm_perm |= VM_READ; + if (prot & PROT_WRITE) vm_perm |= VM_WRITE; + if (prot & PROT_EXEC) vm_perm |= VM_EXEC; + + return vmm_protect_user(mm, addr, len, vm_perm); +} diff --git a/kernel/sys_process.cc b/kernel/sys_process.cc new file mode 100644 index 0000000..438ad1b --- /dev/null +++ b/kernel/sys_process.cc @@ -0,0 +1,185 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * Process Control System Calls + * + * Implements: exit, exit_group, getpid, gettid, getppid, + * sched_yield, clone (stub), wait4 (stub), + * set_tid_address, getuid, getgid. + * + * Reference: docs/specs/syscall.md §7.1 + */ + +#include "lume/klog.h" +#include <lume/types.h> +#include <lume/task.h> +#include <lume/sched.h> +#include <lume/errno.h> +#include <lume/uaccess.h> +#include <lume/wait.h> + +/* External: task lifecycle (kernel/task.cc) */ +extern void task_exit(int 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); + +/* ====================================================================== + * sys_exit / sys_exit_group + * ====================================================================== */ + +int64 sys_exit(uint64 code, uint64, uint64, uint64, uint64, uint64) +{ + task_exit(static_cast<int>(code & 0xFF)); + __builtin_unreachable(); +} + +int64 sys_exit_group(uint64 code, uint64, uint64, uint64, uint64, uint64) +{ + /* Phase 2: single-threaded, equivalent to sys_exit. + * Phase 3: kill all threads in tgid then exit. */ + task_exit(static_cast<int>(code & 0xFF)); + __builtin_unreachable(); +} + +/* ====================================================================== + * sys_getpid / sys_gettid / sys_getppid + * ====================================================================== */ + +int64 sys_getpid(uint64, uint64, uint64, uint64, uint64, uint64) +{ + return current_task()->tgid; /* Linux: getpid() returns tgid */ +} + +int64 sys_gettid(uint64, uint64, uint64, uint64, uint64, uint64) +{ + return current_task()->pid; /* Linux: gettid() returns real tid */ +} + +int64 sys_getppid(uint64, uint64, uint64, uint64, uint64, uint64) +{ + TaskControlBlock* parent = current_task()->parent; + return parent ? parent->tgid : 0; +} + +/* ====================================================================== + * sys_sched_yield + * ====================================================================== */ + +int64 sys_sched_yield_wrapper(uint64, uint64, uint64, uint64, uint64, uint64) +{ + sched_yield(); + return 0; +} + +int64 sys_clone_wrapper(uint64 flags, uint64 child_stack, + uint64 ptid, uint64 ctid, uint64 tls, uint64) +{ + arch::TrapFrame* tf = current_task()->trapframe; + if (!tf) return -EINVAL; + + return task_clone(tf, flags, child_stack, ptid, ctid, tls); +} + +/* ======================================================================== + * sys_wait4 — Wait for child to exit + * ======================================================================== */ + +int64 sys_wait4_wrapper(uint64 pid, uint64 wstatus_ptr, + uint64 options, uint64, uint64, uint64) +{ + int32 exit_code = 0; + 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) { + if (copy_to_user(wstatus_ptr, &exit_code, sizeof(int)) < 0) + return -EFAULT; + } + + return child_pid; +} + +/* ====================================================================== + * sys_set_tid_address — musl libc calls this very early + * ====================================================================== */ + +int64 sys_set_tid_address(uint64 tidptr, uint64, uint64, + uint64, uint64, uint64) +{ + /* 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; +} + +/* ====================================================================== + * sys_getuid / sys_getgid — Phase 2: always root + * ====================================================================== */ + +int64 sys_getuid(uint64, uint64, uint64, uint64, uint64, uint64) +{ + return 0; /* root */ +} + +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/kernel/sys_signal.cc b/kernel/sys_signal.cc new file mode 100644 index 0000000..8b459d5 --- /dev/null +++ b/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/kernel/syscall.cc b/kernel/syscall.cc new file mode 100644 index 0000000..a5b80ec --- /dev/null +++ b/kernel/syscall.cc @@ -0,0 +1,208 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * System Call MI Layer — Jump Table & Dispatcher + * + * O(1) function pointer array dispatch. No switch-case. + * syscall_init() registers all implemented handlers. + * + * Reference: docs/specs/syscall.md §6 + */ + +#include <lume/syscall.h> +#include <lume/syscall_nr.h> +#include <lume/errno.h> +#include <lume/kprintf.h> +#include <lume/klog.h> +#include <lume/console.h> +#include <arch/cpu.h> + +/* ====================================================================== + * Jump table + * ====================================================================== */ + +inline constexpr int kNrSyscalls = 512; + +static SyscallHandler sys_call_table[kNrSyscalls]; + +/* ====================================================================== + * MI Dispatcher + * ====================================================================== */ + +int64 syscall_dispatch(uint64 nr, uint64 args[6]) +{ + 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) { + kprintf("[syscall] unimplemented nr=%llu\n", nr); + return -ENOSYS; + } + + return handler(args[0], args[1], args[2], + args[3], args[4], args[5]); +} + +/* ====================================================================== + * Forward declarations of sys_* handlers + * ====================================================================== */ + +/* sys_process.cc */ +int64 sys_exit(uint64, uint64, uint64, uint64, uint64, uint64); +int64 sys_exit_group(uint64, uint64, uint64, uint64, uint64, uint64); +int64 sys_getpid(uint64, uint64, uint64, uint64, uint64, uint64); +int64 sys_gettid(uint64, uint64, uint64, uint64, uint64, uint64); +int64 sys_getppid(uint64, uint64, uint64, uint64, uint64, uint64); +int64 sys_sched_yield_wrapper(uint64, uint64, uint64, uint64, uint64, uint64); +int64 sys_clone_wrapper(uint64, uint64, uint64, uint64, uint64, uint64); +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); +int64 sys_mmap(uint64, uint64, uint64, uint64, uint64, uint64); +int64 sys_munmap(uint64, uint64, uint64, uint64, uint64, uint64); +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 + * ====================================================================== */ + +void syscall_init() +{ + /* Zero the table (redundant for BSS, but explicit) */ + for (int i = 0; i < kNrSyscalls; i++) + sys_call_table[i] = nullptr; + + /* Phase 2: minimal runnable set */ + sys_call_table[SYS_exit] = sys_exit; + sys_call_table[SYS_exit_group] = sys_exit_group; + sys_call_table[SYS_getpid] = sys_getpid; + sys_call_table[SYS_gettid] = sys_gettid; + sys_call_table[SYS_getppid] = sys_getppid; + sys_call_table[SYS_sched_yield] = sys_sched_yield_wrapper; + sys_call_table[SYS_clone] = sys_clone_wrapper; + sys_call_table[SYS_wait4] = sys_wait4_wrapper; + sys_call_table[SYS_set_tid_address] = sys_set_tid_address; + sys_call_table[SYS_getuid] = sys_getuid; + sys_call_table[SYS_getgid] = sys_getgid; + sys_call_table[SYS_brk] = sys_brk; + sys_call_table[SYS_mmap] = sys_mmap; + sys_call_table[SYS_munmap] = sys_munmap; + sys_call_table[SYS_mprotect] = sys_mprotect; + sys_call_table[SYS_uname] = sys_uname; + + /* 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/kernel/task.cc b/kernel/task.cc new file mode 100644 index 0000000..9f9b043 --- /dev/null +++ b/kernel/task.cc @@ -0,0 +1,780 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * Task Management — create, exit, destroy, kthread + * + * Implements task lifecycle management: kernel thread creation, + * task exit (zombie transition), and resource reclamation. + * + * Reference: docs/specs/task.md §4-§6 + */ + +#include <lume/task.h> +#include <lume/sched.h> +#include <lume/pmm.h> +#include <lume/addr.h> +#include <lume/new.h> +#include <lume/kprintf.h> +#include <lume/klog.h> +#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> + +/* ====================================================================== + * Forward declarations + * ====================================================================== */ + +extern "C" void kthread_entry_trampoline(); +extern "C" void ret_from_fork(); + +/* ====================================================================== + * Global task list + * ====================================================================== */ + +list_node g_all_tasks; +Spinlock g_task_list_lock; +int32 g_nr_tasks = 0; + +/* Special tasks */ +TaskControlBlock* g_kernel_task = nullptr; /* PID 0 container */ +TaskControlBlock* g_init_task = nullptr; /* PID 1 (future) */ + +/* ====================================================================== + * task_init() — called once from kernel_main before sched_init + * ====================================================================== */ + +void task_init() +{ + g_all_tasks.init(); + g_task_list_lock.init("task_list"); + g_pid_alloc.init(); + + printk(KERN_INFO "init: task_init done (pid_alloc ready)\n"); +} + +/* ====================================================================== + * 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. + * ====================================================================== */ + +TaskControlBlock* alloc_task(const char* name, uint8 priority, + uint8 flags, uint8 cpu) +{ + auto* task = new TaskControlBlock; + if (!task) return nullptr; + + task->context = {}; + + /* Allocate 8KB kernel stack (order-1 = 2 pages) */ + 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; + + /* 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 = 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->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 */ + extern const SchedClass g_sched_class_o1; + task->sched_class = &g_sched_class_o1; + task->sched_entity = &task->run_link; + + return task; +} + +/* Add task to global task list */ +static void task_list_add(TaskControlBlock* task) +{ + LockGuard guard(g_task_list_lock); + + list_node* head = &g_all_tasks; + task->all_link.prev = head->prev; + task->all_link.next = head; + head->prev->next = &task->all_link; + head->prev = &task->all_link; + + g_nr_tasks++; +} + +/* Remove task from global task list */ +static void task_list_remove(TaskControlBlock* task) +{ + LockGuard guard(g_task_list_lock); + + task->all_link.prev->next = task->all_link.next; + task->all_link.next->prev = task->all_link.prev; + task->all_link.init(); + + if (g_nr_tasks > 0) + g_nr_tasks--; +} + +/* ====================================================================== + * kthread_create — create a kernel thread + * ====================================================================== */ + +TaskControlBlock* kthread_create(const char* name, + void (*entry)(void*), + void* arg, + uint8 priority) +{ + TaskControlBlock* task = alloc_task(name, priority, TF_KTHREAD, + static_cast<uint8>(arch::cpu::id())); + if (!task) return nullptr; + + /* Allocate PID */ + task->pid = g_pid_alloc.alloc(); + if (task->pid < 0) { + /* PID exhausted — clean up */ + pmm_free_pages(pa_to_page(phys_addr(task->kstack_pa)), 1); + delete task; + return nullptr; + } + task->tgid = task->pid; + + /* 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) 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; + head->prev = &task->sibling; + } + + /* Add to global list and enqueue */ + task_list_add(task); + sched_enqueue(task); + + return task; +} + +/* ====================================================================== + * task_exit — transition current task to Zombie + * ====================================================================== */ + +void task_exit(int code) +{ + TaskControlBlock* curr = current_task(); + if (!curr) kernel_panic("task_exit: no current task"); + + /* Linux ABI: normal exit encodes status as (code & 0xFF) << 8 */ + curr->exit_code = (code & 0xFF) << 8; + + /* 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(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; + + /* Move sibling link to foster's children */ + 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(); + } + + + /* ---- 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 */ + if (curr->parent) { + curr->parent->wait_chld.wake_all(); + } + + /* Yield CPU — never returns */ + schedule(); + + __builtin_unreachable(); +} + +void task_kill_current(int sig) +{ + 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(); +} + +/* ====================================================================== + * task_destroy — fully reclaim a Zombie task (called by waitpid) + * ====================================================================== */ + +void task_destroy(TaskControlBlock* task) +{ + /* Remove from global list */ + task_list_remove(task); + + /* Remove from parent's children */ + task->sibling.prev->next = task->sibling.next; + task->sibling.next->prev = task->sibling.prev; + task->sibling.init(); + + /* 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); + } + + /* Free TCB */ + delete task; +} + +/* ====================================================================== + * 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 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(); + + if (curr->children.is_empty()) { + 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; + } + curr->lock.release(); + + if (zombie) { + curr->wait_chld.finish_wait(curr); + int32 pid = zombie->pid; + if (exit_code) *exit_code = zombie->exit_code; + task_destroy(zombie); + return pid; + } + + 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_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_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("cloned", curr->priority, 0, + static_cast<uint8>(arch::cpu::id())); + if (!child) return -ENOMEM; + + /* Allocate PID */ + child->pid = g_pid_alloc.alloc(); + if (child->pid < 0) { + 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; + } + } + } + + /* Copy TrapFrame to the child's kernel stack */ + uint64 tf_addr = child->kstack_va + kKernelStackSize - sizeof(arch::TrapFrame); + child->trapframe = reinterpret_cast<arch::TrapFrame*>(tf_addr); + *child->trapframe = *tf; + + /* 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; + child->context = {}; + child->context.ra = reinterpret_cast<uint64>(ret_from_fork); + child->context.sp = stack_top; + 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(curr->lock); + list_node* head = &curr->children; + child->sibling.prev = head->prev; + child->sibling.next = head; + head->prev->next = &child->sibling; + head->prev = &child->sibling; + } + + /* Add to global list and enqueue */ + 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 + * ====================================================================== */ + +void task_check_stack_canary(TaskControlBlock* task) +{ + if (!task || task->kstack_va == 0) return; + + auto* canary = reinterpret_cast<uint64*>(task->kstack_va); + if (*canary != kStackCanary) { + kernel_panic("kernel stack overflow detected"); + } +} diff --git a/kernel/timer.cc b/kernel/timer.cc new file mode 100644 index 0000000..3cf03cf --- /dev/null +++ b/kernel/timer.cc @@ -0,0 +1,118 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +#include <lume/timer.h> +#include <lume/fdt.h> +#include <lume/sched.h> +#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); +} + +void timer_init() { + DeviceNode* cpus = fdt_get_node_by_path("/cpus"); + if (cpus) { + g_timer_freq = cpus->get_prop_u32("timebase-frequency", g_timer_freq); + } + + // 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() { + 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/kernel/trap.cc b/kernel/trap.cc new file mode 100644 index 0000000..2116d91 --- /dev/null +++ b/kernel/trap.cc @@ -0,0 +1,258 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * Trap MI Layer — Machine-Independent Semantic Dispatcher + * + * Receives a translated TrapCause from the MD layer and routes to + * the appropriate handler. + * + * *** MI INVARIANT: This file contains ZERO inline assembly, + * ZERO arch-specific constants (no CSR reads, no scause codes). *** + * arch/trapframe.h is included because TrapFrame is the cross-layer + * data contract defined in docs/specs/arch_abstraction.md §3.2. + * + * Reference: docs/specs/trap.md §4 + */ + +#include <arch/cpu.h> +#include <lume/trap.h> +#include <lume/irq.h> +#include <lume/console.h> +#include <lume/kprintf.h> +#include <lume/errno.h> +#include <lume/sched.h> +#include <lume/timer.h> +#include <lume/klog.h> +#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 + * + * Called by MD layer after scause → TrapInfo translation. + * ======================================================================== */ + +static TrapStats g_trap_stats = {}; + +static uint32 trap_cause_index(TrapCause cause) +{ + 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) +{ + uint32 idx = trap_cause_index(info.cause); + __atomic_fetch_add(&g_trap_stats.total, 1, __ATOMIC_RELAXED); + __atomic_fetch_add(&g_trap_stats.by_cause[idx], 1, __ATOMIC_RELAXED); + g_trap_stats.last_cause = info.cause; + g_trap_stats.last_class = info.cls; + g_trap_stats.last_origin = info.origin; + g_trap_stats.last_val = info.val; + g_trap_stats.last_epc = info.epc; + g_trap_stats.last_scause = info.scause; +} + +static VmFaultCause vm_fault_cause_from_trap(TrapCause cause) +{ + switch (cause) { + case TrapCause::PageFaultStore: + return VmFaultCause::Write; + case TrapCause::PageFaultExec: + return VmFaultCause::Exec; + case TrapCause::PageFaultLoad: + default: + return VmFaultCause::Read; + } +} + +void trap_stats_get(TrapStats* out) +{ + if (!out) { + return; + } + *out = g_trap_stats; +} + +void trap_stats_reset() +{ + g_trap_stats = {}; +} + +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) { + + /* 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) { + /* ---- Page Faults ---- */ + case TrapCause::PageFaultLoad: + case TrapCause::PageFaultStore: + case TrapCause::PageFaultExec: + if (info.origin == TrapOrigin::FromUser) { + TaskControlBlock* curr = current_task(); + if (!curr || !curr->mm) { + kernel_panic("user page fault with no current mm"); + } + + int ret = vmm_handle_page_fault(curr->mm, info.val, + vm_fault_cause_from_trap(info.cause)); + if (ret == -EFAULT) { + 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) { + 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) { + 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; + } + + /* 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 ---- */ + case TrapCause::Syscall: + syscall_dispatch_md(tf); + break; + + /* ---- Interrupts ---- */ + case TrapCause::IrqTimer: + /* Timer tick */ + timer_tick(); + break; + + case TrapCause::IrqExternal: { + /* External interrupt: generic claim → dispatch → complete */ + uint32 irq = g_irq_manager.claim(); + if (irq != 0) { + g_irq_manager.dispatch(irq); + g_irq_manager.complete(irq); + } + break; + } + + case TrapCause::IrqSoftware: + /* S-mode software interrupt = IPI from another hart */ + smp_handle_ipi(); + break; + + /* ---- Fatal exceptions ---- */ + case TrapCause::IllegalInst: + 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: + kprintf("[trap] access fault\n"); + kernel_panic("access fault"); + break; + + case TrapCause::AlignFault: + kprintf("[trap] alignment fault\n"); + kernel_panic("alignment fault"); + break; + + case TrapCause::Breakpoint: + kprintf("[trap] breakpoint\n"); + /* ebreak advance is handled by MD layer (arch-specific instruction size) */ + break; + + case TrapCause::Unknown: + default: + kprintf("[trap] unknown trap\n"); + kernel_panic("unknown trap"); + break; + } + + /* 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/kernel/uaccess.cc b/kernel/uaccess.cc new file mode 100644 index 0000000..14c5fb7 --- /dev/null +++ b/kernel/uaccess.cc @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * User-Kernel Memory Copy — Phase 3 (Exception Tables) + * + * Validates user pointers by checking they fall within user address space + * (below KERNEL_VA_BASE). Uses assembly-level safe copy functions that + * integrate with the kernel's Exception Table mechanism. + * + * Reference: docs/specs/syscall.md §8 + */ + +#include <lume/uaccess.h> +#include <lume/task.h> +#include <lume/errno.h> +#include <lume/config.h> +#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); + +int copy_from_user(void* kernel_dst, uint64 user_src, uint64 len) +{ + if (len == 0) return 0; + if (!is_user_range(user_src, len)) return -EFAULT; + + return __uaccess_memcpy(kernel_dst, reinterpret_cast<const void*>(user_src), len); +} + +int copy_to_user(uint64 user_dst, const void* kernel_src, uint64 len) +{ + if (len == 0) return 0; + if (!is_user_range(user_dst, len)) return -EFAULT; + + return __uaccess_memcpy(reinterpret_cast<void*>(user_dst), kernel_src, len); +} + +int64 strncpy_from_user(char* kernel_dst, uint64 user_src, uint64 max_len) +{ + if (!is_user_range(user_src, 1)) return -EFAULT; + + /* 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; + if (max_len > max_allowed) { + max_len = max_allowed; + } + + return __uaccess_strncpy(kernel_dst, reinterpret_cast<const char*>(user_src), max_len); +} diff --git a/kernel/waitqueue.cc b/kernel/waitqueue.cc new file mode 100644 index 0000000..9689417 --- /dev/null +++ b/kernel/waitqueue.cc @@ -0,0 +1,144 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2026 Richard Qin */ + +/* + * WaitQueue — blocking synchronization implementation + * + * 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 all spinlocks to avoid + * holding a lock across a context switch. + */ + +#include <lume/waitqueue.h> +#include <lume/task.h> +#include <lume/sched.h> +#include <lume/kprintf.h> +#include <arch/cpu.h> + +/* ------------------------------------------------------------------ */ +/* Two-phase wait API */ +/* ------------------------------------------------------------------ */ + +void WaitQueue::prepare_to_wait(TaskControlBlock* task, TaskState new_state) +{ + LockGuard guard(lock_); + + /* 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 = &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(); + } + + /* 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_); + + if (head_.is_empty()) + return false; + + /* Dequeue the first (oldest) waiter */ + list_node* node = head_.next; + node->prev->next = node->next; + node->next->prev = node->prev; + node->init(); + + TaskControlBlock* task = + list_entry<TaskControlBlock, &TaskControlBlock::run_link>(node); + + task->state = TaskState::Runnable; + sched_enqueue(task); + + return true; +} + +int WaitQueue::wake_all() +{ + LockGuard guard(lock_); + + int count = 0; + while (!head_.is_empty()) { + list_node* node = head_.next; + node->prev->next = node->next; + node->next->prev = node->prev; + node->init(); + + TaskControlBlock* task = + list_entry<TaskControlBlock, &TaskControlBlock::run_link>(node); + + task->state = TaskState::Runnable; + sched_enqueue(task); + count++; + } + + return count; +} |
