diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-02 21:47:18 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-02 21:47:18 +0200 |
| commit | 0187b802113a10a9ee99af48b811cd332c660a13 (patch) | |
| tree | b0873f7b2dcf6be1c575c8fe1919a72f041bba49 | |
| parent | v0.5.6: fault dumps carry kernel image base + stack backtrace (diff) | |
| download | rs-dos-0187b802113a10a9ee99af48b811cd332c660a13.tar.gz rs-dos-0187b802113a10a9ee99af48b811cd332c660a13.zip | |
v0.5.7: allocator corruption guard + full register dump for real-hardware repro
The OptiPlex #GP (vec=13 in __rust_dealloc) frees a garbage pointer: the
back-pointer at [ptr-8] is clobbered by the time the shell drops its
Parsed struct. A blind store to the bogus block header was an opaque
#GP — now it is a readable report:
- dealloc validates the back-pointer (in-heap block, sane size, in-use)
before trusting it; on failure it prints ptr/block/heap range, the
memory around the block header, and the live stack chain, writes the
crash band + crash.log, and halts (memory.rs: heap_guard_report).
- fault dumps now carry the full register set (rax..rdi, rbx, r8..r15)
so the next repro shows the exact freed ptr and clobbered value.
- new 'corrupt' shell hook deliberately clobbers a live block's
back-pointer to prove the guard fires (test-fault-style halt).
Verified: test-msg/test-disk/test-fault pass; corrupt hook reports
ptr=.. block=0xdeedbeef... with mem dump instead of #GP. rs-dos.img
rebuilt with the release kernel.
| -rw-r--r-- | Cargo.lock | 6 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | rsnix/src/memory.rs | 142 | ||||
| -rw-r--r-- | rsnix/src/sched.rs | 44 | ||||
| -rw-r--r-- | shell/src/lib.rs | 16 |
5 files changed, 205 insertions, 5 deletions
| @@ -4,7 +4,7 @@ version = 4 | |||
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "rsdos" | 6 | name = "rsdos" |
| 7 | version = "0.5.5" | 7 | version = "0.5.7" |
| 8 | dependencies = [ | 8 | dependencies = [ |
| 9 | "rsnix", | 9 | "rsnix", |
| 10 | "shell", | 10 | "shell", |
| @@ -19,11 +19,11 @@ dependencies = [ | |||
| 19 | 19 | ||
| 20 | [[package]] | 20 | [[package]] |
| 21 | name = "rsnix" | 21 | name = "rsnix" |
| 22 | version = "0.5.5" | 22 | version = "0.5.7" |
| 23 | 23 | ||
| 24 | [[package]] | 24 | [[package]] |
| 25 | name = "shell" | 25 | name = "shell" |
| 26 | version = "0.5.5" | 26 | version = "0.5.7" |
| 27 | dependencies = [ | 27 | dependencies = [ |
| 28 | "rsdosedit", | 28 | "rsdosedit", |
| 29 | "rsnix", | 29 | "rsnix", |
| @@ -3,7 +3,7 @@ resolver = "2" | |||
| 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] | 3 | members = ["rsnix", "shell", "rsdos", "rsdosedit"] |
| 4 | 4 | ||
| 5 | [workspace.package] | 5 | [workspace.package] |
| 6 | version = "0.5.5" | 6 | version = "0.5.7" |
| 7 | edition = "2024" | 7 | edition = "2024" |
| 8 | 8 | ||
| 9 | # no_std UEFI kernel: no unwinder available, so panic must abort. | 9 | # no_std UEFI kernel: no unwinder available, so panic must abort. |
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs index 35f3464..3362d94 100644 --- a/rsnix/src/memory.rs +++ b/rsnix/src/memory.rs | |||
| @@ -1,5 +1,6 @@ | |||
| 1 | use crate::uefi::*; | 1 | use crate::uefi::*; |
| 2 | use core::alloc::{GlobalAlloc, Layout}; | 2 | use core::alloc::{GlobalAlloc, Layout}; |
| 3 | use core::fmt; | ||
| 3 | use core::mem; | 4 | use core::mem; |
| 4 | use core::ptr; | 5 | use core::ptr; |
| 5 | 6 | ||
| @@ -304,6 +305,22 @@ unsafe impl GlobalAlloc for Allocator { | |||
| 304 | return; | 305 | return; |
| 305 | } | 306 | } |
| 306 | unsafe { | 307 | unsafe { |
| 308 | // Real-hardware corruption guard: the back-pointer at [ptr-8] | ||
| 309 | // must point at a sane block header inside the heap. A | ||
| 310 | // garbage back-pointer (heap/stack corruption clobbered the | ||
| 311 | // block header, or the caller passed a bogus pointer) used to | ||
| 312 | // turn into a #GP on the store below — now it turns into a | ||
| 313 | // readable report + halt instead. | ||
| 314 | let p = ptr as usize; | ||
| 315 | if !back_pointer_ok(p) { | ||
| 316 | let rsp: usize; | ||
| 317 | core::arch::asm!( | ||
| 318 | "mov {}, rsp", | ||
| 319 | out(reg) rsp, | ||
| 320 | options(nostack, preserves_flags) | ||
| 321 | ); | ||
| 322 | heap_guard_report(p, rsp); | ||
| 323 | } | ||
| 307 | let block = *((ptr as *const usize).sub(1)) as *mut Header; | 324 | let block = *((ptr as *const usize).sub(1)) as *mut Header; |
| 308 | debug_assert!( | 325 | debug_assert!( |
| 309 | (*block).free == 0, | 326 | (*block).free == 0, |
| @@ -320,6 +337,131 @@ unsafe impl GlobalAlloc for Allocator { | |||
| 320 | #[global_allocator] | 337 | #[global_allocator] |
| 321 | static ALLOCATOR: Allocator = Allocator; | 338 | static ALLOCATOR: Allocator = Allocator; |
| 322 | 339 | ||
| 340 | /// Current kernel-heap extent, if the heap has been initialised. | ||
| 341 | pub(crate) fn heap_range() -> Option<(usize, usize)> { | ||
| 342 | unsafe { | ||
| 343 | if HEAP_END > HEAP_START { | ||
| 344 | Some((HEAP_START, HEAP_END)) | ||
| 345 | } else { | ||
| 346 | None | ||
| 347 | } | ||
| 348 | } | ||
| 349 | } | ||
| 350 | |||
| 351 | /// True when `[ptr-8]` holds a plausible back-pointer: the *value* there | ||
| 352 | /// must be a block address inside the heap whose header size is sane and | ||
| 353 | /// whose free flag says the block is still allocated (a block being freed | ||
| 354 | /// must be in-use). | ||
| 355 | unsafe fn back_pointer_ok(ptr: usize) -> bool { | ||
| 356 | unsafe { | ||
| 357 | let Some((hs, he)) = heap_range() else { | ||
| 358 | return true; // heap not initialised yet — trust the pointer | ||
| 359 | }; | ||
| 360 | let slot = ptr.wrapping_sub(8); | ||
| 361 | if slot < hs || slot >= he { | ||
| 362 | // The slot itself is outside the heap — can't even read it | ||
| 363 | // safely (a garbage ptr reads random memory). | ||
| 364 | return false; | ||
| 365 | } | ||
| 366 | let block = *(slot as *const usize); | ||
| 367 | if block < hs || block >= he { | ||
| 368 | return false; // back-pointer points outside the heap | ||
| 369 | } | ||
| 370 | let size = *(block as *const usize); | ||
| 371 | let free = *((block + 8) as *const usize); | ||
| 372 | size >= MIN_BLOCK && block + size <= he && free == 0 | ||
| 373 | } | ||
| 374 | } | ||
| 375 | |||
| 376 | /// Heap-corruption report, called from `dealloc` when the back-pointer | ||
| 377 | /// check fails. Heap-free by construction (we ARE the allocator): raw | ||
| 378 | /// serial, the crash band, crash.log, then halt. | ||
| 379 | fn heap_guard_report(ptr: usize, rsp: usize) -> ! { | ||
| 380 | let (hs, he) = heap_range().unwrap_or((0, 0)); | ||
| 381 | let block = if ptr >= hs + 8 && ptr <= he { unsafe { *(ptr.wrapping_sub(8) as *const usize) } } else { 0 }; | ||
| 382 | |||
| 383 | let mut b1 = [0u8; 96]; | ||
| 384 | let n1 = { | ||
| 385 | let mut w = crate::strbuf::FmtBuf::new(&mut b1); | ||
| 386 | let _ = fmt::write(&mut w, core::format_args!("!!! heap corruption in dealloc")); | ||
| 387 | w.len() | ||
| 388 | }; | ||
| 389 | let mut b2 = [0u8; 96]; | ||
| 390 | let n2 = { | ||
| 391 | let mut w = crate::strbuf::FmtBuf::new(&mut b2); | ||
| 392 | let _ = fmt::write(&mut w, core::format_args!("ptr={:#x} block={:#x}", ptr, block)); | ||
| 393 | w.len() | ||
| 394 | }; | ||
| 395 | let mut b3 = [0u8; 96]; | ||
| 396 | let n3 = { | ||
| 397 | let mut w = crate::strbuf::FmtBuf::new(&mut b3); | ||
| 398 | let _ = fmt::write( | ||
| 399 | &mut w, | ||
| 400 | core::format_args!("heap={:#x}..{:#x} rsp={:#x}", hs, he, rsp), | ||
| 401 | ); | ||
| 402 | w.len() | ||
| 403 | }; | ||
| 404 | |||
| 405 | // Words around the block header — the corruption lives in the | ||
| 406 | // header's size/free slots or the payload. Only when the addresses | ||
| 407 | // are inside the mapped heap (reading a garbage pointer would fault | ||
| 408 | // mid-report). | ||
| 409 | let mut b4 = [0u8; 216]; | ||
| 410 | let n4 = { | ||
| 411 | let mut w = crate::strbuf::FmtBuf::new(&mut b4); | ||
| 412 | let focus = if block >= hs && block < he { block } else { ptr }; | ||
| 413 | if focus >= hs + 8 && focus < he { | ||
| 414 | let lo = (focus - 16).max(hs); | ||
| 415 | let hi = (focus + 40).min(he - 8); | ||
| 416 | let _ = fmt::write(&mut w, core::format_args!("mem:")); | ||
| 417 | let mut a = lo & !7; | ||
| 418 | while a <= hi { | ||
| 419 | let _ = fmt::write( | ||
| 420 | &mut w, | ||
| 421 | core::format_args!(" {:x}={:#x}", a, unsafe { *(a as *const usize) }), | ||
| 422 | ); | ||
| 423 | a += 8; | ||
| 424 | } | ||
| 425 | } else { | ||
| 426 | let _ = fmt::write(&mut w, core::format_args!("mem: out of heap — no dump")); | ||
| 427 | } | ||
| 428 | w.len() | ||
| 429 | }; | ||
| 430 | |||
| 431 | // Live return-address chain on the stack above our rsp (dealloc ← | ||
| 432 | // drop glue ← shell::run); raw stack words, resolves against the PDB. | ||
| 433 | let mut b5 = [0u8; 216]; | ||
| 434 | let n5 = { | ||
| 435 | let mut w = crate::strbuf::FmtBuf::new(&mut b5); | ||
| 436 | let _ = fmt::write(&mut w, core::format_args!("stk:")); | ||
| 437 | let mut a = rsp.wrapping_add(8); | ||
| 438 | for _ in 0..12 { | ||
| 439 | let v = unsafe { *(a as *const usize) }; | ||
| 440 | let _ = fmt::write(&mut w, core::format_args!(" {:x}={:#x}", a, v)); | ||
| 441 | a += 8; | ||
| 442 | } | ||
| 443 | w.len() | ||
| 444 | }; | ||
| 445 | |||
| 446 | let l1 = core::str::from_utf8(&b1[..n1]).unwrap_or("!!!"); | ||
| 447 | let l2 = core::str::from_utf8(&b2[..n2]).unwrap_or(""); | ||
| 448 | let l3 = core::str::from_utf8(&b3[..n3]).unwrap_or(""); | ||
| 449 | let l4 = core::str::from_utf8(&b4[..n4]).unwrap_or(""); | ||
| 450 | let l5 = core::str::from_utf8(&b5[..n5]).unwrap_or(""); | ||
| 451 | crate::serial::write_raw(l2); | ||
| 452 | crate::serial::write_raw("\r\n"); | ||
| 453 | crate::serial::write_raw(l3); | ||
| 454 | crate::serial::write_raw("\r\n"); | ||
| 455 | crate::serial::write_raw(l4); | ||
| 456 | crate::serial::write_raw("\r\n"); | ||
| 457 | crate::serial::write_raw(l5); | ||
| 458 | crate::serial::write_raw("\r\n"); | ||
| 459 | let lines = [l1, l2, l3, l4, l5]; | ||
| 460 | crate::gop::dump_lines(&lines); | ||
| 461 | crate::fs::write_crash_log(&lines); | ||
| 462 | crate::interrupts::halt() | ||
| 463 | } | ||
| 464 | |||
| 323 | /// Capture the UEFI memory map into the static buffer. Uses a pre-allocated | 465 | /// Capture the UEFI memory map into the static buffer. Uses a pre-allocated |
| 324 | /// .bss working buffer so no pool allocation (and thus no memory-map change) | 466 | /// .bss working buffer so no pool allocation (and thus no memory-map change) |
| 325 | /// occurs between GetMemoryMap and ExitBootServices. | 467 | /// occurs between GetMemoryMap and ExitBootServices. |
diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs index a31ea6e..dd1409e 100644 --- a/rsnix/src/sched.rs +++ b/rsnix/src/sched.rs | |||
| @@ -730,7 +730,49 @@ pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFra | |||
| 730 | let l2 = core::str::from_utf8(&b2[..n2]).unwrap_or(""); | 730 | let l2 = core::str::from_utf8(&b2[..n2]).unwrap_or(""); |
| 731 | let l3 = core::str::from_utf8(&b3[..n3]).unwrap_or(""); | 731 | let l3 = core::str::from_utf8(&b3[..n3]).unwrap_or(""); |
| 732 | let l4 = core::str::from_utf8(&b4[..n4]).unwrap_or("bt=?"); | 732 | let l4 = core::str::from_utf8(&b4[..n4]).unwrap_or("bt=?"); |
| 733 | let lines = [l1, l2, l3, l4]; | 733 | |
| 734 | // Register dump — the faulting register state, so a real-hardware | ||
| 735 | // repro can be diagnosed without serial. Two lines on screen (the | ||
| 736 | // band is clamped), full set in crash.log via `lines`. | ||
| 737 | let mut r1 = [0u8; 160]; | ||
| 738 | let rn1 = { | ||
| 739 | let mut w = crate::strbuf::FmtBuf::new(&mut r1); | ||
| 740 | let _ = fmt::write( | ||
| 741 | &mut w, | ||
| 742 | core::format_args!( | ||
| 743 | "rax={:#x} rcx={:#x} rdx={:#x} rsi={:#x} rdi={:#x}", | ||
| 744 | frame.rax, frame.rcx, frame.rdx, frame.rsi, frame.rdi | ||
| 745 | ), | ||
| 746 | ); | ||
| 747 | w.len() | ||
| 748 | }; | ||
| 749 | let mut r2 = [0u8; 160]; | ||
| 750 | let rn2 = { | ||
| 751 | let mut w = crate::strbuf::FmtBuf::new(&mut r2); | ||
| 752 | let _ = fmt::write( | ||
| 753 | &mut w, | ||
| 754 | core::format_args!( | ||
| 755 | "rbx={:#x} r8={:#x} r9={:#x} r10={:#x} r11={:#x} r12={:#x} r13={:#x} r14={:#x} r15={:#x}", | ||
| 756 | frame.rbx, | ||
| 757 | frame.r8, | ||
| 758 | frame.r9, | ||
| 759 | frame.r10, | ||
| 760 | frame.r11, | ||
| 761 | frame.r12, | ||
| 762 | frame.r13, | ||
| 763 | frame.r14, | ||
| 764 | frame.r15 | ||
| 765 | ), | ||
| 766 | ); | ||
| 767 | w.len() | ||
| 768 | }; | ||
| 769 | let l5 = core::str::from_utf8(&r1[..rn1]).unwrap_or(""); | ||
| 770 | let l6 = core::str::from_utf8(&r2[..rn2]).unwrap_or(""); | ||
| 771 | crate::serial::write_raw(l5); | ||
| 772 | crate::serial::write_raw("\r\n"); | ||
| 773 | crate::serial::write_raw(l6); | ||
| 774 | crate::serial::write_raw("\r\n"); | ||
| 775 | let lines = [l1, l2, l3, l4, l5, l6]; | ||
| 734 | crate::gop::dump_lines(&lines); | 776 | crate::gop::dump_lines(&lines); |
| 735 | 777 | ||
| 736 | // Best-effort: persist the report to the disk so it survives the | 778 | // Best-effort: persist the report to the disk so it survives the |
diff --git a/shell/src/lib.rs b/shell/src/lib.rs index a32d259..175a2c7 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs | |||
| @@ -740,6 +740,7 @@ write `{} <path> TIL <marker>`", | |||
| 740 | "kill" => cmd_kill(&parsed), | 740 | "kill" => cmd_kill(&parsed), |
| 741 | "demo" => cmd_demo(&parsed), | 741 | "demo" => cmd_demo(&parsed), |
| 742 | "fault" => cmd_fault(), | 742 | "fault" => cmd_fault(), |
| 743 | "corrupt" => cmd_corrupt(), | ||
| 743 | "crashlog" => cmd_crashlog(), | 744 | "crashlog" => cmd_crashlog(), |
| 744 | "stall" => cmd_stall(&parsed), | 745 | "stall" => cmd_stall(&parsed), |
| 745 | _ => term_err(&alloc::format!("unknown command: {}", cmd)), | 746 | _ => term_err(&alloc::format!("unknown command: {}", cmd)), |
| @@ -839,6 +840,21 @@ fn cmd_fault() { | |||
| 839 | } | 840 | } |
| 840 | } | 841 | } |
| 841 | 842 | ||
| 843 | /// Deliberately clobber a live heap block's back-pointer, then free it — | ||
| 844 | /// test hook that proves the allocator's corruption guard fires with a | ||
| 845 | /// readable report (instead of a #GP inside `__rust_dealloc`). The | ||
| 846 | /// system stops; the log must show `!!! heap corruption in dealloc`. | ||
| 847 | fn cmd_corrupt() { | ||
| 848 | term_println("corrupt: clobbering a live block's back-pointer..."); | ||
| 849 | let s = String::from("hello heap guard"); | ||
| 850 | let ptr = s.as_ptr() as usize; | ||
| 851 | unsafe { | ||
| 852 | *((ptr - 8) as *mut usize) = 0xdeed_beef_deed_beefusize; | ||
| 853 | } | ||
| 854 | drop(s); | ||
| 855 | term_println("corrupt: guard did NOT fire (unexpected)"); | ||
| 856 | } | ||
| 857 | |||
| 842 | /// Debug hook: run the crash-log FAT write from the *shell* context (not | 858 | /// Debug hook: run the crash-log FAT write from the *shell* context (not |
| 843 | /// the fault ISR) to compare behaviour. | 859 | /// the fault ISR) to compare behaviour. |
| 844 | fn cmd_crashlog() { | 860 | fn cmd_crashlog() { |
