diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-03 00:21:52 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-03 00:21:52 +0200 |
| commit | 834b931d068a9a76e2c2a85267f24a22e93276cf (patch) | |
| tree | 55fec8d47398a51a0c40dec8fba4f9664a12233a | |
| parent | v0.5.7: vt verify also as a boot marker (real hardware has no serial) (diff) | |
| download | rs-dos-834b931d068a9a76e2c2a85267f24a22e93276cf.tar.gz rs-dos-834b931d068a9a76e2c2a85267f24a22e93276cf.zip | |
v0.5.7: guard dumps block-header math + 16-word window; vt content verify; shadow marker
The klog -t 2 crash drops a 126-byte String (the vt verify klog line)
whose data ptr 0x40444970 points into the first-heap-block region whose
content is the firmware 0x00AAAAAA scrub — a stale pointer, not a real
allocation (back-pointer slot never written). Upgrades so the next
real-hardware crash shows the full picture:
- guard: explicit candidate block bases (b=ptr-24 / b16=ptr-32) with
their size/free fields, plus a 16-word window (mem..mem4) from
ptr-0x30 instead of the old 8 words
- vt::debug_verify: content probe of the cells grid (must be all-BLANK
at shell entry) — catches a stale in-heap cells pointer that passes
the range checks
- gop: on-screen marker for shadow carved vs NO-SHADOW fallback
(the scrub content at the crash ptr suggests the shadow was rejected
and its block stayed free + firmware-scrubbed)
| -rw-r--r-- | rsnix/src/gop.rs | 7 | ||||
| -rw-r--r-- | rsnix/src/memory.rs | 120 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 33 |
3 files changed, 117 insertions, 43 deletions
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index 7aeb224..8e804e5 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs | |||
| @@ -293,6 +293,13 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { | |||
| 293 | .map(|c| core::str::from_utf8(c).unwrap_or("?")) | 293 | .map(|c| core::str::from_utf8(c).unwrap_or("?")) |
| 294 | .collect(); | 294 | .collect(); |
| 295 | dump_lines(&lines); | 295 | dump_lines(&lines); |
| 296 | crate::gop::boot_marker("gop: NO-SHADOW (fallback mode)"); | ||
| 297 | } else { | ||
| 298 | crate::gop::boot_marker(&alloc::format!( | ||
| 299 | "gop: shadow {:#x} {:.2} MiB", | ||
| 300 | SHADOW as usize, | ||
| 301 | swords as f64 * 4.0 / 1048576.0 | ||
| 302 | )); | ||
| 296 | } | 303 | } |
| 297 | } | 304 | } |
| 298 | } | 305 | } |
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs index e0f0142..f16f943 100644 --- a/rsnix/src/memory.rs +++ b/rsnix/src/memory.rs | |||
| @@ -480,47 +480,84 @@ fn heap_guard_report(ptr: usize, rsp: usize, size: usize, align: usize) -> ! { | |||
| 480 | w.len() | 480 | w.len() |
| 481 | }; | 481 | }; |
| 482 | 482 | ||
| 483 | // Words around the block header — the corruption lives in the | 483 | // Candidate block bases for this ptr (payload sits at block+24 for |
| 484 | // header's size/free slots or the payload. Only when the addresses | 484 | // align-1 allocs, block+32 for align-16) with their size/free |
| 485 | // are inside the mapped heap (reading a garbage pointer would fault | 485 | // fields, plus a longer raw window around ptr so the next crash |
| 486 | // mid-report). Four words per line: the fault band is ~110 chars | 486 | // shows the header region and neighbours, not just the payload. |
| 487 | // wide, so one long line would truncate under hand-copy. | ||
| 488 | let mut b4 = [0u8; 96]; | 487 | let mut b4 = [0u8; 96]; |
| 489 | let mut b4b = [0u8; 96]; | 488 | let mut b4b = [0u8; 96]; |
| 490 | let (n4, n4b) = { | 489 | let mut b4c = [0u8; 96]; |
| 491 | let focus = if block >= hs && block < he { block } else { ptr }; | 490 | let mut b4d = [0u8; 96]; |
| 492 | if focus >= hs + 8 && focus < he { | 491 | let focus = if block >= hs && block < he { block } else { ptr }; |
| 493 | let lo = (focus - 16).max(hs); | 492 | let mut a = (focus.wrapping_sub(48)).max(hs) & !7; |
| 494 | let hi = (focus + 40).min(he - 8); | 493 | let (mut n4, mut n4b, mut n4c, mut n4d) = (0usize, 0usize, 0usize, 0usize); |
| 495 | let mut w = crate::strbuf::FmtBuf::new(&mut b4); | 494 | if focus >= hs + 8 && focus < he { |
| 496 | let _ = fmt::write(&mut w, core::format_args!("mem:")); | 495 | let mut w = crate::strbuf::FmtBuf::new(&mut b4); |
| 497 | let mut a = lo & !7; | 496 | let _ = fmt::write(&mut w, core::format_args!("mem:")); |
| 498 | let mut n = 0; | 497 | for _ in 0..4 { |
| 499 | while a <= hi && n < 4 { | 498 | let _ = fmt::write( |
| 500 | let _ = fmt::write( | 499 | &mut w, |
| 501 | &mut w, | 500 | core::format_args!(" {:x}={:#x}", a, unsafe { *(a as *const usize) }), |
| 502 | core::format_args!(" {:x}={:#x}", a, unsafe { *(a as *const usize) }), | 501 | ); |
| 503 | ); | 502 | a += 8; |
| 504 | a += 8; | 503 | } |
| 505 | n += 1; | 504 | n4 = w.len(); |
| 506 | } | 505 | let mut w = crate::strbuf::FmtBuf::new(&mut b4b); |
| 507 | let mut w2 = crate::strbuf::FmtBuf::new(&mut b4b); | 506 | let _ = fmt::write(&mut w, core::format_args!("mem2:")); |
| 508 | let _ = fmt::write(&mut w2, core::format_args!("mem2:")); | 507 | for _ in 0..4 { |
| 509 | let mut n = 0; | 508 | let _ = fmt::write( |
| 510 | while a <= hi && n < 4 { | 509 | &mut w, |
| 511 | let _ = fmt::write( | 510 | core::format_args!(" {:x}={:#x}", a, unsafe { *(a as *const usize) }), |
| 512 | &mut w2, | 511 | ); |
| 513 | core::format_args!(" {:x}={:#x}", a, unsafe { *(a as *const usize) }), | 512 | a += 8; |
| 514 | ); | ||
| 515 | a += 8; | ||
| 516 | n += 1; | ||
| 517 | } | ||
| 518 | (w.len(), w2.len()) | ||
| 519 | } else { | ||
| 520 | let mut w = crate::strbuf::FmtBuf::new(&mut b4); | ||
| 521 | let _ = fmt::write(&mut w, core::format_args!("mem: out of heap — no dump")); | ||
| 522 | (w.len(), 0) | ||
| 523 | } | 513 | } |
| 514 | n4b = w.len(); | ||
| 515 | let mut w = crate::strbuf::FmtBuf::new(&mut b4c); | ||
| 516 | let _ = fmt::write(&mut w, core::format_args!("mem3:")); | ||
| 517 | for _ in 0..4 { | ||
| 518 | let _ = fmt::write( | ||
| 519 | &mut w, | ||
| 520 | core::format_args!(" {:x}={:#x}", a, unsafe { *(a as *const usize) }), | ||
| 521 | ); | ||
| 522 | a += 8; | ||
| 523 | } | ||
| 524 | n4c = w.len(); | ||
| 525 | let mut w = crate::strbuf::FmtBuf::new(&mut b4d); | ||
| 526 | let _ = fmt::write(&mut w, core::format_args!("mem4:")); | ||
| 527 | for _ in 0..4 { | ||
| 528 | let _ = fmt::write( | ||
| 529 | &mut w, | ||
| 530 | core::format_args!(" {:x}={:#x}", a, unsafe { *(a as *const usize) }), | ||
| 531 | ); | ||
| 532 | a += 8; | ||
| 533 | } | ||
| 534 | n4d = w.len(); | ||
| 535 | } else { | ||
| 536 | let mut w = crate::strbuf::FmtBuf::new(&mut b4); | ||
| 537 | let _ = fmt::write(&mut w, core::format_args!("mem: out of heap — no dump")); | ||
| 538 | n4 = w.len(); | ||
| 539 | }; | ||
| 540 | // Explicit candidate-block header fields (size/free at +0/+8). | ||
| 541 | let mut b4e = [0u8; 96]; | ||
| 542 | let ne = if ptr >= hs + 24 && ptr <= he { | ||
| 543 | let hb = ptr - 24; | ||
| 544 | let hb2 = ptr - 32; | ||
| 545 | let mut w = crate::strbuf::FmtBuf::new(&mut b4e); | ||
| 546 | let _ = fmt::write( | ||
| 547 | &mut w, | ||
| 548 | core::format_args!( | ||
| 549 | "hdr: b={:#x} s={:#x} f={:#x} b16={:#x} s={:#x} f={:#x}", | ||
| 550 | hb, | ||
| 551 | unsafe { *(hb as *const usize) }, | ||
| 552 | unsafe { *((hb + 8) as *const usize) }, | ||
| 553 | hb2, | ||
| 554 | unsafe { *(hb2 as *const usize) }, | ||
| 555 | unsafe { *((hb2 + 8) as *const usize) }, | ||
| 556 | ), | ||
| 557 | ); | ||
| 558 | w.len() | ||
| 559 | } else { | ||
| 560 | 0 | ||
| 524 | }; | 561 | }; |
| 525 | 562 | ||
| 526 | // Live return-address chain on the stack above our rsp (dealloc ← | 563 | // Live return-address chain on the stack above our rsp (dealloc ← |
| @@ -563,15 +600,18 @@ fn heap_guard_report(ptr: usize, rsp: usize, size: usize, align: usize) -> ! { | |||
| 563 | let l3b = core::str::from_utf8(&b3b[..n3b]).unwrap_or(""); | 600 | let l3b = core::str::from_utf8(&b3b[..n3b]).unwrap_or(""); |
| 564 | let l4 = core::str::from_utf8(&b4[..n4]).unwrap_or(""); | 601 | let l4 = core::str::from_utf8(&b4[..n4]).unwrap_or(""); |
| 565 | let l4b = if n4b > 0 { core::str::from_utf8(&b4b[..n4b]).unwrap_or("") } else { "" }; | 602 | let l4b = if n4b > 0 { core::str::from_utf8(&b4b[..n4b]).unwrap_or("") } else { "" }; |
| 603 | let l4c = if n4c > 0 { core::str::from_utf8(&b4c[..n4c]).unwrap_or("") } else { "" }; | ||
| 604 | let l4d = if n4d > 0 { core::str::from_utf8(&b4d[..n4d]).unwrap_or("") } else { "" }; | ||
| 605 | let l4e = if ne > 0 { core::str::from_utf8(&b4e[..ne]).unwrap_or("") } else { "" }; | ||
| 566 | let l5 = core::str::from_utf8(&b5[..n5]).unwrap_or(""); | 606 | let l5 = core::str::from_utf8(&b5[..n5]).unwrap_or(""); |
| 567 | let l6 = core::str::from_utf8(&b6[..n6]).unwrap_or(""); | 607 | let l6 = core::str::from_utf8(&b6[..n6]).unwrap_or(""); |
| 568 | let l7 = core::str::from_utf8(&b7[..n7]).unwrap_or(""); | 608 | let l7 = core::str::from_utf8(&b7[..n7]).unwrap_or(""); |
| 569 | let l8 = core::str::from_utf8(&b8[..n8]).unwrap_or(""); | 609 | let l8 = core::str::from_utf8(&b8[..n8]).unwrap_or(""); |
| 570 | for l in [l2, l3, l3b, l4, l4b, l5, l6, l7, l8] { | 610 | for l in [l2, l3, l3b, l4, l4b, l4c, l4d, l4e, l5, l6, l7, l8] { |
| 571 | crate::serial::write_raw(l); | 611 | crate::serial::write_raw(l); |
| 572 | crate::serial::write_raw("\r\n"); | 612 | crate::serial::write_raw("\r\n"); |
| 573 | } | 613 | } |
| 574 | let lines = [l1, l2, l3, l3b, l4, l4b, l5, l6, l7, l8]; | 614 | let lines = [l1, l2, l3, l3b, l4, l4b, l4c, l4d, l4e, l5, l6, l7, l8]; |
| 575 | crate::gop::dump_lines(&lines); | 615 | crate::gop::dump_lines(&lines); |
| 576 | crate::fs::write_crash_log(&lines); | 616 | crate::fs::write_crash_log(&lines); |
| 577 | crate::interrupts::halt() | 617 | crate::interrupts::halt() |
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 2d8ece6..2906258 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs | |||
| @@ -172,13 +172,40 @@ pub fn debug_verify() { | |||
| 172 | p >= hs && p + bytes <= he | 172 | p >= hs && p + bytes <= he |
| 173 | }) | 173 | }) |
| 174 | .unwrap_or(false); | 174 | .unwrap_or(false); |
| 175 | if c.len() != total || r.len() != total || !heap_ok { | 175 | // Content check: at shell entry nothing has been printed yet, so |
| 176 | // every cell must still be BLANK (all-zero). A stale cells data | ||
| 177 | // pointer that happens to be in-heap still passes the range | ||
| 178 | // checks (len/cap look fine) but its "cells" read back as the | ||
| 179 | // underlying memory's leftover fill — the OptiPlex shows the | ||
| 180 | // firmware 0x00AAAAAA scrub there. This catches that. | ||
| 181 | let mut content_ok = true; | ||
| 182 | if heap_ok && c.len() == total && total > 0 { | ||
| 183 | let mut i = 0usize; | ||
| 184 | // Sample the first cell, the last cell, and a few interior | ||
| 185 | // cells — reading the whole grid here would be ~60 KiB of | ||
| 186 | // checks at every boot for no real benefit. | ||
| 187 | let probes = [0usize, total / 3, (2 * total) / 3, total - 1]; | ||
| 188 | for &p in probes.iter() { | ||
| 189 | if i >= probes.len() { | ||
| 190 | break; | ||
| 191 | } | ||
| 192 | i += 1; | ||
| 193 | let cell = *c.as_ptr().add(p); | ||
| 194 | let raw = cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24); | ||
| 195 | if raw != 0 { | ||
| 196 | content_ok = false; | ||
| 197 | break; | ||
| 198 | } | ||
| 199 | } | ||
| 200 | } | ||
| 201 | if c.len() != total || r.len() != total || !heap_ok || !content_ok { | ||
| 176 | msg("verify: FAILED — cells/rendered not sane at shell entry"); | 202 | msg("verify: FAILED — cells/rendered not sane at shell entry"); |
| 177 | crate::gop::boot_marker(&alloc::format!( | 203 | crate::gop::boot_marker(&alloc::format!( |
| 178 | "vt: cells {:#x} {}/{} FAILED", | 204 | "vt: cells {:#x} {}/{} {}", |
| 179 | c.as_ptr() as usize, | 205 | c.as_ptr() as usize, |
| 180 | c.len(), | 206 | c.len(), |
| 181 | total | 207 | total, |
| 208 | if content_ok { "range" } else { "CONTENT-GARBAGE" } | ||
| 182 | )); | 209 | )); |
| 183 | } else { | 210 | } else { |
| 184 | msg("verify: ok"); | 211 | msg("verify: ok"); |
