diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-02 23:28:29 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-02 23:28:29 +0200 |
| commit | f846a9551bf30d61026fa9c9ccea78479ced321b (patch) | |
| tree | 10158ca300cc51f44fc9afee9c2b3092554d880b | |
| parent | v0.5.7: guard reports the drop's size/align — identifies the clobbered struct (diff) | |
| download | rs-dos-f846a9551bf30d61026fa9c9ccea78479ced321b.tar.gz rs-dos-f846a9551bf30d61026fa9c9ccea78479ced321b.zip | |
v0.5.7: drop test-poison hooks; vt heap-struct verify at shell entry
The OptiPlex's first banner write #GPs when the vt cells VecData holds
firmware 0xAA fill instead of a real buffer pointer. The poison/fill
test hooks (stack + free-heap + free-page 0x00AAAAAA) were removed from
the release image — they changed boot behavior and may walk a corrupt
free list over live blocks. Added vt::debug_verify() (cells/rendered
ptr/cap/len + in-heap check) at shell entry: the next real-hardware
boot shows whether the corruption is present at shell entry or lands
during the banner write.
| -rw-r--r-- | rsdos/src/main.rs | 40 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 42 |
2 files changed, 49 insertions, 33 deletions
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index 52b37ec..42f95e5 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -344,22 +344,13 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 344 | } else { | 344 | } else { |
| 345 | rsnix::serial_println!("cursor: no mouse — overlay disabled"); | 345 | rsnix::serial_println!("cursor: no mouse — overlay disabled"); |
| 346 | } | 346 | } |
| 347 | // TEST HOOK: reproduce the OptiPlex '0xaaaaaa' uninit-drop crash in | 347 | // Shell-entry self-check: verify the vt grid structs are sane right |
| 348 | // QEMU. The real hardware's stack carries 0xAA fill left by | 348 | // before the shell's first print. The OptiPlex crashes in the first |
| 349 | // firmware; any shell value that is dropped without being written | 349 | // banner write when the cells VecData (or the shadow) holds the |
| 350 | // then frees garbage and trips the dealloc guard. Filling the | 350 | // firmware's 0xAA fill instead of real values — this line shows |
| 351 | // shell's future stack region with 0xAA here makes the same bug | 351 | // whether the corruption is already present at shell entry or lands |
| 352 | // visible on QEMU (serial + gdb available) instead of only on the | 352 | // later, and prints the exact field values when it is. |
| 353 | // OptiPlex. | 353 | rsnix::vt::debug_verify(); |
| 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(); | ||
| 363 | shell::run(); | 354 | shell::run(); |
| 364 | } | 355 | } |
| 365 | 356 | ||
| @@ -368,23 +359,6 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 368 | /// headroom. 128 KiB covers the whole shell frame chain on QEMU and | 359 | /// headroom. 128 KiB covers the whole shell frame chain on QEMU and |
| 369 | /// the OptiPlex. Best-effort: the region is below rsp inside the boot | 360 | /// the OptiPlex. Best-effort: the region is below rsp inside the boot |
| 370 | /// stack mapping; the call into `shell::run` overwrites the top word. | 361 | /// stack mapping; the call into `shell::run` overwrites the top word. |
| 371 | fn stack_fill_aa() { | ||
| 372 | let rsp: usize; | ||
| 373 | unsafe { | ||
| 374 | core::arch::asm!("mov {}, rsp", out(reg) rsp); | ||
| 375 | } | ||
| 376 | let top = rsp - 0x100; | ||
| 377 | let bottom = rsp - 0x21100; | ||
| 378 | unsafe { | ||
| 379 | let mut p = bottom; | ||
| 380 | while p < top { | ||
| 381 | core::ptr::write_volatile(p as *mut u64, 0x00AAAAAA00AAAAAA); | ||
| 382 | p += 8; | ||
| 383 | } | ||
| 384 | } | ||
| 385 | rsnix::serial_println!("stack: 0x00AAAAAA-fill 0x{:x}..0x{:x}", bottom, top); | ||
| 386 | } | ||
| 387 | |||
| 388 | // --------------------------------------------------------------------------- | 362 | // --------------------------------------------------------------------------- |
| 389 | // Print a memory summary from the stashed memory map | 363 | // Print a memory summary from the stashed memory map |
| 390 | // --------------------------------------------------------------------------- | 364 | // --------------------------------------------------------------------------- |
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 4578f6c..6343b78 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs | |||
| @@ -138,6 +138,48 @@ pub fn size() -> (usize, usize) { | |||
| 138 | unsafe { (ROWS, COLS) } | 138 | unsafe { (ROWS, COLS) } |
| 139 | } | 139 | } |
| 140 | 140 | ||
| 141 | /// Shell-entry self-check: dump the vt grid's heap structs so a real- | ||
| 142 | /// hardware boot shows whether the cells VecData / data arrays are sane | ||
| 143 | /// at shell entry (the OptiPlex's first banner write #GPs when the | ||
| 144 | /// cells buffer holds firmware 0xAA fill instead of real pointers). | ||
| 145 | pub fn debug_verify() { | ||
| 146 | unsafe { | ||
| 147 | let total = ROWS * COLS; | ||
| 148 | let msg = |m: &str| { | ||
| 149 | crate::serial_println!("vt: {}", m); | ||
| 150 | }; | ||
| 151 | if CELLS.is_null() || RENDERED.is_null() { | ||
| 152 | msg("verify: cells/renderd NULL — vt::init did not run"); | ||
| 153 | return; | ||
| 154 | } | ||
| 155 | let c = &*(CELLS as *const Vec<Cell>); | ||
| 156 | let r = &*(RENDERED as *const Vec<Cell>); | ||
| 157 | msg(&alloc::format!( | ||
| 158 | "verify: cells ptr={:#x} cap={:#x} len={:#x} rendered ptr={:#x} cap={:#x} len={:#x} want={:#x}", | ||
| 159 | c.as_ptr() as usize, | ||
| 160 | c.capacity(), | ||
| 161 | c.len(), | ||
| 162 | r.as_ptr() as usize, | ||
| 163 | r.capacity(), | ||
| 164 | r.len(), | ||
| 165 | total | ||
| 166 | )); | ||
| 167 | // The cells array must cover rows*cols and be inside the heap. | ||
| 168 | let heap_ok = crate::memory::heap_range() | ||
| 169 | .map(|(hs, he)| { | ||
| 170 | let p = c.as_ptr() as usize; | ||
| 171 | let bytes = c.capacity().saturating_mul(core::mem::size_of::<Cell>()); | ||
| 172 | p >= hs && p + bytes <= he | ||
| 173 | }) | ||
| 174 | .unwrap_or(false); | ||
| 175 | if c.len() != total || r.len() != total || !heap_ok { | ||
| 176 | msg("verify: FAILED — cells/rendered not sane at shell entry"); | ||
| 177 | } else { | ||
| 178 | msg("verify: ok"); | ||
| 179 | } | ||
| 180 | } | ||
| 181 | } | ||
| 182 | |||
| 141 | pub fn set_cursor_visible(v: bool) { | 183 | pub fn set_cursor_visible(v: bool) { |
| 142 | sync_scroll(); | 184 | sync_scroll(); |
| 143 | unsafe { | 185 | unsafe { |
