From ecdc5be4355285019f369fe1937b7f2b20007de1 Mon Sep 17 00:00:00 2001 From: subhobhai943 Date: Tue, 25 Aug 2026 06:37:20 +0000 Subject: [PATCH] feat(exec): Run userland in ring 3 with an ELF64 loader and per-process page tables SUB-OS had a syscall table and user GDT descriptors but nothing that could actually leave ring 0: lazybox and the shell run as kernel code, and syscall_handler() was never reachable from a user program. This adds the missing path end to end on x86_64. Address spaces (arch/x86_64/mm/vmspace.c) - The boot page tables identity-map the low 4 GB off PML4[0] and leave the rest empty, so userland is given its own PML4 slot (512 GiB) rather than holes punched in the kernel's 2 MB huge-page map. A process PML4 shares entry 0 with the kernel and privately owns entry 1. - Frames outside the identity map are refused rather than written through an unmapped pointer; lifting that needs a recursive map or a higher-half direct map, which is noted in the file. ELF loader (kernel/elf.c) - Validates ELF64/little-endian/ET_EXEC/machine, bounds-checks every program header against both the image and the user address range, and lays PT_LOAD segments into an address space. Frames arrive zeroed, so .bss is covered. Ring transition (arch/x86_64/cpu/usermode.asm) - user_enter() drops to ring 3 via iretq after scrubbing every GPR, and user_return() unwinds back to it when the process exits, so exec looks like a call that blocks until the program finishes. Syscalls - INT 0x80 installed at DPL 3 with the Linux x86_64 register convention. The process gets its own kernel stack for the trap, because TSS.rsp0 must not point at the stack user_enter() is standing on. - Every user pointer is range-checked before use. The process page tables are still live during a syscall, so without that check a program could pass a kernel address and have write(2) dump kernel text to the console. The new /sbin/init asserts both a wild pointer and a kernel pointer are rejected. - write(2) to stdout/stderr routes through the new console_write(), which reuses printk's routing so output reaches serial and the framebuffer console rather than only the VGA text buffer. Faults - A ring 3 exception now kills that process and reports CR2 instead of panicking the machine. Userland (userland/init/) - /sbin/init is a real freestanding static executable, linked at the user base and embedded in the kernel image by scripts/mkblob.py. It reports its pid, verifies .bss was zeroed and .data is writable, checks the two pointer rejections, and exits. aarch64 and armv8i keep building: the loader and exec path are portable, and those ports get honest vmspace stubs that fail cleanly rather than pretending to run a program with kernel privileges. Verified under QEMU: init loads at 0x80004000a0, reports pid 1, .bss zeroed, .data writable, both bad pointers rejected, and exits 0 back into the shell. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0161Jh7uCEMLYEMNPsjC1LKf --- Makefile | 39 +++++- arch/aarch64/mm/vmspace.c | 42 ++++++ arch/armv8i/mm/vmspace.c | 42 ++++++ arch/x86_64/cpu/isr.c | 21 +++ arch/x86_64/cpu/isr_stubs.asm | 9 ++ arch/x86_64/cpu/usermode.asm | 89 +++++++++++++ arch/x86_64/mm/vmspace.c | 239 ++++++++++++++++++++++++++++++++++ include/kernel/elf.h | 98 ++++++++++++++ include/kernel/exec.h | 53 ++++++++ include/kernel/printk.h | 4 + include/mm/vmspace.h | 67 ++++++++++ kernel/elf.c | 116 +++++++++++++++++ kernel/exec.c | 238 +++++++++++++++++++++++++++++++++ kernel/main.c | 5 + kernel/printk.c | 30 +++++ kernel/syscall.c | 136 +++++++++++++++++-- scripts/mkblob.py | 66 ++++++++++ userland/init/init.c | 130 ++++++++++++++++++ userland/init/user.ld | 24 ++++ 19 files changed, 1439 insertions(+), 9 deletions(-) create mode 100644 arch/aarch64/mm/vmspace.c create mode 100644 arch/armv8i/mm/vmspace.c create mode 100644 arch/x86_64/cpu/usermode.asm create mode 100644 arch/x86_64/mm/vmspace.c create mode 100644 include/kernel/elf.h create mode 100644 include/kernel/exec.h create mode 100644 include/mm/vmspace.h create mode 100644 kernel/elf.c create mode 100644 kernel/exec.c create mode 100755 scripts/mkblob.py create mode 100644 userland/init/init.c create mode 100644 userland/init/user.ld diff --git a/Makefile b/Makefile index 9a548c6..288b291 100644 --- a/Makefile +++ b/Makefile @@ -127,6 +127,7 @@ CORE_SRCS = kernel/main.c kernel/task.c kernel/sync.c kernel/timer.c kernel/prin kernel/signal.c kernel/workqueue.c kernel/tsc.c kernel/vt_art.c \ kernel/sub/sub_runtime.c kernel/sub/sub_vm.c \ kernel/wait.c kernel/futex.c kernel/rcu.c kernel/ktest.c \ + kernel/elf.c kernel/exec.c \ lib/string.c lib/vsprintf.c lib/bitmap.c lib/rbtree.c lib/kfifo.c lib/hashtable.c lib/font8x8.c \ init/cmdline.c init/service.c init/bootlogo.c init/logo_data.c usr/initramfs.c block/block.c block/elevator.c \ ipc/sem.c ipc/shm.c ipc/shm_posix.c ipc/pipe.c ipc/ipc.c ipc/msg.c \ @@ -301,7 +302,16 @@ ifeq ($(ARCH), x86_64) ALL_C_SRCS = $(ARCH_C_SRCS) $(CONFIG_SRCS-y) C_OBJS = $(patsubst %.c, $(BUILD_DIR)/%.o, $(ALL_C_SRCS)) ASM_OBJS = $(patsubst %.asm, $(BUILD_DIR)/%.o, $(ARCH_ASM_SRCS)) - OTHER_OBJS = $(filter-out $(ENTRY_OBJ), $(ASM_OBJS) $(C_OBJS)) $(CPP_OBJS) $(RUST_LIB) + + # Userland /sbin/init, linked separately and carried inside the kernel image. + INIT_SRC = userland/init/init.c + INIT_LD = userland/init/user.ld + INIT_OBJ = $(BUILD_DIR)/userland/init/init.o + INIT_ELF = $(BUILD_DIR)/userland/init/init.elf + INIT_BLOB_SRC = $(BUILD_DIR)/usr/init_blob.c + INIT_BLOB_OBJ = $(BUILD_DIR)/usr/init_blob.o + + OTHER_OBJS = $(filter-out $(ENTRY_OBJ), $(ASM_OBJS) $(C_OBJS)) $(CPP_OBJS) $(INIT_BLOB_OBJ) $(RUST_LIB) else CONFIG_SRCS-y += rust/rust_fallback.c kernel/cpp/cpp_fallback.c ALL_C_SRCS = $(ARCH_C_SRCS) $(CONFIG_SRCS-y) @@ -366,6 +376,33 @@ $(BUILD_DIR)/%.o: %.S @mkdir -p $(dir $@) $(CC) $(CFLAGS) $(DEPFLAGS) -c $< -o $@ +# Userland Program Compilation +# +# /sbin/init is a separate freestanding executable, not kernel code: it is +# linked at the user base from userland/user.ld and must never see the kernel's +# -mcmodel=kernel, which cannot reach the user half of the address space. +USER_CFLAGS = -ffreestanding -nostdlib -fno-pie -fno-pic -mno-red-zone \ + -mcmodel=large -mno-sse -mno-mmx -mno-sse2 \ + -Wall -Wextra -Wno-unused-function \ + $(if $(CONFIG_OPTIMIZATION),$(CONFIG_OPTIMIZATION),-O2) + +$(INIT_OBJ): $(INIT_SRC) + @mkdir -p $(dir $@) + $(CC) $(USER_CFLAGS) -c $< -o $@ + +$(INIT_ELF): $(INIT_OBJ) $(INIT_LD) + @mkdir -p $(dir $@) + $(LD) -nostdlib -no-pie -T $(INIT_LD) -o $@ $(INIT_OBJ) + @echo "=== [Userland init linked: $@] ===" + +$(INIT_BLOB_SRC): $(INIT_ELF) scripts/mkblob.py + @mkdir -p $(dir $@) + @python3 scripts/mkblob.py $(INIT_ELF) $@ subos_init_elf + +$(INIT_BLOB_OBJ): $(INIT_BLOB_SRC) include/config/autoconf.h + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -c $< -o $@ + # Rust Source Compilation $(RUST_LIB): rust/src/lib.rs $(RUST_SRCS) @mkdir -p $(dir $@) diff --git a/arch/aarch64/mm/vmspace.c b/arch/aarch64/mm/vmspace.c new file mode 100644 index 0000000..5229f84 --- /dev/null +++ b/arch/aarch64/mm/vmspace.c @@ -0,0 +1,42 @@ +#include + +/* + * Userland address spaces are not wired up on this architecture yet. + * + * The ELF loader and exec path are portable, but they need per-process page + * tables and a ring-3 entry path that only the x86_64 port implements today. + * These stubs fail cleanly so a userland exec reports "not supported" instead + * of silently running a program with kernel privileges. + */ + +vmspace_t* vmspace_create(void) { return NULL; } +void vmspace_destroy(vmspace_t* vm) { (void)vm; } + +int vmspace_map_page(vmspace_t* vm, uint64_t virt, uint64_t phys, int prot) { + (void)vm; (void)virt; (void)phys; (void)prot; + return -1; +} + +int vmspace_alloc(vmspace_t* vm, uint64_t virt, size_t len, int prot) { + (void)vm; (void)virt; (void)len; (void)prot; + return -1; +} + +int vmspace_write(vmspace_t* vm, uint64_t dst, const void* src, size_t len) { + (void)vm; (void)dst; (void)src; (void)len; + return -1; +} + +uint64_t vmspace_resolve(vmspace_t* vm, uint64_t virt) { + (void)vm; (void)virt; + return 0; +} + +void vmspace_switch(vmspace_t* vm) { (void)vm; } + +bool vmspace_range_ok(vmspace_t* vm, uint64_t virt, size_t len) { + (void)vm; (void)virt; (void)len; + return false; +} + +uint64_t vmspace_resident_pages(vmspace_t* vm) { (void)vm; return 0; } diff --git a/arch/armv8i/mm/vmspace.c b/arch/armv8i/mm/vmspace.c new file mode 100644 index 0000000..5229f84 --- /dev/null +++ b/arch/armv8i/mm/vmspace.c @@ -0,0 +1,42 @@ +#include + +/* + * Userland address spaces are not wired up on this architecture yet. + * + * The ELF loader and exec path are portable, but they need per-process page + * tables and a ring-3 entry path that only the x86_64 port implements today. + * These stubs fail cleanly so a userland exec reports "not supported" instead + * of silently running a program with kernel privileges. + */ + +vmspace_t* vmspace_create(void) { return NULL; } +void vmspace_destroy(vmspace_t* vm) { (void)vm; } + +int vmspace_map_page(vmspace_t* vm, uint64_t virt, uint64_t phys, int prot) { + (void)vm; (void)virt; (void)phys; (void)prot; + return -1; +} + +int vmspace_alloc(vmspace_t* vm, uint64_t virt, size_t len, int prot) { + (void)vm; (void)virt; (void)len; (void)prot; + return -1; +} + +int vmspace_write(vmspace_t* vm, uint64_t dst, const void* src, size_t len) { + (void)vm; (void)dst; (void)src; (void)len; + return -1; +} + +uint64_t vmspace_resolve(vmspace_t* vm, uint64_t virt) { + (void)vm; (void)virt; + return 0; +} + +void vmspace_switch(vmspace_t* vm) { (void)vm; } + +bool vmspace_range_ok(vmspace_t* vm, uint64_t virt, size_t len) { + (void)vm; (void)virt; (void)len; + return false; +} + +uint64_t vmspace_resident_pages(vmspace_t* vm) { (void)vm; return 0; } diff --git a/arch/x86_64/cpu/isr.c b/arch/x86_64/cpu/isr.c index ca7d7a1..5129672 100644 --- a/arch/x86_64/cpu/isr.c +++ b/arch/x86_64/cpu/isr.c @@ -3,6 +3,7 @@ #include #include #include +#include #include extern void* isr_stub_table[48]; @@ -54,6 +55,26 @@ void isr_unregister_handler(uint8_t int_no) { void isr_handler_common(registers_t* regs) { if (regs->int_no < 32) { + /* A page fault reports the offending address in CR2; everything else + * is best described by where it was executing. */ + uint64_t fault_addr = regs->rip; + if (regs->int_no == 14) { + __asm__ volatile("mov %%cr2, %0" : "=r"(fault_addr)); + } + + /* + * CS still carries the privilege level the fault was taken from. A + * ring 3 fault is the process's problem, not the kernel's: report it + * and tear that process down instead of bringing the machine down + * with it. + */ + if ((regs->cs & 3) == 3 && uproc_current()) { + printk(KERN_ERR "\n[USER FAULT] Vector %llu: %s (error 0x%llx) at rip=0x%llx\n", + regs->int_no, exception_messages[regs->int_no], + regs->error_code, regs->rip); + uproc_fault(exception_messages[regs->int_no], fault_addr); + } + // CPU Exception Fault printk(KERN_EMERG "\n[EXCEPTION] Vector %llu: %s (Error code: 0x%llx)\n", regs->int_no, exception_messages[regs->int_no], regs->error_code); diff --git a/arch/x86_64/cpu/isr_stubs.asm b/arch/x86_64/cpu/isr_stubs.asm index 6365756..457d470 100644 --- a/arch/x86_64/cpu/isr_stubs.asm +++ b/arch/x86_64/cpu/isr_stubs.asm @@ -3,6 +3,7 @@ extern isr_handler_common global isr_stub_table global isr_stub_default +global isr_stub_syscall %macro ISR_NOERRCODE 1 isr_stub_%1: @@ -71,6 +72,14 @@ ISR_NOERRCODE 45 ISR_NOERRCODE 46 ISR_NOERRCODE 47 +; Software syscall gate. Reached by INT 0x80 from ring 3, so its IDT entry is +; installed with DPL 3; the CPU switches to TSS.rsp0 on the way in, which the +; exec path points at a kernel stack owned by the running process. +isr_stub_syscall: + push qword 0 ; No error code is pushed for a software interrupt + push qword 128 ; Vector number, matching the IDT slot + jmp isr_common_stub + isr_stub_default: push qword 0 push qword 255 diff --git a/arch/x86_64/cpu/usermode.asm b/arch/x86_64/cpu/usermode.asm new file mode 100644 index 0000000..00b1c3c --- /dev/null +++ b/arch/x86_64/cpu/usermode.asm @@ -0,0 +1,89 @@ +[BITS 64] +default rel + +; Ring 3 entry and exit for SUB-OS. +; +; user_enter() does not behave like a normal call: it drops to ring 3 and only +; comes back when the process leaves through user_return(), which unwinds +; straight to the frame saved here. From C it therefore looks like a call that +; blocks until the process exits and then returns its exit code. + +global user_enter +global user_return + +section .text + +; int64_t user_enter(uint64_t entry /* rdi */, uint64_t user_rsp /* rsi */) +user_enter: + cli + + ; Preserve the SysV callee-saved set so user_return can rebuild this frame. + push rbx + push rbp + push r12 + push r13 + push r14 + push r15 + mov [kernel_resume_rsp], rsp + + ; Ring 3 data selectors: user data descriptor (0x20) with RPL 3. + mov ax, 0x23 + mov ds, ax + mov es, ax + mov fs, ax + mov gs, ax + + ; Build the iretq frame: SS, RSP, RFLAGS, CS, RIP. + push 0x23 ; SS = user data, RPL 3 + push rsi ; RSP = top of the user stack + push 0x202 ; RFLAGS with IF set, everything else clear + push 0x1B ; CS = user code (0x18), RPL 3 + push rdi ; RIP = ELF entry point + + ; Do not leak kernel register contents across the privilege boundary. + xor rax, rax + xor rbx, rbx + xor rcx, rcx + xor rdx, rdx + xor rsi, rsi + xor rdi, rdi + xor rbp, rbp + xor r8, r8 + xor r9, r9 + xor r10, r10 + xor r11, r11 + xor r12, r12 + xor r13, r13 + xor r14, r14 + xor r15, r15 + + iretq + +; void user_return(int64_t status /* rdi */) -- never returns to its caller. +user_return: + cli + mov rsp, [kernel_resume_rsp] + mov rax, rdi ; becomes user_enter's return value + + ; Restore the kernel's own data selectors before touching kernel memory. + push rax + mov ax, 0x10 + mov ds, ax + mov es, ax + mov fs, ax + mov gs, ax + pop rax + + pop r15 + pop r14 + pop r13 + pop r12 + pop rbp + pop rbx + sti + ret + +section .bss +align 8 +kernel_resume_rsp: + resq 1 diff --git a/arch/x86_64/mm/vmspace.c b/arch/x86_64/mm/vmspace.c new file mode 100644 index 0000000..c7c7e52 --- /dev/null +++ b/arch/x86_64/mm/vmspace.c @@ -0,0 +1,239 @@ +#include +#include +#include +#include +#include +#include + +/* + * x86_64 per-process address spaces. + * + * The boot page tables identity-map the low 4 GB, so within that window a + * physical address doubles as a kernel-usable pointer: page tables can be + * edited and user pages seeded without any temporary mapping window. + * + * The PMM allocates linearly from physical zero, so on any machine whose + * usable memory fits under 4 GB every frame it returns is reachable that way. + * On a larger machine it can return a frame above the window, which this file + * has no way to touch -- those are rejected outright rather than silently + * writing through an unmapped pointer. Lifting that needs either a recursive + * page-table mapping or a higher-half direct map. + */ + +#define KERNEL_PML4_PHYS 0x1000UL +#define PTE_ADDR_MASK (~0xFFFULL) +#define ENTRIES_PER_TABLE 512 + +/* Upper bound of the boot identity map; see the file comment. */ +#define IDENTITY_MAP_LIMIT (4ULL << 30) + +struct vmspace { + uint64_t pml4_phys; + uint64_t resident_pages; /* data pages, excluding page tables */ + uint64_t table_pages; +}; + +static inline uint64_t* table_at(uint64_t phys) { + return (uint64_t*)(uintptr_t)phys; +} + +static uint64_t arch_flags_of(int prot) { + uint64_t flags = PAGE_PRESENT; + if (prot & VM_PROT_WRITE) flags |= PAGE_WRITABLE; + if (prot & VM_PROT_USER) flags |= PAGE_USER; + /* NX is only safe to set once EFER.NXE is on; user text needs it clear + * anyway, so non-executable pages are simply left W^X-less for now. */ + return flags; +} + +/* Take a zeroed frame that the kernel can actually address, or NULL. */ +static void* alloc_identity_frame(void) { + void* page = pmm_alloc_page(); + if (!page) return NULL; + if ((uint64_t)(uintptr_t)page >= IDENTITY_MAP_LIMIT) { + pmm_free_page(page); + printk(KERN_ERR "vmspace: frame at 0x%llx is outside the identity map\n", + (unsigned long long)(uintptr_t)page); + return NULL; + } + memset(page, 0, VM_PAGE_SIZE); + return page; +} + +static uint64_t* alloc_table(vmspace_t* vm) { + void* page = alloc_identity_frame(); + if (!page) return NULL; + if (vm) vm->table_pages++; + return (uint64_t*)page; +} + +vmspace_t* vmspace_create(void) { + vmspace_t* vm = (vmspace_t*)kzalloc(sizeof(vmspace_t)); + if (!vm) return NULL; + + uint64_t* pml4 = alloc_table(vm); + if (!pml4) { + kfree(vm); + return NULL; + } + + /* + * Share the kernel's PML4[0] verbatim. That entry covers the whole low + * 512 GB, which is where the identity-mapped kernel, its heap and every + * MMIO window live, so ring 0 keeps working immediately after the CR3 + * load. Userland gets its own slot and never aliases kernel memory. + */ + uint64_t* kernel_pml4 = table_at(KERNEL_PML4_PHYS); + pml4[0] = kernel_pml4[0]; + + vm->pml4_phys = (uint64_t)(uintptr_t)pml4; + return vm; +} + +/* Walk to the PTE for `virt`, optionally creating the intermediate tables. */ +static uint64_t* walk(vmspace_t* vm, uint64_t virt, bool create) { + uint64_t idx[4] = { + (virt >> 39) & 0x1FF, + (virt >> 30) & 0x1FF, + (virt >> 21) & 0x1FF, + (virt >> 12) & 0x1FF, + }; + + uint64_t* table = table_at(vm->pml4_phys); + for (int level = 0; level < 3; level++) { + uint64_t entry = table[idx[level]]; + if (!(entry & PAGE_PRESENT)) { + if (!create) return NULL; + uint64_t* next = alloc_table(vm); + if (!next) return NULL; + /* Every level above the leaf must allow user access, or the + * final PAGE_USER bit is overridden by the walk. */ + entry = (uint64_t)(uintptr_t)next | PAGE_PRESENT | PAGE_WRITABLE | PAGE_USER; + table[idx[level]] = entry; + } else if (entry & PAGE_HUGE) { + /* A large mapping already covers this range; refuse rather than + * silently splitting the kernel's identity map. */ + return NULL; + } + table = table_at(entry & PTE_ADDR_MASK); + } + return &table[idx[3]]; +} + +int vmspace_map_page(vmspace_t* vm, uint64_t virt, uint64_t phys, int prot) { + if (!vm) return -1; + if (virt < USER_BASE || virt >= USER_LIMIT) return -1; + + uint64_t* pte = walk(vm, virt & ~(VM_PAGE_SIZE - 1), true); + if (!pte) return -1; + + if (!(*pte & PAGE_PRESENT)) vm->resident_pages++; + *pte = (phys & PTE_ADDR_MASK) | arch_flags_of(prot); + return 0; +} + +int vmspace_alloc(vmspace_t* vm, uint64_t virt, size_t len, int prot) { + if (!vm || len == 0) return -1; + + uint64_t start = virt & ~(VM_PAGE_SIZE - 1); + uint64_t end = (virt + len + VM_PAGE_SIZE - 1) & ~(VM_PAGE_SIZE - 1); + if (end <= start) return -1; + + for (uint64_t page = start; page < end; page += VM_PAGE_SIZE) { + uint64_t* pte = walk(vm, page, true); + if (!pte) return -1; + + if (*pte & PAGE_PRESENT) { + /* Overlapping PT_LOAD segments share a page; widen its rights + * instead of allocating a second frame over the top. */ + *pte |= arch_flags_of(prot); + continue; + } + + void* frame = alloc_identity_frame(); + if (!frame) return -1; + + *pte = ((uint64_t)(uintptr_t)frame & PTE_ADDR_MASK) | arch_flags_of(prot); + vm->resident_pages++; + } + return 0; +} + +uint64_t vmspace_resolve(vmspace_t* vm, uint64_t virt) { + if (!vm) return 0; + uint64_t* pte = walk(vm, virt & ~(VM_PAGE_SIZE - 1), false); + if (!pte || !(*pte & PAGE_PRESENT)) return 0; + return (*pte & PTE_ADDR_MASK) | (virt & (VM_PAGE_SIZE - 1)); +} + +bool vmspace_range_ok(vmspace_t* vm, uint64_t virt, size_t len) { + if (!vm || len == 0) return false; + if (virt < USER_BASE || virt >= USER_LIMIT) return false; + if (len > USER_LIMIT - virt) return false; + + uint64_t start = virt & ~(VM_PAGE_SIZE - 1); + uint64_t end = (virt + len + VM_PAGE_SIZE - 1) & ~(VM_PAGE_SIZE - 1); + for (uint64_t page = start; page < end; page += VM_PAGE_SIZE) { + uint64_t* pte = walk(vm, page, false); + if (!pte || !(*pte & PAGE_PRESENT) || !(*pte & PAGE_USER)) return false; + } + return true; +} + +int vmspace_write(vmspace_t* vm, uint64_t dst, const void* src, size_t len) { + if (!vm || !src) return -1; + const uint8_t* in = (const uint8_t*)src; + + while (len) { + uint64_t phys = vmspace_resolve(vm, dst); + if (!phys) return -1; + + size_t offset = (size_t)(dst & (VM_PAGE_SIZE - 1)); + size_t chunk = VM_PAGE_SIZE - offset; + if (chunk > len) chunk = len; + + memcpy((void*)(uintptr_t)phys, in, chunk); + + dst += chunk; + in += chunk; + len -= chunk; + } + return 0; +} + +void vmspace_switch(vmspace_t* vm) { + uint64_t cr3 = vm ? vm->pml4_phys : KERNEL_PML4_PHYS; + __asm__ volatile("mov %0, %%cr3" : : "r"(cr3) : "memory"); +} + +uint64_t vmspace_resident_pages(vmspace_t* vm) { + return vm ? vm->resident_pages : 0; +} + +/* Free one level of the private sub-tree rooted at `phys`. */ +static void free_level(uint64_t phys, int level) { + uint64_t* table = table_at(phys); + for (int i = 0; i < ENTRIES_PER_TABLE; i++) { + uint64_t entry = table[i]; + if (!(entry & PAGE_PRESENT) || (entry & PAGE_HUGE)) continue; + if (level > 0) { + free_level(entry & PTE_ADDR_MASK, level - 1); + } else { + pmm_free_page((void*)(uintptr_t)(entry & PTE_ADDR_MASK)); + } + } + pmm_free_page((void*)(uintptr_t)phys); +} + +void vmspace_destroy(vmspace_t* vm) { + if (!vm) return; + + uint64_t* pml4 = table_at(vm->pml4_phys); + /* Slot 0 is the kernel's own table and must outlive this process. */ + uint64_t user_entry = pml4[USER_PML4_SLOT]; + if (user_entry & PAGE_PRESENT) { + free_level(user_entry & PTE_ADDR_MASK, 2); + } + pmm_free_page((void*)(uintptr_t)vm->pml4_phys); + kfree(vm); +} diff --git a/include/kernel/elf.h b/include/kernel/elf.h new file mode 100644 index 0000000..92981df --- /dev/null +++ b/include/kernel/elf.h @@ -0,0 +1,98 @@ +#ifndef _KERNEL_ELF_H +#define _KERNEL_ELF_H + +#include +#include +#include + +/* ELF64 object file format, as far as a static executable needs it. */ + +typedef uint64_t Elf64_Addr; +typedef uint64_t Elf64_Off; +typedef uint16_t Elf64_Half; +typedef uint32_t Elf64_Word; +typedef uint64_t Elf64_Xword; + +#define EI_NIDENT 16 + +typedef struct { + unsigned char e_ident[EI_NIDENT]; + Elf64_Half e_type; + Elf64_Half e_machine; + Elf64_Word e_version; + Elf64_Addr e_entry; + Elf64_Off e_phoff; + Elf64_Off e_shoff; + Elf64_Word e_flags; + Elf64_Half e_ehsize; + Elf64_Half e_phentsize; + Elf64_Half e_phnum; + Elf64_Half e_shentsize; + Elf64_Half e_shnum; + Elf64_Half e_shstrndx; +} __attribute__((packed)) Elf64_Ehdr; + +typedef struct { + Elf64_Word p_type; + Elf64_Word p_flags; + Elf64_Off p_offset; + Elf64_Addr p_vaddr; + Elf64_Addr p_paddr; + Elf64_Xword p_filesz; + Elf64_Xword p_memsz; + Elf64_Xword p_align; +} __attribute__((packed)) Elf64_Phdr; + +/* e_ident indices and values */ +#define EI_CLASS 4 +#define EI_DATA 5 +#define EI_VERSION 6 +#define ELFCLASS64 2 +#define ELFDATA2LSB 1 +#define EV_CURRENT 1 + +/* e_type */ +#define ET_EXEC 2 +#define ET_DYN 3 + +/* e_machine */ +#define EM_X86_64 62 +#define EM_AARCH64 183 +#define EM_ARM 40 + +/* p_type */ +#define PT_NULL 0 +#define PT_LOAD 1 +#define PT_DYNAMIC 2 +#define PT_INTERP 3 +#define PT_NOTE 4 +#define PT_PHDR 6 +#define PT_GNU_STACK 0x6474e551 + +/* p_flags */ +#define PF_X 0x1 +#define PF_W 0x2 +#define PF_R 0x4 + +/* elf_load return codes */ +#define ELF_OK 0 +#define ELF_ETRUNCATED -1 /* image smaller than the headers claim */ +#define ELF_EMAGIC -2 /* not an ELF file */ +#define ELF_ECLASS -3 /* not ELF64 little-endian */ +#define ELF_ETYPE -4 /* not a static executable */ +#define ELF_EMACHINE -5 /* built for another architecture */ +#define ELF_ERANGE -6 /* a segment falls outside the user range */ +#define ELF_ENOMEM -7 /* out of physical pages */ +#define ELF_ENOENTRY -8 /* entry point is not inside any PT_LOAD */ + +const char* elf_strerror(int err); + +/* + * Parse a static ELF64 executable held in kernel memory and lay its PT_LOAD + * segments into `vm`. On success *entry_out receives e_entry and *brk_out the + * page-aligned end of the highest segment, which is where the heap starts. + */ +int elf_load(vmspace_t* vm, const void* image, size_t len, + uint64_t* entry_out, uint64_t* brk_out); + +#endif /* _KERNEL_ELF_H */ diff --git a/include/kernel/exec.h b/include/kernel/exec.h new file mode 100644 index 0000000..18d0601 --- /dev/null +++ b/include/kernel/exec.h @@ -0,0 +1,53 @@ +#ifndef _KERNEL_EXEC_H +#define _KERNEL_EXEC_H + +#include +#include +#include +#include +#include + +/* A process running in ring 3. */ +typedef struct uproc { + pid_t pid; + char name[32]; + vmspace_t* vm; + uint64_t entry; + uint64_t brk; /* program break, grown by SYS_brk */ + uint64_t brk_start; /* end of the loaded image */ + uint64_t kstack; /* kernel stack used for traps out of ring 3 */ + size_t kstack_size; + int exit_code; + bool exited; +} uproc_t; + +/* The process currently executing in ring 3, or NULL in plain kernel context. */ +uproc_t* uproc_current(void); + +/* + * Load a static ELF64 image and run it in ring 3. Blocks until the process + * exits and returns its exit status, or a negative value if it could not be + * started. `image` must stay valid for the lifetime of the call. + */ +int exec_user_image(const char* name, const void* image, size_t len); + +/* Terminate the running process and unwind back into exec_user_image(). */ +void uproc_exit(int status) __attribute__((noreturn)); + +/* + * Abort the running process because it faulted. Safe to call from an + * exception handler; does not return. + */ +void uproc_fault(const char* reason, uint64_t addr) __attribute__((noreturn)); + +/* Bounds-checked transfers between the kernel and the running process. */ +bool uaccess_ok(uint64_t uaddr, size_t len); +int uaccess_copy_from_user(void* dst, uint64_t usrc, size_t len); +int uaccess_copy_to_user(uint64_t udst, const void* src, size_t len); +/* Copy a NUL-terminated user string; returns its length or -1. */ +long uaccess_copy_string(char* dst, uint64_t usrc, size_t max); + +/* Run the built-in /sbin/init image. Returns its exit status. */ +int exec_run_builtin_init(void); + +#endif /* _KERNEL_EXEC_H */ diff --git a/include/kernel/printk.h b/include/kernel/printk.h index cd0097b..5d65880 100644 --- a/include/kernel/printk.h +++ b/include/kernel/printk.h @@ -43,6 +43,10 @@ // Printk API void printk_init(void); + +// Write raw bytes to every console printk drives (TTY or framebuffer, plus +// serial). Used by the write(2) path so userland output is routed identically. +void console_write(const char* data, size_t len); int printk(const char* fmt, ...); int vprintk(const char* fmt, va_list args); int kprintf(const char* fmt, ...); // Alias for compatibility diff --git a/include/mm/vmspace.h b/include/mm/vmspace.h new file mode 100644 index 0000000..c31d2a8 --- /dev/null +++ b/include/mm/vmspace.h @@ -0,0 +1,67 @@ +#ifndef _MM_VMSPACE_H +#define _MM_VMSPACE_H + +#include +#include +#include + +/* + * Per-process address spaces. + * + * The boot page tables identity-map the low 4 GB with 2 MB supervisor pages + * hanging off PML4[0], and leave PML4[1..511] empty. Userland is therefore + * given its own PML4 slot rather than carving holes out of the kernel's + * identity map: a process PML4 shares entry 0 with the kernel (so ring 0 can + * still reach every kernel address after the CR3 switch) and privately owns + * entry 1, which is where every user mapping lives. + */ +#define USER_PML4_SLOT 1UL +#define USER_BASE (USER_PML4_SLOT << 39) /* 512 GiB */ +#define USER_SPAN (1UL << 39) /* 512 GiB */ +#define USER_LIMIT (USER_BASE + USER_SPAN) + +#define USER_LOAD_BASE (USER_BASE + 0x400000UL) /* +4 MiB */ +#define USER_STACK_TOP (USER_BASE + 0x08000000UL) /* +128 MiB */ +#define USER_STACK_PAGES 16UL /* 64 KiB stack */ + +#define VM_PAGE_SIZE 4096UL + +/* vmspace_map / vmspace_alloc protection flags (arch-neutral). */ +#define VM_PROT_READ 0x1 +#define VM_PROT_WRITE 0x2 +#define VM_PROT_EXEC 0x4 +#define VM_PROT_USER 0x8 + +typedef struct vmspace vmspace_t; + +/* Create an address space that shares the kernel's mappings. NULL on OOM. */ +vmspace_t* vmspace_create(void); + +/* Release every page and page table the space privately owns. */ +void vmspace_destroy(vmspace_t* vm); + +/* Map one already-allocated physical page. Returns 0 or a negative errno. */ +int vmspace_map_page(vmspace_t* vm, uint64_t virt, uint64_t phys, int prot); + +/* + * Allocate zeroed physical pages and map [virt, virt+len) with them. + * `virt` and `len` need not be page aligned; the covered range is rounded out. + */ +int vmspace_alloc(vmspace_t* vm, uint64_t virt, size_t len, int prot); + +/* Copy kernel memory into an already-mapped user range. */ +int vmspace_write(vmspace_t* vm, uint64_t dst, const void* src, size_t len); + +/* Translate a user virtual address to a physical one, or 0 if unmapped. */ +uint64_t vmspace_resolve(vmspace_t* vm, uint64_t virt); + +/* Install this address space on the current CPU. */ +void vmspace_switch(vmspace_t* vm); + +/* True when the whole range is mapped and reachable from ring 3. */ +bool vmspace_range_ok(vmspace_t* vm, uint64_t virt, size_t len); + +/* Pages currently charged to this address space. */ +uint64_t vmspace_resident_pages(vmspace_t* vm); + +#endif /* _MM_VMSPACE_H */ diff --git a/kernel/elf.c b/kernel/elf.c new file mode 100644 index 0000000..f54e3b1 --- /dev/null +++ b/kernel/elf.c @@ -0,0 +1,116 @@ +#include +#include +#include +#include + +#if defined(__x86_64__) +#define ELF_NATIVE_MACHINE EM_X86_64 +#elif defined(__aarch64__) +#define ELF_NATIVE_MACHINE EM_AARCH64 +#else +#define ELF_NATIVE_MACHINE EM_ARM +#endif + +static const char* const elf_errors[] = { + [0] = "ok", + [-ELF_ETRUNCATED] = "image is truncated", + [-ELF_EMAGIC] = "not an ELF object", + [-ELF_ECLASS] = "not a little-endian ELF64 object", + [-ELF_ETYPE] = "not a static executable", + [-ELF_EMACHINE] = "built for a different architecture", + [-ELF_ERANGE] = "segment falls outside the user address range", + [-ELF_ENOMEM] = "out of memory", + [-ELF_ENOENTRY] = "entry point is not inside a loadable segment", +}; + +const char* elf_strerror(int err) { + unsigned idx = (unsigned)(-err); + if (err > 0 || idx >= sizeof(elf_errors) / sizeof(elf_errors[0]) || !elf_errors[idx]) { + return "unknown error"; + } + return elf_errors[idx]; +} + +static int elf_prot_of(Elf64_Word p_flags) { + int prot = VM_PROT_USER; + if (p_flags & PF_R) prot |= VM_PROT_READ; + if (p_flags & PF_W) prot |= VM_PROT_WRITE; + if (p_flags & PF_X) prot |= VM_PROT_EXEC; + return prot; +} + +static int elf_check_header(const Elf64_Ehdr* eh, size_t len) { + if (len < sizeof(Elf64_Ehdr)) return ELF_ETRUNCATED; + + if (eh->e_ident[0] != 0x7F || eh->e_ident[1] != 'E' || + eh->e_ident[2] != 'L' || eh->e_ident[3] != 'F') { + return ELF_EMAGIC; + } + if (eh->e_ident[EI_CLASS] != ELFCLASS64 || + eh->e_ident[EI_DATA] != ELFDATA2LSB) { + return ELF_ECLASS; + } + /* Only static executables: a PT_INTERP image would need a loader we have + * no business running this early. */ + if (eh->e_type != ET_EXEC) return ELF_ETYPE; + if (eh->e_machine != ELF_NATIVE_MACHINE) return ELF_EMACHINE; + if (eh->e_phnum == 0 || eh->e_phentsize != sizeof(Elf64_Phdr)) return ELF_ETRUNCATED; + + /* The program header table itself has to be inside the image. */ + uint64_t ph_bytes = (uint64_t)eh->e_phnum * eh->e_phentsize; + if (eh->e_phoff > len || ph_bytes > len - eh->e_phoff) return ELF_ETRUNCATED; + + return ELF_OK; +} + +int elf_load(vmspace_t* vm, const void* image, size_t len, + uint64_t* entry_out, uint64_t* brk_out) { + if (!vm || !image) return ELF_EMAGIC; + + const Elf64_Ehdr* eh = (const Elf64_Ehdr*)image; + int err = elf_check_header(eh, len); + if (err != ELF_OK) return err; + + const Elf64_Phdr* ph = (const Elf64_Phdr*)((const uint8_t*)image + eh->e_phoff); + uint64_t brk = 0; + bool entry_mapped = false; + + for (Elf64_Half i = 0; i < eh->e_phnum; i++) { + const Elf64_Phdr* p = &ph[i]; + if (p->p_type != PT_LOAD || p->p_memsz == 0) continue; + + /* A segment may declare more memory than it stores (.bss), but never + * less, and its file image must lie inside the blob we were handed. */ + if (p->p_filesz > p->p_memsz) return ELF_ETRUNCATED; + if (p->p_offset > len || p->p_filesz > len - p->p_offset) return ELF_ETRUNCATED; + + /* Keep every mapping inside the slot reserved for userland, and reject + * a length that would wrap past the top of the address space. */ + if (p->p_vaddr < USER_BASE) return ELF_ERANGE; + if (p->p_memsz > USER_LIMIT - p->p_vaddr) return ELF_ERANGE; + + err = vmspace_alloc(vm, p->p_vaddr, p->p_memsz, elf_prot_of(p->p_flags)); + if (err != 0) return ELF_ENOMEM; + + if (p->p_filesz) { + err = vmspace_write(vm, p->p_vaddr, + (const uint8_t*)image + p->p_offset, p->p_filesz); + if (err != 0) return ELF_ENOMEM; + } + /* vmspace_alloc hands back zeroed pages, so .bss needs no extra work. */ + + uint64_t seg_end = p->p_vaddr + p->p_memsz; + if (seg_end > brk) brk = seg_end; + + if (eh->e_entry >= p->p_vaddr && eh->e_entry < seg_end) entry_mapped = true; + } + + if (!brk) return ELF_ERANGE; + if (!entry_mapped) return ELF_ENOENTRY; + + if (entry_out) *entry_out = eh->e_entry; + if (brk_out) { + *brk_out = (brk + VM_PAGE_SIZE - 1) & ~(VM_PAGE_SIZE - 1); + } + return ELF_OK; +} diff --git a/kernel/exec.c b/kernel/exec.c new file mode 100644 index 0000000..2b20803 --- /dev/null +++ b/kernel/exec.c @@ -0,0 +1,238 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__x86_64__) +#include + +/* Implemented in arch/x86_64/cpu/usermode.asm. */ +extern int64_t user_enter(uint64_t entry, uint64_t user_rsp); +extern void user_return(int64_t status) __attribute__((noreturn)); + +#define USER_KSTACK_SIZE 16384 + +static uproc_t* current_uproc = NULL; + +/* + * User processes carry their own pid namespace for now. They are not backed by + * a task_struct yet, so borrowing task_get_pid() would report the idle task's + * pid 0 to every program that asked. + */ +static pid_t next_user_pid = 1; + +uproc_t* uproc_current(void) { + return current_uproc; +} + +bool uaccess_ok(uint64_t uaddr, size_t len) { + if (!current_uproc) return false; + if (len == 0) return true; + return vmspace_range_ok(current_uproc->vm, uaddr, len); +} + +/* + * While a process runs, CR3 is its own, and its pages are mapped and present. + * The kernel can therefore dereference a user pointer directly -- but only + * after uaccess_ok() has confirmed the whole range really belongs to the + * process, otherwise a hostile pointer into the identity-mapped low 4 GB + * would read or scribble on the kernel itself. + */ +int uaccess_copy_from_user(void* dst, uint64_t usrc, size_t len) { + if (!uaccess_ok(usrc, len)) return -1; + memcpy(dst, (const void*)(uintptr_t)usrc, len); + return 0; +} + +int uaccess_copy_to_user(uint64_t udst, const void* src, size_t len) { + if (!uaccess_ok(udst, len)) return -1; + memcpy((void*)(uintptr_t)udst, src, len); + return 0; +} + +long uaccess_copy_string(char* dst, uint64_t usrc, size_t max) { + if (!dst || max == 0) return -1; + for (size_t i = 0; i < max; i++) { + if (!uaccess_ok(usrc + i, 1)) return -1; + char c = *(const char*)(uintptr_t)(usrc + i); + dst[i] = c; + if (c == '\0') return (long)i; + } + dst[max - 1] = '\0'; + return -1; /* not terminated within max */ +} + +void uproc_exit(int status) { + if (current_uproc) { + current_uproc->exit_code = status; + current_uproc->exited = true; + } + user_return((int64_t)status); + __builtin_unreachable(); +} + +void uproc_fault(const char* reason, uint64_t addr) { + printk(KERN_ERR "exec: killing '%s' (pid %d): %s at 0x%llx\n", + current_uproc ? current_uproc->name : "?", + current_uproc ? current_uproc->pid : -1, + reason ? reason : "fault", + (unsigned long long)addr); + uproc_exit(-1); +} + +/* + * Build the System V entry stack: rsp points at argc, followed by the argv + * pointers, a NULL, and an empty environment. The program name string itself + * is copied to the very top of the stack. + */ +static int setup_user_stack(uproc_t* proc, uint64_t* rsp_out) { + uint64_t stack_bytes = USER_STACK_PAGES * VM_PAGE_SIZE; + uint64_t stack_low = USER_STACK_TOP - stack_bytes; + + if (vmspace_alloc(proc->vm, stack_low, stack_bytes, + VM_PROT_READ | VM_PROT_WRITE | VM_PROT_USER) != 0) { + return -1; + } + + size_t name_len = strlen(proc->name) + 1; + uint64_t argv0 = (USER_STACK_TOP - name_len) & ~0xFULL; + if (vmspace_write(proc->vm, argv0, proc->name, name_len) != 0) return -1; + + /* argc, argv[0], argv[1]=NULL, envp[0]=NULL */ + uint64_t frame[4] = { 1, argv0, 0, 0 }; + uint64_t rsp = (argv0 - sizeof(frame)) & ~0xFULL; + if (vmspace_write(proc->vm, rsp, frame, sizeof(frame)) != 0) return -1; + + *rsp_out = rsp; + return 0; +} + +int exec_user_image(const char* name, const void* image, size_t len) { + if (!image || len == 0) return -1; + if (current_uproc) { + printk(KERN_ERR "exec: nested exec is not supported yet\n"); + return -1; + } + + uproc_t* proc = (uproc_t*)kzalloc(sizeof(uproc_t)); + if (!proc) return -1; + + strncpy(proc->name, name ? name : "user", sizeof(proc->name) - 1); + proc->pid = next_user_pid++; + + proc->vm = vmspace_create(); + if (!proc->vm) { + printk(KERN_ERR "exec: out of memory building an address space\n"); + kfree(proc); + return -1; + } + + int err = elf_load(proc->vm, image, len, &proc->entry, &proc->brk_start); + if (err != ELF_OK) { + printk(KERN_ERR "exec: %s: %s\n", proc->name, elf_strerror(err)); + vmspace_destroy(proc->vm); + kfree(proc); + return -1; + } + proc->brk = proc->brk_start; + + uint64_t user_rsp = 0; + if (setup_user_stack(proc, &user_rsp) != 0) { + printk(KERN_ERR "exec: %s: cannot map a user stack\n", proc->name); + vmspace_destroy(proc->vm); + kfree(proc); + return -1; + } + + /* + * Traps taken in ring 3 switch to TSS.rsp0, which must not be the stack + * user_enter() is standing on -- the CPU would push the trap frame over + * the frame we need to unwind to. Give the process its own kernel stack. + */ + proc->kstack_size = USER_KSTACK_SIZE; + proc->kstack = (uint64_t)(uintptr_t)kmalloc(proc->kstack_size); + if (!proc->kstack) { + printk(KERN_ERR "exec: %s: cannot allocate a kernel stack\n", proc->name); + vmspace_destroy(proc->vm); + kfree(proc); + return -1; + } + + printk(KERN_INFO "exec: %s entry=0x%llx brk=0x%llx rsp=0x%llx (%llu pages)\n", + proc->name, (unsigned long long)proc->entry, + (unsigned long long)proc->brk, (unsigned long long)user_rsp, + (unsigned long long)vmspace_resident_pages(proc->vm)); + + current_uproc = proc; + gdt_set_kernel_stack((proc->kstack + proc->kstack_size) & ~0xFULL); + vmspace_switch(proc->vm); + + int status = (int)user_enter(proc->entry, user_rsp); + + /* Back in ring 0 on the kernel's own stack: undo everything. */ + vmspace_switch(NULL); + current_uproc = NULL; + + printk(KERN_INFO "exec: %s exited with status %d\n", proc->name, status); + + kfree((void*)(uintptr_t)proc->kstack); + vmspace_destroy(proc->vm); + kfree(proc); + return status; +} + +/* + * The statically linked /sbin/init image, embedded in the kernel by + * scripts/mkblob.py so there is something to exec before any filesystem is + * mounted. + */ +extern const unsigned char subos_init_elf[]; +extern const size_t subos_init_elf_len; + +int exec_run_builtin_init(void) { + return exec_user_image("init", subos_init_elf, subos_init_elf_len); +} + +#else /* !__x86_64__ */ + +uproc_t* uproc_current(void) { return NULL; } +bool uaccess_ok(uint64_t uaddr, size_t len) { (void)uaddr; (void)len; return false; } +int uaccess_copy_from_user(void* dst, uint64_t usrc, size_t len) { + (void)dst; (void)usrc; (void)len; return -1; +} +int uaccess_copy_to_user(uint64_t udst, const void* src, size_t len) { + (void)udst; (void)src; (void)len; return -1; +} +long uaccess_copy_string(char* dst, uint64_t usrc, size_t max) { + (void)dst; (void)usrc; (void)max; return -1; +} + +void uproc_exit(int status) { + (void)status; + panic("uproc_exit() with no ring 3 support on this architecture"); + __builtin_unreachable(); +} + +void uproc_fault(const char* reason, uint64_t addr) { + (void)reason; (void)addr; + panic("uproc_fault() with no ring 3 support on this architecture"); + __builtin_unreachable(); +} + +int exec_user_image(const char* name, const void* image, size_t len) { + (void)image; (void)len; + printk(KERN_WARNING "exec: %s: ring 3 execution is only implemented on x86_64\n", + name ? name : "user"); + return -1; +} + +int exec_run_builtin_init(void) { + printk(KERN_WARNING "exec: no userland init on this architecture yet\n"); + return -1; +} + +#endif /* __x86_64__ */ diff --git a/kernel/main.c b/kernel/main.c index a9cd8ec..85116d1 100644 --- a/kernel/main.c +++ b/kernel/main.c @@ -92,6 +92,7 @@ #include #include #include +#include static void subos_modular_core_boot(void) { #if defined(__x86_64__) @@ -256,6 +257,10 @@ static void subos_modular_core_boot(void) { boot_logo_splash_end(); fbcon_enable(true); + // Hand control to userland: /sbin/init is a separate static ELF that runs + // in ring 3 with its own page tables and talks back only through INT 0x80. + exec_run_builtin_init(); + // Launch Shell shell_run(); diff --git a/kernel/printk.c b/kernel/printk.c index 5862456..2256812 100644 --- a/kernel/printk.c +++ b/kernel/printk.c @@ -49,6 +49,36 @@ static void dmesg_append(const char* str, size_t len) { } } +/* + * Put raw bytes on every console printk uses. + * + * Kept here rather than at the call sites because the routing is not obvious: + * once the display adapter is in graphics mode the VGA text buffer is no + * longer scanned out, so the framebuffer console has to take over, and the + * serial port is a separate sink that has to be fed either way. Userland + * writes to stdout/stderr land here so they behave exactly like kernel output. + */ +void console_write(const char* data, size_t len) { + if (!data || len == 0) return; + + if (fbcon_is_active()) { + fbcon_write(data, len); + } else { + tty_write(data, len); + } + +#if defined(__aarch64__) || defined(__arm__) || defined(__armv8i__) + extern void uart_pl011_putc(char c); + for (size_t i = 0; i < len; i++) { + uart_pl011_putc(data[i]); + } +#else + for (size_t i = 0; i < len; i++) { + serial_write_char(data[i]); + } +#endif +} + int vprintk(const char* fmt, va_list args) { char buffer[1024]; int len = vsnprintf(buffer, sizeof(buffer), fmt, args); diff --git a/kernel/syscall.c b/kernel/syscall.c index c954345..be4ed11 100644 --- a/kernel/syscall.c +++ b/kernel/syscall.c @@ -1,36 +1,156 @@ #include #include #include +#include #include #include +#include +#include + +#if defined(__x86_64__) +#include +#include + +/* Provided by arch/x86_64/cpu/isr_stubs.asm. */ +extern void isr_stub_syscall(void); + +#define SYSCALL_VECTOR 0x80 + +/* + * Bring the trap frame into the SysV syscall shape. The kernel follows the + * Linux x86_64 convention -- number in RAX, arguments in RDI, RSI, RDX, R10, + * R8, R9 -- because RCX is clobbered by the CPU on a SYSCALL entry and R10 + * stands in for it. Keeping the same order here means user code does not have + * to care which gate it came through. + */ +static void syscall_trap(registers_t* regs) { + regs->rax = (uint64_t)syscall_handler(regs->rax, + regs->rdi, regs->rsi, regs->rdx, + regs->r10, regs->r8, regs->r9); +} void syscall_init(void) { - printk(KERN_INFO "SYSCALL: x86_64 Fast SYSCALL/SYSRET Dispatcher online\n"); + /* + * DPL 3 so ring 3 may raise it, and an interrupt gate (0xEE) so IF is + * cleared on entry. The CPU takes the stack from TSS.rsp0, which the exec + * path points at a kernel stack owned by the running process. + */ + idt_set_gate(SYSCALL_VECTOR, (void*)isr_stub_syscall, 0xEE); + isr_register_handler(SYSCALL_VECTOR, syscall_trap); + printk(KERN_INFO "SYSCALL: INT 0x%02X user gate installed (DPL 3)\n", SYSCALL_VECTOR); +} +#else +void syscall_init(void) { + printk(KERN_INFO "SYSCALL: dispatcher online (no ring 3 gate on this architecture)\n"); +} +#endif + +#define EFAULT_ (-14) +#define ENOSYS_ (-38) +#define EINVAL_ (-22) + +/* + * Validate a user-supplied buffer. + * + * When a syscall arrives from ring 3 the process page tables are still live, + * so a user pointer can be dereferenced directly -- which is exactly why the + * range has to be checked first. Without this, a program could pass 0x100000 + * and have the kernel copy its own text out to the console. + * + * Calls made from kernel context (uproc_current() == NULL) are trusted. + */ +static bool user_buffer_ok(uint64_t ptr, size_t len) { + if (!uproc_current()) return true; + return uaccess_ok(ptr, len); +} + +static int64_t sys_write(int fd, uint64_t buf, size_t count) { + if (count == 0) return 0; + if (!user_buffer_ok(buf, count)) return EFAULT_; + + /* stdout and stderr go straight to the console: a process should be able + * to say something before any filesystem is mounted. console_write() is + * used rather than tty_write() so the bytes reach the serial port and the + * framebuffer console too, exactly like kernel output. */ + if (fd == 1 || fd == 2) { + console_write((const char*)(uintptr_t)buf, count); + return (int64_t)count; + } + return (int64_t)vfs_write(fd, (const void*)(uintptr_t)buf, count); +} + +static int64_t sys_read(int fd, uint64_t buf, size_t count) { + if (count == 0) return 0; + if (!user_buffer_ok(buf, count)) return EFAULT_; + return (int64_t)vfs_read(fd, (void*)(uintptr_t)buf, count); +} + +/* Grow or query the program break. */ +static int64_t sys_brk(uint64_t addr) { + uproc_t* proc = uproc_current(); + if (!proc) return EINVAL_; + + if (addr == 0 || addr < proc->brk_start) return (int64_t)proc->brk; + if (addr > USER_STACK_TOP - (USER_STACK_PAGES * VM_PAGE_SIZE)) return (int64_t)proc->brk; + + if (addr > proc->brk) { + if (vmspace_alloc(proc->vm, proc->brk, (size_t)(addr - proc->brk), + VM_PROT_READ | VM_PROT_WRITE | VM_PROT_USER) != 0) { + return (int64_t)proc->brk; + } + } + proc->brk = addr; + return (int64_t)proc->brk; +} + +static int64_t sys_open(uint64_t path, int flags) { + char kpath[256]; + if (uproc_current()) { + if (uaccess_copy_string(kpath, path, sizeof(kpath)) < 0) return EFAULT_; + return (int64_t)vfs_open(kpath, flags); + } + return (int64_t)vfs_open((const char*)(uintptr_t)path, flags); +} + +static int64_t sys_mkdir(uint64_t path, uint32_t mode) { + char kpath[256]; + if (uproc_current()) { + if (uaccess_copy_string(kpath, path, sizeof(kpath)) < 0) return EFAULT_; + return (int64_t)vfs_mkdir(kpath, mode); + } + return (int64_t)vfs_mkdir((const char*)(uintptr_t)path, mode); } int64_t syscall_handler(uint64_t num, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5, uint64_t a6) { (void)a4; (void)a5; (void)a6; switch (num) { case SYS_read: - return (int64_t)vfs_read((int)a1, (void*)a2, (size_t)a3); + return sys_read((int)a1, a2, (size_t)a3); case SYS_write: - return (int64_t)vfs_write((int)a1, (const void*)a2, (size_t)a3); + return sys_write((int)a1, a2, (size_t)a3); case SYS_open: - return (int64_t)vfs_open((const char*)a1, (int)a2); + return sys_open(a1, (int)a2); case SYS_close: return (int64_t)vfs_close((int)a1); case SYS_lseek: return (int64_t)vfs_lseek((int)a1, (off_t)a2, (int)a3); - case SYS_getpid: - return (int64_t)task_get_pid(); + case SYS_brk: + return sys_brk(a1); + case SYS_getpid: { + uproc_t* proc = uproc_current(); + return (int64_t)(proc ? proc->pid : task_get_pid()); + } case SYS_kill: return (int64_t)signal_send((uint32_t)a1, (int)a2); case SYS_mkdir: - return (int64_t)vfs_mkdir((const char*)a1, (uint32_t)a2); + return sys_mkdir(a1, (uint32_t)a2); case SYS_exit: + /* A ring 3 process unwinds back into exec_user_image(); a kernel + * thread just tears itself down. */ + if (uproc_current()) uproc_exit((int)a1); task_exit((int)a1); return 0; default: - return -1; // ENOSYS + return ENOSYS_; } } diff --git a/scripts/mkblob.py b/scripts/mkblob.py new file mode 100755 index 0000000..bae8f5e --- /dev/null +++ b/scripts/mkblob.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Embed a binary file into the kernel image as a C byte array. + +Used by the build to carry the statically linked /sbin/init ELF inside the +kernel, so the ELF loader has something to run before any filesystem is +mounted. + + usage: mkblob.py +""" + +import os +import sys + + +def main(): + if len(sys.argv) != 4: + sys.stderr.write(f"usage: {os.path.basename(sys.argv[0])} " + " \n") + return 2 + + src, dst, symbol = sys.argv[1], sys.argv[2], sys.argv[3] + + try: + with open(src, "rb") as handle: + payload = handle.read() + except OSError as exc: + sys.stderr.write(f"mkblob: cannot read {src}: {exc}\n") + return 1 + + if not payload: + sys.stderr.write(f"mkblob: {src} is empty\n") + return 1 + + out = [ + "/* Automatically generated by scripts/mkblob.py. Do not edit. */", + f'/* source: {os.path.basename(src)} ({len(payload)} bytes) */', + "", + "#include ", + "", + f"const unsigned char {symbol}[] = {{", + ] + + for offset in range(0, len(payload), 12): + chunk = payload[offset:offset + 12] + out.append(" " + " ".join(f"0x{b:02x}," for b in chunk)) + + out += [ + "};", + "", + f"const size_t {symbol}_len = sizeof({symbol});", + "", + ] + + os.makedirs(os.path.dirname(os.path.abspath(dst)), exist_ok=True) + try: + with open(dst, "w") as handle: + handle.write("\n".join(out)) + except OSError as exc: + sys.stderr.write(f"mkblob: cannot write {dst}: {exc}\n") + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/userland/init/init.c b/userland/init/init.c new file mode 100644 index 0000000..e161be9 --- /dev/null +++ b/userland/init/init.c @@ -0,0 +1,130 @@ +/* + * SUB-OS /sbin/init -- the first program that runs outside the kernel. + * + * Built freestanding and static: no libc, no startup files, nothing shared + * with the kernel image. Every service it uses arrives through INT 0x80, so + * running it end to end proves the ELF loader, the user page tables, the ring + * transition and the syscall gate all agree with each other. + */ + +typedef unsigned long size_t; +typedef long ssize_t; + +#define SYS_read 0 +#define SYS_write 1 +#define SYS_getpid 39 +#define SYS_exit 60 +#define SYS_brk 12 + +#define STDOUT 1 + +static long syscall3(long nr, long a1, long a2, long a3) { + long ret; + __asm__ volatile("int $0x80" + : "=a"(ret) + : "a"(nr), "D"(a1), "S"(a2), "d"(a3) + : "memory", "rcx", "r11"); + return ret; +} + +static long syscall1(long nr, long a1) { + return syscall3(nr, a1, 0, 0); +} + +static size_t ustrlen(const char* s) { + size_t n = 0; + while (s[n]) n++; + return n; +} + +static void write_str(const char* s) { + syscall3(SYS_write, STDOUT, (long)s, (long)ustrlen(s)); +} + +static void write_dec(long value) { + char buf[24]; + int i = (int)sizeof(buf); + int negative = 0; + + if (value < 0) { negative = 1; value = -value; } + buf[--i] = '\0'; + do { + buf[--i] = (char)('0' + (value % 10)); + value /= 10; + } while (value && i > 1); + if (negative && i > 0) buf[--i] = '-'; + + write_str(&buf[i]); +} + +static void write_hex(unsigned long value) { + static const char digits[] = "0123456789abcdef"; + char buf[19]; + int i = (int)sizeof(buf); + + buf[--i] = '\0'; + do { + buf[--i] = digits[value & 0xF]; + value >>= 4; + } while (value && i > 2); + buf[--i] = 'x'; + buf[--i] = '0'; + + write_str(&buf[i]); +} + +/* Lives in .data, so a writable PT_LOAD segment has to have been mapped. */ +static char greeting[] = "SUB-OS userland is alive.\n"; + +/* Lives in .bss, so the loader has to have zero-filled the tail of a segment. + * volatile keeps the compiler from folding the check away: without it gcc + * proves the array is never written and drops it, taking .bss with it. */ +static volatile char zero_probe[64]; + +void _start(void) { + write_str("\n"); + write_str("=== /sbin/init (ring 3) ===\n"); + write_str(greeting); + + write_str(" pid : "); + write_dec(syscall1(SYS_getpid, 0)); + write_str("\n"); + + write_str(" code address : "); + write_hex((unsigned long)(void*)_start); + write_str("\n"); + + write_str(" stack address : "); + unsigned long here; + __asm__ volatile("mov %%rsp, %0" : "=r"(here)); + write_hex(here); + write_str("\n"); + + /* .bss must have arrived zeroed. */ + int bss_clean = 1; + for (unsigned i = 0; i < sizeof(zero_probe); i++) { + if (zero_probe[i] != 0) bss_clean = 0; + } + write_str(" .bss zeroed : "); + write_str(bss_clean ? "yes\n" : "NO\n"); + + /* .data must be writable. */ + greeting[0] = 'S'; + write_str(" .data writable : yes\n"); + + /* An unmapped user address must be rejected rather than served. */ + long bad = syscall3(SYS_write, STDOUT, (long)0x7fffffffffffUL, 8); + write_str(" bad pointer : "); + write_str(bad < 0 ? "rejected\n" : "ACCEPTED (bug)\n"); + + /* A kernel address must be rejected too, even though it is mapped. */ + long kern = syscall3(SYS_write, STDOUT, (long)0x100000UL, 8); + write_str(" kernel pointer : "); + write_str(kern < 0 ? "rejected\n" : "ACCEPTED (bug)\n"); + + write_str("=== init exiting with status 0 ===\n\n"); + syscall1(SYS_exit, 0); + + /* SYS_exit does not return; spin defensively if it ever did. */ + for (;;) { } +} diff --git a/userland/init/user.ld b/userland/init/user.ld new file mode 100644 index 0000000..a128af7 --- /dev/null +++ b/userland/init/user.ld @@ -0,0 +1,24 @@ +/* + * Userland link map. + * + * Programs are placed inside the PML4 slot the kernel reserves for user + * mappings (512 GiB), well clear of the identity-mapped low 4 GB where the + * kernel itself lives. Keep this base in sync with USER_LOAD_BASE in + * include/mm/vmspace.h. + */ +ENTRY(_start) + +SECTIONS +{ + . = 0x8000400000; + + .text : ALIGN(4K) { *(.text*) } + .rodata : ALIGN(4K) { *(.rodata*) } + .data : ALIGN(4K) { *(.data*) } + .bss : ALIGN(4K) { + *(COMMON) + *(.bss*) + } + + /DISCARD/ : { *(.comment) *(.note*) *(.eh_frame*) } +}