From ab7d87bcde31c1681ea09c30534dde840a58d6fa Mon Sep 17 00:00:00 2001 From: salmonik Date: Sat, 1 Aug 2026 22:28:34 +0200 Subject: rsnix v0.5.2: fault diagnostics + real-hardware scroll-crash hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real-hardware scroll crash (white bands + instant freeze, QEMU-clean) was invisible because the only trace was a single serial line on machines with no serial console. This makes every crash self-identifying and hardens the real-hardware failure surfaces: - Every CPU exception vector 0-31 now has an IDT entry (macro-generated stubs; 8/14 keep their IST stacks). A fault in the shell prints a white-on-red band to the framebuffer via gop::dump_lines (raw fb writes bypassing the vt, fully clamped) plus a parseable serial line: '!!! vec=N err=… rip=… cr2=… rsp=… rflags=… task=N "name"', then halts — instead of converging to a cryptic double fault. Background-task faults abort the task as before. - Boot log now reports the authoritative post-SetMode geometry: 'gop: fb base=… size=… pitch=… WxH bpp=… bgr=…', so the next real machine's report identifies whether pitch*height exceeds the mapping. - Framebuffer writes (clear, push_rect, shift_up_n bulk push, cursor overlay) are clamped to the firmware's framebuffer_size: a pitch- padded mode degrades to a truncated view instead of faulting past the mapping. - The GOP framebuffer region is excluded from the page allocator (and subtracted from the heap's conventional-region pick) even when the firmware lists it as ConventionalMemory — kernel allocations can never overwrite the display again. The GOP protocol is located and peeked before the memory map is captured so the exclusion is known at heap-init time. - Shell gains a hidden 'fault' command (deliberate unmapped write) and 'make test-fault' verifies the dump: vec=14, cr2=0x400000000000, task=0, halt — no triple fault. All other suites re-pass. - CONTEXT.md: fault-dump format documented for real-hardware reports. --- rsnix/src/interrupts.rs | 146 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 2 deletions(-) (limited to 'rsnix/src/interrupts.rs') diff --git a/rsnix/src/interrupts.rs b/rsnix/src/interrupts.rs index 7ea0b07..be252df 100644 --- a/rsnix/src/interrupts.rs +++ b/rsnix/src/interrupts.rs @@ -269,6 +269,56 @@ core::arch::global_asm!( " push 0xFF", " jmp isr_common", "", + // Exception stubs for the remaining vectors 0-31 so every fault gets + // a diagnosable dump instead of converging to a cryptic double fault. + // Vectors that push their own error code (10, 11, 12, 13, 17, 18 — 8 + // and 14 have dedicated stubs above) skip the fake push-0. + ".macro ISR_NOERR vec", + " .globl isr_stub_x_\\vec", + " .p2align 4", + "isr_stub_x_\\vec:", + " push 0", + " push \\vec", + " jmp isr_common", + ".endm", + ".macro ISR_ERR vec", + " .globl isr_stub_x_\\vec", + " .p2align 4", + "isr_stub_x_\\vec:", + " push \\vec", + " jmp isr_common", + ".endm", + "ISR_NOERR 0", + "ISR_NOERR 1", + "ISR_NOERR 2", + "ISR_NOERR 3", + "ISR_NOERR 4", + "ISR_NOERR 5", + "ISR_NOERR 6", + "ISR_NOERR 7", + "ISR_NOERR 9", + "ISR_ERR 10", + "ISR_ERR 11", + "ISR_ERR 12", + "ISR_ERR 13", + "ISR_NOERR 15", + "ISR_NOERR 16", + "ISR_ERR 17", + "ISR_ERR 18", + "ISR_NOERR 19", + "ISR_NOERR 20", + "ISR_NOERR 21", + "ISR_NOERR 22", + "ISR_NOERR 23", + "ISR_NOERR 24", + "ISR_NOERR 25", + "ISR_NOERR 26", + "ISR_NOERR 27", + "ISR_NOERR 28", + "ISR_NOERR 29", + "ISR_NOERR 30", + "ISR_NOERR 31", + "", // Common handler — save all registers, call into Rust (which returns // the next task's frame pointer in rax), switch rsp, restore, iretq. "isr_common:", @@ -381,8 +431,9 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize { fp } 8 => { - crate::serial_println!("double fault — halting"); - halt() + // Double fault — dump the frame (we're on the IST stack + // so this is safe) and halt. + crate::sched::fault_dump("double fault", 0, frame) } 14 => { let cr2: u64; @@ -391,6 +442,11 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize { crate::sched::page_fault(cr2, fp) } 0x60 => crate::sched::on_yield(fp), + v if v < 32 => { + // Any other CPU exception: full dump (screen + serial), + // then halt the shell or abort the faulting background task. + crate::sched::fault_dump("exception", 0, frame) + } _ => fp, } }; @@ -418,6 +474,47 @@ pub fn read_mouse_byte() -> Option { /// sub-tick fraction. pub static TICKS: AtomicU64 = AtomicU64::new(0); +/// Address of the stub for CPU exception vector `vec` (0-31), or 0 for +/// vectors without a stub. 8 (double fault) and 14 (page fault) use +/// dedicated stubs that run on IST stacks. +fn exc_stub_addr(vec: u8) -> u64 { + match vec { + 8 => isr_stub_08 as *const () as u64, + 14 => isr_stub_14 as *const () as u64, + 0 => isr_stub_x_0 as *const () as u64, + 1 => isr_stub_x_1 as *const () as u64, + 2 => isr_stub_x_2 as *const () as u64, + 3 => isr_stub_x_3 as *const () as u64, + 4 => isr_stub_x_4 as *const () as u64, + 5 => isr_stub_x_5 as *const () as u64, + 6 => isr_stub_x_6 as *const () as u64, + 7 => isr_stub_x_7 as *const () as u64, + 9 => isr_stub_x_9 as *const () as u64, + 10 => isr_stub_x_10 as *const () as u64, + 11 => isr_stub_x_11 as *const () as u64, + 12 => isr_stub_x_12 as *const () as u64, + 13 => isr_stub_x_13 as *const () as u64, + 15 => isr_stub_x_15 as *const () as u64, + 16 => isr_stub_x_16 as *const () as u64, + 17 => isr_stub_x_17 as *const () as u64, + 18 => isr_stub_x_18 as *const () as u64, + 19 => isr_stub_x_19 as *const () as u64, + 20 => isr_stub_x_20 as *const () as u64, + 21 => isr_stub_x_21 as *const () as u64, + 22 => isr_stub_x_22 as *const () as u64, + 23 => isr_stub_x_23 as *const () as u64, + 24 => isr_stub_x_24 as *const () as u64, + 25 => isr_stub_x_25 as *const () as u64, + 26 => isr_stub_x_26 as *const () as u64, + 27 => isr_stub_x_27 as *const () as u64, + 28 => isr_stub_x_28 as *const () as u64, + 29 => isr_stub_x_29 as *const () as u64, + 30 => isr_stub_x_30 as *const () as u64, + 31 => isr_stub_x_31 as *const () as u64, + _ => 0, + } +} + /// Initialise the IDT, remap the PIC, set up the PIT, and enable interrupts. pub fn init() { unsafe { @@ -435,6 +532,20 @@ pub fn init() { idt_set_entry(idt_ptr, vec, spurious, cs, 0x0Eu8, 0); } + // Wire up every CPU exception vector (0-31) so any fault produces + // a diagnosable dump instead of converging to a double fault. + for vec in 0..=31u8 { + let a = exc_stub_addr(vec); + if a != 0 { + let ist = match vec { + 8 => 2u8, + 14 => 1u8, + _ => 0u8, + }; + idt_set_entry(idt_ptr, vec, a, cs, 0x0Eu8, ist); + } + } + // Override vectors for interrupt we actually handle. idt_set_entry(idt_ptr, 32, isr_stub_32 as *const () as u64, cs, 0x0Eu8, 0); idt_set_entry(idt_ptr, 33, isr_stub_33 as *const () as u64, cs, 0x0Eu8, 0); @@ -513,4 +624,35 @@ unsafe extern "C" { fn isr_stub_44(); fn isr_stub_60(); fn isr_stub_spurious(); + // Macro-generated exception stubs (isr_stub_x_). + fn isr_stub_x_0(); + fn isr_stub_x_1(); + fn isr_stub_x_2(); + fn isr_stub_x_3(); + fn isr_stub_x_4(); + fn isr_stub_x_5(); + fn isr_stub_x_6(); + fn isr_stub_x_7(); + fn isr_stub_x_9(); + fn isr_stub_x_10(); + fn isr_stub_x_11(); + fn isr_stub_x_12(); + fn isr_stub_x_13(); + fn isr_stub_x_15(); + fn isr_stub_x_16(); + fn isr_stub_x_17(); + fn isr_stub_x_18(); + fn isr_stub_x_19(); + fn isr_stub_x_20(); + fn isr_stub_x_21(); + fn isr_stub_x_22(); + fn isr_stub_x_23(); + fn isr_stub_x_24(); + fn isr_stub_x_25(); + fn isr_stub_x_26(); + fn isr_stub_x_27(); + fn isr_stub_x_28(); + fn isr_stub_x_29(); + fn isr_stub_x_30(); + fn isr_stub_x_31(); } -- cgit v1.2.3