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 /shell | |
| 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-- | shell/src/lib.rs | 129 |
1 files changed, 129 insertions, 0 deletions
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 `{} <path> TIL <marker>`", | |||
| 716 | "hello" => cmd_hello(&parsed), | 716 | "hello" => cmd_hello(&parsed), |
| 717 | "info" => cmd_info(&parsed), | 717 | "info" => cmd_info(&parsed), |
| 718 | "mem" => cmd_mem(&parsed), | 718 | "mem" => cmd_mem(&parsed), |
| 719 | "heapdbg" => cmd_heapdbg(&parsed), | ||
| 719 | "mmap" => cmd_mmap(&parsed), | 720 | "mmap" => cmd_mmap(&parsed), |
| 720 | "stress" => cmd_stress(&parsed), | 721 | "stress" => cmd_stress(&parsed), |
| 721 | "date" => cmd_date(&parsed), | 722 | "date" => cmd_date(&parsed), |
| @@ -1571,6 +1572,134 @@ fn cmd_halt() -> ! { | |||
| 1571 | rsnix::power::halt() | 1572 | rsnix::power::halt() |
| 1572 | } | 1573 | } |
| 1573 | 1574 | ||
| 1575 | /// Debug the crash-family: free-list state + the historical crash | ||
| 1576 | /// offset (heap+0x443e0) + the shadow block's header. On the OptiPlex | ||
| 1577 | /// this shows whether the shadow span leaked into the free list and | ||
| 1578 | /// whether the probe region is zeroed/0xAA before any command runs. | ||
| 1579 | fn cmd_heapdbg(_p: &Parsed) { | ||
| 1580 | let h = rsnix::memory::heap_stats(); | ||
| 1581 | term_println(&alloc::format!( | ||
| 1582 | "heap: {:#x}..{:#x} cap={} MiB free={} KiB blocks={}", | ||
| 1583 | h.heap_start, | ||
| 1584 | h.heap_end, | ||
| 1585 | h.heap_size / 1048576, | ||
| 1586 | h.free_bytes / 1024, | ||
| 1587 | h.free_blocks | ||
| 1588 | )); | ||
| 1589 | for l in rsnix::memory::freelist_lines(12) { | ||
| 1590 | term_println(&l); | ||
| 1591 | } | ||
| 1592 | // fb vs heap overlap — OptiPlex crash hypothesis: the display's | ||
| 1593 | // framebuffer may sit inside the heap region (GPU BAR not reserved in | ||
| 1594 | // the EFI map), so display writes clobber live heap allocations. | ||
| 1595 | if let Some((fb, fsz)) = rsnix::gop::framebuffer_region() { | ||
| 1596 | let ov = h.heap_start < fb.saturating_add(fsz) && fb < h.heap_end; | ||
| 1597 | term_println(&alloc::format!( | ||
| 1598 | "fb {:#x} size={:#x} overlap-heap={}", | ||
| 1599 | fb, fsz, ov | ||
| 1600 | )); | ||
| 1601 | } | ||
| 1602 | let probe = h.heap_start.wrapping_add(0x443e0); // historical crash offset | ||
| 1603 | match rsnix::memory::freelist_contains(probe) { | ||
| 1604 | Some((b, s, off)) => term_println(&alloc::format!( | ||
| 1605 | "probe {:#x} (+0x443e0) IN free blk {:#x} size={:#x} off={:#x}", | ||
| 1606 | probe, b, s, off | ||
| 1607 | )), | ||
| 1608 | None => term_println(&alloc::format!("probe {:#x} (+0x443e0) NOT free", probe)), | ||
| 1609 | } | ||
| 1610 | let mut s = alloc::string::String::new(); | ||
| 1611 | unsafe { | ||
| 1612 | for i in 0..8 { | ||
| 1613 | if i > 0 { | ||
| 1614 | s.push(' '); | ||
| 1615 | } | ||
| 1616 | let v = *(probe as *const u32).add(i); | ||
| 1617 | s.push_str(&alloc::format!("{:08x}", v)); | ||
| 1618 | } | ||
| 1619 | } | ||
| 1620 | term_println(&alloc::format!("probe mem: {}", s)); | ||
| 1621 | // Scan the live shadow payload for 0x00aaaaaa words. NOTE: this is | ||
| 1622 | // the GRAY text color (rgb(170,170,170) = PALETTE[7] = fg 7), NOT a | ||
| 1623 | // firmware scrub — the "0x00AAAAAA fill" in the OptiPlex crash dumps | ||
| 1624 | // is just rendered text pixels. The scan's real value: untouched | ||
| 1625 | // shadow rows must be 0; non-zero far rows = out-of-bounds writes. | ||
| 1626 | if let Some((b, size, _)) = rsnix::gop::shadow_block_state() { | ||
| 1627 | let pay = b + 32; | ||
| 1628 | let words = (size.saturating_sub(32)) / 4; | ||
| 1629 | let mut n = 0u32; | ||
| 1630 | let mut first: [usize; 8] = [0; 8]; | ||
| 1631 | let mut nf = 0usize; | ||
| 1632 | unsafe { | ||
| 1633 | let mut i = 0usize; | ||
| 1634 | let cap = words.min(240_000); // bound the scan to avoid stalls | ||
| 1635 | while i < cap { | ||
| 1636 | let v = *(pay as *const u32).add(i); | ||
| 1637 | if v == 0x00aa_aaaa { | ||
| 1638 | if nf < first.len() { | ||
| 1639 | first[nf] = i; | ||
| 1640 | } | ||
| 1641 | nf += 1; | ||
| 1642 | n += 1; | ||
| 1643 | } | ||
| 1644 | i += 1; | ||
| 1645 | } | ||
| 1646 | } | ||
| 1647 | let mut fs = alloc::string::String::new(); | ||
| 1648 | for &f in first[..nf.min(8)].iter() { | ||
| 1649 | fs.push_str(&alloc::format!("@{}(r{}c{}) ", f, f / 800, (f % 800) / 8)); | ||
| 1650 | } | ||
| 1651 | term_println(&alloc::format!( | ||
| 1652 | "shadow scan: {}w/{} ({}%) {}", | ||
| 1653 | n, | ||
| 1654 | words, | ||
| 1655 | (n * 1000 / (words as u32).max(1)) / 10, | ||
| 1656 | fs | ||
| 1657 | )); | ||
| 1658 | // Probe far rows (untouched by text) to check the init zeroing. | ||
| 1659 | let mut d = alloc::string::String::new(); | ||
| 1660 | unsafe { | ||
| 1661 | for (label, off) in [("r100", 80_000usize), ("r300", 240_000), ("r500", 400_000)] { | ||
| 1662 | let w0 = *(pay as *const u32).add(off); | ||
| 1663 | let w1 = *(pay as *const u32).add(off + 1); | ||
| 1664 | d.push_str(&alloc::format!(" {}={:08x}{:08x}", label, w0, w1)); | ||
| 1665 | } | ||
| 1666 | } | ||
| 1667 | term_println(&alloc::format!("shadow far: {}", d)); | ||
| 1668 | // Underlying cell/rendered values at the first 0x00aaaaaa (gray | ||
| 1669 | // text pixel) hit, if any: confirms the shadow word is the glyph | ||
| 1670 | // pixel of a fg=7 cell (consistent) vs a shadow write outside | ||
| 1671 | // the vt path (inconsistent). | ||
| 1672 | if nf > 0 { | ||
| 1673 | let f = first[0]; | ||
| 1674 | let tr = f / 800 / 16; | ||
| 1675 | let tc = (f / 8) % 100; | ||
| 1676 | let (rc, rr) = rsnix::vt::cell_raw(tr, tc); | ||
| 1677 | term_println(&alloc::format!( | ||
| 1678 | "shadow@{:x} cell={:08x} rendered={:08x}", | ||
| 1679 | f, rc, rr | ||
| 1680 | )); | ||
| 1681 | } | ||
| 1682 | } | ||
| 1683 | match rsnix::gop::shadow_block_state() { | ||
| 1684 | Some((b, size, free)) => { | ||
| 1685 | term_println(&alloc::format!( | ||
| 1686 | "shadow blk {:#x} size={:#x} free={}", | ||
| 1687 | b, | ||
| 1688 | size, | ||
| 1689 | free | ||
| 1690 | )); | ||
| 1691 | match rsnix::memory::freelist_contains(b) { | ||
| 1692 | Some((fb, fs, fo)) => term_println(&alloc::format!( | ||
| 1693 | "shadow base IN free list: blk {:#x} size={:#x} off={:#x}", | ||
| 1694 | fb, fs, fo | ||
| 1695 | )), | ||
| 1696 | None => term_println("shadow base NOT in free list"), | ||
| 1697 | } | ||
| 1698 | } | ||
| 1699 | None => term_println("shadow: NONE (no-shadow mode)"), | ||
| 1700 | } | ||
| 1701 | } | ||
| 1702 | |||
| 1574 | fn cmd_date(p: &Parsed) { | 1703 | fn cmd_date(p: &Parsed) { |
| 1575 | let t = rsnix::rtc::now(); | 1704 | let t = rsnix::rtc::now(); |
| 1576 | let mut pipe = Pipeline::new(&p.filters, &p.output); | 1705 | let mut pipe = Pipeline::new(&p.filters, &p.output); |
