diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 22:28:34 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 22:28:34 +0200 |
| commit | ab7d87bcde31c1681ea09c30534dde840a58d6fa (patch) | |
| tree | 42eed2c86663cf6ecfa3507ca46399e31b441c99 /rsnix | |
| parent | rsboot: track latest upstream HEAD (92367a8) instead of pinned ee1c85b (diff) | |
| download | rs-dos-ab7d87bcde31c1681ea09c30534dde840a58d6fa.tar.gz rs-dos-ab7d87bcde31c1681ea09c30534dde840a58d6fa.zip | |
rsnix v0.5.2: fault diagnostics + real-hardware scroll-crash hardening
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:
'!!! <kind> 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.
Diffstat (limited to '')
| -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 |
4 files changed, 339 insertions, 15 deletions
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; |
