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/gop.rs | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 4 deletions(-) (limited to 'rsnix/src/gop.rs') diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index dad4505..47a2208 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs @@ -19,12 +19,49 @@ pub struct Framebuffer { height: usize, pitch: usize, // bytes per scan line bytes_per_pixel: usize, + /// Size of the firmware's linear framebuffer mapping, in bytes. + /// Everything written to the display is clamped to this, so a mode + /// whose `pitch * height` exceeds the actual mapping (some real + /// firmware pads the scan-line pitch but sizes the buffer for the + /// resolution) degrades to a truncated view instead of faulting. + framebuffer_size: usize, bg: u32, // packed BGRA or RGBA background colour bgr: bool, } static mut FB: Option = None; +/// The GOP framebuffer region (base + mapping size), stashed before the +/// memory map is captured so the heap and page allocator can exclude it. +/// Some real-world firmware lists the framebuffer's memory as +/// `EfiConventionalMemory`; handing those pages out would let kernel +/// allocations (stacks, DMA buffers) overwrite the display. +static mut FB_REGION_BASE: usize = 0; +static mut FB_REGION_SIZE: usize = 0; + +/// Stash the current GOP framebuffer region *before* the memory map is +/// captured (must run before `memory::capture_memory_map`). Updated +/// again after `SetMode` in `init` — the base can move. +pub fn peek_fb_region(gop: &mut GraphicsOutputProtocol) { + unsafe { + let mode = &*gop.mode; + FB_REGION_BASE = mode.framebuffer_base as usize; + FB_REGION_SIZE = mode.framebuffer_size; + } +} + +/// The framebuffer region currently excluded from the allocators +/// (base, size in bytes), if any. +pub fn framebuffer_region() -> Option<(usize, usize)> { + unsafe { + if FB_REGION_SIZE == 0 { + None + } else { + Some((FB_REGION_BASE, FB_REGION_SIZE)) + } + } +} + /// Software backbuffer: a RAM copy of the whole framebuffer (same pitch /// layout, u32 pixels). /// @@ -147,6 +184,7 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { height, pitch, bytes_per_pixel: bpp, + framebuffer_size: mode.framebuffer_size, bg, bgr, }; @@ -162,6 +200,23 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { } } + // Re-stash the post-SetMode region (base can move) for the page + // allocator, and log the authoritative geometry for diagnostics. + unsafe { + FB_REGION_BASE = mode.framebuffer_base as usize; + FB_REGION_SIZE = mode.framebuffer_size; + } + crate::serial_println!( + "gop: fb base={:#x} size={:#x} pitch={:#x} {}x{} bpp={} bgr={}", + base as usize, + { mode.framebuffer_size }, + pitch, + width, + height, + bpp, + bgr + ); + fb.clear(); unsafe { @@ -231,10 +286,17 @@ pub(crate) fn push_rect(x: usize, y: usize, w: usize, h: usize) { let w = w.min(fb.width - x); let h = h.min(fb.height - y); let row_words = fb.pitch / 4; + // Words of the firmware's actual display mapping — per-row clamp so a + // pitch-padded mode never writes past the buffer. + let fb_words = fb.framebuffer_size / 4; unsafe { for dy in 0..h { let off = (y + dy) * row_words + x; - ptr::copy_nonoverlapping(SHADOW.add(off), fb.base.add(off * 4) as *mut u32, w); + let row_w = w.min(fb_words.saturating_sub(off)); + if row_w == 0 { + continue; + } + ptr::copy_nonoverlapping(SHADOW.add(off), fb.base.add(off * 4) as *mut u32, row_w); } } overlay_cursor(x, y, w, h); @@ -275,6 +337,39 @@ pub fn fill_rect(x: usize, y: usize, w: usize, h: usize, color: u32) { push_rect(x, y, w, h); } +/// Print crash/fault lines directly to the framebuffer, bypassing the vt +/// entirely (the vt state may be corrupt when an exception fires). +/// +/// White-on-red band at the top of the screen, written through the same +/// clamped glyph path — safe even if the fault was inside the vt/gop +/// machinery, and visible on real hardware with no serial console. +pub fn dump_lines(lines: &[&str]) { + let Some(fb) = fb().as_ref() else { + return; + }; + let cols = fb.width / 8; + let rows = fb.height / 16; + let red = (0xC0u8, 0x00u8, 0x00u8); + let white = (0xFFu8, 0xFFu8, 0xFFu8); + let band = lines.len().min(rows); + // Red band behind the text. + if cols > 0 && band > 0 { + fill_rect(0, 0, cols * 8, band * 16, rgb(red.0, red.1, red.2)); + } + for (i, line) in lines.iter().enumerate().take(band) { + let mut c = 0; + for &b in line.as_bytes() { + if c >= cols { + break; + } + if b < 0x80 { + blit_glyph(c, i, b as u16, white, red); + c += 1; + } + } + } +} + /// Read one pixel back from the shadow (RAM; cheap). Test/demo use only. pub fn shadow_pixel(x: usize, y: usize) -> Option { let fb = fb().as_ref()?; @@ -433,7 +528,10 @@ fn draw_bmp(x: usize, y: usize, color: u32) { } if row & (1u16 << (11 - dx)) != 0 { let off = yy * row_words + xx; - ptr::write_volatile(fb.base.add(off * 4) as *mut u32, color); + // Clamp to the display mapping (defensive). + if off < fb.framebuffer_size / 4 { + ptr::write_volatile(fb.base.add(off * 4) as *mut u32, color); + } } } } @@ -453,7 +551,10 @@ pub fn rgb(r: u8, g: u8, b: u8) -> u32 { // --------------------------------------------------------------------------- impl Framebuffer { fn clear(&self) { - let size = self.pitch * self.height; + // Clamp the display write to the firmware's actual mapping — some + // real GPUs size the buffer for the resolution while the pitch + // (with scan-line padding) is larger. + let size = (self.pitch * self.height).min(self.framebuffer_size); // Fill with background colour (32-bit writes), display + shadow. let mut p = self.base; let end = unsafe { p.add(size) }; @@ -511,7 +612,12 @@ impl Framebuffer { // A bulk copy (rep movsb) writes in 64-byte bursts that // coalesce well on WC framebuffers — far fewer bus // transactions than per-pixel stores. - ptr::copy_nonoverlapping(SHADOW, self.base as *mut u32, words); + // + // Clamp to the firmware's mapping size; a pitch-padded mode + // whose `pitch * height` exceeds the buffer degrades to a + // truncated view instead of faulting past the mapping. + let push_words = words.min(self.framebuffer_size / 4); + ptr::copy_nonoverlapping(SHADOW, self.base as *mut u32, push_words); // The scroll copied the shadow (which never contains the // cursor); re-composite it on top of the pushed frame. crate::gop::cursor_redraw(); -- cgit v1.2.3