From 0187b802113a10a9ee99af48b811cd332c660a13 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 2 Aug 2026 21:47:18 +0200 Subject: v0.5.7: allocator corruption guard + full register dump for real-hardware repro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- rsnix/src/memory.rs | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) (limited to 'rsnix/src/memory.rs') 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 @@ use crate::uefi::*; use core::alloc::{GlobalAlloc, Layout}; +use core::fmt; use core::mem; use core::ptr; @@ -304,6 +305,22 @@ unsafe impl GlobalAlloc for Allocator { return; } unsafe { + // Real-hardware corruption guard: the back-pointer at [ptr-8] + // must point at a sane block header inside the heap. A + // garbage back-pointer (heap/stack corruption clobbered the + // block header, or the caller passed a bogus pointer) used to + // turn into a #GP on the store below — now it turns into a + // readable report + halt instead. + let p = ptr as usize; + if !back_pointer_ok(p) { + let rsp: usize; + core::arch::asm!( + "mov {}, rsp", + out(reg) rsp, + options(nostack, preserves_flags) + ); + heap_guard_report(p, rsp); + } let block = *((ptr as *const usize).sub(1)) as *mut Header; debug_assert!( (*block).free == 0, @@ -320,6 +337,131 @@ unsafe impl GlobalAlloc for Allocator { #[global_allocator] static ALLOCATOR: Allocator = Allocator; +/// Current kernel-heap extent, if the heap has been initialised. +pub(crate) fn heap_range() -> Option<(usize, usize)> { + unsafe { + if HEAP_END > HEAP_START { + Some((HEAP_START, HEAP_END)) + } else { + None + } + } +} + +/// True when `[ptr-8]` holds a plausible back-pointer: the *value* there +/// must be a block address inside the heap whose header size is sane and +/// whose free flag says the block is still allocated (a block being freed +/// must be in-use). +unsafe fn back_pointer_ok(ptr: usize) -> bool { + unsafe { + let Some((hs, he)) = heap_range() else { + return true; // heap not initialised yet — trust the pointer + }; + let slot = ptr.wrapping_sub(8); + if slot < hs || slot >= he { + // The slot itself is outside the heap — can't even read it + // safely (a garbage ptr reads random memory). + return false; + } + let block = *(slot as *const usize); + if block < hs || block >= he { + return false; // back-pointer points outside the heap + } + let size = *(block as *const usize); + let free = *((block + 8) as *const usize); + size >= MIN_BLOCK && block + size <= he && free == 0 + } +} + +/// Heap-corruption report, called from `dealloc` when the back-pointer +/// check fails. Heap-free by construction (we ARE the allocator): raw +/// serial, the crash band, crash.log, then halt. +fn heap_guard_report(ptr: usize, rsp: usize) -> ! { + let (hs, he) = heap_range().unwrap_or((0, 0)); + let block = if ptr >= hs + 8 && ptr <= he { unsafe { *(ptr.wrapping_sub(8) as *const usize) } } else { 0 }; + + let mut b1 = [0u8; 96]; + let n1 = { + let mut w = crate::strbuf::FmtBuf::new(&mut b1); + let _ = fmt::write(&mut w, core::format_args!("!!! heap corruption in dealloc")); + w.len() + }; + let mut b2 = [0u8; 96]; + let n2 = { + let mut w = crate::strbuf::FmtBuf::new(&mut b2); + let _ = fmt::write(&mut w, core::format_args!("ptr={:#x} block={:#x}", ptr, block)); + w.len() + }; + let mut b3 = [0u8; 96]; + let n3 = { + let mut w = crate::strbuf::FmtBuf::new(&mut b3); + let _ = fmt::write( + &mut w, + core::format_args!("heap={:#x}..{:#x} rsp={:#x}", hs, he, rsp), + ); + w.len() + }; + + // Words around the block header — the corruption lives in the + // header's size/free slots or the payload. Only when the addresses + // are inside the mapped heap (reading a garbage pointer would fault + // mid-report). + let mut b4 = [0u8; 216]; + let n4 = { + let mut w = crate::strbuf::FmtBuf::new(&mut b4); + let focus = if block >= hs && block < he { block } else { ptr }; + if focus >= hs + 8 && focus < he { + let lo = (focus - 16).max(hs); + let hi = (focus + 40).min(he - 8); + let _ = fmt::write(&mut w, core::format_args!("mem:")); + let mut a = lo & !7; + while a <= hi { + let _ = fmt::write( + &mut w, + core::format_args!(" {:x}={:#x}", a, unsafe { *(a as *const usize) }), + ); + a += 8; + } + } else { + let _ = fmt::write(&mut w, core::format_args!("mem: out of heap — no dump")); + } + w.len() + }; + + // Live return-address chain on the stack above our rsp (dealloc ← + // drop glue ← shell::run); raw stack words, resolves against the PDB. + let mut b5 = [0u8; 216]; + let n5 = { + let mut w = crate::strbuf::FmtBuf::new(&mut b5); + let _ = fmt::write(&mut w, core::format_args!("stk:")); + let mut a = rsp.wrapping_add(8); + for _ in 0..12 { + let v = unsafe { *(a as *const usize) }; + let _ = fmt::write(&mut w, core::format_args!(" {:x}={:#x}", a, v)); + a += 8; + } + w.len() + }; + + let l1 = core::str::from_utf8(&b1[..n1]).unwrap_or("!!!"); + let l2 = core::str::from_utf8(&b2[..n2]).unwrap_or(""); + let l3 = core::str::from_utf8(&b3[..n3]).unwrap_or(""); + let l4 = core::str::from_utf8(&b4[..n4]).unwrap_or(""); + let l5 = core::str::from_utf8(&b5[..n5]).unwrap_or(""); + crate::serial::write_raw(l2); + crate::serial::write_raw("\r\n"); + crate::serial::write_raw(l3); + crate::serial::write_raw("\r\n"); + crate::serial::write_raw(l4); + crate::serial::write_raw("\r\n"); + crate::serial::write_raw(l5); + crate::serial::write_raw("\r\n"); + let lines = [l1, l2, l3, l4, l5]; + crate::gop::dump_lines(&lines); + crate::fs::write_crash_log(&lines); + crate::interrupts::halt() +} + /// Capture the UEFI memory map into the static buffer. Uses a pre-allocated /// .bss working buffer so no pool allocation (and thus no memory-map change) /// occurs between GetMemoryMap and ExitBootServices. -- cgit v1.2.3