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 /rsnix/src | |
| 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
Diffstat (limited to 'rsnix/src')
| -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 |
8 files changed, 349 insertions, 59 deletions
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 | // --------------------------------------------------------------------------- |
