⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/arch/x86_64/
idt.rs

1//! Interrupt Descriptor Table
2//!
3//! Sets up handlers for CPU exceptions (breakpoint, page fault, GPF,
4//! double fault) and hardware interrupts (timer). Fatal exception
5//! handlers log diagnostic information and halt the CPU instead of
6//! panicking, which avoids triggering a double fault from within an
7//! interrupt context.
8
9use lazy_static::lazy_static;
10use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame, PageFaultErrorCode};
11
12lazy_static! {
13    static ref IDT: InterruptDescriptorTable = {
14        let mut idt = InterruptDescriptorTable::new();
15        idt.breakpoint.set_handler_fn(breakpoint_handler);
16        idt.page_fault.set_handler_fn(page_fault_handler);
17        idt.general_protection_fault.set_handler_fn(general_protection_fault_handler);
18        // SAFETY: DOUBLE_FAULT_IST_INDEX is a valid IST index that was set up
19        // during GDT initialization. Using a dedicated interrupt stack prevents
20        // a triple fault when the kernel stack is corrupted.
21        unsafe {
22            idt.double_fault
23                .set_handler_fn(double_fault_handler)
24                .set_stack_index(crate::arch::x86_64::gdt::DOUBLE_FAULT_IST_INDEX);
25        }
26        // Add timer interrupt handler (IRQ0 = interrupt 32)
27        idt[32].set_handler_fn(timer_interrupt_handler);
28        // Add keyboard interrupt handler (IRQ1 = interrupt 33)
29        idt[33].set_handler_fn(keyboard_interrupt_handler);
30        // Add APIC timer interrupt handler (vector 48, separate from PIC timer at 32)
31        idt[48].set_handler_fn(apic_timer_interrupt_handler);
32        // Add TLB shootdown IPI handler (vector 49)
33        idt[49].set_handler_fn(tlb_shootdown_handler);
34        // Add scheduler wake IPI handler (vector 50)
35        idt[50].set_handler_fn(sched_wake_handler);
36        idt
37    };
38}
39
40pub fn init() {
41    IDT.load();
42}
43
44extern "x86-interrupt" fn breakpoint_handler(stack_frame: InterruptStackFrame) {
45    println!("EXCEPTION: BREAKPOINT\n{:#?}", stack_frame);
46}
47
48extern "x86-interrupt" fn double_fault_handler(
49    stack_frame: InterruptStackFrame,
50    _error_code: u64,
51) -> ! {
52    // Raw serial output ONLY -- println! uses spinlocks which deadlock in
53    // interrupt context, causing re-entrant DF cascades.
54    // SAFETY: Writing to COM1 data register at I/O port 0x3F8 is safe
55    // for diagnostics.
56    unsafe {
57        raw_serial_str(b"FATAL:DF rip=0x");
58        // Print the instruction pointer from the exception frame
59        let rip = stack_frame.instruction_pointer.as_u64();
60        raw_serial_hex(rip);
61        raw_serial_str(b" rsp=0x");
62        let rsp = stack_frame.stack_pointer.as_u64();
63        raw_serial_hex(rsp);
64        // Read CR2 (faulting address from the page fault that triggered DF)
65        let cr2: u64;
66        core::arch::asm!("mov {}, cr2", out(reg) cr2, options(nomem, nostack));
67        raw_serial_str(b" cr2=0x");
68        raw_serial_hex(cr2);
69        // Read CS from the stack frame to determine privilege level
70        let cs = stack_frame.code_segment.0;
71        raw_serial_str(b" cs=0x");
72        raw_serial_hex(cs as u64);
73        raw_serial_str(b"\n");
74    }
75
76    loop {
77        x86_64::instructions::hlt();
78    }
79}
80
81extern "x86-interrupt" fn page_fault_handler(
82    stack_frame: InterruptStackFrame,
83    error_code: PageFaultErrorCode,
84) {
85    crate::perf::count_page_fault();
86    crate::trace!(
87        crate::perf::trace::TraceEventType::PageFault,
88        0, // CR2 not yet read; filled in after read below
89        error_code.bits()
90    );
91
92    // SAFETY: Read CR2 (faulting address) before any code that might trigger
93    // another page fault, which would overwrite CR2.
94    let cr2_val: u64 = unsafe {
95        let val: u64;
96        core::arch::asm!("mov {}, cr2", out(reg) val, options(nomem, nostack));
97        val
98    };
99
100    let ec = error_code.bits();
101    let rip_val = stack_frame.instruction_pointer.as_u64();
102    let was_user = ec & 4 != 0; // U/S bit
103
104    // Early diagnostic: raw serial before ANY other work to catch cascading faults
105    // SAFETY: Port I/O writes to COM1 (0x3F8) for diagnostic serial output.
106    unsafe {
107        raw_serial_str(b"PF! cr2=0x");
108        raw_serial_hex(cr2_val);
109        raw_serial_str(b" ec=0x");
110        raw_serial_hex(ec);
111        raw_serial_str(b" rip=0x");
112        raw_serial_hex(rip_val);
113        // Print current PID to identify which process faulted
114        let pf_pid = crate::process::current_process()
115            .map(|p| p.pid.0)
116            .unwrap_or(0xDEAD);
117        raw_serial_str(b" pid=0x");
118        raw_serial_hex(pf_pid);
119        raw_serial_str(b"\n");
120    }
121
122    // Attempt to resolve via demand paging framework.
123    // Skip demand paging for NULL dereferences (addr < PAGE_SIZE) since no
124    // valid mapping can exist there, and the demand paging code may GP fault
125    // while iterating the VAS mappings from interrupt context.
126    if cr2_val >= 0x1000 {
127        let info = crate::mm::page_fault::from_x86_64(ec, cr2_val, rip_val);
128        if let Ok(()) = crate::mm::page_fault::handle_page_fault(info) {
129            // Fault resolved (demand page, CoW, or stack growth) — resume.
130            return;
131        }
132    }
133
134    // Unresolvable fault — print diagnostics via raw serial, then halt or
135    // kill the process.
136    // SAFETY: Writing to COM1 data register at I/O port 0x3F8 for diagnostics.
137    unsafe {
138        raw_serial_str(b"PF@0x");
139        raw_serial_hex(cr2_val);
140        raw_serial_str(b" ec=0x");
141        raw_serial_hex(ec);
142        raw_serial_str(b" rip=0x");
143        raw_serial_hex(rip_val);
144        raw_serial_str(b"\n");
145    }
146
147    if was_user {
148        // User-mode fault: unresolvable. Kill the process directly.
149        // Cannot call sys_exit() from interrupt context (it uses println!
150        // and locks which risk deadlock). Instead, mark the process as
151        // Zombie and call boot_return_to_kernel directly.
152        // SAFETY: Port I/O writes to COM1 (0x3F8) for diagnostic serial output.
153        unsafe {
154            raw_serial_str(b"SEGFAULT pid=0x");
155            raw_serial_hex(
156                crate::process::current_process()
157                    .map(|p| p.pid.0)
158                    .unwrap_or(0xDEAD),
159            );
160            raw_serial_str(b" addr=0x");
161            raw_serial_hex(cr2_val);
162            raw_serial_str(b" rip=0x");
163            raw_serial_hex(rip_val);
164            raw_serial_str(b"\n");
165        }
166
167        // Dump user stack to identify the call chain at crash time.
168        // Since we don't switch CR3, user pages are mapped.
169        // SAFETY: User stack is mapped (no CR3 switch). RSP is bounds-checked
170        // before dereferencing. Port I/O to COM1 for serial output.
171        unsafe {
172            let user_rsp = stack_frame.stack_pointer.as_u64();
173            raw_serial_str(b"  RSP=0x");
174            raw_serial_hex(user_rsp);
175            raw_serial_str(b"\n");
176            // Dump first 12 qwords from the user stack
177            if user_rsp > 0x1000 && user_rsp < 0x0000_8000_0000_0000 {
178                for i in 0u64..12 {
179                    let addr = user_rsp + i * 8;
180                    let val = *(addr as *const u64);
181                    raw_serial_str(b"  [RSP+0x");
182                    raw_serial_hex(i * 8);
183                    raw_serial_str(b"]=0x");
184                    raw_serial_hex(val);
185                    raw_serial_str(b"\n");
186                }
187            }
188        }
189
190        // Mark process as Zombie before returning to boot context.
191        // Only use atomic state operations (set_exit_code, set_state).
192        // Do NOT iterate threads BTreeMap or look up parent via
193        // get_process() — those BTreeMap operations GP fault from
194        // interrupt context on the TSS stack.
195        if let Some(process) = crate::process::current_process() {
196            process.set_exit_code(128 + 11); // SIGSEGV
197            process.set_state(crate::process::pcb::ProcessState::Zombie);
198        }
199
200        // Return to boot context.
201        // The page fault handler runs in interrupt context (no swapgs on
202        // entry). boot_return_to_kernel expects the swapgs state from
203        // syscall_entry. Do swapgs first to balance boot_return's swapgs.
204        if crate::arch::x86_64::usermode::has_boot_return_context() {
205            // SAFETY: swapgs balances the GS base for boot_return_to_kernel.
206            // boot_return context was verified by has_boot_return_context().
207            unsafe {
208                raw_serial_str(b"[PF_KILL] boot_return\n");
209                core::arch::asm!("swapgs", options(nomem, nostack));
210                crate::arch::x86_64::usermode::boot_return_to_kernel();
211            }
212        }
213        // No boot context — halt.
214        loop {
215            x86_64::instructions::hlt();
216        }
217    } else {
218        // Kernel fault — unrecoverable. Print and halt.
219        println!(
220            "FATAL: kernel page fault at {:#x} ec={:#x} rip={:#x}",
221            cr2_val, ec, rip_val
222        );
223        println!("{:#?}", stack_frame);
224        loop {
225            x86_64::instructions::hlt();
226        }
227    }
228}
229
230extern "x86-interrupt" fn general_protection_fault_handler(
231    stack_frame: InterruptStackFrame,
232    error_code: u64,
233) {
234    // All output via raw serial to avoid spinlock deadlocks.
235    // SAFETY: Writing to COM1 data register at I/O port 0x3F8 is safe for
236    // diagnostics. We bypass the serial spinlock because we may have
237    // interrupted code that holds it.
238    unsafe {
239        raw_serial_str(b"FATAL:GP err=0x");
240        raw_serial_hex(error_code);
241        raw_serial_str(b"\n");
242
243        // Read the saved interrupt frame directly from the stack.
244        // The x86_64 CPU pushes [SS, RSP, RFLAGS, CS, RIP] and the error
245        // code. The x86-interrupt calling convention passes us a reference
246        // to the saved frame. We cast through the InterruptStackFrame
247        // (which is a repr(C) wrapper around the saved values) to get at
248        // the raw u64 fields. The frame layout (from low address):
249        //   [0]: RIP, [1]: CS, [2]: RFLAGS, [3]: RSP, [4]: SS
250        let frame_base = &stack_frame as *const _ as *const u64;
251        raw_serial_str(b"RIP=0x");
252        raw_serial_hex(core::ptr::read_volatile(frame_base));
253        raw_serial_str(b" CS=0x");
254        raw_serial_hex(core::ptr::read_volatile(frame_base.add(1)));
255        raw_serial_str(b"\n");
256        raw_serial_str(b"RFLAGS=0x");
257        raw_serial_hex(core::ptr::read_volatile(frame_base.add(2)));
258        raw_serial_str(b"\nRSP=0x");
259        raw_serial_hex(core::ptr::read_volatile(frame_base.add(3)));
260        raw_serial_str(b" SS=0x");
261        raw_serial_hex(core::ptr::read_volatile(frame_base.add(4)));
262        raw_serial_str(b"\n");
263    }
264
265    loop {
266        x86_64::instructions::hlt();
267    }
268}
269
270/// Write a byte string to COM1 serial, bypassing all locks.
271///
272/// # Safety
273/// Port 0x3F8 must be a valid COM1 data register.
274pub(crate) unsafe fn raw_serial_str(s: &[u8]) {
275    for &b in s {
276        core::arch::asm!("out dx, al", in("dx") 0x3F8u16, in("al") b, options(nomem, nostack));
277    }
278}
279
280/// Write a u64 as hex to COM1 serial, bypassing all locks.
281///
282/// # Safety
283/// Port 0x3F8 must be a valid COM1 data register.
284pub(crate) unsafe fn raw_serial_hex(val: u64) {
285    const HEX: &[u8; 16] = b"0123456789abcdef";
286    // Print 16 hex digits (skip leading zeros after first nonzero)
287    let mut started = false;
288    for i in (0..16).rev() {
289        let nibble = ((val >> (i * 4)) & 0xF) as usize;
290        if nibble != 0 || started || i == 0 {
291            started = true;
292            let b = HEX[nibble];
293            core::arch::asm!("out dx, al", in("dx") 0x3F8u16, in("al") b, options(nomem, nostack));
294        }
295    }
296}
297
298extern "x86-interrupt" fn timer_interrupt_handler(_stack_frame: InterruptStackFrame) {
299    crate::perf::count_interrupt();
300
301    // Notify the scheduler of a timer tick for preemptive scheduling.
302    // Use try_lock to avoid deadlock: if the scheduler lock is already held
303    // (e.g., we interrupted mid-schedule), skip the tick — the holder will
304    // complete its scheduling decision and release the lock.
305    if let Some(mut sched) = crate::sched::scheduler::current_scheduler().try_lock() {
306        sched.tick();
307    }
308
309    // SAFETY: Writing the EOI (End of Interrupt) byte (0x20) to the master
310    // PIC command port (0x20) is required to acknowledge the timer interrupt.
311    // Failing to send EOI would mask all further IRQs at this priority level.
312    unsafe {
313        use x86_64::instructions::port::Port;
314        let mut pic_command: Port<u8> = Port::new(0x20);
315        pic_command.write(0x20); // EOI command
316    }
317}
318
319extern "x86-interrupt" fn apic_timer_interrupt_handler(_stack_frame: InterruptStackFrame) {
320    crate::perf::count_interrupt();
321
322    // Increment the global tick counter (atomic, always safe from interrupt
323    // context).
324    super::timer::tick();
325
326    // Send APIC End-Of-Interrupt (NOT PIC EOI -- APIC timer uses its own EOI path).
327    crate::arch::x86_64::apic::send_eoi();
328}
329
330extern "x86-interrupt" fn tlb_shootdown_handler(_stack_frame: InterruptStackFrame) {
331    // Flush the local CPU's TLB in response to a remote CPU modifying shared
332    // page tables. On single-CPU systems this handler is never invoked, but
333    // it is registered for correctness when SMP is enabled.
334    crate::arch::x86_64::tlb_flush_all();
335    crate::arch::x86_64::apic::send_eoi();
336}
337
338extern "x86-interrupt" fn sched_wake_handler(_stack_frame: InterruptStackFrame) {
339    // Wake handler: a remote CPU placed a task on our run queue and sent this
340    // IPI to break us out of HLT. No action needed beyond EOI -- the scheduler
341    // will pick up the new task on the next scheduling decision.
342    crate::arch::x86_64::apic::send_eoi();
343}
344
345extern "x86-interrupt" fn keyboard_interrupt_handler(_stack_frame: InterruptStackFrame) {
346    crate::perf::count_interrupt();
347
348    // Read scancode from PS/2 data port (0x60) and forward to keyboard driver.
349    // This handler must NOT call println! or acquire any spinlock used by
350    // the serial/fbcon output path.
351    // SAFETY: Port 0x60 is the PS/2 keyboard data port. Reading it clears
352    // the keyboard controller's output buffer.
353    let scancode: u8 = unsafe {
354        use x86_64::instructions::port::Port;
355        Port::<u8>::new(0x60).read()
356    };
357    crate::drivers::keyboard::handle_scancode(scancode);
358    // SAFETY: EOI to PIC1 (port 0x20) acknowledges the keyboard interrupt.
359    unsafe {
360        use x86_64::instructions::port::Port;
361        Port::<u8>::new(0x20).write(0x20);
362    }
363}