M3 step 3: syscall/sysret fast path

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 <noreply@anthropic.com>
This commit is contained in:
Daniel Samson 2026-07-08 23:17:31 +01:00
parent 8a7235c725
commit 5d57e7b01c
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
5 changed files with 91 additions and 2 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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:

View File

@ -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
}

View File

@ -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)