Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
893 changes: 893 additions & 0 deletions docs/plans/syscall-system.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions kernel/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ pub const USB_XHCI_EVENT_RING_DIAGNOSTICS: bool = false;
/// of PCI/MSI delivery.
pub const USB_RUN_SW_INT_SELF_TEST: bool = false;

/// When `true`, run the ring 3 syscall self-test (maps user pages, iretqs into
/// an embedded user binary that calls SYS_NULL, SYS_GET_TICKS, and SYS_WRITE_SERIAL).
/// The kernel does not reach the idle loop while this is enabled.
pub const RUN_SYSCALL_TEST: bool = false;

// ============================================================================
// Logging Configuration
// ============================================================================
Expand Down
30 changes: 30 additions & 0 deletions kernel/src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,22 @@ pub unsafe extern "C" fn continue_after_stack_switch() -> ! {
);
}
}

// Map the syscall kernel stack (needed by syscall_init later)
{
let (base, size) = crate::syscall::percpu::syscall_stack_range();
physical_memory::record_boot_consumed_region(ConsumedRegion { start: base, size });
unsafe {
map_existing_region_va_to_its_pa(
pml4_pa,
h,
base,
size,
PTE_PRESENT | PTE_WRITABLE | PTE_GLOBAL | PTE_NO_EXEC,
&mut BootFrameAllocator::empty(),
);
}
}
}
// Detect CPU features and apply CR4 hardening (SMEP/SMAP/FSGSBASE)
crate::cpu_features::CpuFeatures::init();
Expand Down Expand Up @@ -189,6 +205,12 @@ pub unsafe extern "C" fn continue_after_stack_switch() -> ! {
}
log_debug!("MSRs configured");

// Initialize syscall subsystem (STAR/LSTAR/SFMASK + per-CPU data + kernel stack).
// Must be after setup_msrs (EFER.SCE) and after GDT has user segments.
unsafe {
crate::syscall::syscall_init();
}

// Verify LAPIC timer delivery (later test)
// Detect and cache the APIC access mode (xAPIC vs x2APIC) before any LAPIC access
unsafe {
Expand Down Expand Up @@ -399,6 +421,14 @@ pub unsafe extern "C" fn continue_after_stack_switch() -> ! {
serial_debug::run_reverse_echo_session();
}

// Run ring 3 syscall self-test if configured.
// This replaces the idle loop and never returns.
if crate::config::RUN_SYSCALL_TEST {
unsafe {
crate::syscall::usermode::run_usermode_test();
}
}

if crate::config::KERNEL_SHOULD_IDLE {
log_warn!("Entering idle loop - heart animation active");
log_warn!("Kill QEMU to stop the kernel");
Expand Down
17 changes: 17 additions & 0 deletions kernel/src/gdt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@ use x86_64::{
VirtAddr,
};

#[allow(dead_code)]
struct GdtState {
gdt: GlobalDescriptorTable,
code_sel: SegmentSelector,
data_sel: SegmentSelector,
user_data_sel: SegmentSelector,
user_code_sel: SegmentSelector,
tss_sel: SegmentSelector,
}

Expand Down Expand Up @@ -84,6 +87,12 @@ unsafe fn build_gdt_state() -> GdtState {
let mut gdt = GlobalDescriptorTable::new();
let code_sel = gdt.add_entry(Descriptor::kernel_code_segment());
let data_sel = gdt.add_entry(Descriptor::kernel_data_segment());
// User segments for ring 3 (required by syscall/sysretq).
// Layout: null, kcode(0x08), kdata(0x10), udata(0x18), ucode(0x20), TSS(0x28+0x30).
// STAR[47:32] = 0x0008 (kernel CS; kernel SS = 0x0008+8 = 0x0010).
// STAR[63:48] = 0x0010 (sysretq CS = 0x0010+16 = 0x0020; SS = 0x0010+8 = 0x0018).
let user_data_sel = gdt.add_entry(Descriptor::user_data_segment());
let user_code_sel = gdt.add_entry(Descriptor::user_code_segment());
TSS_STATIC = tss;
let tss_ref: &'static TaskStateSegment = core::mem::transmute::<
*const TaskStateSegment,
Expand All @@ -94,6 +103,8 @@ unsafe fn build_gdt_state() -> GdtState {
gdt,
code_sel,
data_sel,
user_data_sel,
user_code_sel,
tss_sel,
}
}
Expand All @@ -113,6 +124,12 @@ pub fn ist_stack_ranges() -> [(u64, u64); 4] {

/// Kernel code-segment selector used by the runtime GDT.
pub const KERNEL_CS: u16 = 0x08;
/// Kernel data-segment selector.
pub const KERNEL_SS: u16 = 0x10;
/// User data-segment selector (ring 3). Loaded by sysretq as SS with RPL=3.
pub const USER_DS: u16 = 0x18;
/// User code-segment selector (ring 3, L=1). Loaded by sysretq as CS with RPL=3.
pub const USER_CS: u16 = 0x20;

/// Build and load the runtime GDT/TSS state.
pub unsafe fn setup_gdt() {
Expand Down
1 change: 1 addition & 0 deletions kernel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub mod panic;
pub mod physical_memory;
pub mod serial_debug;
pub mod stack;
pub mod syscall;

// Config must come after logging since it references logging types
pub mod config;
Expand Down
170 changes: 170 additions & 0 deletions kernel/src/syscall/dispatch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
//! Syscall dispatch table and handlers.
//!
//! Called from entry.rs trampoline via `syscall_dispatch(number, frame)`.

use super::percpu;
use crate::log_info;

/// Saved register frame pushed by `syscall_entry` assembly.
///
/// Layout MUST match the push order in entry.rs (top of struct = lowest address = RSP).
#[repr(C)]
pub struct SyscallFrame {
/// RAX: syscall number on entry; return value on exit.
pub rax: u64,
pub rdi: u64,
pub rsi: u64,
pub rdx: u64,
/// R10 holds the 4th syscall argument (replaces RCX, which CPU uses for saved RIP).
pub r10: u64,
pub r9: u64,
pub r8: u64,
/// Saved user RIP (from RCX after `syscall` instruction).
pub rcx: u64,
pub rbx: u64,
pub rbp: u64,
/// Saved user RFLAGS (from R11 after `syscall` instruction).
pub r11: u64,
pub r12: u64,
pub r13: u64,
pub r14: u64,
pub r15: u64,
}

impl SyscallFrame {
/// Syscall arg1 (user SysV ABI: RDI).
pub fn arg1(&self) -> u64 {
self.rdi
}
/// Syscall arg2 (user SysV ABI: RSI).
pub fn arg2(&self) -> u64 {
self.rsi
}
/// Syscall arg3 (user SysV ABI: RDX).
pub fn arg3(&self) -> u64 {
self.rdx
}
/// Syscall arg4 (user SysV ABI: R10 — not RCX, which holds saved RIP).
pub fn arg4(&self) -> u64 {
self.r10
}
/// Syscall arg5 (user SysV ABI: R8).
pub fn arg5(&self) -> u64 {
self.r8
}
/// Syscall arg6 (user SysV ABI: R9).
pub fn arg6(&self) -> u64 {
self.r9
}
}

/// Syscall numbers (RAX on entry).
pub mod numbers {
/// No-op; returns 0. Useful for overhead benchmarks.
pub const SYS_NULL: u64 = 0;
/// Write bytes to the debug serial port (port 0xE9).
/// arg1 (RDI) = buffer pointer
/// arg2 (RSI) = length
pub const SYS_WRITE_SERIAL: u64 = 1;
/// Return the current LAPIC timer tick count. No arguments.
pub const SYS_GET_TICKS: u64 = 2;
}

/// Maximum syscall number + 1 (table size).
const MAX_SYSCALL: usize = 16;

/// Syscall handler signature.
type SyscallFn = fn(frame: &mut SyscallFrame) -> i64;

/// Static syscall dispatch table.
/// Index = syscall number. `None` → returns -ENOSYS.
static SYSCALL_TABLE: [Option<SyscallFn>; MAX_SYSCALL] = {
let mut table: [Option<SyscallFn>; MAX_SYSCALL] = [None; MAX_SYSCALL];
table[numbers::SYS_NULL as usize] = Some(sys_null);
table[numbers::SYS_WRITE_SERIAL as usize] = Some(sys_write_serial);
table[numbers::SYS_GET_TICKS as usize] = Some(sys_get_ticks);
table
};

/// Rust dispatcher called from the syscall_entry trampoline.
///
/// This is the single assembly → Rust ABI boundary:
/// - Microsoft x64 ABI: arg1=RCX (syscall number), arg2=RDX (&SyscallFrame)
/// - Returns i64 in RAX (copied back into the frame by the trampoline)
///
/// # Safety
/// Called only from entry.rs assembly with a valid, mapped kernel stack frame.
#[no_mangle]
pub extern "C" fn syscall_dispatch(number: u64, frame: *mut SyscallFrame) -> i64 {
let frame = unsafe { &mut *frame };

let handler = match SYSCALL_TABLE.get(number as usize) {
Some(Some(h)) => *h,
_ => {
log_info!("syscall: unknown number {}", number);
return -1; // ENOSYS
}
};

handler(frame)
}

// ============================================================================
// Handlers
// ============================================================================

/// SYS_NULL: returns 0. Used as a baseline latency measurement.
fn sys_null(_frame: &mut SyscallFrame) -> i64 {
0
}

/// SYS_WRITE_SERIAL: write bytes to debug port 0xE9.
///
/// arg1 = buffer pointer, arg2 = length.
/// Returns bytes written on success, negative on error.
///
/// Uses STAC/CLAC to temporarily disable SMAP for accessing user buffer.
/// Production should validate pointer range against USER_SPACE_END.
fn sys_write_serial(frame: &mut SyscallFrame) -> i64 {
let ptr = frame.arg1() as *const u8;
let len = frame.arg2() as usize;

if len == 0 {
return 0;
}

// Temporarily disable SMAP (set AC flag) to read from user page.
unsafe { core::arch::asm!("stac", options(nostack)); }
let slice = unsafe { core::slice::from_raw_parts(ptr, len) };

let mut written: i64 = 0;
for &byte in slice {
// Write to QEMU debug exit port (0xE9) — same path as serial_debug helpers.
unsafe {
core::arch::asm!(
"out dx, al",
in("dx") 0xE9u16,
in("al") byte,
options(nostack, preserves_flags)
);
}
written += 1;
}

// Re-enable SMAP (clear AC flag).
unsafe { core::arch::asm!("clac", options(nostack)); }

written
}

/// SYS_GET_TICKS: return LAPIC timer tick count. No arguments.
fn sys_get_ticks(_frame: &mut SyscallFrame) -> i64 {
crate::interrupts::timer_tick_count() as i64
}

/// Return the per-CPU user_rsp for the current CPU (BSP only for now).
///
/// Useful for debug/test: the syscall trampoline saves user RSP here.
pub fn debug_user_rsp() -> u64 {
unsafe { percpu::bsp_percpu_mut().user_rsp }
}
Loading