From c10eb2333f277f815234f9fc26a5441e68ef7b40 Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 2 Aug 2026 23:13:46 +0200 Subject: v0.5.7: guard reports the drop's size/align — identifies the clobbered struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OptiPlex crashes free garbage (0xaaaaaa, 0xaaaaaa00aaaaaa) whose value is the firmware's 0x00AAAAAA scrub pattern left in stack/heap. The drop size/align (the dealloc layout) now prints on the band, so the next repro shows both the garbage ptr AND the size it was freed with — cap×elem_size pinpoints which struct's fields were clobbered. --- rsnix/src/memory.rs | 91 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 6 deletions(-) (limited to 'rsnix') diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs index ecaf806..e0f0142 100644 --- a/rsnix/src/memory.rs +++ b/rsnix/src/memory.rs @@ -71,6 +71,72 @@ struct FreeLinks { /// Head of the address-sorted free list (lowest address first). static mut FREE_LIST: *mut Header = ptr::null_mut(); +/// TEST HOOK: fill every free block's payload with 0xAA (header + links +/// preserved). Mirrors the OptiPlex, whose heap pages carry 0xAA +/// firmware leftover — any code that reads uninitialized heap memory +/// then sees the same pattern the real machine shows in its crashes +/// (ptr=0xaaaaaa…, allocation-size panics, zeroed block headers). +pub fn poison_free_blocks_aa() { + unsafe { + let mut b = FREE_LIST; + let mut blocks = 0u32; + let mut bytes = 0u64; + while !b.is_null() { + let size = (*b).size; + let off = HEADER_SIZE + core::mem::size_of::(); + if size > off { + let p = (b as *mut u8).add(off); + let n = size - off; + // 8-byte words, then a byte tail (blocks are 16-aligned). + let mut i = 0; + while i + 8 <= n { + (p.add(i) as *mut u64).write_volatile(0xAAAAAAAAAAAAAAAA); + i += 8; + } + while i < n { + p.add(i).write_volatile(0xAA); + i += 1; + } + bytes += n as u64; + } + blocks += 1; + b = (*(*b).links()).next; + } + crate::serial_println!( + "heap: poisoned {} free block(s), {} KiB with 0x00AAAAAA", + blocks, + bytes / 1024 + ); + } +} + +/// TEST HOOK: fill every free page (per the bitmap) with the OptiPlex's +/// 0x00AAAAAA scrub pattern — the firmware's leftover fills pages the +/// page allocator will hand out (task stacks, heap growth), so any +/// uninitialized read there matches the real machine byte-for-byte. +pub fn poison_free_pages_aa() { + unsafe { + if BITMAP.is_null() { + crate::serial_println!("pages: poison skipped (no bitmap)"); + return; + } + let mut n = 0u32; + for idx in 0..SPAN_PAGES { + let free = (*BITMAP.add(idx / 8) >> (idx % 8)) & 1 == 0; + if !free { + continue; + } + let base = (BASE_PAGE + idx) * PAGE_SIZE; + let p = base as *mut u64; + for k in 0..(PAGE_SIZE / 8) { + p.add(k).write_volatile(0x00AAAAAA00AAAAAA); + } + n += 1; + } + crate::serial_println!("pages: poisoned {} free page(s) with 0x00AAAAAA", n); + } +} + /// Heap region bounds (set by `init_heap`). static mut HEAP_START: usize = 0; static mut HEAP_END: usize = 0; @@ -300,7 +366,7 @@ unsafe impl GlobalAlloc for Allocator { } } - unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) { + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { if ptr.is_null() { return; } @@ -319,7 +385,7 @@ unsafe impl GlobalAlloc for Allocator { out(reg) rsp, options(nostack, preserves_flags) ); - heap_guard_report(p, rsp); + heap_guard_report(p, rsp, layout.size(), layout.align()); } let block = *((ptr as *const usize).sub(1)) as *mut Header; debug_assert!( @@ -376,7 +442,7 @@ unsafe fn back_pointer_ok(ptr: usize) -> bool { /// 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) -> ! { +fn heap_guard_report(ptr: usize, rsp: usize, size: usize, align: 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 }; @@ -397,7 +463,19 @@ fn heap_guard_report(ptr: usize, rsp: usize) -> ! { 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), + core::format_args!( + "heap={:#x}..{:#x} rsp={:#x}", + hs, he, rsp + ), + ); + w.len() + }; + let mut b3b = [0u8; 96]; + let n3b = { + let mut w = crate::strbuf::FmtBuf::new(&mut b3b); + let _ = fmt::write( + &mut w, + core::format_args!("drop size={:#x} align={:#x}", size, align), ); w.len() }; @@ -482,17 +560,18 @@ fn heap_guard_report(ptr: usize, rsp: usize) -> ! { 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 l3b = core::str::from_utf8(&b3b[..n3b]).unwrap_or(""); let l4 = core::str::from_utf8(&b4[..n4]).unwrap_or(""); let l4b = if n4b > 0 { core::str::from_utf8(&b4b[..n4b]).unwrap_or("") } else { "" }; let l5 = core::str::from_utf8(&b5[..n5]).unwrap_or(""); let l6 = core::str::from_utf8(&b6[..n6]).unwrap_or(""); let l7 = core::str::from_utf8(&b7[..n7]).unwrap_or(""); let l8 = core::str::from_utf8(&b8[..n8]).unwrap_or(""); - for l in [l2, l3, l4, l4b, l5, l6, l7, l8] { + for l in [l2, l3, l3b, l4, l4b, l5, l6, l7, l8] { crate::serial::write_raw(l); crate::serial::write_raw("\r\n"); } - let lines = [l1, l2, l3, l4, l4b, l5, l6, l7, l8]; + let lines = [l1, l2, l3, l3b, l4, l4b, l5, l6, l7, l8]; crate::gop::dump_lines(&lines); crate::fs::write_crash_log(&lines); crate::interrupts::halt() -- cgit v1.2.3