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}