diff options
Diffstat (limited to '')
| -rw-r--r-- | CONTEXT.md | 23 | ||||
| -rw-r--r-- | Cargo.lock | 6 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | Makefile | 7 | ||||
| -rw-r--r-- | rsdos/src/main.rs | 28 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 114 | ||||
| -rw-r--r-- | rsnix/src/interrupts.rs | 146 | ||||
| -rw-r--r-- | rsnix/src/memory.rs | 35 | ||||
| -rw-r--r-- | rsnix/src/sched.rs | 59 | ||||
| -rwxr-xr-x | scripts/test-fault.sh | 90 | ||||
| -rw-r--r-- | shell/src/lib.rs | 13 |
11 files changed, 489 insertions, 34 deletions
| @@ -113,10 +113,13 @@ Workspace version is `a.b.c` (single version for all crates): | |||
| 113 | 113 | ||
| 114 | Bump the workspace version in the root `Cargo.toml` when a change of | 114 | Bump the workspace version in the root `Cargo.toml` when a change of |
| 115 | that class lands — do not leave the version to drift. Current: | 115 | that class lands — do not leave the version to drift. Current: |
| 116 | `0.5.1` — the GUI era landed with 0.5.0 (graphics layer + cursor | 116 | `0.5.2` — the GUI era landed with 0.5.0 (graphics layer + cursor |
| 117 | compositor) and the canvas was fixed at 800×600 as a `c` bump; a | 117 | compositor); 0.5.1 fixed the canvas at 800×600; 0.5.2 added fault |
| 118 | future major GUI overhaul (graphics windows beyond the text surface) | 118 | diagnostics (screen + serial dumps for every CPU exception) and |
| 119 | would be the `a` bump (1.0.0). | 119 | real-hardware hardening (framebuffer-size clamping, fb-region exclusion |
| 120 | from the allocators, authoritative fb geometry logging). A future | ||
| 121 | major GUI overhaul (graphics windows beyond the text surface) would be | ||
| 122 | the `a` bump (1.0.0). | ||
| 120 | 123 | ||
| 121 | ## Project layout | 124 | ## Project layout |
| 122 | 125 | ||
| @@ -388,6 +391,18 @@ Workspace of four crates, all built for the `x86_64-unknown-uefi` target: | |||
| 388 | hardware pushes only 24 bytes at same privilege, so the extra | 391 | hardware pushes only 24 bytes at same privilege, so the extra |
| 389 | fields are inert there; the synthetic frames set them | 392 | fields are inert there; the synthetic frames set them |
| 390 | explicitly either way. | 393 | explicitly either way. |
| 394 | - `interrupts`/`sched` fault dumps (0.5.2): every CPU exception | ||
| 395 | vector 0-31 has an IDT entry (macro-generated stubs; 8/14 keep | ||
| 396 | their ISTs). Any fault in the shell (task ≤ 1) prints a | ||
| 397 | white-on-red band via `gop::dump_lines` (raw fb, bypasses vt, | ||
| 398 | clamped) *and* a parseable serial line — `!!! <kind> vec=N | ||
| 399 | err=… rip=… cr2=… rsp=… rflags=… task=N "name"` — then halts; | ||
| 400 | faults in background tasks abort the task instead. Real | ||
| 401 | hardware has no serial console, so the screen band is how a | ||
| 402 | crash reports itself; a non-canonical write shows vec=13 (GP, | ||
| 403 | cr2=0), an unmapped canonical address shows vec=14 with cr2= | ||
| 404 | the address. `fault` shell command + `make test-fault` verify | ||
| 405 | the path. | ||
| 391 | - Panic handling: a task panic (or guard-page overflow) is caught, | 406 | - Panic handling: a task panic (or guard-page overflow) is caught, |
| 392 | logged as `sched: task N "name" panicked: …`, the task is marked | 407 | logged as `sched: task N "name" panicked: …`, the task is marked |
| 393 | Done, and the scheduler switches away — the shell keeps running. | 408 | Done, and the scheduler switches away — the shell keeps running. |
| @@ -4,7 +4,7 @@ version = 4 | |||
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "rsdos" | 6 | name = "rsdos" |
| 7 | version = "0.5.1" | 7 | version = "0.5.2" |
| 8 | dependencies = [ | 8 | dependencies = [ |
| 9 | "rsnix", | 9 | "rsnix", |
| 10 | "shell", | 10 | "shell", |
| @@ -19,11 +19,11 @@ dependencies = [ | |||
| 19 | 19 | ||
| 20 | [[package]] | 20 | [[package]] |
| 21 | name = "rsnix" | 21 | name = "rsnix" |
| 22 | version = "0.5.1" | 22 | version = "0.5.2" |
| 23 | 23 | ||
| 24 | [[package]] | 24 | [[package]] |
| 25 | name = "shell" | 25 | name = "shell" |
| 26 | version = "0.5.1" | 26 | version = "0.5.2" |
| 27 | dependencies = [ | 27 | dependencies = [ |
| 28 | "rsdosedit", | 28 | "rsdosedit", |
| 29 | "rsnix", | 29 | "rsnix", |
| @@ -3,7 +3,7 @@ resolver = "2" | |||
| 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] | 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] |
| 4 | 4 | ||
| 5 | [workspace.package] | 5 | [workspace.package] |
| 6 | version = "0.5.1" | 6 | version = "0.5.2" |
| 7 | edition = "2024" | 7 | edition = "2024" |
| 8 | 8 | ||
| 9 | # no_std UEFI kernel: no unwinder available, so panic must abort. | 9 | # no_std UEFI kernel: no unwinder available, so panic must abort. |
| @@ -14,7 +14,7 @@ export BOOT_EFI=$(ESP_DIR)/EFI/BOOT/BOOTX64.EFI | |||
| 14 | # Create one with: make disk (or: ./scripts/mkdisk.sh) | 14 | # Create one with: make disk (or: ./scripts/mkdisk.sh) |
| 15 | export DISK_IMG ?= /tmp/disk.img | 15 | export DISK_IMG ?= /tmp/disk.img |
| 16 | 16 | ||
| 17 | .PHONY: all run run-gui clean image debug release fmt bootloader boot-image disk disk-fresh inspect test-disk test-mouse test-gfx test-msg | 17 | .PHONY: all run run-gui clean image debug release fmt bootloader boot-image disk disk-fresh inspect test-disk test-mouse test-gfx test-fault test-msg |
| 18 | 18 | ||
| 19 | # ---- Build --------------------------------------------------------------- | 19 | # ---- Build --------------------------------------------------------------- |
| 20 | 20 | ||
| @@ -121,6 +121,11 @@ test-mouse: | |||
| 121 | test-gfx: | 121 | test-gfx: |
| 122 | ./scripts/test-gfx.sh | 122 | ./scripts/test-gfx.sh |
| 123 | 123 | ||
| 124 | # Fault-diagnostics round trip: boot -> `fault` (deliberate page fault) -> | ||
| 125 | # verify the full dump (vec/err/rip/cr2/task) hits serial + screen -> halt. | ||
| 126 | test-fault: | ||
| 127 | ./scripts/test-fault.sh | ||
| 128 | |||
| 124 | # Automated scheduler message-queue round trip: msgrecv/send, real-input | 129 | # Automated scheduler message-queue round trip: msgrecv/send, real-input |
| 125 | # routing into a task's queue, queue-full boundary. | 130 | # routing into a task's queue, queue-full boundary. |
| 126 | test-msg: | 131 | test-msg: |
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index 13923ea..6dbad5f 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -87,16 +87,10 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 87 | rsnix::serial::init(); | 87 | rsnix::serial::init(); |
| 88 | rsnix::serial_println!("=== rsnix boot ==="); | 88 | rsnix::serial_println!("=== rsnix boot ==="); |
| 89 | 89 | ||
| 90 | // Stash the memory map | 90 | // Locate GOP (Graphics Output Protocol) *before* the memory map is |
| 91 | let map_key = match rsnix::memory::capture_memory_map(bs) { | 91 | // captured: the heap/page allocators need to know the framebuffer |
| 92 | Ok(k) => k, | 92 | // region so they never hand out its pages (some real firmware lists |
| 93 | Err(e) => { | 93 | // it as conventional memory). |
| 94 | rsnix::serial_println!("mem: capture_memory_map failed 0x{:x}", e); | ||
| 95 | 0 | ||
| 96 | } | ||
| 97 | }; | ||
| 98 | |||
| 99 | // Locate GOP (Graphics Output Protocol) | ||
| 100 | let mut gop_interface: *mut core::ffi::c_void = core::ptr::null_mut(); | 94 | let mut gop_interface: *mut core::ffi::c_void = core::ptr::null_mut(); |
| 101 | let gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID; | 95 | let gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID; |
| 102 | let gop_result = unsafe { | 96 | let gop_result = unsafe { |
| @@ -108,6 +102,20 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 108 | }; | 102 | }; |
| 109 | if gop_result == EFI_SUCCESS { | 103 | if gop_result == EFI_SUCCESS { |
| 110 | let gop = unsafe { &mut *(gop_interface as *mut GraphicsOutputProtocol) }; | 104 | let gop = unsafe { &mut *(gop_interface as *mut GraphicsOutputProtocol) }; |
| 105 | rsnix::gop::peek_fb_region(gop); | ||
| 106 | } | ||
| 107 | |||
| 108 | // Stash the memory map | ||
| 109 | let map_key = match rsnix::memory::capture_memory_map(bs) { | ||
| 110 | Ok(k) => k, | ||
| 111 | Err(e) => { | ||
| 112 | rsnix::serial_println!("mem: capture_memory_map failed 0x{:x}", e); | ||
| 113 | 0 | ||
| 114 | } | ||
| 115 | }; | ||
| 116 | |||
| 117 | if gop_result == EFI_SUCCESS { | ||
| 118 | let gop = unsafe { &mut *(gop_interface as *mut GraphicsOutputProtocol) }; | ||
| 111 | rsnix::serial_println!("gop: framebuffer found"); | 119 | rsnix::serial_println!("gop: framebuffer found"); |
| 112 | rsnix::gop::init(gop); | 120 | rsnix::gop::init(gop); |
| 113 | } else { | 121 | } else { |
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 { | |||
| 19 | height: usize, | 19 | height: usize, |
| 20 | pitch: usize, // bytes per scan line | 20 | pitch: usize, // bytes per scan line |
| 21 | bytes_per_pixel: usize, | 21 | bytes_per_pixel: usize, |
| 22 | /// Size of the firmware's linear framebuffer mapping, in bytes. | ||
| 23 | /// Everything written to the display is clamped to this, so a mode | ||
| 24 | /// whose `pitch * height` exceeds the actual mapping (some real | ||
| 25 | /// firmware pads the scan-line pitch but sizes the buffer for the | ||
| 26 | /// resolution) degrades to a truncated view instead of faulting. | ||
| 27 | framebuffer_size: usize, | ||
| 22 | bg: u32, // packed BGRA or RGBA background colour | 28 | bg: u32, // packed BGRA or RGBA background colour |
| 23 | bgr: bool, | 29 | bgr: bool, |
| 24 | } | 30 | } |
| 25 | 31 | ||
| 26 | static mut FB: Option<Framebuffer> = None; | 32 | static mut FB: Option<Framebuffer> = None; |
| 27 | 33 | ||
| 34 | /// The GOP framebuffer region (base + mapping size), stashed before the | ||
| 35 | /// memory map is captured so the heap and page allocator can exclude it. | ||
| 36 | /// Some real-world firmware lists the framebuffer's memory as | ||
| 37 | /// `EfiConventionalMemory`; handing those pages out would let kernel | ||
| 38 | /// allocations (stacks, DMA buffers) overwrite the display. | ||
| 39 | static mut FB_REGION_BASE: usize = 0; | ||
| 40 | static mut FB_REGION_SIZE: usize = 0; | ||
| 41 | |||
| 42 | /// Stash the current GOP framebuffer region *before* the memory map is | ||
| 43 | /// captured (must run before `memory::capture_memory_map`). Updated | ||
| 44 | /// again after `SetMode` in `init` — the base can move. | ||
| 45 | pub fn peek_fb_region(gop: &mut GraphicsOutputProtocol) { | ||
| 46 | unsafe { | ||
| 47 | let mode = &*gop.mode; | ||
| 48 | FB_REGION_BASE = mode.framebuffer_base as usize; | ||
| 49 | FB_REGION_SIZE = mode.framebuffer_size; | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | /// The framebuffer region currently excluded from the allocators | ||
| 54 | /// (base, size in bytes), if any. | ||
| 55 | pub fn framebuffer_region() -> Option<(usize, usize)> { | ||
| 56 | unsafe { | ||
| 57 | if FB_REGION_SIZE == 0 { | ||
| 58 | None | ||
| 59 | } else { | ||
| 60 | Some((FB_REGION_BASE, FB_REGION_SIZE)) | ||
| 61 | } | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 28 | /// Software backbuffer: a RAM copy of the whole framebuffer (same pitch | 65 | /// Software backbuffer: a RAM copy of the whole framebuffer (same pitch |
| 29 | /// layout, u32 pixels). | 66 | /// layout, u32 pixels). |
| 30 | /// | 67 | /// |
| @@ -147,6 +184,7 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { | |||
| 147 | height, | 184 | height, |
| 148 | pitch, | 185 | pitch, |
| 149 | bytes_per_pixel: bpp, | 186 | bytes_per_pixel: bpp, |
| 187 | framebuffer_size: mode.framebuffer_size, | ||
| 150 | bg, | 188 | bg, |
| 151 | bgr, | 189 | bgr, |
| 152 | }; | 190 | }; |
| @@ -162,6 +200,23 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { | |||
| 162 | } | 200 | } |
| 163 | } | 201 | } |
| 164 | 202 | ||
| 203 | // Re-stash the post-SetMode region (base can move) for the page | ||
| 204 | // allocator, and log the authoritative geometry for diagnostics. | ||
| 205 | unsafe { | ||
| 206 | FB_REGION_BASE = mode.framebuffer_base as usize; | ||
| 207 | FB_REGION_SIZE = mode.framebuffer_size; | ||
| 208 | } | ||
| 209 | crate::serial_println!( | ||
| 210 | "gop: fb base={:#x} size={:#x} pitch={:#x} {}x{} bpp={} bgr={}", | ||
| 211 | base as usize, | ||
| 212 | { mode.framebuffer_size }, | ||
| 213 | pitch, | ||
| 214 | width, | ||
| 215 | height, | ||
| 216 | bpp, | ||
| 217 | bgr | ||
| 218 | ); | ||
| 219 | |||
| 165 | fb.clear(); | 220 | fb.clear(); |
| 166 | 221 | ||
| 167 | unsafe { | 222 | unsafe { |
| @@ -231,10 +286,17 @@ pub(crate) fn push_rect(x: usize, y: usize, w: usize, h: usize) { | |||
| 231 | let w = w.min(fb.width - x); | 286 | let w = w.min(fb.width - x); |
| 232 | let h = h.min(fb.height - y); | 287 | let h = h.min(fb.height - y); |
| 233 | let row_words = fb.pitch / 4; | 288 | let row_words = fb.pitch / 4; |
| 289 | // Words of the firmware's actual display mapping — per-row clamp so a | ||
| 290 | // pitch-padded mode never writes past the buffer. | ||
| 291 | let fb_words = fb.framebuffer_size / 4; | ||
| 234 | unsafe { | 292 | unsafe { |
| 235 | for dy in 0..h { | 293 | for dy in 0..h { |
| 236 | let off = (y + dy) * row_words + x; | 294 | let off = (y + dy) * row_words + x; |
| 237 | ptr::copy_nonoverlapping(SHADOW.add(off), fb.base.add(off * 4) as *mut u32, w); | 295 | let row_w = w.min(fb_words.saturating_sub(off)); |
| 296 | if row_w == 0 { | ||
| 297 | continue; | ||
| 298 | } | ||
| 299 | ptr::copy_nonoverlapping(SHADOW.add(off), fb.base.add(off * 4) as *mut u32, row_w); | ||
| 238 | } | 300 | } |
| 239 | } | 301 | } |
| 240 | overlay_cursor(x, y, w, h); | 302 | overlay_cursor(x, y, w, h); |
| @@ -275,6 +337,39 @@ pub fn fill_rect(x: usize, y: usize, w: usize, h: usize, color: u32) { | |||
| 275 | push_rect(x, y, w, h); | 337 | push_rect(x, y, w, h); |
| 276 | } | 338 | } |
| 277 | 339 | ||
| 340 | /// Print crash/fault lines directly to the framebuffer, bypassing the vt | ||
| 341 | /// entirely (the vt state may be corrupt when an exception fires). | ||
| 342 | /// | ||
| 343 | /// White-on-red band at the top of the screen, written through the same | ||
| 344 | /// clamped glyph path — safe even if the fault was inside the vt/gop | ||
| 345 | /// machinery, and visible on real hardware with no serial console. | ||
| 346 | pub fn dump_lines(lines: &[&str]) { | ||
| 347 | let Some(fb) = fb().as_ref() else { | ||
| 348 | return; | ||
| 349 | }; | ||
| 350 | let cols = fb.width / 8; | ||
| 351 | let rows = fb.height / 16; | ||
| 352 | let red = (0xC0u8, 0x00u8, 0x00u8); | ||
| 353 | let white = (0xFFu8, 0xFFu8, 0xFFu8); | ||
| 354 | let band = lines.len().min(rows); | ||
| 355 | // Red band behind the text. | ||
| 356 | if cols > 0 && band > 0 { | ||
| 357 | fill_rect(0, 0, cols * 8, band * 16, rgb(red.0, red.1, red.2)); | ||
| 358 | } | ||
| 359 | for (i, line) in lines.iter().enumerate().take(band) { | ||
| 360 | let mut c = 0; | ||
| 361 | for &b in line.as_bytes() { | ||
| 362 | if c >= cols { | ||
| 363 | break; | ||
| 364 | } | ||
| 365 | if b < 0x80 { | ||
| 366 | blit_glyph(c, i, b as u16, white, red); | ||
| 367 | c += 1; | ||
| 368 | } | ||
| 369 | } | ||
| 370 | } | ||
| 371 | } | ||
| 372 | |||
| 278 | /// Read one pixel back from the shadow (RAM; cheap). Test/demo use only. | 373 | /// Read one pixel back from the shadow (RAM; cheap). Test/demo use only. |
| 279 | pub fn shadow_pixel(x: usize, y: usize) -> Option<u32> { | 374 | pub fn shadow_pixel(x: usize, y: usize) -> Option<u32> { |
| 280 | let fb = fb().as_ref()?; | 375 | let fb = fb().as_ref()?; |
| @@ -433,7 +528,10 @@ fn draw_bmp(x: usize, y: usize, color: u32) { | |||
| 433 | } | 528 | } |
| 434 | if row & (1u16 << (11 - dx)) != 0 { | 529 | if row & (1u16 << (11 - dx)) != 0 { |
| 435 | let off = yy * row_words + xx; | 530 | let off = yy * row_words + xx; |
| 436 | ptr::write_volatile(fb.base.add(off * 4) as *mut u32, color); | 531 | // Clamp to the display mapping (defensive). |
| 532 | if off < fb.framebuffer_size / 4 { | ||
| 533 | ptr::write_volatile(fb.base.add(off * 4) as *mut u32, color); | ||
| 534 | } | ||
| 437 | } | 535 | } |
| 438 | } | 536 | } |
| 439 | } | 537 | } |
| @@ -453,7 +551,10 @@ pub fn rgb(r: u8, g: u8, b: u8) -> u32 { | |||
| 453 | // --------------------------------------------------------------------------- | 551 | // --------------------------------------------------------------------------- |
| 454 | impl Framebuffer { | 552 | impl Framebuffer { |
| 455 | fn clear(&self) { | 553 | fn clear(&self) { |
| 456 | let size = self.pitch * self.height; | 554 | // Clamp the display write to the firmware's actual mapping — some |
| 555 | // real GPUs size the buffer for the resolution while the pitch | ||
| 556 | // (with scan-line padding) is larger. | ||
| 557 | let size = (self.pitch * self.height).min(self.framebuffer_size); | ||
| 457 | // Fill with background colour (32-bit writes), display + shadow. | 558 | // Fill with background colour (32-bit writes), display + shadow. |
| 458 | let mut p = self.base; | 559 | let mut p = self.base; |
| 459 | let end = unsafe { p.add(size) }; | 560 | let end = unsafe { p.add(size) }; |
| @@ -511,7 +612,12 @@ impl Framebuffer { | |||
| 511 | // A bulk copy (rep movsb) writes in 64-byte bursts that | 612 | // A bulk copy (rep movsb) writes in 64-byte bursts that |
| 512 | // coalesce well on WC framebuffers — far fewer bus | 613 | // coalesce well on WC framebuffers — far fewer bus |
| 513 | // transactions than per-pixel stores. | 614 | // transactions than per-pixel stores. |
| 514 | ptr::copy_nonoverlapping(SHADOW, self.base as *mut u32, words); | 615 | // |
| 616 | // Clamp to the firmware's mapping size; a pitch-padded mode | ||
| 617 | // whose `pitch * height` exceeds the buffer degrades to a | ||
| 618 | // truncated view instead of faulting past the mapping. | ||
| 619 | let push_words = words.min(self.framebuffer_size / 4); | ||
| 620 | ptr::copy_nonoverlapping(SHADOW, self.base as *mut u32, push_words); | ||
| 515 | // The scroll copied the shadow (which never contains the | 621 | // The scroll copied the shadow (which never contains the |
| 516 | // cursor); re-composite it on top of the pushed frame. | 622 | // cursor); re-composite it on top of the pushed frame. |
| 517 | crate::gop::cursor_redraw(); | 623 | crate::gop::cursor_redraw(); |
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!( | |||
| 269 | " push 0xFF", | 269 | " push 0xFF", |
| 270 | " jmp isr_common", | 270 | " jmp isr_common", |
| 271 | "", | 271 | "", |
| 272 | // Exception stubs for the remaining vectors 0-31 so every fault gets | ||
| 273 | // a diagnosable dump instead of converging to a cryptic double fault. | ||
| 274 | // Vectors that push their own error code (10, 11, 12, 13, 17, 18 — 8 | ||
| 275 | // and 14 have dedicated stubs above) skip the fake push-0. | ||
| 276 | ".macro ISR_NOERR vec", | ||
| 277 | " .globl isr_stub_x_\\vec", | ||
| 278 | " .p2align 4", | ||
| 279 | "isr_stub_x_\\vec:", | ||
| 280 | " push 0", | ||
| 281 | " push \\vec", | ||
| 282 | " jmp isr_common", | ||
| 283 | ".endm", | ||
| 284 | ".macro ISR_ERR vec", | ||
| 285 | " .globl isr_stub_x_\\vec", | ||
| 286 | " .p2align 4", | ||
| 287 | "isr_stub_x_\\vec:", | ||
| 288 | " push \\vec", | ||
| 289 | " jmp isr_common", | ||
| 290 | ".endm", | ||
| 291 | "ISR_NOERR 0", | ||
| 292 | "ISR_NOERR 1", | ||
| 293 | "ISR_NOERR 2", | ||
| 294 | "ISR_NOERR 3", | ||
| 295 | "ISR_NOERR 4", | ||
| 296 | "ISR_NOERR 5", | ||
| 297 | "ISR_NOERR 6", | ||
| 298 | "ISR_NOERR 7", | ||
| 299 | "ISR_NOERR 9", | ||
| 300 | "ISR_ERR 10", | ||
| 301 | "ISR_ERR 11", | ||
| 302 | "ISR_ERR 12", | ||
| 303 | "ISR_ERR 13", | ||
| 304 | "ISR_NOERR 15", | ||
| 305 | "ISR_NOERR 16", | ||
| 306 | "ISR_ERR 17", | ||
| 307 | "ISR_ERR 18", | ||
| 308 | "ISR_NOERR 19", | ||
| 309 | "ISR_NOERR 20", | ||
| 310 | "ISR_NOERR 21", | ||
| 311 | "ISR_NOERR 22", | ||
| 312 | "ISR_NOERR 23", | ||
| 313 | "ISR_NOERR 24", | ||
| 314 | "ISR_NOERR 25", | ||
| 315 | "ISR_NOERR 26", | ||
| 316 | "ISR_NOERR 27", | ||
| 317 | "ISR_NOERR 28", | ||
| 318 | "ISR_NOERR 29", | ||
| 319 | "ISR_NOERR 30", | ||
| 320 | "ISR_NOERR 31", | ||
| 321 | "", | ||
| 272 | // Common handler — save all registers, call into Rust (which returns | 322 | // Common handler — save all registers, call into Rust (which returns |
| 273 | // the next task's frame pointer in rax), switch rsp, restore, iretq. | 323 | // the next task's frame pointer in rax), switch rsp, restore, iretq. |
| 274 | "isr_common:", | 324 | "isr_common:", |
| @@ -381,8 +431,9 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize { | |||
| 381 | fp | 431 | fp |
| 382 | } | 432 | } |
| 383 | 8 => { | 433 | 8 => { |
| 384 | crate::serial_println!("double fault — halting"); | 434 | // Double fault — dump the frame (we're on the IST stack |
| 385 | halt() | 435 | // so this is safe) and halt. |
| 436 | crate::sched::fault_dump("double fault", 0, frame) | ||
| 386 | } | 437 | } |
| 387 | 14 => { | 438 | 14 => { |
| 388 | let cr2: u64; | 439 | let cr2: u64; |
| @@ -391,6 +442,11 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize { | |||
| 391 | crate::sched::page_fault(cr2, fp) | 442 | crate::sched::page_fault(cr2, fp) |
| 392 | } | 443 | } |
| 393 | 0x60 => crate::sched::on_yield(fp), | 444 | 0x60 => crate::sched::on_yield(fp), |
| 445 | v if v < 32 => { | ||
| 446 | // Any other CPU exception: full dump (screen + serial), | ||
| 447 | // then halt the shell or abort the faulting background task. | ||
| 448 | crate::sched::fault_dump("exception", 0, frame) | ||
| 449 | } | ||
| 394 | _ => fp, | 450 | _ => fp, |
| 395 | } | 451 | } |
| 396 | }; | 452 | }; |
| @@ -418,6 +474,47 @@ pub fn read_mouse_byte() -> Option<u8> { | |||
| 418 | /// sub-tick fraction. | 474 | /// sub-tick fraction. |
| 419 | pub static TICKS: AtomicU64 = AtomicU64::new(0); | 475 | pub static TICKS: AtomicU64 = AtomicU64::new(0); |
| 420 | 476 | ||
| 477 | /// Address of the stub for CPU exception vector `vec` (0-31), or 0 for | ||
| 478 | /// vectors without a stub. 8 (double fault) and 14 (page fault) use | ||
| 479 | /// dedicated stubs that run on IST stacks. | ||
| 480 | fn exc_stub_addr(vec: u8) -> u64 { | ||
| 481 | match vec { | ||
| 482 | 8 => isr_stub_08 as *const () as u64, | ||
| 483 | 14 => isr_stub_14 as *const () as u64, | ||
| 484 | 0 => isr_stub_x_0 as *const () as u64, | ||
| 485 | 1 => isr_stub_x_1 as *const () as u64, | ||
| 486 | 2 => isr_stub_x_2 as *const () as u64, | ||
| 487 | 3 => isr_stub_x_3 as *const () as u64, | ||
| 488 | 4 => isr_stub_x_4 as *const () as u64, | ||
| 489 | 5 => isr_stub_x_5 as *const () as u64, | ||
| 490 | 6 => isr_stub_x_6 as *const () as u64, | ||
| 491 | 7 => isr_stub_x_7 as *const () as u64, | ||
| 492 | 9 => isr_stub_x_9 as *const () as u64, | ||
| 493 | 10 => isr_stub_x_10 as *const () as u64, | ||
| 494 | 11 => isr_stub_x_11 as *const () as u64, | ||
| 495 | 12 => isr_stub_x_12 as *const () as u64, | ||
| 496 | 13 => isr_stub_x_13 as *const () as u64, | ||
| 497 | 15 => isr_stub_x_15 as *const () as u64, | ||
| 498 | 16 => isr_stub_x_16 as *const () as u64, | ||
| 499 | 17 => isr_stub_x_17 as *const () as u64, | ||
| 500 | 18 => isr_stub_x_18 as *const () as u64, | ||
| 501 | 19 => isr_stub_x_19 as *const () as u64, | ||
| 502 | 20 => isr_stub_x_20 as *const () as u64, | ||
| 503 | 21 => isr_stub_x_21 as *const () as u64, | ||
| 504 | 22 => isr_stub_x_22 as *const () as u64, | ||
| 505 | 23 => isr_stub_x_23 as *const () as u64, | ||
| 506 | 24 => isr_stub_x_24 as *const () as u64, | ||
| 507 | 25 => isr_stub_x_25 as *const () as u64, | ||
| 508 | 26 => isr_stub_x_26 as *const () as u64, | ||
| 509 | 27 => isr_stub_x_27 as *const () as u64, | ||
| 510 | 28 => isr_stub_x_28 as *const () as u64, | ||
| 511 | 29 => isr_stub_x_29 as *const () as u64, | ||
| 512 | 30 => isr_stub_x_30 as *const () as u64, | ||
| 513 | 31 => isr_stub_x_31 as *const () as u64, | ||
| 514 | _ => 0, | ||
| 515 | } | ||
| 516 | } | ||
| 517 | |||
| 421 | /// Initialise the IDT, remap the PIC, set up the PIT, and enable interrupts. | 518 | /// Initialise the IDT, remap the PIC, set up the PIT, and enable interrupts. |
| 422 | pub fn init() { | 519 | pub fn init() { |
| 423 | unsafe { | 520 | unsafe { |
| @@ -435,6 +532,20 @@ pub fn init() { | |||
| 435 | idt_set_entry(idt_ptr, vec, spurious, cs, 0x0Eu8, 0); | 532 | idt_set_entry(idt_ptr, vec, spurious, cs, 0x0Eu8, 0); |
| 436 | } | 533 | } |
| 437 | 534 | ||
| 535 | // Wire up every CPU exception vector (0-31) so any fault produces | ||
| 536 | // a diagnosable dump instead of converging to a double fault. | ||
| 537 | for vec in 0..=31u8 { | ||
| 538 | let a = exc_stub_addr(vec); | ||
| 539 | if a != 0 { | ||
| 540 | let ist = match vec { | ||
| 541 | 8 => 2u8, | ||
| 542 | 14 => 1u8, | ||
| 543 | _ => 0u8, | ||
| 544 | }; | ||
| 545 | idt_set_entry(idt_ptr, vec, a, cs, 0x0Eu8, ist); | ||
| 546 | } | ||
| 547 | } | ||
| 548 | |||
| 438 | // Override vectors for interrupt we actually handle. | 549 | // Override vectors for interrupt we actually handle. |
| 439 | idt_set_entry(idt_ptr, 32, isr_stub_32 as *const () as u64, cs, 0x0Eu8, 0); | 550 | idt_set_entry(idt_ptr, 32, isr_stub_32 as *const () as u64, cs, 0x0Eu8, 0); |
| 440 | idt_set_entry(idt_ptr, 33, isr_stub_33 as *const () as u64, cs, 0x0Eu8, 0); | 551 | idt_set_entry(idt_ptr, 33, isr_stub_33 as *const () as u64, cs, 0x0Eu8, 0); |
| @@ -513,4 +624,35 @@ unsafe extern "C" { | |||
| 513 | fn isr_stub_44(); | 624 | fn isr_stub_44(); |
| 514 | fn isr_stub_60(); | 625 | fn isr_stub_60(); |
| 515 | fn isr_stub_spurious(); | 626 | fn isr_stub_spurious(); |
| 627 | // Macro-generated exception stubs (isr_stub_x_<vec>). | ||
| 628 | fn isr_stub_x_0(); | ||
| 629 | fn isr_stub_x_1(); | ||
| 630 | fn isr_stub_x_2(); | ||
| 631 | fn isr_stub_x_3(); | ||
| 632 | fn isr_stub_x_4(); | ||
| 633 | fn isr_stub_x_5(); | ||
| 634 | fn isr_stub_x_6(); | ||
| 635 | fn isr_stub_x_7(); | ||
| 636 | fn isr_stub_x_9(); | ||
| 637 | fn isr_stub_x_10(); | ||
| 638 | fn isr_stub_x_11(); | ||
| 639 | fn isr_stub_x_12(); | ||
| 640 | fn isr_stub_x_13(); | ||
| 641 | fn isr_stub_x_15(); | ||
| 642 | fn isr_stub_x_16(); | ||
| 643 | fn isr_stub_x_17(); | ||
| 644 | fn isr_stub_x_18(); | ||
| 645 | fn isr_stub_x_19(); | ||
| 646 | fn isr_stub_x_20(); | ||
| 647 | fn isr_stub_x_21(); | ||
| 648 | fn isr_stub_x_22(); | ||
| 649 | fn isr_stub_x_23(); | ||
| 650 | fn isr_stub_x_24(); | ||
| 651 | fn isr_stub_x_25(); | ||
| 652 | fn isr_stub_x_26(); | ||
| 653 | fn isr_stub_x_27(); | ||
| 654 | fn isr_stub_x_28(); | ||
| 655 | fn isr_stub_x_29(); | ||
| 656 | fn isr_stub_x_30(); | ||
| 657 | fn isr_stub_x_31(); | ||
| 516 | } | 658 | } |
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs index e03af8b..a947c53 100644 --- a/rsnix/src/memory.rs +++ b/rsnix/src/memory.rs | |||
| @@ -354,14 +354,29 @@ pub unsafe fn refresh_and_exit(bs: &BootServices, image_handle: Handle) -> Resul | |||
| 354 | 354 | ||
| 355 | fn init_heap() { | 355 | fn init_heap() { |
| 356 | unsafe { | 356 | unsafe { |
| 357 | // Region the firmware maps for the GOP framebuffer, if known — | ||
| 358 | // exclude it even when the memory map lists it as conventional | ||
| 359 | // (some real firmware does exactly that, which would let heap | ||
| 360 | // allocations overwrite the display). | ||
| 361 | let fb = crate::gop::framebuffer_region(); | ||
| 357 | let mut best_start = 0usize; | 362 | let mut best_start = 0usize; |
| 358 | let mut best_size = 0usize; | 363 | let mut best_size = 0usize; |
| 359 | for i in 0..MAP_ENTRIES { | 364 | for i in 0..MAP_ENTRIES { |
| 360 | let d = &MEMORY_MAP[i]; | 365 | let d = &MEMORY_MAP[i]; |
| 361 | if d.kind == MEMORY_TYPE_CONVENTIONAL { | 366 | if d.kind == MEMORY_TYPE_CONVENTIONAL { |
| 362 | let sz = d.number_of_pages as usize * 4096; | 367 | let s = d.physical_start as usize; |
| 368 | let e = s + d.number_of_pages as usize * 4096; | ||
| 369 | // Subtract any overlap with the framebuffer region. | ||
| 370 | let (cs, ce) = match fb { | ||
| 371 | Some((b, sz)) if e > b && s < b + sz => (s.max(b), e.min(b + sz)), | ||
| 372 | _ => (e, s), // no overlap → empty range | ||
| 373 | }; | ||
| 374 | let mut sz = e - s; | ||
| 375 | if ce > cs { | ||
| 376 | sz -= ce - cs; | ||
| 377 | } | ||
| 363 | if sz > best_size { | 378 | if sz > best_size { |
| 364 | best_start = d.physical_start as usize; | 379 | best_start = s; |
| 365 | best_size = sz; | 380 | best_size = sz; |
| 366 | } | 381 | } |
| 367 | } | 382 | } |
| @@ -634,6 +649,22 @@ pub fn init_page_allocator() -> bool { | |||
| 634 | } | 649 | } |
| 635 | } | 650 | } |
| 636 | 651 | ||
| 652 | // Reserve the GOP framebuffer region even if the map lists it as | ||
| 653 | // conventional — kernel allocations (stacks, DMA buffers) must | ||
| 654 | // never land on the display. | ||
| 655 | if let Some((fbase, fsize)) = crate::gop::framebuffer_region() | ||
| 656 | && fsize > 0 | ||
| 657 | { | ||
| 658 | let s = fbase / PAGE_SIZE; | ||
| 659 | let e = (fbase + fsize).div_ceil(PAGE_SIZE); | ||
| 660 | for p in s..e { | ||
| 661 | let idx = p - base_page; | ||
| 662 | if idx < span_pages { | ||
| 663 | *bm.add(idx / 8) |= 1 << (idx % 8); | ||
| 664 | } | ||
| 665 | } | ||
| 666 | } | ||
| 667 | |||
| 637 | // Never hand out page 0 (the null-pointer trap page). | 668 | // Never hand out page 0 (the null-pointer trap page). |
| 638 | if base_page == 0 { | 669 | if base_page == 0 { |
| 639 | *bm |= 0x01; | 670 | *bm |= 0x01; |
diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs index dbe3647..cfce58f 100644 --- a/rsnix/src/sched.rs +++ b/rsnix/src/sched.rs | |||
| @@ -28,7 +28,9 @@ use core::fmt; | |||
| 28 | use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; | 28 | use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; |
| 29 | 29 | ||
| 30 | extern crate alloc; | 30 | extern crate alloc; |
| 31 | use alloc::format; | ||
| 31 | use alloc::string::String; | 32 | use alloc::string::String; |
| 33 | use alloc::string::ToString; | ||
| 32 | use alloc::vec::Vec; | 34 | use alloc::vec::Vec; |
| 33 | 35 | ||
| 34 | use crate::interrupts::IntFrame; | 36 | use crate::interrupts::IntFrame; |
| @@ -280,11 +282,7 @@ pub fn spawn(entry: fn(), name: &str) -> Option<usize> { | |||
| 280 | }; | 282 | }; |
| 281 | let ok = spawn_at(id, entry, name); | 283 | let ok = spawn_at(id, entry, name); |
| 282 | unlock(); | 284 | unlock(); |
| 283 | if ok { | 285 | if ok { Some(id) } else { None } |
| 284 | Some(id) | ||
| 285 | } else { | ||
| 286 | None | ||
| 287 | } | ||
| 288 | } | 286 | } |
| 289 | 287 | ||
| 290 | unsafe fn reap(id: usize) { | 288 | unsafe fn reap(id: usize) { |
| @@ -548,10 +546,57 @@ pub fn abort_current(msg: &str) -> ! { | |||
| 548 | /// Vector 14 (page fault) handler — runs on the dedicated IST stack. | 546 | /// Vector 14 (page fault) handler — runs on the dedicated IST stack. |
| 549 | /// A fault in a real task (id >= 2) kills the task gracefully; anything | 547 | /// A fault in a real task (id >= 2) kills the task gracefully; anything |
| 550 | /// else (shell, idle, pre-scheduler) is a kernel bug: halt. | 548 | /// else (shell, idle, pre-scheduler) is a kernel bug: halt. |
| 549 | /// Dump a fatal exception to serial *and* the screen (raw, bypassing the | ||
| 550 | /// vt), then halt the shell or abort the faulting background task. | ||
| 551 | /// | ||
| 552 | /// Real hardware has no serial console, so the screen dump is what lets | ||
| 553 | /// a user report a crash precisely — vector, fault address, RIP and task. | ||
| 554 | /// Runs with interrupts disabled (we're inside the ISR). | ||
| 555 | #[allow(clippy::needless_return)] | ||
| 556 | pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFrame) -> ! { | ||
| 557 | let vector = frame.vector; | ||
| 558 | let error = frame.error; | ||
| 559 | let rip = frame.rip; | ||
| 560 | let rflags = frame.rflags; | ||
| 561 | let rsp = frame.rsp; | ||
| 562 | let id = unsafe { CURRENT }; | ||
| 563 | let name = name_str(id); | ||
| 564 | let name_buf = name.to_string(); | ||
| 565 | let in_kernel = unsafe { CURRENT } <= IDLE_TID; | ||
| 566 | |||
| 567 | // Serial: a full, parseable line. | ||
| 568 | crate::serial_println!( | ||
| 569 | "!!! {} vec={} err={:#x} rip={:#x} cr2={:#x} rsp={:#x} rflags={:#x} task={} \"{}\"", | ||
| 570 | kind, | ||
| 571 | vector, | ||
| 572 | error, | ||
| 573 | rip, | ||
| 574 | cr2, | ||
| 575 | rsp, | ||
| 576 | rflags, | ||
| 577 | id, | ||
| 578 | name_buf | ||
| 579 | ); | ||
| 580 | crate::serial_println!("!!! halting (shell task fault — machine stops)"); | ||
| 581 | |||
| 582 | // Screen: white-on-red band via the clamped raw dump path. | ||
| 583 | let l1 = alloc::format!("!!! {} vec={} err={:#x}", kind, vector, error); | ||
| 584 | let l2 = alloc::format!("rip={:#x} cr2={:#x}", rip, cr2); | ||
| 585 | let l3 = alloc::format!("rsp={:#x} task={} {}", rsp, id, name_buf); | ||
| 586 | let lines = [l1.as_str(), l2.as_str(), l3.as_str()]; | ||
| 587 | crate::gop::dump_lines(&lines); | ||
| 588 | |||
| 589 | if in_kernel { | ||
| 590 | crate::interrupts::halt() | ||
| 591 | } else { | ||
| 592 | abort_current(&format!("{} vec={}", kind, vector)) | ||
| 593 | } | ||
| 594 | } | ||
| 595 | |||
| 551 | pub fn page_fault(cr2: u64, frame_ptr: usize) -> usize { | 596 | pub fn page_fault(cr2: u64, frame_ptr: usize) -> usize { |
| 552 | if !is_ready() || unsafe { CURRENT } <= IDLE_TID { | 597 | if !is_ready() || unsafe { CURRENT } <= IDLE_TID { |
| 553 | crate::serial_println!("pgfault: kernel page fault at {:#x} — halting", cr2); | 598 | let frame = unsafe { &*(frame_ptr as *const crate::interrupts::IntFrame) }; |
| 554 | crate::interrupts::halt(); | 599 | fault_dump("page fault", cr2, frame) |
| 555 | } | 600 | } |
| 556 | let g = unsafe { TASKS[CURRENT].guard }; | 601 | let g = unsafe { TASKS[CURRENT].guard }; |
| 557 | let in_guard = g != 0 && cr2 >= g as u64 && cr2 < (g + memory::PAGE_SIZE) as u64; | 602 | let in_guard = g != 0 && cr2 >= g as u64 && cr2 < (g + memory::PAGE_SIZE) as u64; |
diff --git a/scripts/test-fault.sh b/scripts/test-fault.sh new file mode 100755 index 0000000..49ba791 --- /dev/null +++ b/scripts/test-fault.sh | |||
| @@ -0,0 +1,90 @@ | |||
| 1 | #!/usr/bin/env bash | ||
| 2 | # | ||
| 3 | # scripts/test-fault.sh — fault-diagnostics verification. | ||
| 4 | # | ||
| 5 | # Real hardware has no serial console, so a kernel crash previously left | ||
| 6 | # only a frozen screen ("unresponsive"). This test proves the fault-dump | ||
| 7 | # path: every CPU exception now prints a white-on-red band to the | ||
| 8 | # framebuffer *and* a parseable serial line (vector, error, RIP, CR2, | ||
| 9 | # task) before halting — so the next real-hardware failure identifies | ||
| 10 | # itself. The `fault` command deliberately writes to an unmapped | ||
| 11 | # address; the machine must halt with a full dump, not triple-fault. | ||
| 12 | # | ||
| 13 | # Usage: ./scripts/test-fault.sh | ||
| 14 | # Env: OVMF, QEMU (same defaults as other test scripts) | ||
| 15 | # | ||
| 16 | set -euo pipefail | ||
| 17 | |||
| 18 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 19 | PROJECT_DIR="$(dirname "$SCRIPT_DIR")" | ||
| 20 | cd "$PROJECT_DIR" | ||
| 21 | |||
| 22 | OVMF="${OVMF:-/usr/share/edk2/x64/OVMF.4m.fd}" | ||
| 23 | QEMU="${QEMU:-qemu-system-x86_64}" | ||
| 24 | ESP_DIR="$PROJECT_DIR/esp" | ||
| 25 | QPID=/tmp/rsdos-fault-qpid | ||
| 26 | SOCK=/tmp/rsdos-fault-qmon.sock | ||
| 27 | LOG=/tmp/rsdos-fault-serial.log | ||
| 28 | QLOG=/tmp/rsdos-fault-qemu.log | ||
| 29 | |||
| 30 | trap 'kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true' EXIT | ||
| 31 | |||
| 32 | fail() { | ||
| 33 | echo "FAIL: $1" | ||
| 34 | [ -f "$LOG" ] && tail -40 "$LOG" | ||
| 35 | exit 1 | ||
| 36 | } | ||
| 37 | |||
| 38 | wait_log() { | ||
| 39 | local i=0 | ||
| 40 | while [ "$i" -lt $((${3:-60} * 10)) ]; do | ||
| 41 | grep -q "$2" "$1" 2>/dev/null && return 0 | ||
| 42 | sleep 0.1 | ||
| 43 | i=$((i + 1)) | ||
| 44 | done | ||
| 45 | return 1 | ||
| 46 | } | ||
| 47 | |||
| 48 | echo "==> 1/5 build kernel (debug)" | ||
| 49 | cargo build --package rsdos --target x86_64-unknown-uefi | ||
| 50 | |||
| 51 | echo "==> 2/5 deploy to ESP" | ||
| 52 | mkdir -p "$ESP_DIR/EFI/BOOT" | ||
| 53 | cp target/x86_64-unknown-uefi/debug/rsdos.efi "$ESP_DIR/EFI/BOOT/BOOTX64.EFI" | ||
| 54 | |||
| 55 | echo "==> 3/5 boot QEMU (headless)" | ||
| 56 | kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true | ||
| 57 | sleep 1 | ||
| 58 | rm -f "$SOCK" "$LOG" | ||
| 59 | "$QEMU" -bios "$OVMF" \ | ||
| 60 | -drive file=fat:rw:"$ESP_DIR",format=raw \ | ||
| 61 | -serial file:"$LOG" \ | ||
| 62 | -monitor unix:"$SOCK",server,nowait \ | ||
| 63 | -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \ | ||
| 64 | >"$QLOG" 2>&1 & | ||
| 65 | echo $! >"$QPID" | ||
| 66 | |||
| 67 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" | ||
| 68 | |||
| 69 | echo "==> 4/5 boot diagnostics present" | ||
| 70 | grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing" | ||
| 71 | grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing" | ||
| 72 | |||
| 73 | echo "==> 5/5 inject a page fault (fault command)" | ||
| 74 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ | ||
| 75 | "clear" \ | ||
| 76 | "fault" | ||
| 77 | sleep 2 | ||
| 78 | |||
| 79 | # The dump must appear on serial: vector 14, the CR2 we wrote, RIP, and | ||
| 80 | # the shell task identity — then the machine halts (never triple-faults). | ||
| 81 | grep -aq "!!! page fault vec=14 .*rip=" "$LOG" || fail "page-fault dump line missing" | ||
| 82 | grep -aq "cr2=0x400000000000" "$LOG" || fail "CR2 not reported (faulting address)" | ||
| 83 | grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified" | ||
| 84 | grep -aq "!!! halting" "$LOG" || fail "no halt message" | ||
| 85 | |||
| 86 | # The screen dump itself is exercised in-kernel (dump_lines); the serial | ||
| 87 | # path sharing the same format line is verified above. | ||
| 88 | echo "" | ||
| 89 | echo "PASS — fault dump works (vec, err, rip, cr2, task → screen + serial)" | ||
| 90 | echo " every CPU exception now halts with a diagnosable dump, not a freeze" | ||
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index e1d54c8..3090776 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -721,6 +721,7 @@ write `{} <path> TIL <marker>`", | |||
| 721 | "sleep" => cmd_sleep(&parsed), | 721 | "sleep" => cmd_sleep(&parsed), |
| 722 | "kill" => cmd_kill(&parsed), | 722 | "kill" => cmd_kill(&parsed), |
| 723 | "demo" => cmd_demo(&parsed), | 723 | "demo" => cmd_demo(&parsed), |
| 724 | "fault" => cmd_fault(), | ||
| 724 | _ => term_err(&alloc::format!("unknown command: {}", cmd)), | 725 | _ => term_err(&alloc::format!("unknown command: {}", cmd)), |
| 725 | } | 726 | } |
| 726 | } | 727 | } |
| @@ -806,6 +807,18 @@ fn cmd_hello(p: &Parsed) { | |||
| 806 | pipe.end(); | 807 | pipe.end(); |
| 807 | } | 808 | } |
| 808 | 809 | ||
| 810 | /// Deliberately fault on an unmapped address — test hook that verifies | ||
| 811 | /// the fault-dump path (screen + serial) halts cleanly instead of | ||
| 812 | /// triple-faulting. The system stops; the log must show the dump. | ||
| 813 | /// Canonical-but-unmapped address (below 2^47, above any RAM/MMIO): the | ||
| 814 | /// page walk terminates at a not-present entry → #PF with CR2 = address. | ||
| 815 | fn cmd_fault() { | ||
| 816 | term_println("fault: writing to 0x0000_4000_0000_0000..."); | ||
| 817 | unsafe { | ||
| 818 | core::ptr::write_volatile(0x0000_4000_0000_0000usize as *mut u32, 0x1234u32); | ||
| 819 | } | ||
| 820 | } | ||
| 821 | |||
| 809 | fn cmd_clear() { | 822 | fn cmd_clear() { |
| 810 | rsnix::gop::clear_screen(); | 823 | rsnix::gop::clear_screen(); |
| 811 | } | 824 | } |
