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 <[email protected]>
Claude-Session: https://claude.ai/code/session_0161Jh7uCEMLYEMNPsjC1LKf
This commit is contained in:
subhobhai943andClaude Opus 5 committed 2026-08-25 06:37:20 +00:00
1 parent 7929e48b4e
commit ecdc5be435
19 files changed
+1439 -9

No files matched your search

+38 -1
View File
@@ -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 $@)
+42
View File
@@ -0,0 +1,42 @@
#include <mm/vmspace.h>
/*
* 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; }
+42
View File
@@ -0,0 +1,42 @@
#include <mm/vmspace.h>
/*
* 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; }
+21
View File
@@ -3,6 +3,7 @@
#include <arch/x86_64/pic.h>
#include <kernel/printk.h>
#include <kernel/panic.h>
#include <kernel/exec.h>
#include <lib/string.h>
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);
+9
View File
@@ -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
+89
View File
@@ -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
+239
View File
@@ -0,0 +1,239 @@
#include <mm/vmspace.h>
#include <mm/pmm.h>
#include <mm/kmalloc.h>
#include <arch/x86_64/paging.h>
#include <kernel/printk.h>
#include <lib/string.h>
/*
* 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);
}
+98
View File
@@ -0,0 +1,98 @@
#ifndef _KERNEL_ELF_H
#define _KERNEL_ELF_H
#include <stdint.h>
#include <stddef.h>
#include <mm/vmspace.h>
/* 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 */
+53
View File
@@ -0,0 +1,53 @@
#ifndef _KERNEL_EXEC_H
#define _KERNEL_EXEC_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <kernel/types.h>
#include <mm/vmspace.h>
/* 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 */
+4
View File
@@ -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
+67
View File
@@ -0,0 +1,67 @@
#ifndef _MM_VMSPACE_H
#define _MM_VMSPACE_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
/*
* 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 */
+116
View File
@@ -0,0 +1,116 @@
#include <kernel/elf.h>
#include <kernel/printk.h>
#include <lib/string.h>
#include <mm/vmspace.h>
#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;
}
+238
View File
@@ -0,0 +1,238 @@
#include <kernel/exec.h>
#include <kernel/elf.h>
#include <kernel/printk.h>
#include <kernel/panic.h>
#include <kernel/task.h>
#include <mm/vmspace.h>
#include <mm/kmalloc.h>
#include <lib/string.h>
#if defined(__x86_64__)
#include <arch/x86_64/gdt.h>
/* 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__ */
+5
View File
@@ -92,6 +92,7 @@
#include <userland/lazybox.h>
#include <userland/sh.h>
#include <userland/shell.h>
#include <kernel/exec.h>
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();
+30
View File
@@ -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);
+128 -8
View File
@@ -1,36 +1,156 @@
#include <kernel/syscall.h>
#include <fs/vfs.h>
#include <kernel/task.h>
#include <kernel/exec.h>
#include <kernel/signal.h>
#include <kernel/printk.h>
#include <drivers/tty.h>
#include <mm/vmspace.h>
#if defined(__x86_64__)
#include <arch/x86_64/idt.h>
#include <arch/x86_64/isr.h>
/* 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_;
}
}
+66
View File
@@ -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 <input-binary> <output.c> <symbol-name>
"""
import os
import sys
def main():
if len(sys.argv) != 4:
sys.stderr.write(f"usage: {os.path.basename(sys.argv[0])} "
"<input-binary> <output.c> <symbol-name>\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 <stddef.h>",
"",
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())
+130
View File
@@ -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 (;;) { }
}
+24
View File
@@ -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*) }
}