From 5d57e7b01c1e0e3c80dce703267e7dd0f76880a3 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Wed, 8 Jul 2026 23:17:31 +0100 Subject: [PATCH] M3 step 3: syscall/sysret fast path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-core MSR setup (EFER.SCE, STAR, LSTAR, SFMASK) enables syscall; the GDT layout was already chosen so sysret lands on CS 0x23 / SS 0x1B. A new syscall_entry stub swaps in the kernel GS, switches to the task's kernel stack via the per-CPU block, builds a CpuState frame identical to the interrupt path's, and reuses interruptDispatch (vector 128) — then sysretq back. enter_user now also publishes kernel_rsp so the borrowed path's syscalls land on a good stack. /sbin/init uses the `syscall` instruction. int 0x80 stays for the test blobs. Suite 27/27. (Noted in the stub: sysretq #GPs in ring 0 on a non-canonical return RIP — a hardening item once untrusted user code exists.) Co-Authored-By: Claude Fable 5 --- sbin/init.zig | 6 ++- src/kernel/arch/x86_64/cpu.zig | 1 + src/kernel/arch/x86_64/isr.s | 64 +++++++++++++++++++++++++++++++ src/kernel/arch/x86_64/percpu.zig | 21 ++++++++++ src/kernel/arch/x86_64/smp.zig | 1 + 5 files changed, 91 insertions(+), 2 deletions(-) diff --git a/sbin/init.zig b/sbin/init.zig index b5e0bd1..f5533c3 100644 --- a/sbin/init.zig +++ b/sbin/init.zig @@ -14,12 +14,14 @@ const sys_exit = 0; const sys_write = 2; fn syscall2(n: u64, a: u64, b: u64) u64 { - return asm volatile ("int $0x80" + // The `syscall` instruction clobbers RCX (return RIP) and R11 (saved RFLAGS); + // the kernel entry stub preserves everything else. + return asm volatile ("syscall" : [ret] "={rax}" (-> u64), : [n] "{rax}" (n), [a] "{rdi}" (a), [b] "{rsi}" (b), - : .{ .memory = true }); + : .{ .rcx = true, .r11 = true, .memory = true }); } fn write(msg: []const u8) void { diff --git a/src/kernel/arch/x86_64/cpu.zig b/src/kernel/arch/x86_64/cpu.zig index 18df1ae..6c0b4e1 100644 --- a/src/kernel/arch/x86_64/cpu.zig +++ b/src/kernel/arch/x86_64/cpu.zig @@ -58,6 +58,7 @@ pub fn init() void { gdt.init(); tss.init(); idt.init(); + pcpu.initSyscall(); } /// Build the kernel's own page tables (with real permissions) and switch onto diff --git a/src/kernel/arch/x86_64/isr.s b/src/kernel/arch/x86_64/isr.s index a6e12ad..185278e 100644 --- a/src/kernel/arch/x86_64/isr.s +++ b/src/kernel/arch/x86_64/isr.s @@ -117,6 +117,7 @@ enter_user: push %r15 mov %rsp, user_saved_rsp(%rip) # where user_exit_to_kernel unwinds to mov %rsp, (%rdx) # TSS.rsp0: ring-3 interrupts stack here + mov %rsp, %gs:0 # kernel_rsp: the syscall stub stacks here too push $0x1B # user SS (0x18 | RPL 3) push %rsi # user RSP push $0x202 # RFLAGS: IF | reserved-1 @@ -140,6 +141,69 @@ user_exit_to_kernel: pop %rbx ret +# syscall_entry: the target of the SYSCALL instruction (LSTAR). The CPU does NOT +# switch stacks — it puts the return RIP in RCX, the saved RFLAGS in R11, loads +# CS/SS from STAR, masks RFLAGS with SFMASK (so IF is already clear), and jumps +# here with RSP still the *user* stack. We swap in the kernel GS, switch to the +# task's kernel stack via the per-CPU block, build a CpuState frame identical to +# the interrupt path's, and reuse interruptDispatch (vector 128) — then SYSRET. +# +# Hazard (acceptable while init is the only, trusted, user program): SYSRETQ #GPs +# in ring 0 if the return RIP (RCX) is non-canonical. A hostile user could arrange +# that; hardening (canonical check / iretq fallback) is a later security-track item. +.global syscall_entry +syscall_entry: + swapgs # kernel GS base + movq %rsp, %gs:8 # stash user rsp in the scratch slot + movq %gs:0, %rsp # switch to this task's kernel stack + # Build the trap frame (same field order as isr_common), highest field first. + pushq $0x1B # ss (user data | 3) + pushq %gs:8 # rsp (user, from scratch) + pushq %r11 # rflags (saved by syscall) + pushq $0x23 # cs (user code | 3) + pushq %rcx # rip (saved by syscall) + pushq $0 # error_code (none for a syscall) + pushq $128 # vector (same as the int 0x80 gate) + push %rax + push %rbx + push %rcx + push %rdx + push %rsi + push %rdi + push %rbp + push %r8 + push %r9 + push %r10 + push %r11 + push %r12 + push %r13 + push %r14 + push %r15 + mov %rsp, %rdi # trap-frame pointer + call interruptDispatch + pop %r15 + pop %r14 + pop %r13 + pop %r12 + pop %r11 + pop %r10 + pop %r9 + pop %r8 + pop %rbp + pop %rdi + pop %rsi + pop %rdx + pop %rcx + pop %rbx + pop %rax + add $16, %rsp # drop vector + error_code -> rsp at rip + popq %rcx # rip -> RCX (SYSRETQ restores RIP from RCX) + addq $8, %rsp # skip the cs slot (SYSRETQ loads CS from STAR) + popq %r11 # rflags -> R11 (SYSRETQ restores RFLAGS from R11) + popq %rsp # user rsp (the ss slot below is abandoned) + swapgs # user GS base + sysretq # -> ring 3: RIP=RCX, RFLAGS=R11, CS/SS from STAR + .section .bss .balign 8 user_saved_rsp: diff --git a/src/kernel/arch/x86_64/percpu.zig b/src/kernel/arch/x86_64/percpu.zig index dee8c3c..149e308 100644 --- a/src/kernel/arch/x86_64/percpu.zig +++ b/src/kernel/arch/x86_64/percpu.zig @@ -54,3 +54,24 @@ pub fn sched() usize { pub fn setKernelRsp(index: usize, top: usize) void { blocks[index].kernel_rsp = top; } + +// Fast-syscall MSRs. +const ia32_efer = 0xC000_0080; +const ia32_star = 0xC000_0081; +const ia32_lstar = 0xC000_0082; +const ia32_sfmask = 0xC000_0084; + +/// Enable the `syscall`/`sysret` fast path on this core (BSP and each AP). EFER.SCE +/// turns the instructions on; STAR sets the selectors syscall/sysret load; LSTAR +/// is the entry stub (isr.s); SFMASK clears RFLAGS bits on entry (notably IF — +/// the handler runs with interrupts off, like the int-gate path). The GDT is laid +/// out (kernel code 0x08, then user data 0x18 / code 0x20) precisely so these line +/// up: syscall loads CS 0x08 / SS 0x10; sysret loads CS = base+16 and SS = base+8 +/// with RPL forced to 3, so base 0x10 gives CS 0x23 (user code|3) and SS 0x1B. +pub fn initSyscall() void { + io.wrmsr(ia32_efer, io.rdmsr(ia32_efer) | 1); // SCE + io.wrmsr(ia32_star, (@as(u64, 0x08) << 32) | (@as(u64, 0x10) << 48)); + const entry = @extern(*const anyopaque, .{ .name = "syscall_entry" }); + io.wrmsr(ia32_lstar, @intFromPtr(entry)); + io.wrmsr(ia32_sfmask, 0x4_0700); // clear IF, TF, DF, AC on entry +} diff --git a/src/kernel/arch/x86_64/smp.zig b/src/kernel/arch/x86_64/smp.zig index ee54042..419764a 100644 --- a/src/kernel/arch/x86_64/smp.zig +++ b/src/kernel/arch/x86_64/smp.zig @@ -170,6 +170,7 @@ fn apEntry(percpu: usize) callconv(.c) noreturn { tss.setupThisCpu(cpu); // this core's TSS + IST stack, loaded into TR idt.loadOnThisCpu(); // the shared IDT pcpu.setLocal(cpu, percpu); // per-CPU block via GS base — *after* the GDT reload + pcpu.initSyscall(); // enable syscall/sysret on this core apic.initSecondary(); // software-enable this core's LAPIC apic.initTimer(apic.frequencyHz()); // arm its timer (still masked: interrupts off)