diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-02 23:13:46 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-02 23:13:46 +0200 |
| commit | c10eb2333f277f815234f9fc26a5441e68ef7b40 (patch) | |
| tree | 9bff5b38a21c7edfa997a25a0ee51cb43c570a68 | |
| parent | v0.5.7: guard report 4-words-per-line — 16 stack words survive band hand-copy (diff) | |
| download | rs-dos-c10eb2333f277f815234f9fc26a5441e68ef7b40.tar.gz rs-dos-c10eb2333f277f815234f9fc26a5441e68ef7b40.zip | |
v0.5.7: guard reports the drop's size/align — identifies the clobbered struct
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.
| -rw-r--r-- | rsdos/src/main.rs | 25 | ||||
| -rw-r--r-- | rsnix/src/memory.rs | 91 |
2 files changed, 102 insertions, 14 deletions
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index 806f150..52b37ec 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -352,28 +352,37 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 352 | // visible on QEMU (serial + gdb available) instead of only on the | 352 | // visible on QEMU (serial + gdb available) instead of only on the |
| 353 | // OptiPlex. | 353 | // OptiPlex. |
| 354 | stack_fill_aa(); | 354 | stack_fill_aa(); |
| 355 | // Poison the free heap AND the free pages with the OptiPlex's | ||
| 356 | // 0x00AAAAAA scrub pattern — the firmware's leftover fills both the | ||
| 357 | // heap (freed pool) and the pages the allocators hand out (heap | ||
| 358 | // growth, task stacks). Together with the stack fill this makes | ||
| 359 | // any uninitialized read reproduce the real machine's crashes in | ||
| 360 | // QEMU (garbage sizes → alloc panics, garbage ptrs → dealloc guard). | ||
| 361 | rsnix::memory::poison_free_blocks_aa(); | ||
| 362 | rsnix::memory::poison_free_pages_aa(); | ||
| 355 | shell::run(); | 363 | shell::run(); |
| 356 | } | 364 | } |
| 357 | 365 | ||
| 358 | /// Write 0xAA over the stack region the shell's frames will reuse, | 366 | /// Write the OptiPlex's 0x00AAAAAA scrub pattern over the stack region |
| 359 | /// leaving the current frames (top 4 KiB) intact. 128 KiB covers the | 367 | /// the shell's frames will reuse, leaving only 256 B of live frame |
| 360 | /// whole shell frame chain on QEMU and the OptiPlex. Best-effort: the | 368 | /// headroom. 128 KiB covers the whole shell frame chain on QEMU and |
| 361 | /// region is below rsp inside the boot stack mapping. | 369 | /// the OptiPlex. Best-effort: the region is below rsp inside the boot |
| 370 | /// stack mapping; the call into `shell::run` overwrites the top word. | ||
| 362 | fn stack_fill_aa() { | 371 | fn stack_fill_aa() { |
| 363 | let rsp: usize; | 372 | let rsp: usize; |
| 364 | unsafe { | 373 | unsafe { |
| 365 | core::arch::asm!("mov {}, rsp", out(reg) rsp); | 374 | core::arch::asm!("mov {}, rsp", out(reg) rsp); |
| 366 | } | 375 | } |
| 367 | let top = rsp - 0x1000; | 376 | let top = rsp - 0x100; |
| 368 | let bottom = rsp - 0x21000; | 377 | let bottom = rsp - 0x21100; |
| 369 | unsafe { | 378 | unsafe { |
| 370 | let mut p = bottom; | 379 | let mut p = bottom; |
| 371 | while p < top { | 380 | while p < top { |
| 372 | core::ptr::write_volatile(p as *mut u64, 0xAAAAAAAAAAAAAAAA); | 381 | core::ptr::write_volatile(p as *mut u64, 0x00AAAAAA00AAAAAA); |
| 373 | p += 8; | 382 | p += 8; |
| 374 | } | 383 | } |
| 375 | } | 384 | } |
| 376 | rsnix::serial_println!("stack: 0xAA-fill 0x{:x}..0x{:x}", bottom, top); | 385 | rsnix::serial_println!("stack: 0x00AAAAAA-fill 0x{:x}..0x{:x}", bottom, top); |
| 377 | } | 386 | } |
| 378 | 387 | ||
| 379 | // --------------------------------------------------------------------------- | 388 | // --------------------------------------------------------------------------- |
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 { | |||
| 71 | /// Head of the address-sorted free list (lowest address first). | 71 | /// Head of the address-sorted free list (lowest address first). |
| 72 | static mut FREE_LIST: *mut Header = ptr::null_mut(); | 72 | static mut FREE_LIST: *mut Header = ptr::null_mut(); |
| 73 | 73 | ||
| 74 | /// TEST HOOK: fill every free block's payload with 0xAA (header + links | ||
| 75 | /// preserved). Mirrors the OptiPlex, whose heap pages carry 0xAA | ||
| 76 | /// firmware leftover — any code that reads uninitialized heap memory | ||
| 77 | /// then sees the same pattern the real machine shows in its crashes | ||
| 78 | /// (ptr=0xaaaaaa…, allocation-size panics, zeroed block headers). | ||
| 79 | pub fn poison_free_blocks_aa() { | ||
| 80 | unsafe { | ||
| 81 | let mut b = FREE_LIST; | ||
| 82 | let mut blocks = 0u32; | ||
| 83 | let mut bytes = 0u64; | ||
| 84 | while !b.is_null() { | ||
| 85 | let size = (*b).size; | ||
| 86 | let off = HEADER_SIZE + core::mem::size_of::<FreeLinks>(); | ||
| 87 | if size > off { | ||
| 88 | let p = (b as *mut u8).add(off); | ||
| 89 | let n = size - off; | ||
| 90 | // 8-byte words, then a byte tail (blocks are 16-aligned). | ||
| 91 | let mut i = 0; | ||
| 92 | while i + 8 <= n { | ||
| 93 | (p.add(i) as *mut u64).write_volatile(0xAAAAAAAAAAAAAAAA); | ||
| 94 | i += 8; | ||
| 95 | } | ||
| 96 | while i < n { | ||
| 97 | p.add(i).write_volatile(0xAA); | ||
| 98 | i += 1; | ||
| 99 | } | ||
| 100 | bytes += n as u64; | ||
| 101 | } | ||
| 102 | blocks += 1; | ||
| 103 | b = (*(*b).links()).next; | ||
| 104 | } | ||
| 105 | crate::serial_println!( | ||
| 106 | "heap: poisoned {} free block(s), {} KiB with 0x00AAAAAA", | ||
| 107 | blocks, | ||
| 108 | bytes / 1024 | ||
| 109 | ); | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | /// TEST HOOK: fill every free page (per the bitmap) with the OptiPlex's | ||
| 114 | /// 0x00AAAAAA scrub pattern — the firmware's leftover fills pages the | ||
| 115 | /// page allocator will hand out (task stacks, heap growth), so any | ||
| 116 | /// uninitialized read there matches the real machine byte-for-byte. | ||
| 117 | pub fn poison_free_pages_aa() { | ||
| 118 | unsafe { | ||
| 119 | if BITMAP.is_null() { | ||
| 120 | crate::serial_println!("pages: poison skipped (no bitmap)"); | ||
| 121 | return; | ||
| 122 | } | ||
| 123 | let mut n = 0u32; | ||
| 124 | for idx in 0..SPAN_PAGES { | ||
| 125 | let free = (*BITMAP.add(idx / 8) >> (idx % 8)) & 1 == 0; | ||
| 126 | if !free { | ||
| 127 | continue; | ||
| 128 | } | ||
| 129 | let base = (BASE_PAGE + idx) * PAGE_SIZE; | ||
| 130 | let p = base as *mut u64; | ||
| 131 | for k in 0..(PAGE_SIZE / 8) { | ||
| 132 | p.add(k).write_volatile(0x00AAAAAA00AAAAAA); | ||
| 133 | } | ||
| 134 | n += 1; | ||
| 135 | } | ||
| 136 | crate::serial_println!("pages: poisoned {} free page(s) with 0x00AAAAAA", n); | ||
| 137 | } | ||
| 138 | } | ||
| 139 | |||
| 74 | /// Heap region bounds (set by `init_heap`). | 140 | /// Heap region bounds (set by `init_heap`). |
| 75 | static mut HEAP_START: usize = 0; | 141 | static mut HEAP_START: usize = 0; |
| 76 | static mut HEAP_END: usize = 0; | 142 | static mut HEAP_END: usize = 0; |
| @@ -300,7 +366,7 @@ unsafe impl GlobalAlloc for Allocator { | |||
| 300 | } | 366 | } |
| 301 | } | 367 | } |
| 302 | 368 | ||
| 303 | unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) { | 369 | unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { |
| 304 | if ptr.is_null() { | 370 | if ptr.is_null() { |
| 305 | return; | 371 | return; |
| 306 | } | 372 | } |
| @@ -319,7 +385,7 @@ unsafe impl GlobalAlloc for Allocator { | |||
| 319 | out(reg) rsp, | 385 | out(reg) rsp, |
| 320 | options(nostack, preserves_flags) | 386 | options(nostack, preserves_flags) |
| 321 | ); | 387 | ); |
| 322 | heap_guard_report(p, rsp); | 388 | heap_guard_report(p, rsp, layout.size(), layout.align()); |
| 323 | } | 389 | } |
| 324 | let block = *((ptr as *const usize).sub(1)) as *mut Header; | 390 | let block = *((ptr as *const usize).sub(1)) as *mut Header; |
| 325 | debug_assert!( | 391 | debug_assert!( |
| @@ -376,7 +442,7 @@ unsafe fn back_pointer_ok(ptr: usize) -> bool { | |||
| 376 | /// Heap-corruption report, called from `dealloc` when the back-pointer | 442 | /// Heap-corruption report, called from `dealloc` when the back-pointer |
| 377 | /// check fails. Heap-free by construction (we ARE the allocator): raw | 443 | /// check fails. Heap-free by construction (we ARE the allocator): raw |
| 378 | /// serial, the crash band, crash.log, then halt. | 444 | /// serial, the crash band, crash.log, then halt. |
| 379 | fn heap_guard_report(ptr: usize, rsp: usize) -> ! { | 445 | fn heap_guard_report(ptr: usize, rsp: usize, size: usize, align: usize) -> ! { |
| 380 | let (hs, he) = heap_range().unwrap_or((0, 0)); | 446 | 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 }; | 447 | let block = if ptr >= hs + 8 && ptr <= he { unsafe { *(ptr.wrapping_sub(8) as *const usize) } } else { 0 }; |
| 382 | 448 | ||
| @@ -397,7 +463,19 @@ fn heap_guard_report(ptr: usize, rsp: usize) -> ! { | |||
| 397 | let mut w = crate::strbuf::FmtBuf::new(&mut b3); | 463 | let mut w = crate::strbuf::FmtBuf::new(&mut b3); |
| 398 | let _ = fmt::write( | 464 | let _ = fmt::write( |
| 399 | &mut w, | 465 | &mut w, |
| 400 | core::format_args!("heap={:#x}..{:#x} rsp={:#x}", hs, he, rsp), | 466 | core::format_args!( |
| 467 | "heap={:#x}..{:#x} rsp={:#x}", | ||
| 468 | hs, he, rsp | ||
| 469 | ), | ||
| 470 | ); | ||
| 471 | w.len() | ||
| 472 | }; | ||
| 473 | let mut b3b = [0u8; 96]; | ||
| 474 | let n3b = { | ||
| 475 | let mut w = crate::strbuf::FmtBuf::new(&mut b3b); | ||
| 476 | let _ = fmt::write( | ||
| 477 | &mut w, | ||
| 478 | core::format_args!("drop size={:#x} align={:#x}", size, align), | ||
| 401 | ); | 479 | ); |
| 402 | w.len() | 480 | w.len() |
| 403 | }; | 481 | }; |
| @@ -482,17 +560,18 @@ fn heap_guard_report(ptr: usize, rsp: usize) -> ! { | |||
| 482 | let l1 = core::str::from_utf8(&b1[..n1]).unwrap_or("!!!"); | 560 | let l1 = core::str::from_utf8(&b1[..n1]).unwrap_or("!!!"); |
| 483 | let l2 = core::str::from_utf8(&b2[..n2]).unwrap_or(""); | 561 | let l2 = core::str::from_utf8(&b2[..n2]).unwrap_or(""); |
| 484 | let l3 = core::str::from_utf8(&b3[..n3]).unwrap_or(""); | 562 | let l3 = core::str::from_utf8(&b3[..n3]).unwrap_or(""); |
| 563 | let l3b = core::str::from_utf8(&b3b[..n3b]).unwrap_or(""); | ||
| 485 | let l4 = core::str::from_utf8(&b4[..n4]).unwrap_or(""); | 564 | let l4 = core::str::from_utf8(&b4[..n4]).unwrap_or(""); |
| 486 | let l4b = if n4b > 0 { core::str::from_utf8(&b4b[..n4b]).unwrap_or("") } else { "" }; | 565 | let l4b = if n4b > 0 { core::str::from_utf8(&b4b[..n4b]).unwrap_or("") } else { "" }; |
| 487 | let l5 = core::str::from_utf8(&b5[..n5]).unwrap_or(""); | 566 | let l5 = core::str::from_utf8(&b5[..n5]).unwrap_or(""); |
| 488 | let l6 = core::str::from_utf8(&b6[..n6]).unwrap_or(""); | 567 | let l6 = core::str::from_utf8(&b6[..n6]).unwrap_or(""); |
| 489 | let l7 = core::str::from_utf8(&b7[..n7]).unwrap_or(""); | 568 | let l7 = core::str::from_utf8(&b7[..n7]).unwrap_or(""); |
| 490 | let l8 = core::str::from_utf8(&b8[..n8]).unwrap_or(""); | 569 | let l8 = core::str::from_utf8(&b8[..n8]).unwrap_or(""); |
| 491 | for l in [l2, l3, l4, l4b, l5, l6, l7, l8] { | 570 | for l in [l2, l3, l3b, l4, l4b, l5, l6, l7, l8] { |
| 492 | crate::serial::write_raw(l); | 571 | crate::serial::write_raw(l); |
| 493 | crate::serial::write_raw("\r\n"); | 572 | crate::serial::write_raw("\r\n"); |
| 494 | } | 573 | } |
| 495 | let lines = [l1, l2, l3, l4, l4b, l5, l6, l7, l8]; | 574 | let lines = [l1, l2, l3, l3b, l4, l4b, l5, l6, l7, l8]; |
| 496 | crate::gop::dump_lines(&lines); | 575 | crate::gop::dump_lines(&lines); |
| 497 | crate::fs::write_crash_log(&lines); | 576 | crate::fs::write_crash_log(&lines); |
| 498 | crate::interrupts::halt() | 577 | crate::interrupts::halt() |
