summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-02 15:21:34 +0200
committersalmonik <salmonik@tuta.io>2026-08-02 15:21:34 +0200
commitc7d92071e1dd11c45999457b16554b4abaa73b28 (patch)
treeaff7c7f19adcffe1dd91e1c2514af02c77fd524e
parentv0.5.5: fix real-hardware boot crash — shadow back in heap as first allocation (diff)
downloadrs-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.rs37
-rw-r--r--rsnix/src/fat.rs31
-rw-r--r--rsnix/src/fs.rs93
-rw-r--r--rsnix/src/gdt.rs24
-rw-r--r--rsnix/src/gop.rs6
-rw-r--r--rsnix/src/interrupts.rs41
-rw-r--r--rsnix/src/memory.rs88
-rw-r--r--rsnix/src/sched.rs99
-rw-r--r--rsnix/src/uefi.rs26
-rwxr-xr-xscripts/mkdisk.sh10
-rwxr-xr-xscripts/test-fault.sh28
-rw-r--r--shell/src/lib.rs27
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.
77pub 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.
85pub 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.
472pub 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.
510pub 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.
468pub fn set_len(h: usize, len: u64) -> Result<(), ()> { 558pub 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
121struct 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.
125const IST_STACK_SIZE: usize = 32 * 1024;
122 126
123static mut PF_STACK: Page = Page([0; 4096]);
124static mut DF_STACK: Page = Page([0; 4096]);
125static mut TSS: Tss = Tss::zero(); 127static 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.
407static mut IN_DUMP: bool = false;
408
409pub fn enter_dump() {
410 unsafe {
411 IN_DUMP = true;
412 }
413}
414
415fn in_dump() -> bool {
416 unsafe { IN_DUMP }
417}
418
419/// Raw, heap-free fallback: a fault fired inside the fault dump.
420fn 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.
77static mut HEAP_CAPACITY: usize = 0; 77static 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.
83static mut KERNEL_IMAGE_START: usize = 0;
84static mut KERNEL_IMAGE_END: usize = 0;
85
86/// Record the loaded-image memory range (from EFI_LOADED_IMAGE_PROTOCOL).
87pub 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
94fn 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.
80const HEAP_GROW_PAGES: usize = 64; 105const 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
355fn init_heap() { 380fn 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;
28use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; 28use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
29 29
30extern crate alloc; 30extern crate alloc;
31use alloc::format;
32use alloc::string::String; 31use alloc::string::String;
33use alloc::string::ToString;
34use alloc::vec::Vec; 32use alloc::vec::Vec;
35 33
36use crate::interrupts::IntFrame; 34use crate::interrupts::IntFrame;
@@ -40,7 +38,7 @@ use crate::paging;
40pub const MAX_TASKS: usize = 16; 38pub const MAX_TASKS: usize = 16;
41pub const SHELL_TID: usize = 0; 39pub const SHELL_TID: usize = 0;
42pub const IDLE_TID: usize = 1; 40pub const IDLE_TID: usize = 1;
43const STACK_PAGES: usize = 3; // 1 guard page + 8 KiB stack 41const STACK_PAGES: usize = 9; // 1 guard page + 32 KiB stack — faults dump on the task's own stack
44const NAME_MAX: usize = 24; 42const NAME_MAX: usize = 24;
45const PANIC_MAX: usize = 64; 43const 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)]
560pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFrame) -> ! { 564pub(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
43pub 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)]
54pub 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"
23SIZE="64M" 24SIZE="64M"
24FRESH=0 25FRESH=0
25SEED_DIR="$PROJECT_DIR/seed" 26SEED_DIR="$PROJECT_DIR/seed"
27LABEL="RSDOS"
26 28
27while [ $# -gt 0 ]; do 29while [ $# -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
66fi 72fi
67 73
68echo "mkdisk: creating $IMG ($SIZE FAT32, label RSDOS)" 74echo "mkdisk: creating $IMG ($SIZE FAT32, label $LABEL)"
69rm -f "$IMG" 75rm -f "$IMG"
70mkfs.fat -C -F 32 -n RSDOS "$IMG" "$SIZE_BLOCKS" 76mkfs.fat -C -F 32 -n "$LABEL" "$IMG" "$SIZE_BLOCKS"
71 77
72if [ -n "$SEED_DIR" ] && [ -d "$SEED_DIR" ]; then 78if [ -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"
52mkdir -p "$ESP_DIR/EFI/BOOT" 52mkdir -p "$ESP_DIR/EFI/BOOT"
53cp target/x86_64-unknown-uefi/debug/rsdos.efi "$ESP_DIR/EFI/BOOT/BOOTX64.EFI" 53cp target/x86_64-unknown-uefi/debug/rsdos.efi "$ESP_DIR/EFI/BOOT/BOOTX64.EFI"
54 54
55echo "==> 3/5 boot QEMU (headless)" 55echo "==> 3/6 boot QEMU (headless, + dedicated CRASHLOG stick)"
56kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true 56kill "$(cat "$QPID" 2>/dev/null)" 2>/dev/null || true
57sleep 1 57sleep 1
58rm -f "$SOCK" "$LOG" 58rm -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.
61CRASH_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
67wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell" 73wait_log "$LOG" "boot: entering shell" 90 || fail "kernel never reached the shell"
68 74
69echo "==> 4/5 boot diagnostics present" 75echo "==> 4/6 boot diagnostics present"
70grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing" 76grep -aq "gop: fb base=" "$LOG" || fail "fb geometry diagnostic missing"
71grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing" 77grep -aq "gop: mode .* selected" "$LOG" || fail "mode-selection diagnostic missing"
78grep -aq "CRASHLOG" "$LOG" || fail "CRASHLOG stick not mounted"
72 79
73echo "==> 5/5 inject a page fault (fault command)" 80echo "==> 5/6 inject a page fault (fault command)"
74python3 "$SCRIPT_DIR/sendkeys.py" --sock "$SOCK" \ 81python3 "$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
83grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified" 90grep -aq "task=0 \"shell\"" "$LOG" || fail "faulting task not identified"
84grep -aq "!!! halting" "$LOG" || fail "no halt message" 91grep -aq "!!! halting" "$LOG" || fail "no halt message"
85 92
93echo "==> 6/6 crash.log persisted on the CRASHLOG stick"
94grep -aq "crashlog: vol .* (CRASHLOG)" "$LOG" || fail "crashlog did not target the CRASHLOG stick"
95grep -aq "crashlog: wrote" "$LOG" || fail "crashlog write did not complete"
96mdir -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.
99if [ -f "$ESP_DIR/crash.log" ]; then
100 fail "crash.log landed on the boot ESP instead of the CRASHLOG stick"
101fi
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.
88echo "" 105echo ""
89echo "PASS — fault dump works (vec, err, rip, cr2, task → screen + serial)" 106echo "PASS — fault dump + crash.log on the CRASHLOG stick"
90echo " every CPU exception now halts with a diagnosable dump, not a freeze" 107echo " every CPU exception halts with a diagnosable dump; the report"
108echo " 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.
844fn 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