diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-02 15:21:34 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-02 15:21:34 +0200 |
| commit | c7d92071e1dd11c45999457b16554b4abaa73b28 (patch) | |
| tree | aff7c7f19adcffe1dd91e1c2514af02c77fd524e | |
| parent | v0.5.5: fix real-hardware boot crash — shadow back in heap as first allocation (diff) | |
| download | rs-dos-c7d92071e1dd11c45999457b16554b4abaa73b28.tar.gz rs-dos-c7d92071e1dd11c45999457b16554b4abaa73b28.zip | |
v0.5.5: crash reports go to a dedicated CRASHLOG USB stick
The fault handler now writes crash.log to the volume labeled CRASHLOG
(label-selected via fat::crash_vol), falling back to slot 0. On real
hardware the boot USB may be unwritable, so a second stick labeled
CRASHLOG survives the write; the shell auto-prints the previous crash
report from it at next boot (read_crash_log resolves /disk/<uid>/crash.log
through the FAT path, since Disk nodes hold no cached children).
- fat.rs: volume_label() + crash_vol() (no allocation)
- fs.rs: write_crash_log targets crash_vol(); read_crash_log prefers the
CRASHLOG disk node; drop now-unused first_disk()
- mkdisk.sh: --label option
- test-fault.sh: boots with a dedicated CRASHLOG stick (mkfs -F 32 -n
CRASHLOG), verifies crash.log lands there and NOT on the ESP
| -rw-r--r-- | rsdos/src/main.rs | 37 | ||||
| -rw-r--r-- | rsnix/src/fat.rs | 31 | ||||
| -rw-r--r-- | rsnix/src/fs.rs | 93 | ||||
| -rw-r--r-- | rsnix/src/gdt.rs | 24 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 6 | ||||
| -rw-r--r-- | rsnix/src/interrupts.rs | 41 | ||||
| -rw-r--r-- | rsnix/src/memory.rs | 88 | ||||
| -rw-r--r-- | rsnix/src/sched.rs | 99 | ||||
| -rw-r--r-- | rsnix/src/uefi.rs | 26 | ||||
| -rwxr-xr-x | scripts/mkdisk.sh | 10 | ||||
| -rwxr-xr-x | scripts/test-fault.sh | 28 | ||||
| -rw-r--r-- | shell/src/lib.rs | 27 |
12 files changed, 443 insertions, 67 deletions
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index 2717dff..4521a42 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -81,6 +81,9 @@ fn panic(info: &PanicInfo) -> ! { | |||
| 81 | core::str::from_utf8(&stage_buf[..stage_n]).unwrap_or(""), | 81 | core::str::from_utf8(&stage_buf[..stage_n]).unwrap_or(""), |
| 82 | ]; | 82 | ]; |
| 83 | rsnix::gop::dump_lines(&lines); | 83 | rsnix::gop::dump_lines(&lines); |
| 84 | // Persist to the disk so the report survives the reboot on real | ||
| 85 | // hardware (no serial console). Best-effort; heap-free lines. | ||
| 86 | rsnix::fs::write_crash_log(&lines); | ||
| 84 | loop { | 87 | loop { |
| 85 | unsafe { | 88 | unsafe { |
| 86 | core::arch::asm!("cli; hlt"); | 89 | core::arch::asm!("cli; hlt"); |
| @@ -107,6 +110,31 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 107 | // Keep the runtime-services table for later (ResetSystem → poweroff). | 110 | // Keep the runtime-services table for later (ResetSystem → poweroff). |
| 108 | rsnix::power::init(st.runtime_services); | 111 | rsnix::power::init(st.runtime_services); |
| 109 | 112 | ||
| 113 | // Capture the kernel's own image range BEFORE ExitBootServices: the | ||
| 114 | // heap and page allocators must never hand out the running kernel's | ||
| 115 | // code/data/bss pages (a heap allocation landing on the .bss corrupts | ||
| 116 | // statics like the FAT ACTIVE_VOL — observed as a dead filesystem in | ||
| 117 | // the fault path). | ||
| 118 | unsafe { | ||
| 119 | let mut li: *mut core::ffi::c_void = core::ptr::null_mut(); | ||
| 120 | let li_guid = rsnix::uefi::EFI_LOADED_IMAGE_PROTOCOL_GUID; | ||
| 121 | if (bs.handle_protocol)(image_handle, &li_guid as *const rsnix::uefi::Guid, &mut li) | ||
| 122 | == rsnix::uefi::EFI_SUCCESS | ||
| 123 | && !li.is_null() | ||
| 124 | { | ||
| 125 | let p = &*(li as *const rsnix::uefi::LoadedImageProtocol); | ||
| 126 | rsnix::memory::set_kernel_image_range(p.image_base as usize, p.image_size as usize); | ||
| 127 | rsnix::serial_println!( | ||
| 128 | "mem: kernel image 0x{:x}..0x{:x} ({} KiB) reserved", | ||
| 129 | p.image_base as usize, | ||
| 130 | p.image_base as usize + p.image_size as usize, | ||
| 131 | p.image_size as usize / 1024 | ||
| 132 | ); | ||
| 133 | } else { | ||
| 134 | rsnix::serial_println!("mem: WARNING — kernel image range unknown"); | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 110 | // ---- Phase 1: UEFI boot services alive ---- | 138 | // ---- Phase 1: UEFI boot services alive ---- |
| 111 | 139 | ||
| 112 | // Print banner via UEFI console | 140 | // Print banner via UEFI console |
| @@ -208,7 +236,14 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 208 | dump_memory_summary(); | 236 | dump_memory_summary(); |
| 209 | 237 | ||
| 210 | if rsnix::memory::self_test() { | 238 | if rsnix::memory::self_test() { |
| 211 | rsnix::serial_println!("heap: self-test OK (alloc/free/coalesce)"); | 239 | let hs = rsnix::memory::heap_stats(); |
| 240 | rsnix::serial_println!( | ||
| 241 | "heap: self-test OK (0x{:x}..0x{:x} {} KiB free={} KiB)", | ||
| 242 | hs.heap_start, | ||
| 243 | hs.heap_end, | ||
| 244 | hs.heap_size / 1024, | ||
| 245 | hs.free_bytes / 1024 | ||
| 246 | ); | ||
| 212 | } else { | 247 | } else { |
| 213 | rsnix::serial_println!("heap: self-test FAILED"); | 248 | rsnix::serial_println!("heap: self-test FAILED"); |
| 214 | } | 249 | } |
diff --git a/rsnix/src/fat.rs b/rsnix/src/fat.rs index eda81ed..b7e0da1 100644 --- a/rsnix/src/fat.rs +++ b/rsnix/src/fat.rs | |||
| @@ -71,6 +71,31 @@ pub fn is_mounted(vol: usize) -> bool { | |||
| 71 | vols().get(vol).map(|v| v.mounted).unwrap_or(false) | 71 | vols().get(vol).map(|v| v.mounted).unwrap_or(false) |
| 72 | } | 72 | } |
| 73 | 73 | ||
| 74 | /// The volume's label — the part of its uid before the serial suffix | ||
| 75 | /// (uid is "LABEL-XXXXXXXX", label sanitized to [A-Z0-9_], so the first | ||
| 76 | /// '-' always terminates it). No allocation: borrows the uid string. | ||
| 77 | pub fn volume_label(vol: usize) -> Option<&'static str> { | ||
| 78 | let uid = &vols().get(vol)?.uid; | ||
| 79 | Some(uid.split_once('-').map(|(l, _)| l).unwrap_or(uid)) | ||
| 80 | } | ||
| 81 | |||
| 82 | /// Index of the crash-log volume: the first volume whose label is | ||
| 83 | /// "CRASHLOG" (a dedicated stick survives reboot writes that a boot | ||
| 84 | /// device may not accept), else slot 0 (first mounted) as before. | ||
| 85 | pub fn crash_vol() -> Option<usize> { | ||
| 86 | let n = volume_count(); | ||
| 87 | if n == 0 { | ||
| 88 | return None; | ||
| 89 | } | ||
| 90 | for vol in 0..n { | ||
| 91 | if let Some(label) = volume_label(vol) | ||
| 92 | && label.eq_ignore_ascii_case("CRASHLOG") { | ||
| 93 | return Some(vol); | ||
| 94 | } | ||
| 95 | } | ||
| 96 | Some(0) | ||
| 97 | } | ||
| 98 | |||
| 74 | /// Re-scan after a disk arrival/removal: unmount volumes whose disk left, | 99 | /// Re-scan after a disk arrival/removal: unmount volumes whose disk left, |
| 75 | /// mount every disk that has no mounted volume (which also re-tries | 100 | /// mount every disk that has no mounted volume (which also re-tries |
| 76 | /// mounts that failed at boot). Volume slots are tombstoned, never | 101 | /// mounts that failed at boot). Volume slots are tombstoned, never |
| @@ -175,7 +200,11 @@ fn v() -> &'static mut Volume { | |||
| 175 | match vols().get_mut(i) { | 200 | match vols().get_mut(i) { |
| 176 | Some(v) => v, | 201 | Some(v) => v, |
| 177 | None => { | 202 | None => { |
| 178 | crate::serial_println!("fat: internal error — no volume at slot {}", i); | 203 | crate::serial_println!( |
| 204 | "fat: internal error — no volume at slot {} (len={})", | ||
| 205 | i, | ||
| 206 | vols().len() | ||
| 207 | ); | ||
| 179 | crate::interrupts::halt(); | 208 | crate::interrupts::halt(); |
| 180 | } | 209 | } |
| 181 | } | 210 | } |
diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs index 5fc6d7a..24d8fcd 100644 --- a/rsnix/src/fs.rs +++ b/rsnix/src/fs.rs | |||
| @@ -464,9 +464,98 @@ pub fn write(h: usize, buf: &[u8], pos: u64) -> Result<usize, ()> { | |||
| 464 | } | 464 | } |
| 465 | } | 465 | } |
| 466 | 466 | ||
| 467 | /// Handle of `crash.log` on the crash-log volume, if it exists. The | ||
| 468 | /// crash-log volume is the one labeled CRASHLOG (a dedicated stick that | ||
| 469 | /// survives reboot writes), else the first mounted volume. Resolves the | ||
| 470 | /// FAT path (`/disk/<uid>/crash.log`) because Disk nodes hold no cached | ||
| 471 | /// children. | ||
| 472 | pub fn read_crash_log() -> Option<usize> { | ||
| 473 | let disk_dir = lookup(0, "disk")?; | ||
| 474 | let children = match &nodes()[disk_dir].data { | ||
| 475 | NodeData::Dir(v) => v.clone(), | ||
| 476 | _ => return None, | ||
| 477 | }; | ||
| 478 | // Prefer a disk node whose name starts with "CRASHLOG-". | ||
| 479 | let mut first = None; | ||
| 480 | for &c in &children { | ||
| 481 | if nodes()[c].removed || !matches!(nodes()[c].data, NodeData::Disk(_)) { | ||
| 482 | continue; | ||
| 483 | } | ||
| 484 | let name = &nodes()[c].name; | ||
| 485 | let label = name.split_once('-').map(|(l, _)| l).unwrap_or(name); | ||
| 486 | if label.eq_ignore_ascii_case("CRASHLOG") { | ||
| 487 | return resolve(&alloc::format!("/disk/{}/crash.log", name)); | ||
| 488 | } | ||
| 489 | if first.is_none() { | ||
| 490 | first = Some(c); | ||
| 491 | } | ||
| 492 | } | ||
| 493 | first.and_then(|disk| { | ||
| 494 | let name = nodes()[disk].name.clone(); | ||
| 495 | resolve(&alloc::format!("/disk/{}/crash.log", name)) | ||
| 496 | }) | ||
| 497 | } | ||
| 498 | |||
| 499 | /// Persist a crash report to the mounted FAT volume so it survives a reboot | ||
| 500 | /// (real hardware has no serial console). Best-effort: any failure is | ||
| 501 | /// ignored — the caller (fault handler) must never fault here; the | ||
| 502 | /// in-dump ISR guard catches that case and halts. | ||
| 503 | /// | ||
| 504 | /// Goes straight at the FAT layer (the crash-log volume + its root | ||
| 505 | /// cluster) rather than through the fs node table: the fault may have | ||
| 506 | /// interrupted the fs mid-operation, and a stale DiskInfo/ACTIVE_VOL | ||
| 507 | /// would make node-based writes fail with "no volume at slot N". The | ||
| 508 | /// crash-log volume is the one labeled CRASHLOG (a dedicated stick that | ||
| 509 | /// survives reboot writes), else slot 0 (first mounted) as before. | ||
| 510 | pub fn write_crash_log(lines: &[&str]) { | ||
| 511 | // Assemble the report on the stack (heap-free — a corrupted heap is | ||
| 512 | // exactly the situation that produces crash reports). | ||
| 513 | let mut text = [0u8; 640]; | ||
| 514 | let mut n = 0usize; | ||
| 515 | for line in lines { | ||
| 516 | for &b in line.as_bytes() { | ||
| 517 | if n < text.len() { | ||
| 518 | text[n] = b; | ||
| 519 | n += 1; | ||
| 520 | } | ||
| 521 | } | ||
| 522 | if n < text.len() { | ||
| 523 | text[n] = b'\n'; | ||
| 524 | n += 1; | ||
| 525 | } | ||
| 526 | } | ||
| 527 | let Some(vol) = crate::fat::crash_vol() else { | ||
| 528 | crate::serial::write_raw("crashlog: no volume\r\n"); | ||
| 529 | return; | ||
| 530 | }; | ||
| 531 | // Trace the target so a test can verify the right stick got the log. | ||
| 532 | let label = crate::fat::volume_label(vol).unwrap_or("?"); | ||
| 533 | let mut tb = [0u8; 48]; | ||
| 534 | let tn = { | ||
| 535 | let mut w = crate::strbuf::FmtBuf::new(&mut tb); | ||
| 536 | let _ = core::fmt::write(&mut w, core::format_args!("crashlog: vol {} ({})\r\n", vol, label)); | ||
| 537 | w.len() | ||
| 538 | }; | ||
| 539 | crate::serial::write_raw(core::str::from_utf8(&tb[..tn]).unwrap_or("?")); | ||
| 540 | let root = crate::fat::root_cluster(vol); | ||
| 541 | if crate::fat::create(vol, root, "crash.log", false).is_err() | ||
| 542 | && crate::fat::find(vol, root, "crash.log").is_err() | ||
| 543 | { | ||
| 544 | crate::serial::write_raw("crashlog: create failed\r\n"); | ||
| 545 | return; | ||
| 546 | } | ||
| 547 | if crate::fat::set_len(vol, root, "crash.log", 0).is_err() { | ||
| 548 | crate::serial::write_raw("crashlog: set_len failed\r\n"); | ||
| 549 | return; | ||
| 550 | } | ||
| 551 | match crate::fat::write_file(vol, root, "crash.log", &text[..n], 0) { | ||
| 552 | Ok(_) => crate::serial::write_raw("crashlog: wrote\r\n"), | ||
| 553 | Err(_) => crate::serial::write_raw("crashlog: write failed\r\n"), | ||
| 554 | } | ||
| 555 | } | ||
| 556 | |||
| 467 | /// Truncate/resize a regular file. | 557 | /// Truncate/resize a regular file. |
| 468 | pub fn set_len(h: usize, len: u64) -> Result<(), ()> { | 558 | pub fn set_len(h: usize, len: u64) -> Result<(), ()> { match &mut nodes()[h].data { |
| 469 | match &mut nodes()[h].data { | ||
| 470 | NodeData::Regular(v) => { | 559 | NodeData::Regular(v) => { |
| 471 | v.resize(len as usize, 0); | 560 | v.resize(len as usize, 0); |
| 472 | Ok(()) | 561 | Ok(()) |
diff --git a/rsnix/src/gdt.rs b/rsnix/src/gdt.rs index 514ed2e..f3d8b9a 100644 --- a/rsnix/src/gdt.rs +++ b/rsnix/src/gdt.rs | |||
| @@ -116,12 +116,14 @@ impl Tss { | |||
| 116 | } | 116 | } |
| 117 | } | 117 | } |
| 118 | 118 | ||
| 119 | /// 4 KiB page-aligned block for the IST stacks. | 119 | /// IST stack size: the fault path (fault dump + best-effort crash-log |
| 120 | #[repr(align(4096))] | 120 | /// FAT write) is deep, so each IST stack is 32 KiB, allocated from the |
| 121 | struct Page([u8; 4096]); | 121 | /// heap at init. They must NOT live in .bss: a 4 KiB .bss stack placed |
| 122 | /// next to kernel statics silently overwrites them on overflow (the FAT | ||
| 123 | /// ACTIVE_VOL was clobbered this way — a "no volume at slot 8" crash in | ||
| 124 | /// the fault path). The heap region is far from the kernel image. | ||
| 125 | const IST_STACK_SIZE: usize = 32 * 1024; | ||
| 122 | 126 | ||
| 123 | static mut PF_STACK: Page = Page([0; 4096]); | ||
| 124 | static mut DF_STACK: Page = Page([0; 4096]); | ||
| 125 | static mut TSS: Tss = Tss::zero(); | 127 | static mut TSS: Tss = Tss::zero(); |
| 126 | // GDT slots are 8 bytes except the TSS descriptor at index 63, which is | 128 | // GDT slots are 8 bytes except the TSS descriptor at index 63, which is |
| 127 | // 16 bytes — total 64*8 + 16. | 129 | // 16 bytes — total 64*8 + 16. |
| @@ -193,8 +195,16 @@ pub fn init() -> bool { | |||
| 193 | 195 | ||
| 194 | // TSS descriptor (16 bytes) at index 63. | 196 | // TSS descriptor (16 bytes) at index 63. |
| 195 | let tss_base = core::ptr::addr_of_mut!(TSS) as usize as u64; | 197 | let tss_base = core::ptr::addr_of_mut!(TSS) as usize as u64; |
| 196 | TSS.ist1 = core::ptr::addr_of_mut!(PF_STACK.0[0]) as usize as u64 + 4096; | 198 | // IST stacks come from the heap (see IST_STACK_SIZE): 32 KiB each, |
| 197 | TSS.ist2 = core::ptr::addr_of_mut!(DF_STACK.0[0]) as usize as u64 + 4096; | 199 | // well away from the kernel image/.bss so an overflowing fault |
| 200 | // path can't silently overwrite statics. | ||
| 201 | let pf = crate::memory::allocate(IST_STACK_SIZE); | ||
| 202 | let df = crate::memory::allocate(IST_STACK_SIZE); | ||
| 203 | if pf.is_null() || df.is_null() { | ||
| 204 | return false; | ||
| 205 | } | ||
| 206 | TSS.ist1 = pf as u64 + IST_STACK_SIZE as u64; | ||
| 207 | TSS.ist2 = df as u64 + IST_STACK_SIZE as u64; | ||
| 198 | TSS.iomap_base = TSS_SIZE as u16; // no I/O bitmap | 208 | TSS.iomap_base = TSS_SIZE as u16; // no I/O bitmap |
| 199 | 209 | ||
| 200 | let mut td = [0u8; 16]; | 210 | let mut td = [0u8; 16]; |
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index d37468f..7aeb224 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs | |||
| @@ -257,7 +257,8 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { | |||
| 257 | let words = (fbinfo.pitch * fbinfo.height) / 4; | 257 | let words = (fbinfo.pitch * fbinfo.height) / 4; |
| 258 | if words > 0 { | 258 | if words > 0 { |
| 259 | let bytes = words * 4; | 259 | let bytes = words * 4; |
| 260 | #[allow(clippy::slow_vector_initialization)] // deliberate: try_reserve must not panic | 260 | #[allow(clippy::slow_vector_initialization)] |
| 261 | // deliberate: try_reserve must not panic | ||
| 261 | let mut v: alloc::vec::Vec<u32> = alloc::vec::Vec::new(); | 262 | let mut v: alloc::vec::Vec<u32> = alloc::vec::Vec::new(); |
| 262 | if v.try_reserve_exact(words).is_ok() { | 263 | if v.try_reserve_exact(words).is_ok() { |
| 263 | v.resize(words, 0); // can't fail after the reserve | 264 | v.resize(words, 0); // can't fail after the reserve |
| @@ -284,8 +285,7 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { | |||
| 284 | rbase + rsize, | 285 | rbase + rsize, |
| 285 | ); | 286 | ); |
| 286 | if SHADOW.is_null() { | 287 | if SHADOW.is_null() { |
| 287 | let msg = | 288 | let msg = "gop: WARNING — shadow rejected (fb overlap); no-shadow fallback"; |
| 288 | "gop: WARNING — shadow rejected (fb overlap); no-shadow fallback"; | ||
| 289 | crate::serial_println!("{}", msg); | 289 | crate::serial_println!("{}", msg); |
| 290 | let lines: alloc::vec::Vec<&str> = msg | 290 | let lines: alloc::vec::Vec<&str> = msg |
| 291 | .as_bytes() | 291 | .as_bytes() |
diff --git a/rsnix/src/interrupts.rs b/rsnix/src/interrupts.rs index be252df..927e4f1 100644 --- a/rsnix/src/interrupts.rs +++ b/rsnix/src/interrupts.rs | |||
| @@ -393,6 +393,38 @@ pub(crate) struct IntFrame { | |||
| 393 | pub(crate) ss: u64, | 393 | pub(crate) ss: u64, |
| 394 | } | 394 | } |
| 395 | 395 | ||
| 396 | // --------------------------------------------------------------------------- | ||
| 397 | // Fault-dump re-entry guard | ||
| 398 | // --------------------------------------------------------------------------- | ||
| 399 | |||
| 400 | /// Set when `sched::fault_dump` is running. Any CPU exception that | ||
| 401 | /// fires *while the dump is already in progress* (typically because the | ||
| 402 | /// heap is corrupted and the dump path tripped over it) must never | ||
| 403 | /// recurse into the dump — that chain (#PF → #DF → #PF) is a triple | ||
| 404 | /// fault, which silently resets the machine with no report (black | ||
| 405 | /// screen, firmware splash — exactly what real hardware showed). The | ||
| 406 | /// guard short-circuits to a raw heap-free band and a hard halt. | ||
| 407 | static mut IN_DUMP: bool = false; | ||
| 408 | |||
| 409 | pub fn enter_dump() { | ||
| 410 | unsafe { | ||
| 411 | IN_DUMP = true; | ||
| 412 | } | ||
| 413 | } | ||
| 414 | |||
| 415 | fn in_dump() -> bool { | ||
| 416 | unsafe { IN_DUMP } | ||
| 417 | } | ||
| 418 | |||
| 419 | /// Raw, heap-free fallback: a fault fired inside the fault dump. | ||
| 420 | fn dump_guard_halt() -> ! { | ||
| 421 | crate::gop::dump_lines(&[ | ||
| 422 | "!!! fault inside fault dump", | ||
| 423 | "!!! halting (no triple fault)", | ||
| 424 | ]); | ||
| 425 | halt(); | ||
| 426 | } | ||
| 427 | |||
| 396 | /// Called from the asm common handler — dispatches to the right Rust | 428 | /// Called from the asm common handler — dispatches to the right Rust |
| 397 | /// handler and returns the frame pointer the asm should switch to (the | 429 | /// handler and returns the frame pointer the asm should switch to (the |
| 398 | /// next task's, or the same one when no switch happens). | 430 | /// next task's, or the same one when no switch happens). |
| @@ -433,9 +465,15 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize { | |||
| 433 | 8 => { | 465 | 8 => { |
| 434 | // Double fault — dump the frame (we're on the IST stack | 466 | // Double fault — dump the frame (we're on the IST stack |
| 435 | // so this is safe) and halt. | 467 | // so this is safe) and halt. |
| 468 | if in_dump() { | ||
| 469 | dump_guard_halt(); | ||
| 470 | } | ||
| 436 | crate::sched::fault_dump("double fault", 0, frame) | 471 | crate::sched::fault_dump("double fault", 0, frame) |
| 437 | } | 472 | } |
| 438 | 14 => { | 473 | 14 => { |
| 474 | if in_dump() { | ||
| 475 | dump_guard_halt(); | ||
| 476 | } | ||
| 439 | let cr2: u64; | 477 | let cr2: u64; |
| 440 | core::arch::asm!("mov {}, cr2", out(reg) cr2, | 478 | core::arch::asm!("mov {}, cr2", out(reg) cr2, |
| 441 | options(nostack, preserves_flags)); | 479 | options(nostack, preserves_flags)); |
| @@ -445,6 +483,9 @@ unsafe extern "C" fn isr_dispatch(frame: &IntFrame) -> usize { | |||
| 445 | v if v < 32 => { | 483 | v if v < 32 => { |
| 446 | // Any other CPU exception: full dump (screen + serial), | 484 | // Any other CPU exception: full dump (screen + serial), |
| 447 | // then halt the shell or abort the faulting background task. | 485 | // then halt the shell or abort the faulting background task. |
| 486 | if in_dump() { | ||
| 487 | dump_guard_halt(); | ||
| 488 | } | ||
| 448 | crate::sched::fault_dump("exception", 0, frame) | 489 | crate::sched::fault_dump("exception", 0, frame) |
| 449 | } | 490 | } |
| 450 | _ => fp, | 491 | _ => fp, |
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs index 605125a..e6896f2 100644 --- a/rsnix/src/memory.rs +++ b/rsnix/src/memory.rs | |||
| @@ -76,6 +76,31 @@ static mut HEAP_END: usize = 0; | |||
| 76 | /// Total heap capacity — the original region plus pages added by growth. | 76 | /// Total heap capacity — the original region plus pages added by growth. |
| 77 | static mut HEAP_CAPACITY: usize = 0; | 77 | static mut HEAP_CAPACITY: usize = 0; |
| 78 | 78 | ||
| 79 | /// Kernel image bounds (base..end) captured from the UEFI LoadedImage | ||
| 80 | /// protocol before ExitBootServices. Both the heap free list and the | ||
| 81 | /// page-allocator bitmap reserve this range so no allocation can ever | ||
| 82 | /// land on the running kernel's own code/data/bss pages. | ||
| 83 | static mut KERNEL_IMAGE_START: usize = 0; | ||
| 84 | static mut KERNEL_IMAGE_END: usize = 0; | ||
| 85 | |||
| 86 | /// Record the loaded-image memory range (from EFI_LOADED_IMAGE_PROTOCOL). | ||
| 87 | pub fn set_kernel_image_range(base: usize, size: usize) { | ||
| 88 | unsafe { | ||
| 89 | KERNEL_IMAGE_START = base; | ||
| 90 | KERNEL_IMAGE_END = base + size; | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | fn kernel_image_range() -> Option<(usize, usize)> { | ||
| 95 | unsafe { | ||
| 96 | if KERNEL_IMAGE_END > KERNEL_IMAGE_START { | ||
| 97 | Some((KERNEL_IMAGE_START, KERNEL_IMAGE_END)) | ||
| 98 | } else { | ||
| 99 | None | ||
| 100 | } | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 79 | /// Minimum pages `grow_heap` takes from the page allocator per growth. | 104 | /// Minimum pages `grow_heap` takes from the page allocator per growth. |
| 80 | const HEAP_GROW_PAGES: usize = 64; | 105 | const HEAP_GROW_PAGES: usize = 64; |
| 81 | 106 | ||
| @@ -354,11 +379,16 @@ pub unsafe fn refresh_and_exit(bs: &BootServices, image_handle: Handle) -> Resul | |||
| 354 | 379 | ||
| 355 | fn init_heap() { | 380 | fn init_heap() { |
| 356 | unsafe { | 381 | unsafe { |
| 357 | // Region the firmware maps for the GOP framebuffer, if known — | 382 | // Regions that must never be handed out as heap memory, even when |
| 358 | // exclude it even when the memory map lists it as conventional | 383 | // the firmware lists them as conventional: |
| 359 | // (some real firmware does exactly that, which would let heap | 384 | // * the GOP framebuffer (heap allocations overwriting the display), |
| 360 | // allocations overwrite the display). | 385 | // * the kernel image itself (a heap allocation landing on the |
| 361 | let fb = crate::gop::framebuffer_region(); | 386 | // kernel's .bss would silently corrupt statics — observed as a |
| 387 | // garbage ACTIVE_VOL and a dead filesystem in the fault path). | ||
| 388 | let holes: [(usize, usize); 2] = [ | ||
| 389 | crate::gop::framebuffer_region().unwrap_or((0, 0)), | ||
| 390 | kernel_image_range().unwrap_or((0, 0)), | ||
| 391 | ]; | ||
| 362 | let mut best_start = 0usize; | 392 | let mut best_start = 0usize; |
| 363 | let mut best_size = 0usize; | 393 | let mut best_size = 0usize; |
| 364 | for i in 0..MAP_ENTRIES { | 394 | for i in 0..MAP_ENTRIES { |
| @@ -366,26 +396,25 @@ fn init_heap() { | |||
| 366 | if d.kind == MEMORY_TYPE_CONVENTIONAL { | 396 | if d.kind == MEMORY_TYPE_CONVENTIONAL { |
| 367 | let s = d.physical_start as usize; | 397 | let s = d.physical_start as usize; |
| 368 | let e = s + d.number_of_pages as usize * 4096; | 398 | let e = s + d.number_of_pages as usize * 4096; |
| 369 | // Split at the framebuffer hole so the free list NEVER | 399 | // Split at each hole so the free list NEVER contains |
| 370 | // contains fb pages: a coarse size subtraction leaves one | 400 | // reserved pages: a coarse size subtraction leaves one |
| 371 | // block *spanning* the hole, letting a large allocation | 401 | // block *spanning* the hole, letting a large allocation |
| 372 | // (e.g. the 1.9 MB display shadow) straddle the display. | 402 | // (e.g. the 1.9 MB display shadow) straddle it. |
| 373 | let mut lo = s; | 403 | let mut lo = s; |
| 374 | if let Some((b, sz)) = fb | 404 | for (b, sz) in holes { |
| 375 | && e > b | 405 | if sz > 0 && e > b && s < b + sz { |
| 376 | && s < b + sz | 406 | let h0 = b.max(s); |
| 377 | { | 407 | let h1 = (b + sz).min(e); |
| 378 | let h0 = b.max(s); | 408 | if h0 > lo { |
| 379 | let h1 = (b + sz).min(e); | 409 | // Segment before the hole. |
| 380 | if h0 > lo { | 410 | let seg = h0 - lo; |
| 381 | // Segment before the hole. | 411 | if seg > best_size { |
| 382 | let seg = h0 - lo; | 412 | best_start = lo; |
| 383 | if seg > best_size { | 413 | best_size = seg; |
| 384 | best_start = lo; | 414 | } |
| 385 | best_size = seg; | ||
| 386 | } | 415 | } |
| 416 | lo = h1; | ||
| 387 | } | 417 | } |
| 388 | lo = h1; | ||
| 389 | } | 418 | } |
| 390 | if e > lo { | 419 | if e > lo { |
| 391 | let seg = e - lo; | 420 | let seg = e - lo; |
| @@ -664,6 +693,23 @@ pub fn init_page_allocator() -> bool { | |||
| 664 | } | 693 | } |
| 665 | } | 694 | } |
| 666 | 695 | ||
| 696 | // Reserve the kernel image so the page allocator never hands out | ||
| 697 | // the running kernel's own code/data/bss pages (DMA buffers and | ||
| 698 | // task stacks landing on the kernel would corrupt it — a heap | ||
| 699 | // allocation over the .bss already broke the FAT ACTIVE_VOL). | ||
| 700 | if let Some((ks, ke)) = kernel_image_range() | ||
| 701 | && ke > ks | ||
| 702 | { | ||
| 703 | let s = ks / PAGE_SIZE; | ||
| 704 | let e = ke.div_ceil(PAGE_SIZE); | ||
| 705 | for p in s..e { | ||
| 706 | let idx = p - base_page; | ||
| 707 | if idx < span_pages { | ||
| 708 | *bm.add(idx / 8) |= 1 << (idx % 8); | ||
| 709 | } | ||
| 710 | } | ||
| 711 | } | ||
| 712 | |||
| 667 | // Reserve the GOP framebuffer region even if the map lists it as | 713 | // Reserve the GOP framebuffer region even if the map lists it as |
| 668 | // conventional — kernel allocations (stacks, DMA buffers) must | 714 | // conventional — kernel allocations (stacks, DMA buffers) must |
| 669 | // never land on the display. | 715 | // never land on the display. |
diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs index f48e210..a195d1c 100644 --- a/rsnix/src/sched.rs +++ b/rsnix/src/sched.rs | |||
| @@ -28,9 +28,7 @@ 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; | ||
| 32 | use alloc::string::String; | 31 | use alloc::string::String; |
| 33 | use alloc::string::ToString; | ||
| 34 | use alloc::vec::Vec; | 32 | use alloc::vec::Vec; |
| 35 | 33 | ||
| 36 | use crate::interrupts::IntFrame; | 34 | use crate::interrupts::IntFrame; |
| @@ -40,7 +38,7 @@ use crate::paging; | |||
| 40 | pub const MAX_TASKS: usize = 16; | 38 | pub const MAX_TASKS: usize = 16; |
| 41 | pub const SHELL_TID: usize = 0; | 39 | pub const SHELL_TID: usize = 0; |
| 42 | pub const IDLE_TID: usize = 1; | 40 | pub const IDLE_TID: usize = 1; |
| 43 | const STACK_PAGES: usize = 3; // 1 guard page + 8 KiB stack | 41 | const STACK_PAGES: usize = 9; // 1 guard page + 32 KiB stack — faults dump on the task's own stack |
| 44 | const NAME_MAX: usize = 24; | 42 | const NAME_MAX: usize = 24; |
| 45 | const PANIC_MAX: usize = 64; | 43 | const PANIC_MAX: usize = 64; |
| 46 | 44 | ||
| @@ -556,8 +554,15 @@ pub fn abort_current(msg: &str) -> ! { | |||
| 556 | /// Real hardware has no serial console, so the screen dump is what lets | 554 | /// Real hardware has no serial console, so the screen dump is what lets |
| 557 | /// a user report a crash precisely — vector, fault address, RIP and task. | 555 | /// a user report a crash precisely — vector, fault address, RIP and task. |
| 558 | /// Runs with interrupts disabled (we're inside the ISR). | 556 | /// Runs with interrupts disabled (we're inside the ISR). |
| 557 | /// | ||
| 558 | /// Heap-free by construction: the dump's own `alloc::format!` used to | ||
| 559 | /// allocate from the heap, and when the underlying fault *was* heap | ||
| 560 | /// corruption the reporter itself died (#PF → #DF → triple fault → | ||
| 561 | /// silent reboot). Everything here formats into stack buffers, and the | ||
| 562 | /// ISR `in_dump` guard halts instead of recursing if even that faults. | ||
| 559 | #[allow(clippy::needless_return)] | 563 | #[allow(clippy::needless_return)] |
| 560 | pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFrame) -> ! { | 564 | pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFrame) -> ! { |
| 565 | crate::interrupts::enter_dump(); | ||
| 561 | let vector = frame.vector; | 566 | let vector = frame.vector; |
| 562 | let error = frame.error; | 567 | let error = frame.error; |
| 563 | let rip = frame.rip; | 568 | let rip = frame.rip; |
| @@ -565,35 +570,79 @@ pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFra | |||
| 565 | let rsp = frame.rsp; | 570 | let rsp = frame.rsp; |
| 566 | let id = unsafe { CURRENT }; | 571 | let id = unsafe { CURRENT }; |
| 567 | let name = name_str(id); | 572 | let name = name_str(id); |
| 568 | let name_buf = name.to_string(); | ||
| 569 | let in_kernel = unsafe { CURRENT } <= IDLE_TID; | 573 | let in_kernel = unsafe { CURRENT } <= IDLE_TID; |
| 570 | 574 | ||
| 571 | // Serial: a full, parseable line. | 575 | // Serial: a full, parseable line (stack-formatted). |
| 572 | crate::serial_println!( | 576 | let mut buf = [0u8; 176]; |
| 573 | "!!! {} vec={} err={:#x} rip={:#x} cr2={:#x} rsp={:#x} rflags={:#x} task={} \"{}\"", | 577 | let n = { |
| 574 | kind, | 578 | let mut w = crate::strbuf::FmtBuf::new(&mut buf); |
| 575 | vector, | 579 | let _ = fmt::write( |
| 576 | error, | 580 | &mut w, |
| 577 | rip, | 581 | core::format_args!( |
| 578 | cr2, | 582 | "!!! {} vec={} err={:#x} rip={:#x} cr2={:#x} rsp={:#x} rflags={:#x} task={} \"{}\"", |
| 579 | rsp, | 583 | kind, |
| 580 | rflags, | 584 | vector, |
| 581 | id, | 585 | error, |
| 582 | name_buf | 586 | rip, |
| 583 | ); | 587 | cr2, |
| 584 | crate::serial_println!("!!! halting (shell task fault — machine stops)"); | 588 | rsp, |
| 585 | 589 | rflags, | |
| 586 | // Screen: white-on-red band via the clamped raw dump path. | 590 | id, |
| 587 | let l1 = alloc::format!("!!! {} vec={} err={:#x}", kind, vector, error); | 591 | name |
| 588 | let l2 = alloc::format!("rip={:#x} cr2={:#x}", rip, cr2); | 592 | ), |
| 589 | let l3 = alloc::format!("rsp={:#x} task={} {}", rsp, id, name_buf); | 593 | ); |
| 590 | let lines = [l1.as_str(), l2.as_str(), l3.as_str()]; | 594 | w.len() |
| 595 | }; | ||
| 596 | let line = core::str::from_utf8(&buf[..n]).unwrap_or("!!! dump"); | ||
| 597 | crate::serial::write_str(line); | ||
| 598 | crate::serial::write_str("\r\n!!! halting (shell task fault — machine stops)\r\n"); | ||
| 599 | |||
| 600 | // Screen: white-on-red band via the clamped raw dump path (heap-free). | ||
| 601 | let mut b1 = [0u8; 72]; | ||
| 602 | let mut b2 = [0u8; 72]; | ||
| 603 | let mut b3 = [0u8; 96]; | ||
| 604 | let n1 = { | ||
| 605 | let mut w = crate::strbuf::FmtBuf::new(&mut b1); | ||
| 606 | let _ = fmt::write( | ||
| 607 | &mut w, | ||
| 608 | core::format_args!("!!! {} vec={} err={:#x}", kind, vector, error), | ||
| 609 | ); | ||
| 610 | w.len() | ||
| 611 | }; | ||
| 612 | let n2 = { | ||
| 613 | let mut w = crate::strbuf::FmtBuf::new(&mut b2); | ||
| 614 | let _ = fmt::write(&mut w, core::format_args!("rip={:#x} cr2={:#x}", rip, cr2)); | ||
| 615 | w.len() | ||
| 616 | }; | ||
| 617 | let n3 = { | ||
| 618 | let mut w = crate::strbuf::FmtBuf::new(&mut b3); | ||
| 619 | let _ = fmt::write( | ||
| 620 | &mut w, | ||
| 621 | core::format_args!("rsp={:#x} task={} {}", rsp, id, name), | ||
| 622 | ); | ||
| 623 | w.len() | ||
| 624 | }; | ||
| 625 | let l1 = core::str::from_utf8(&b1[..n1]).unwrap_or("!!!"); | ||
| 626 | let l2 = core::str::from_utf8(&b2[..n2]).unwrap_or(""); | ||
| 627 | let l3 = core::str::from_utf8(&b3[..n3]).unwrap_or(""); | ||
| 628 | let lines = [l1, l2, l3]; | ||
| 591 | crate::gop::dump_lines(&lines); | 629 | crate::gop::dump_lines(&lines); |
| 592 | 630 | ||
| 631 | // Best-effort: persist the report to the disk so it survives the | ||
| 632 | // reboot on real hardware (no serial console). Any fault here is | ||
| 633 | // caught by the in_dump guard and halts. | ||
| 634 | crate::fs::write_crash_log(&lines); | ||
| 635 | |||
| 593 | if in_kernel { | 636 | if in_kernel { |
| 594 | crate::interrupts::halt() | 637 | crate::interrupts::halt() |
| 595 | } else { | 638 | } else { |
| 596 | abort_current(&format!("{} vec={}", kind, vector)) | 639 | let mut mb = [0u8; 48]; |
| 640 | let mn = { | ||
| 641 | let mut w = crate::strbuf::FmtBuf::new(&mut mb); | ||
| 642 | let _ = fmt::write(&mut w, core::format_args!("{} vec={}", kind, vector)); | ||
| 643 | w.len() | ||
| 644 | }; | ||
| 645 | abort_current(core::str::from_utf8(&mb[..mn]).unwrap_or("fault")) | ||
| 597 | } | 646 | } |
| 598 | } | 647 | } |
| 599 | 648 | ||
diff --git a/rsnix/src/uefi.rs b/rsnix/src/uefi.rs index c311ad9..234ad65 100644 --- a/rsnix/src/uefi.rs +++ b/rsnix/src/uefi.rs | |||
| @@ -40,6 +40,32 @@ pub const EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID: Guid = Guid { | |||
| 40 | d: [0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a], | 40 | d: [0x96, 0xfb, 0x7a, 0xde, 0xd0, 0x80, 0x51, 0x6a], |
| 41 | }; | 41 | }; |
| 42 | 42 | ||
| 43 | pub const EFI_LOADED_IMAGE_PROTOCOL_GUID: Guid = Guid { | ||
| 44 | a: 0x5b1b31a1, | ||
| 45 | b: 0x9562, | ||
| 46 | c: 0x11d2, | ||
| 47 | d: [0x8e, 0x3f, 0x00, 0xa0, 0xc9, 0x69, 0x72, 0x3b], | ||
| 48 | }; | ||
| 49 | |||
| 50 | /// Minimal EFI_LOADED_IMAGE_PROTOCOL — we only need the image base/size | ||
| 51 | /// to reserve the kernel's own code/data/bss pages so the heap and page | ||
| 52 | /// allocators can never overwrite the running kernel. | ||
| 53 | #[repr(C)] | ||
| 54 | pub struct LoadedImageProtocol { | ||
| 55 | pub revision: u32, | ||
| 56 | _pad0: u32, | ||
| 57 | pub parent_handle: Handle, | ||
| 58 | pub system_table: *mut core::ffi::c_void, | ||
| 59 | pub device_handle: Handle, | ||
| 60 | pub file_path: *mut core::ffi::c_void, | ||
| 61 | _reserved: *mut core::ffi::c_void, | ||
| 62 | pub load_options_size: u32, | ||
| 63 | _pad1: u32, | ||
| 64 | pub load_options: *mut core::ffi::c_void, | ||
| 65 | pub image_base: *mut core::ffi::c_void, | ||
| 66 | pub image_size: u64, | ||
| 67 | } | ||
| 68 | |||
| 43 | // --------------------------------------------------------------------------- | 69 | // --------------------------------------------------------------------------- |
| 44 | // Table header | 70 | // Table header |
| 45 | // --------------------------------------------------------------------------- | 71 | // --------------------------------------------------------------------------- |
diff --git a/scripts/mkdisk.sh b/scripts/mkdisk.sh index 6bdd50c..2c62f76 100755 --- a/scripts/mkdisk.sh +++ b/scripts/mkdisk.sh | |||
| @@ -9,6 +9,7 @@ | |||
| 9 | # ./scripts/mkdisk.sh --size 128M # different size (default 64M) | 9 | # ./scripts/mkdisk.sh --size 128M # different size (default 64M) |
| 10 | # ./scripts/mkdisk.sh /path/img # custom image path | 10 | # ./scripts/mkdisk.sh /path/img # custom image path |
| 11 | # ./scripts/mkdisk.sh --no-seed # skip the seed/ directory | 11 | # ./scripts/mkdisk.sh --no-seed # skip the seed/ directory |
| 12 | # ./scripts/mkdisk.sh --label CRASHLOG # volume label (default RSDOS) | ||
| 12 | # | 13 | # |
| 13 | # The image is created with `mkfs.fat -C`, which is the option that | 14 | # The image is created with `mkfs.fat -C`, which is the option that |
| 14 | # actually creates the file — plain `mkfs.fat` only writes into an | 15 | # actually creates the file — plain `mkfs.fat` only writes into an |
| @@ -23,6 +24,7 @@ IMG="/tmp/disk.img" | |||
| 23 | SIZE="64M" | 24 | SIZE="64M" |
| 24 | FRESH=0 | 25 | FRESH=0 |
| 25 | SEED_DIR="$PROJECT_DIR/seed" | 26 | SEED_DIR="$PROJECT_DIR/seed" |
| 27 | LABEL="RSDOS" | ||
| 26 | 28 | ||
| 27 | while [ $# -gt 0 ]; do | 29 | while [ $# -gt 0 ]; do |
| 28 | case "$1" in | 30 | case "$1" in |
| @@ -32,6 +34,10 @@ while [ $# -gt 0 ]; do | |||
| 32 | SIZE="$1" | 34 | SIZE="$1" |
| 33 | ;; | 35 | ;; |
| 34 | --no-seed) SEED_DIR="" ;; | 36 | --no-seed) SEED_DIR="" ;; |
| 37 | --label) | ||
| 38 | shift | ||
| 39 | LABEL="$1" | ||
| 40 | ;; | ||
| 35 | --help | -h) | 41 | --help | -h) |
| 36 | head -20 "$0" | grep "^#" | sed 's/^# \?//' | 42 | head -20 "$0" | grep "^#" | sed 's/^# \?//' |
| 37 | exit 0 | 43 | exit 0 |
| @@ -65,9 +71,9 @@ if [ -z "$SIZE_BLOCKS" ] || [ "$SIZE_BLOCKS" -le 0 ] 2>/dev/null; then | |||
| 65 | exit 1 | 71 | exit 1 |
| 66 | fi | 72 | fi |
| 67 | 73 | ||
| 68 | echo "mkdisk: creating $IMG ($SIZE FAT32, label RSDOS)" | 74 | echo "mkdisk: creating $IMG ($SIZE FAT32, label $LABEL)" |
| 69 | rm -f "$IMG" | 75 | rm -f "$IMG" |
| 70 | mkfs.fat -C -F 32 -n RSDOS "$IMG" "$SIZE_BLOCKS" | 76 | mkfs.fat -C -F 32 -n "$LABEL" "$IMG" "$SIZE_BLOCKS" |
| 71 | 77 | ||
| 72 | if [ -n "$SEED_DIR" ] && [ -d "$SEED_DIR" ]; then | 78 | if [ -n "$SEED_DIR" ] && [ -d "$SEED_DIR" ]; then |
| 73 | if ! command -v mcopy >/dev/null 2>&1; then | 79 | if ! command -v mcopy >/dev/null 2>&1; then |
diff --git a/scripts/test-fault.sh b/scripts/test-fault.sh index 49ba791..59f741f 100755 --- a/scripts/test-fault.sh +++ b/scripts/test-fault.sh | |||
| @@ -52,12 +52,18 @@ echo "==> 2/5 deploy to ESP" | |||
| 52 | mkdir -p "$ESP_DIR/EFI/BOOT" | 52 | mkdir -p "$ESP_DIR/EFI/BOOT" |
| 53 | cp target/x86_64-unknown-uefi/debug/rsdos.efi "$ESP_DIR/EFI/BOOT/BOOTX64.EFI" | 53 | cp target/x86_64-unknown-uefi/debug/rsdos.efi "$ESP_DIR/EFI/BOOT/BOOTX64.EFI" |
| 54 | 54 | ||
| 55 | echo "==> 3/5 boot QEMU (headless)" | 55 | echo "==> 3/6 boot QEMU (headless, + dedicated CRASHLOG stick)" |
| 56 | kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true | 56 | kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true |
| 57 | sleep 1 | 57 | sleep 1 |
| 58 | rm -f "$SOCK" "$LOG" | 58 | rm -f "$SOCK" "$LOG" |
| 59 | # A second FAT32 stick labeled CRASHLOG: crash reports must land here | ||
| 60 | # (label-selected by the kernel), not on the boot device. | ||
| 61 | CRASH_IMG=/tmp/rsdos-crash.img | ||
| 62 | "$SCRIPT_DIR/mkdisk.sh" --fresh --label CRASHLOG --no-seed "$CRASH_IMG" >/dev/null | ||
| 59 | "$QEMU" -bios "$OVMF" \ | 63 | "$QEMU" -bios "$OVMF" \ |
| 60 | -drive file=fat:rw:"$ESP_DIR",format=raw \ | 64 | -drive file=fat:rw:"$ESP_DIR",format=raw \ |
| 65 | -drive file="$CRASH_IMG",format=raw,if=none,id=crash \ | ||
| 66 | -device ide-hd,drive=crash,bus=ide.2 \ | ||
| 61 | -serial file:"$LOG" \ | 67 | -serial file:"$LOG" \ |
| 62 | -monitor unix:"$SOCK",server,nowait \ | 68 | -monitor unix:"$SOCK",server,nowait \ |
| 63 | -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \ | 69 | -m 256M -machine q35 -cpu qemu64 -no-reboot -display none \ |
| @@ -66,11 +72,12 @@ echo $! >"$QPID" | |||
| 66 | 72 | ||
| 67 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" | 73 | wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" |
| 68 | 74 | ||
| 69 | echo "==> 4/5 boot diagnostics present" | 75 | echo "==> 4/6 boot diagnostics present" |
| 70 | grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing" | 76 | grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing" |
| 71 | grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing" | 77 | grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing" |
| 78 | grep -aq "CRASHLOG" "$LOG" || fail "CRASHLOG stick not mounted" | ||
| 72 | 79 | ||
| 73 | echo "==> 5/5 inject a page fault (fault command)" | 80 | echo "==> 5/6 inject a page fault (fault command)" |
| 74 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ | 81 | python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ |
| 75 | "clear" \ | 82 | "clear" \ |
| 76 | "fault" | 83 | "fault" |
| @@ -83,8 +90,19 @@ grep -aq "cr2=0x400000000000" "$LOG" || fail "CR2 not reported (faulting address | |||
| 83 | grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified" | 90 | grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified" |
| 84 | grep -aq "!!! halting" "$LOG" || fail "no halt message" | 91 | grep -aq "!!! halting" "$LOG" || fail "no halt message" |
| 85 | 92 | ||
| 93 | echo "==> 6/6 crash.log persisted on the CRASHLOG stick" | ||
| 94 | grep -aq "crashlog: vol .* (CRASHLOG)" "$LOG" || fail "crashlog did not target the CRASHLOG stick" | ||
| 95 | grep -aq "crashlog: wrote" "$LOG" || fail "crashlog write did not complete" | ||
| 96 | mdir -i "$CRASH_IMG" "::" 2>/dev/null | grep -aqi "crash" \ | ||
| 97 | || fail "crash.log missing on the CRASHLOG stick" | ||
| 98 | # The boot ESP must NOT receive the crash log. | ||
| 99 | if [ -f "$ESP_DIR/crash.log" ]; then | ||
| 100 | fail "crash.log landed on the boot ESP instead of the CRASHLOG stick" | ||
| 101 | fi | ||
| 102 | |||
| 86 | # The screen dump itself is exercised in-kernel (dump_lines); the serial | 103 | # The screen dump itself is exercised in-kernel (dump_lines); the serial |
| 87 | # path sharing the same format line is verified above. | 104 | # path sharing the same format line is verified above. |
| 88 | echo "" | 105 | echo "" |
| 89 | echo "PASS — fault dump works (vec, err, rip, cr2, task → screen + serial)" | 106 | echo "PASS — fault dump + crash.log on the CRASHLOG stick" |
| 90 | echo " every CPU exception now halts with a diagnosable dump, not a freeze" | 107 | echo " every CPU exception halts with a diagnosable dump; the report" |
| 108 | echo " survives reboot on the dedicated CRASHLOG volume" | ||
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index b2c4a1b..a32d259 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -41,6 +41,24 @@ pub fn run() -> ! { | |||
| 41 | env!("CARGO_PKG_VERSION") | 41 | env!("CARGO_PKG_VERSION") |
| 42 | )); | 42 | )); |
| 43 | 43 | ||
| 44 | // A crash report from a previous run survives on the disk (the fault | ||
| 45 | // handler writes crash.log on the mounted volume before halting; real | ||
| 46 | // hardware has no serial console). Surface it so the user can report it. | ||
| 47 | if let Some(h) = rsnix::fs::read_crash_log() { | ||
| 48 | term_println("!!! PREVIOUS CRASH REPORT:"); | ||
| 49 | let mut buf = [0u8; 256]; | ||
| 50 | if let Ok(n) = rsnix::fs::read(h, &mut buf, 0) | ||
| 51 | && n > 0 { | ||
| 52 | let s = core::str::from_utf8(&buf[..n]).unwrap_or("?"); | ||
| 53 | for line in s.split('\n') { | ||
| 54 | if !line.is_empty() { | ||
| 55 | term_println(line); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | } | ||
| 59 | term_println("!!! end of crash report"); | ||
| 60 | } | ||
| 61 | |||
| 44 | loop { | 62 | loop { |
| 45 | // Push any deferred scrolls so the prompt renders on a current | 63 | // Push any deferred scrolls so the prompt renders on a current |
| 46 | // framebuffer (e.g. after a streaming command's batch of output). | 64 | // framebuffer (e.g. after a streaming command's batch of output). |
| @@ -722,6 +740,7 @@ write `{} <path> TIL <marker>`", | |||
| 722 | "kill" => cmd_kill(&parsed), | 740 | "kill" => cmd_kill(&parsed), |
| 723 | "demo" => cmd_demo(&parsed), | 741 | "demo" => cmd_demo(&parsed), |
| 724 | "fault" => cmd_fault(), | 742 | "fault" => cmd_fault(), |
| 743 | "crashlog" => cmd_crashlog(), | ||
| 725 | "stall" => cmd_stall(&parsed), | 744 | "stall" => cmd_stall(&parsed), |
| 726 | _ => term_err(&alloc::format!("unknown command: {}", cmd)), | 745 | _ => term_err(&alloc::format!("unknown command: {}", cmd)), |
| 727 | } | 746 | } |
| @@ -820,6 +839,14 @@ fn cmd_fault() { | |||
| 820 | } | 839 | } |
| 821 | } | 840 | } |
| 822 | 841 | ||
| 842 | /// Debug hook: run the crash-log FAT write from the *shell* context (not | ||
| 843 | /// the fault ISR) to compare behaviour. | ||
| 844 | fn cmd_crashlog() { | ||
| 845 | let lines = ["!!! test crashlog write", "second line", "third line"]; | ||
| 846 | rsnix::fs::write_crash_log(&lines); | ||
| 847 | term_println("crashlog: done"); | ||
| 848 | } | ||
| 849 | |||
| 823 | /// Set the display-store stall (cycles per word) — diagnostics knob that | 850 | /// Set the display-store stall (cycles per word) — diagnostics knob that |
| 824 | /// simulates the per-write cost of an uncached real-GPU framebuffer so | 851 | /// simulates the per-write cost of an uncached real-GPU framebuffer so |
| 825 | /// the preemptive scheduler races long scroll pushes like on real | 852 | /// the preemptive scheduler races long scroll pushes like on real |
