summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-02 21:47:18 +0200
committersalmonik <salmonik@tuta.io>2026-08-02 21:47:18 +0200
commit0187b802113a10a9ee99af48b811cd332c660a13 (patch)
treeb0873f7b2dcf6be1c575c8fe1919a72f041bba49
parentv0.5.6: fault dumps carry kernel image base + stack backtrace (diff)
downloadrs-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.lock6
-rw-r--r--Cargo.toml2
-rw-r--r--rsnix/src/memory.rs142
-rw-r--r--rsnix/src/sched.rs44
-rw-r--r--shell/src/lib.rs16
5 files changed, 205 insertions, 5 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 431db32..4cef15c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,7 +4,7 @@ version = 4
4 4
5[[package]] 5[[package]]
6name = "rsdos" 6name = "rsdos"
7version = "0.5.5" 7version = "0.5.7"
8dependencies = [ 8dependencies = [
9 "rsnix", 9 "rsnix",
10 "shell", 10 "shell",
@@ -19,11 +19,11 @@ dependencies = [
19 19
20[[package]] 20[[package]]
21name = "rsnix" 21name = "rsnix"
22version = "0.5.5" 22version = "0.5.7"
23 23
24[[package]] 24[[package]]
25name = "shell" 25name = "shell"
26version = "0.5.5" 26version = "0.5.7"
27dependencies = [ 27dependencies = [
28 "rsdosedit", 28 "rsdosedit",
29 "rsnix", 29 "rsnix",
diff --git a/Cargo.toml b/Cargo.toml
index 76139bb..b55a0aa 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,7 +3,7 @@ resolver = "2"
3members = ["rsnix", "shell", "rsdos", "rsdosedit"] 3members = ["rsnix", "shell", "rsdos", "rsdosedit"]
4 4
5[workspace.package] 5[workspace.package]
6version = "0.5.5" 6version = "0.5.7"
7edition = "2024" 7edition = "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 @@
1use crate::uefi::*; 1use crate::uefi::*;
2use core::alloc::{GlobalAlloc, Layout}; 2use core::alloc::{GlobalAlloc, Layout};
3use core::fmt;
3use core::mem; 4use core::mem;
4use core::ptr; 5use 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]
321static ALLOCATOR: Allocator = Allocator; 338static ALLOCATOR: Allocator = Allocator;
322 339
340/// Current kernel-heap extent, if the heap has been initialised.
341pub(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).
355unsafe 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.
379fn 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`.
847fn 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.
844fn cmd_crashlog() { 860fn cmd_crashlog() {