diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-04 01:44:05 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-04 01:44:05 +0200 |
| commit | 3e2261d83176752f3ea21e0c97f330b4c1757437 (patch) | |
| tree | 8bbc92f917121fafaf193f1af1c42fba624abda9 /rsnix/src/gop.rs | |
| parent | v0.5.8: heap-free QR encoder + crash-dump QR on framebuffer (diff) | |
| download | rs-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.rs | 51 |
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 | 258 | pub 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. | ||
| 324 | pub 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, |
