From 3e2261d83176752f3ea21e0c97f330b4c1757437 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 4 Aug 2026 01:44:05 +0200 Subject: v0.5.9: fix OptiPlex heap crash — EBS retry must not rebuild the free list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: on real hardware SetMode can invalidate the pre-captured ExitBootServices map key, so EBS fails and the retry refreshed the map via capture_memory_map -> init_heap, which REBUILT the free list from the raw map and wiped the allocation state of every pre-EBS live object (the display shadow and the vt grids). Their spans became free while still live, later allocations carved inside the shadow, and the display writes clobbered them — zeroed back-pointers (family A ptr=0x404443e0 block=0x0) and gray glyph pixels (family B 0xaaaaaa00aaaaaa = 0x00AAAAAA = PALETTE[7] fg-7 text colour, not a firmware scrub). Fix: refresh_and_exit now re-captures the map and returns a fresh EBS key without rebuilding the free list (refresh_map_key); the free list is the authoritative live-allocation state. Verified in QEMU under a forced EBS failure (SIMULATE_EBS_FAILURE test hook): verify ok, probe NOT free, flhead outside the shadow — identical to a clean boot, and the full shell sequence is crash-free. Also: removed the crash-family debug markers from the screen (boot-time heapdbg lines, vt verify marker, shadow marker) — the vt grid self-check stays serial-only. Version 0.5.8 -> 0.5.9. --- shell/src/lib.rs | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) (limited to 'shell/src') diff --git a/shell/src/lib.rs b/shell/src/lib.rs index 175a2c7..0f5ac4b 100644 --- a/shell/src/lib.rs +++ b/shell/src/lib.rs @@ -716,6 +716,7 @@ write `{} TIL `", "hello" => cmd_hello(&parsed), "info" => cmd_info(&parsed), "mem" => cmd_mem(&parsed), + "heapdbg" => cmd_heapdbg(&parsed), "mmap" => cmd_mmap(&parsed), "stress" => cmd_stress(&parsed), "date" => cmd_date(&parsed), @@ -1571,6 +1572,134 @@ fn cmd_halt() -> ! { rsnix::power::halt() } +/// Debug the crash-family: free-list state + the historical crash +/// offset (heap+0x443e0) + the shadow block's header. On the OptiPlex +/// this shows whether the shadow span leaked into the free list and +/// whether the probe region is zeroed/0xAA before any command runs. +fn cmd_heapdbg(_p: &Parsed) { + let h = rsnix::memory::heap_stats(); + term_println(&alloc::format!( + "heap: {:#x}..{:#x} cap={} MiB free={} KiB blocks={}", + h.heap_start, + h.heap_end, + h.heap_size / 1048576, + h.free_bytes / 1024, + h.free_blocks + )); + for l in rsnix::memory::freelist_lines(12) { + term_println(&l); + } + // fb vs heap overlap — OptiPlex crash hypothesis: the display's + // framebuffer may sit inside the heap region (GPU BAR not reserved in + // the EFI map), so display writes clobber live heap allocations. + if let Some((fb, fsz)) = rsnix::gop::framebuffer_region() { + let ov = h.heap_start < fb.saturating_add(fsz) && fb < h.heap_end; + term_println(&alloc::format!( + "fb {:#x} size={:#x} overlap-heap={}", + fb, fsz, ov + )); + } + let probe = h.heap_start.wrapping_add(0x443e0); // historical crash offset + match rsnix::memory::freelist_contains(probe) { + Some((b, s, off)) => term_println(&alloc::format!( + "probe {:#x} (+0x443e0) IN free blk {:#x} size={:#x} off={:#x}", + probe, b, s, off + )), + None => term_println(&alloc::format!("probe {:#x} (+0x443e0) NOT free", probe)), + } + let mut s = alloc::string::String::new(); + unsafe { + for i in 0..8 { + if i > 0 { + s.push(' '); + } + let v = *(probe as *const u32).add(i); + s.push_str(&alloc::format!("{:08x}", v)); + } + } + term_println(&alloc::format!("probe mem: {}", s)); + // Scan the live shadow payload for 0x00aaaaaa words. NOTE: this is + // the GRAY text color (rgb(170,170,170) = PALETTE[7] = fg 7), NOT a + // firmware scrub — the "0x00AAAAAA fill" in the OptiPlex crash dumps + // is just rendered text pixels. The scan's real value: untouched + // shadow rows must be 0; non-zero far rows = out-of-bounds writes. + if let Some((b, size, _)) = rsnix::gop::shadow_block_state() { + let pay = b + 32; + let words = (size.saturating_sub(32)) / 4; + let mut n = 0u32; + let mut first: [usize; 8] = [0; 8]; + let mut nf = 0usize; + unsafe { + let mut i = 0usize; + let cap = words.min(240_000); // bound the scan to avoid stalls + while i < cap { + let v = *(pay as *const u32).add(i); + if v == 0x00aa_aaaa { + if nf < first.len() { + first[nf] = i; + } + nf += 1; + n += 1; + } + i += 1; + } + } + let mut fs = alloc::string::String::new(); + for &f in first[..nf.min(8)].iter() { + fs.push_str(&alloc::format!("@{}(r{}c{}) ", f, f / 800, (f % 800) / 8)); + } + term_println(&alloc::format!( + "shadow scan: {}w/{} ({}%) {}", + n, + words, + (n * 1000 / (words as u32).max(1)) / 10, + fs + )); + // Probe far rows (untouched by text) to check the init zeroing. + let mut d = alloc::string::String::new(); + unsafe { + for (label, off) in [("r100", 80_000usize), ("r300", 240_000), ("r500", 400_000)] { + let w0 = *(pay as *const u32).add(off); + let w1 = *(pay as *const u32).add(off + 1); + d.push_str(&alloc::format!(" {}={:08x}{:08x}", label, w0, w1)); + } + } + term_println(&alloc::format!("shadow far: {}", d)); + // Underlying cell/rendered values at the first 0x00aaaaaa (gray + // text pixel) hit, if any: confirms the shadow word is the glyph + // pixel of a fg=7 cell (consistent) vs a shadow write outside + // the vt path (inconsistent). + if nf > 0 { + let f = first[0]; + let tr = f / 800 / 16; + let tc = (f / 8) % 100; + let (rc, rr) = rsnix::vt::cell_raw(tr, tc); + term_println(&alloc::format!( + "shadow@{:x} cell={:08x} rendered={:08x}", + f, rc, rr + )); + } + } + match rsnix::gop::shadow_block_state() { + Some((b, size, free)) => { + term_println(&alloc::format!( + "shadow blk {:#x} size={:#x} free={}", + b, + size, + free + )); + match rsnix::memory::freelist_contains(b) { + Some((fb, fs, fo)) => term_println(&alloc::format!( + "shadow base IN free list: blk {:#x} size={:#x} off={:#x}", + fb, fs, fo + )), + None => term_println("shadow base NOT in free list"), + } + } + None => term_println("shadow: NONE (no-shadow mode)"), + } +} + fn cmd_date(p: &Parsed) { let t = rsnix::rtc::now(); let mut pipe = Pipeline::new(&p.filters, &p.output); -- cgit v1.2.3