Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
53e68d6
feat: enhance process and TTY management with job control features an…
n4mlz Jan 27, 2026
26bb43e
feat: implement container start with wait for exit and TTY attachment
n4mlz Jan 28, 2026
b4c201a
refactor: remove unnecessary println! statements
n4mlz Jan 28, 2026
6ea7569
feat: implement blocking read and canonical input handling for TTY de…
n4mlz Jan 28, 2026
f6ebbd4
tmp: add debug logging for TTY and Linux syscall operations
n4mlz Jan 28, 2026
fbfb9b7
feat: add poll syscall support for TTY input availability
n4mlz Jan 28, 2026
5ee7dd0
feat: implement seek functionality for file and memory filesystems
n4mlz Jan 28, 2026
65d4540
feat: implement clone functionality for FdTable and ProcessFs
n4mlz Jan 28, 2026
bfbcb03
fix: add interrupt management functions and enhance stack initializat…
n4mlz Jan 29, 2026
1631577
refactor: enhance trap layer documentation and improve error handling…
n4mlz Jan 29, 2026
a75a165
refactor: improve ELF loader documentation and enhance stack initiali…
n4mlz Jan 29, 2026
0036e40
refactor: enhance documentation for syscall handling and improve erro…
n4mlz Jan 29, 2026
2929653
refactor: update process subsystem documentation and enhance thread s…
n4mlz Jan 29, 2026
641db66
tmp: enhance logging in loader, process, syscall, and thread manageme…
n4mlz Jan 29, 2026
642a276
fix: relocation processes for better traceability
n4mlz Jan 29, 2026
76d500a
refactor: enhance page fault handling and logging for better traceabi…
n4mlz Jan 29, 2026
68bc31e
test: add user-mode page fault fixture and validation tests
n4mlz Jan 29, 2026
d4c46cb
fix: correct relocation address in user-mode tests for accuracy
n4mlz Jan 29, 2026
4987262
tmp: enhance syscall rewrite logging and coverage verification
n4mlz Jan 29, 2026
29dd900
fix: implement syscall emulation for #UD in user mode and enhance sys…
n4mlz Jan 29, 2026
4632672
feat: add support for getdents64 syscall and enhance directory handli…
n4mlz Jan 29, 2026
8769960
feat: add lstat syscall implementation and enhance related tests
n4mlz Jan 29, 2026
6ddb0c6
refactor: kernel loader and syscall handling
n4mlz Jan 29, 2026
1b409fc
feat: enhance ContainerContext with UTS support and update related sy…
n4mlz Jan 29, 2026
259a553
fix: update test bootloader configuration to increase stack size and …
n4mlz Jan 30, 2026
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
1 change: 1 addition & 0 deletions kernel/src/arch/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ pub trait ArchInterrupt {

fn init_interrupts(boot_info: &'static BootInfo) -> Result<(), InterruptInitError>;

fn are_interrupts_enabled() -> bool;
fn enable_interrupts();
fn disable_interrupts();
fn end_of_interrupt(vector: u8);
Expand Down
14 changes: 14 additions & 0 deletions kernel/src/arch/x86_64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,27 @@ pub fn halt() {
x86_64::instructions::hlt();
}

#[cfg(test)]
pub(crate) fn arm_user_pf_frame_check(pid: crate::process::ProcessId, expected_fault_addr: u64) {
trap::arm_user_pf_frame_check(pid, expected_fault_addr);
}

#[cfg(test)]
pub(crate) fn user_pf_frame_check_passed() -> bool {
trap::user_pf_frame_check_passed()
}

impl ArchInterrupt for X86_64 {
type Timer = interrupt::LocalApicTimer;

fn init_interrupts(boot_info: &'static BootInfo) -> Result<(), InterruptInitError> {
interrupt::LOCAL_APIC.init(boot_info)
}

fn are_interrupts_enabled() -> bool {
x86_64::instructions::interrupts::are_enabled()
}

fn enable_interrupts() {
interrupt::LOCAL_APIC.enable();
}
Expand Down
11 changes: 7 additions & 4 deletions kernel/src/arch/x86_64/trap/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
- Own the GDT/TSS, IDT, and the hand-written trap stubs that translate hardware events into the architecture-neutral trap dispatcher.

## Descriptor Tables
- `gdt` installs both ring-0 and ring-3 segment descriptors alongside the TSS entry, allocating three IST stacks (NMI, double-fault, machine-check) from a statically mapped buffer.
- `set_privilege_stack` updates `TSS.rsp0` on every context switch so user→kernel transitions enter on the scheduled thread's kernel stack, while a fallback ring-0 stack remains available for bootstrap paths.
- `gdt` installs both ring-0 and ring-3 segment descriptors alongside the TSS entry, allocating three IST stacks (NMI, double-fault, machine-check) from a statically mapped buffer (single-CPU only until per-CPU storage exists).
- `set_privilege_stack` updates `TSS.rsp0` on every context switch so user→kernel transitions enter on the scheduled thread's kernel stack, while a fallback ring-0 stack remains available for bootstrap paths. Updates must occur with interrupts disabled on the current CPU.
- `idt` populates exception vectors with dedicated stubs and assigns IST indices where architectural guidance recommends hardened stacks.
- The IDT exposes vector `0x80` with DPL=3, providing an initial software interrupt entry point for user mode before the syscall MSRs are wired up. The vector is registered with the syscall dispatcher so `int 0x80` routes into ABI-aware handling.
- `init()` loads both tables during early boot and must run per-CPU prior to enabling interrupts.
- The syscall software interrupt (vector `0x80`) is registered in `arch/x86_64/syscall.rs`, which forwards to the generic syscall dispatcher to keep ABI logic architecture-neutral.

## Trap Stubs
- Naked assembly routines in `stubs` save general-purpose registers, normalise error codes, maintain stack alignment for `call`, and end with `iretq`.
- Naked assembly routines in `stubs` clear DF, save general-purpose registers, normalise error codes, maintain stack alignment for `call`, and end with `iretq`.
- The exception table must match the architectural error-code list; a mismatch corrupts the stack frame.
- The kernel is built with red-zone disabled, and SIMD/FPU usage in kernel mode is currently prohibited until save/restore support is added.
- Each stub invokes `dispatch_trap`, which constructs a `TrapInfo` (vector, origin, description) and hands control to `ArchTrap::dispatch_trap` so the architecture layer can route through the generic trap handler without hard-wiring the entry point.
- Timer interrupts reuse the same mechanism, so the scheduler observes consistent metadata regardless of source.

Expand All @@ -27,9 +29,10 @@
- `thread::Context` derives from a trap frame after interrupts, allowing seamless handoff between interrupt context and scheduled threads.

## Exception Handlers
- `handlers` provides the architecture-specific fast path for #PF/#GP/#DF, decoding hardware error codes and emitting structured diagnostics before panicking.
- `handlers` provides the architecture-specific fast path for #PF/#GP/#DF, decoding hardware error codes and emitting structured diagnostics before panicking. Double-fault handling avoids logging and halts to reduce re-entrancy risk.
- The dispatcher in `mod.rs` delegates to these helpers via `ArchTrap::handle_exception`; returning `true` suppresses the generic logging path.
- Page-fault handling records the CR2 fault address and access type bits so future user-mode recovery logic has the required context.
- While `SYSCALL/SYSRET` is not wired up, #UD in user mode checks for the `0f 05` opcode and emulates a syscall as a temporary workaround; kernel-mode #UD remains fatal.

- Expand syscall handling beyond `int 0x80` by enabling `SYSCALL/SYSRET` once MSR management is implemented.
- Incorporate per-CPU IST buffers to support SMP and avoid contention on the global static region.
Expand Down
2 changes: 2 additions & 0 deletions kernel/src/arch/x86_64/trap/context.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::trap::TrapFrame as TrapFrameTrait;

pub(super) const GENERAL_REGS_SIZE: usize = core::mem::size_of::<GeneralRegisters>();
/// Byte offset from the trap frame base to the CPU-pushed error code slot.
pub(super) const ORIGINAL_ERROR_OFFSET: usize = 8 + GENERAL_REGS_SIZE;

#[repr(C)]
Expand Down Expand Up @@ -79,6 +80,7 @@ impl TrapFrame {

impl TrapFrameTrait for TrapFrame {
fn error_code(&self) -> Option<u64> {
// The stubs push a placeholder even for no-error exceptions, so `0` means "no error".
Some(self.error_code)
}
}
13 changes: 13 additions & 0 deletions kernel/src/arch/x86_64/trap/gdt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ const PRIVILEGE_STACK_SIZE: usize = 32 * 1024;
#[repr(align(16))]
struct IstStackArea([u8; IST_STACK_SIZE * IST_STACK_COUNT]);

// NOTE: These stacks are global, so SMP will require per-CPU allocations.
static mut IST_STACKS: IstStackArea = IstStackArea([0u8; IST_STACK_SIZE * IST_STACK_COUNT]);

#[repr(align(16))]
struct PrivilegeStack([u8; PRIVILEGE_STACK_SIZE]);

// NOTE: This is a single fallback ring-0 stack; SMP must provide per-CPU stacks.
static mut PRIV_STACK: PrivilegeStack = PrivilegeStack([0u8; PRIVILEGE_STACK_SIZE]);

pub(crate) struct GdtSelectors {
Expand All @@ -49,6 +51,11 @@ static GDT: LazyLock<GdtInit, GdtBuilder> = LazyLock::new_const(build_gdt);
///
/// Must be called on each CPU before enabling interrupts so that IST stacks and
/// privilege transitions reference valid descriptors.
///
/// # SMP limitation
///
/// The current implementation uses global IST/privilege stacks. This is only
/// safe for a single CPU and must be replaced with per-CPU storage.
pub(super) fn load() {
let (gdt, selectors) = &*GDT;
gdt.load();
Expand All @@ -65,6 +72,12 @@ pub(crate) fn selectors() -> &'static GdtSelectors {
selectors
}

/// Updates the ring-0 stack used on CPL3->CPL0 transitions.
///
/// # Safety contract
///
/// Must be called for the current CPU while its interrupts are disabled, and
/// on SMP this must update the per-CPU TSS instance instead of the global one.
pub(crate) fn set_privilege_stack(stack_top: KernelVirtAddr) {
let mut tss = TSS.lock();
let top = VirtAddr::new(stack_top.as_raw() as u64);
Expand Down
215 changes: 181 additions & 34 deletions kernel/src/arch/x86_64/trap/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
use x86_64::instructions::interrupts;
use x86_64::registers::control::Cr2;

use crate::println;
use crate::arch::api::ArchPageTableAccess;
use crate::mem::paging::{PageTableOps, PhysMapper};
use crate::mem::{
addr::{VirtAddr, VirtIntoPtr},
manager,
};
use crate::process::PROCESS_TABLE;
use crate::syscall::{self, SyscallInvocation};
use crate::thread::SCHEDULER;
use crate::trap::TrapInfo;

use super::TrapFrame;
#[cfg(test)]
use super::context::ORIGINAL_ERROR_OFFSET;
#[cfg(test)]
use crate::process::ProcessId;
#[cfg(test)]
use core::sync::atomic::{AtomicU8, AtomicU64, Ordering};

#[cfg(test)]
static USER_PF_FRAME_CHECK_STATE: AtomicU8 = AtomicU8::new(0);
#[cfg(test)]
static USER_PF_FRAME_CHECK_PID: AtomicU64 = AtomicU64::new(0);
#[cfg(test)]
static USER_PF_FRAME_CHECK_ADDR: AtomicU64 = AtomicU64::new(0);

#[cfg(test)]
pub(crate) fn arm_user_pf_frame_check(pid: ProcessId, expected_fault_addr: u64) {
USER_PF_FRAME_CHECK_PID.store(pid, Ordering::SeqCst);
USER_PF_FRAME_CHECK_ADDR.store(expected_fault_addr, Ordering::SeqCst);
USER_PF_FRAME_CHECK_STATE.store(1, Ordering::SeqCst);
}

#[cfg(test)]
pub(crate) fn user_pf_frame_check_passed() -> bool {
USER_PF_FRAME_CHECK_STATE.load(Ordering::SeqCst) == 2
}

pub fn handle_exception(info: TrapInfo, frame: &mut TrapFrame) -> bool {
// Be careful with logging/locking here: traps can occur while locks are held or
// interrupts are disabled, and double faults must avoid re-entrancy entirely.
match info.vector {
14 => {
handle_page_fault(frame);
true
}
14 => handle_page_fault(frame),
6 => {
handle_invalid_opcode(frame);
true
Expand All @@ -19,59 +52,173 @@ pub fn handle_exception(info: TrapInfo, frame: &mut TrapFrame) -> bool {
handle_general_protection(frame);
true
}
8 => {
handle_double_fault(frame);
true
}
8 => handle_double_fault(frame),
_ => false,
}
}

fn handle_page_fault(frame: &TrapFrame) {
fn handle_page_fault(frame: &mut TrapFrame) -> bool {
let fault_addr = Cr2::read().expect("CR2 must contain a canonical address");
let code = frame.error_code;
let user = (code & 1 << 2) != 0 || (frame.cs & 3) != 0;

let present = (code & 1) != 0;
let write = (code & 1 << 1) != 0;
let user = (code & 1 << 2) != 0;
let reserved = (code & 1 << 3) != 0;
let instruction = (code & 1 << 4) != 0;
#[cfg(test)]
maybe_check_user_pf_frame(frame, fault_addr.as_u64());

println!(
"[#PF] fault_addr={:#x} present={} write={} user={} reserved={} instruction={}",
if user {
if let Some(pid) = SCHEDULER.current_process_id()
&& let Ok(process) = PROCESS_TABLE.process_handle(pid)
{
// Use a conventional non-zero status for user faults.
process.set_exit_code(139);
}
SCHEDULER.terminate_current(frame);
return true;
}
panic!(
"page fault in kernel: addr={:#x} code={:#x}",
fault_addr.as_u64(),
present,
write,
user,
reserved,
instruction
code
);
println!("[#PF] frame={:#?}", frame);
panic!("page fault while executing in kernel context");
}

fn handle_general_protection(frame: &TrapFrame) {
println!("[#GP] error_code={:#x}", frame.error_code);
println!("[#GP] frame={:#?}", frame);
panic!("general protection fault");
panic!("general protection fault: code={:#x}", frame.error_code);
}

fn handle_invalid_opcode(frame: &TrapFrame) {
println!("[#UD] invalid opcode");
println!("[#UD] frame={:#?}", frame);
fn handle_invalid_opcode(frame: &mut TrapFrame) {
if emulate_syscall_from_ud(frame) {
return;
}
panic!("invalid opcode");
}

fn handle_double_fault(frame: &TrapFrame) {
println!("[#DF] double fault encountered");
println!("[#DF] frame={:#?}", frame);
panic!("double fault");
fn handle_double_fault(_frame: &TrapFrame) -> bool {
// Double faults are fatal; avoid logging/locking to reduce the chance of
// re-faulting and triggering a triple fault.
interrupts::disable();
loop {
x86_64::instructions::hlt();
}
}

/// Emulate `syscall` on #UD in user mode as a compatibility workaround.
///
/// # Implicit dependencies
/// - Relies on the current process address space being active and readable.
/// - Assumes the syscall ABI register layout matches the Linux `syscall` calling convention
/// (rax, rdi, rsi, rdx, r10, r8, r9).
/// - Assumes #UD is raised because `SYSCALL/SYSRET` MSRs are not wired yet.
fn emulate_syscall_from_ud(frame: &mut TrapFrame) -> bool {
let cpl = (frame.cs & 3) as u8;
if cpl == 0 {
return false;
}
let pid = match SCHEDULER.current_process_id() {
Some(pid) => pid,
None => return false,
};
let process = match PROCESS_TABLE.process_handle(pid) {
Ok(proc) => proc,
Err(_) => return false,
};
let rip = frame.rip as usize;
let mut bytes = [0u8; 2];
let mut ok = true;
process.address_space().with_page_table(|table, _| {
for (idx, out) in bytes.iter_mut().enumerate() {
let addr = match rip.checked_add(idx) {
Some(addr) => addr,
None => {
ok = false;
break;
}
};
let virt = VirtAddr::new(addr);
let phys = match table.translate(virt) {
Ok(phys) => phys,
Err(_) => {
ok = false;
break;
}
};
let mapper = manager::phys_mapper();
unsafe {
let ptr = mapper.phys_to_virt(phys).into_ptr();
*out = core::ptr::read(ptr);
}
}
});
if !ok || bytes != [0x0f, 0x05] {
return false;
}

let abi = syscall::current_abi();
let invocation = SyscallInvocation::new(
frame.regs.rax,
[
frame.regs.rdi,
frame.regs.rsi,
frame.regs.rdx,
frame.regs.r10,
frame.regs.r8,
frame.regs.r9,
],
);
match syscall::dispatch_with_frame(abi, &invocation, Some(frame)) {
syscall::DispatchResult::Completed(result) => {
frame.regs.rax = syscall::encode_result(abi, result);
frame.rip = frame.rip.wrapping_add(2);
}
syscall::DispatchResult::Terminate(_code) => {
SCHEDULER.terminate_current(frame);
}
}
true
}

#[cfg(test)]
fn maybe_check_user_pf_frame(frame: &TrapFrame, fault_addr: u64) {
if USER_PF_FRAME_CHECK_STATE.load(Ordering::SeqCst) != 1 {
return;
}

let expected_pid = USER_PF_FRAME_CHECK_PID.load(Ordering::SeqCst);
let expected_addr = USER_PF_FRAME_CHECK_ADDR.load(Ordering::SeqCst);
if SCHEDULER.current_process_id().unwrap_or(0) != expected_pid {
return;
}
if fault_addr != expected_addr {
return;
}

// Read the CPU-pushed exception frame starting at ORIGINAL_ERROR_OFFSET.
let base = frame as *const TrapFrame as *const u8;
let cpu_frame_ptr = unsafe { base.add(ORIGINAL_ERROR_OFFSET) as *const u64 };
let mut cpu = [0u64; 5];
for idx in 0..5 {
unsafe {
cpu[idx] = core::ptr::read_volatile(cpu_frame_ptr.add(idx));
}
}
let cpl = (cpu[1] & 3) as u8;
assert!(cpl != 0, "expected user-mode page fault");

assert_eq!(frame.error_code, 4, "pf error_code unexpected");
assert_eq!(cpu[0], frame.rip, "rip mismatch");
assert_eq!(cpu[1], frame.cs, "cs mismatch");
assert_eq!(cpu[2], frame.rflags, "rflags mismatch");
assert_eq!(cpu[3], frame.rsp, "rsp mismatch");
assert_eq!(cpu[4], frame.ss, "ss mismatch");

USER_PF_FRAME_CHECK_STATE.store(2, Ordering::SeqCst);
}

#[cfg(test)]
mod tests {
use super::*;
use crate::arch::x86_64::trap::context::GeneralRegisters;
use crate::println;
use crate::test::kernel_test_case;
use crate::trap::TrapOrigin;

Expand Down
2 changes: 2 additions & 0 deletions kernel/src/arch/x86_64/trap/idt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ fn build_idt() -> InterruptDescriptorTable {
idt[super::SYSCALL_VECTOR]
.set_handler_addr(VirtAddr::from_ptr(software_interrupt_syscall as *const ()))
.set_privilege_level(PrivilegeLevel::Ring3);
// External interrupt handlers must issue EOI to PIC/APIC in their
// dispatch path; the stubs themselves do not handle it.
for (offset, stub) in EXTERNAL_INTERRUPT_STUBS.iter().enumerate() {
let vector = crate::interrupt::DEVICE_VECTOR_BASE + offset as u8;
idt[vector].set_handler_addr(VirtAddr::from_ptr(*stub as *const ()));
Expand Down
Loading