From d5f41d6e057300c82b83653b4afadeffb45a9f19 Mon Sep 17 00:00:00 2001 From: salmonik Date: Mon, 3 Aug 2026 01:34:31 +0200 Subject: v0.5.7: fix vt verify content check — expect BLANK (0x70020), not zero MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pre-verify 0x70020 'mystery' was a debug artifact, not corruption: BLANK = {ch:32, fg:7, bg:0} = raw 0x70020, and vec![BLANK; n] fills both grids with it at init. The old check compared cells against 0 and its probe loop break-ed on the first non-zero cell, so probes 1-3 stayed at their zero-initialized slots — c=[0x70020,0,0,0] was bogus on every healthy boot. A full-grid dump proves QEMU grids are 100% BLANK at shell entry (nz=3700 blank=3700). Fix: probes compare against blank_raw (0x70020), record all four values (no early break), probe both cells and rendered, and add a full-grid nz/blank summary line. The OptiPlex firmware 0x00AAAAAA scrub remains cleanly distinguishable (scrub != blank). Markers: the shell banner occupies grid rows 0-1 and the prompt row 2 (proven by screendump), and a stale crash.log replays at rows 2+, so the shadow and vt markers moved to the bottom of the screen via the new gop::text_rows()/bottom_row(off) helpers instead of rows 1-2. QEMU verified: verify: ok every boot; test-msg + test-fault PASS; debug and release builds 0 warnings. --- rsnix/src/vt.rs | 132 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 104 insertions(+), 28 deletions(-) (limited to 'rsnix/src/vt.rs') diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 2906258..6e1f12f 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs @@ -173,48 +173,124 @@ pub fn debug_verify() { }) .unwrap_or(false); // Content check: at shell entry nothing has been printed yet, so - // every cell must still be BLANK (all-zero). A stale cells data - // pointer that happens to be in-heap still passes the range - // checks (len/cap look fine) but its "cells" read back as the - // underlying memory's leftover fill — the OptiPlex shows the - // firmware 0x00AAAAAA scrub there. This catches that. + // every cell must still hold the grid's init fill. NOTE: that + // fill is BLANK (space + fg 7 + bg 0 = raw 0x70020), NOT zero — + // `vec![BLANK; n]` fills the grid with BLANK, and all boot + // output to this point is serial-only (boot markers are a + // direct-FB blit, never the vt grid). The v0.5.7 check + // compared against 0 and therefore tripped on every healthy + // boot (the "0x70020 at grid(0,0)" mystery — the grid's own + // init value, with the probe loop's early `break` leaving the + // other three probe slots as zero-initialized). A stale cells + // data pointer that happens to be in-heap still passes the + // range checks (len/cap look fine) but its "cells" read back as + // the underlying memory's leftover fill — the OptiPlex shows + // the firmware 0x00AAAAAA scrub there. BLANK (0x70020) vs + // scrub (0x00AAAAAA) is cleanly distinguishable. + let blank_raw = BLANK.ch as u32 | ((BLANK.fg as u32) << 16) | ((BLANK.bg as u32) << 24); let mut content_ok = true; + let mut rcontent_ok = true; + let mut c_probes_raw: [u32; 4] = [0; 4]; + let mut r_probes_raw: [u32; 4] = [0; 4]; if heap_ok && c.len() == total && total > 0 { - let mut i = 0usize; - // Sample the first cell, the last cell, and a few interior + // Sample the first cell, the last cell, and two interior // cells — reading the whole grid here would be ~60 KiB of - // checks at every boot for no real benefit. + // checks at every boot for no real benefit. All four + // probes are recorded unconditionally (no early break) so + // a serial log shows every value. let probes = [0usize, total / 3, (2 * total) / 3, total - 1]; - for &p in probes.iter() { - if i >= probes.len() { - break; - } - i += 1; + for (pi, &p) in probes.iter().enumerate() { let cell = *c.as_ptr().add(p); let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); - if raw != 0 { + c_probes_raw[pi] = raw; + if raw != blank_raw { content_ok = false; - break; + } + } + // Same probe on the rendered grid: it is a *separate* + // allocation, so its content discriminates "only the cells + // VecData's ptr field is stale" (rendered ok) from "both + // grids' memory was clobbered / both VecData ptrs are + // stale" (rendered garbage too). + for (pi, &p) in probes.iter().enumerate() { + let cell = *r.as_ptr().add(p); + let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); + r_probes_raw[pi] = raw; + if raw != blank_raw { + rcontent_ok = false; + } + } + } + // Full-grid summary (cheap: a few thousand reads, serial-only). + // Definitive health signal on QEMU: healthy = every cell is + // BLANK (nz == blanks == total); the probes above are just the + // tripwire, this catches a partial clobber that the four + // samples happen to miss. + let (mut c_nz, mut c_blank) = (0usize, 0usize); + let (mut r_nz, mut r_blank) = (0usize, 0usize); + if heap_ok && c.len() == total && r.len() == total && total > 0 { + for i in 0..total { + let cell = *c.as_ptr().add(i); + let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); + if raw != 0 { + c_nz += 1; + } + if raw == blank_raw { + c_blank += 1; + } + let cell = *r.as_ptr().add(i); + let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); + if raw != 0 { + r_nz += 1; + } + if raw == blank_raw { + r_blank += 1; } } } - if c.len() != total || r.len() != total || !heap_ok || !content_ok { + msg(&alloc::format!( + "verify: grids c nz={} blank={} r nz={} blank={} (of {})", + c_nz, c_blank, r_nz, r_blank, total + )); + if c.len() != total || r.len() != total || !heap_ok || !content_ok || !rcontent_ok { msg("verify: FAILED — cells/rendered not sane at shell entry"); - crate::gop::boot_marker(&alloc::format!( - "vt: cells {:#x} {}/{} {}", - c.as_ptr() as usize, - c.len(), - total, - if content_ok { "range" } else { "CONTENT-GARBAGE" } + msg(&alloc::format!( + "verify: probes c=[{:#x},{:#x},{:#x},{:#x}] r=[{:#x},{:#x},{:#x},{:#x}]", + c_probes_raw[0], + c_probes_raw[1], + c_probes_raw[2], + c_probes_raw[3], + r_probes_raw[0], + r_probes_raw[1], + r_probes_raw[2], + r_probes_raw[3] )); + // Bottom-anchored: row 0-1 get overwritten by the shell + // banner, row 2 by the prompt (and a stale crash.log + // replays at rows 2+), so this marker must live at the very + // bottom to stay readable until the user can copy it. + crate::gop::boot_marker_at( + &alloc::format!( + "vt: c {:#x} cap={:#x} {} | r {:#x} {}", + c.as_ptr() as usize, + c.capacity(), + if content_ok { "ok" } else { "GARBAGE" }, + r.as_ptr() as usize, + if rcontent_ok { "ok" } else { "GARBAGE" } + ), + crate::gop::bottom_row(1), + ); } else { msg("verify: ok"); - crate::gop::boot_marker(&alloc::format!( - "vt: cells {:#x} {}/{} ok", - c.as_ptr() as usize, - c.len(), - total - )); + crate::gop::boot_marker_at( + &alloc::format!( + "vt: c {:#x} cap={:#x} ok | r {:#x} ok", + c.as_ptr() as usize, + c.capacity(), + r.as_ptr() as usize + ), + crate::gop::bottom_row(1), + ); } } } -- cgit v1.2.3