summaryrefslogtreecommitdiffstats
path: root/rsnix/src/gop.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-04 01:44:05 +0200
committersalmonik <salmonik@tuta.io>2026-08-04 01:44:05 +0200
commit3e2261d83176752f3ea21e0c97f330b4c1757437 (patch)
tree8bbc92f917121fafaf193f1af1c42fba624abda9 /rsnix/src/gop.rs
parentv0.5.8: heap-free QR encoder + crash-dump QR on framebuffer (diff)
downloadrs-dos-3e2261d83176752f3ea21e0c97f330b4c1757437.tar.gz
rs-dos-3e2261d83176752f3ea21e0c97f330b4c1757437.zip
v0.5.9: fix OptiPlex heap crash — EBS retry must not rebuild the free list
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.
Diffstat (limited to '')
-rw-r--r--rsnix/src/gop.rs51
1 files changed, 32 insertions, 19 deletions
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs
index ecf8fa9..ccc6509 100644
--- a/rsnix/src/gop.rs
+++ b/rsnix/src/gop.rs
@@ -242,16 +242,20 @@ pub fn init(gop: &mut GraphicsOutputProtocol) {
242 unsafe { 242 unsafe {
243 FB = Some(fbf); 243 FB = Some(fbf);
244 } 244 }
245 // The RAM backbuffer is the *first* heap allocation. It is allocated
246 // pre-ExitBootServices (before vt::init builds its grids) so it sits at
247 // the front of the free list; the heap free list already excludes the
248 // fb region (init_heap split at the fb hole) and the range-overlap
249 // check below cannot alias the display. An ExitBootServices retry on
250 // real hardware no longer rebuilds the free list (see
251 // memory::refresh_map_key), so this pre-EBS allocation is preserved.
252 shadow_late_init();
253}
245 254
246 // The RAM backbuffer is the *first* heap allocation: allocated here 255/// Allocate the RAM backbuffer as the FIRST heap allocation (called from
247 // (pre-ExitBootServices, before vt::init builds its grids) so no 256/// `init`, pre-ExitBootServices). Degrades to no-shadow mode (never
248 // other heap object can collide with it — the v0.5.3 layout that 257/// panics) when the range would overlap the framebuffer or the alloc fails.
249 // provably boots on real hardware. The heap free list excludes the 258pub fn shadow_late_init() {
250 // fb region (init_heap splits at the fb hole) and we additionally
251 // reject any range overlapping the fb, so the shadow can never
252 // alias the display (an aliased shadow turns every scroll into a
253 // 1.8 MB MMIO→MMIO memcpy on an uncached real fb — a wedge).
254 // Allocation failure degrades to no-shadow mode, never panics.
255 unsafe { 259 unsafe {
256 if let Some(fbinfo) = fb().as_ref() { 260 if let Some(fbinfo) = fb().as_ref() {
257 let words = (fbinfo.pitch * fbinfo.height) / 4; 261 let words = (fbinfo.pitch * fbinfo.height) / 4;
@@ -293,16 +297,6 @@ pub fn init(gop: &mut GraphicsOutputProtocol) {
293 .map(|c| core::str::from_utf8(c).unwrap_or("?")) 297 .map(|c| core::str::from_utf8(c).unwrap_or("?"))
294 .collect(); 298 .collect();
295 dump_lines(&lines); 299 dump_lines(&lines);
296 crate::gop::boot_marker_at("gop: NO-SHADOW (fallback)", bottom_row(0));
297 } else {
298 crate::gop::boot_marker_at(
299 &alloc::format!(
300 "gop: shadow {:#x} {:.2} MiB",
301 SHADOW as usize,
302 swords as f64 * 4.0 / 1048576.0
303 ),
304 bottom_row(0),
305 );
306 } 300 }
307 } 301 }
308 } 302 }
@@ -319,6 +313,25 @@ pub fn boot_marker(msg: &str) {
319 boot_marker_at(msg, 0); 313 boot_marker_at(msg, 0);
320} 314}
321 315
316/// DEBUG (crash-family): read the shadow block's header words.
317/// Returns `(block_base, size, free_flag)` if a shadow is live.
318///
319/// The shadow is the first heap allocation: payload at base+32, header
320/// at base. A live shadow shows `free == 0`; if its block ever leaked
321/// back into the free list (while `SHADOW` still points at it), later
322/// allocations carve into the zeroed span and the display writes keep
323/// zeroing them — the OptiPlex crash family signature.
324pub fn shadow_block_state() -> Option<(usize, usize, usize)> {
325 unsafe {
326 if SHADOW.is_null() {
327 return None;
328 }
329 let base = (SHADOW as usize).wrapping_sub(32);
330 let size = *(base as *const usize);
331 let free = *((base + 8) as *const usize);
332 Some((base, size, free))
333 }
334}
322/// boot_marker at an explicit text row. The shell's first paint 335/// boot_marker at an explicit text row. The shell's first paint
323/// occupies grid rows 0 (motd) and 1 (=== banner) and the prompt sits 336/// occupies grid rows 0 (motd) and 1 (=== banner) and the prompt sits
324/// at row 2 — markers whose *values* matter after boot (shadow state, 337/// at row 2 — markers whose *values* matter after boot (shadow state,