diff options
Diffstat (limited to 'rsnix/src')
| -rw-r--r-- | rsnix/src/gop.rs | 51 | ||||
| -rw-r--r-- | rsnix/src/memory.rs | 107 | ||||
| -rw-r--r-- | rsnix/src/vt.rs | 69 |
3 files changed, 181 insertions, 46 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, |
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs index 3ba53fc..099f607 100644 --- a/rsnix/src/memory.rs +++ b/rsnix/src/memory.rs | |||
| @@ -624,6 +624,20 @@ fn heap_guard_report(ptr: usize, rsp: usize, size: usize, align: usize) -> ! { | |||
| 624 | /// .bss working buffer so no pool allocation (and thus no memory-map change) | 624 | /// .bss working buffer so no pool allocation (and thus no memory-map change) |
| 625 | /// occurs between GetMemoryMap and ExitBootServices. | 625 | /// occurs between GetMemoryMap and ExitBootServices. |
| 626 | pub fn capture_memory_map(bs: &BootServices) -> Result<usize, Status> { | 626 | pub fn capture_memory_map(bs: &BootServices) -> Result<usize, Status> { |
| 627 | capture_memory_map_impl(bs, true) | ||
| 628 | } | ||
| 629 | |||
| 630 | /// Re-capture the memory map and return a FRESH ExitBootServices key | ||
| 631 | /// WITHOUT rebuilding the heap free list. Used by the EBS retry path: | ||
| 632 | /// a rebuild from the raw map wipes the allocation state of every | ||
| 633 | /// pre-EBS live allocation (the display shadow, the vt grids) — their | ||
| 634 | /// spans then become free while still in use, and later allocations | ||
| 635 | /// land inside the live shadow (the OptiPlex crash family). | ||
| 636 | pub fn refresh_map_key(bs: &BootServices) -> Result<usize, Status> { | ||
| 637 | capture_memory_map_impl(bs, false) | ||
| 638 | } | ||
| 639 | |||
| 640 | fn capture_memory_map_impl(bs: &BootServices, rebuild: bool) -> Result<usize, Status> { | ||
| 627 | let mut map_key: usize = 0; | 641 | let mut map_key: usize = 0; |
| 628 | let mut desc_size: usize = 0; | 642 | let mut desc_size: usize = 0; |
| 629 | let mut desc_ver: u32 = 0; | 643 | let mut desc_ver: u32 = 0; |
| @@ -694,7 +708,9 @@ pub fn capture_memory_map(bs: &BootServices) -> Result<usize, Status> { | |||
| 694 | // Do NOT free anything — the static buffer stays. Map key is still | 708 | // Do NOT free anything — the static buffer stays. Map key is still |
| 695 | // valid because no pool allocation/free happened after GetMemoryMap. | 709 | // valid because no pool allocation/free happened after GetMemoryMap. |
| 696 | 710 | ||
| 697 | init_heap(); | 711 | if rebuild { |
| 712 | init_heap(); | ||
| 713 | } | ||
| 698 | Ok(map_key) | 714 | Ok(map_key) |
| 699 | } | 715 | } |
| 700 | 716 | ||
| @@ -705,7 +721,13 @@ pub fn capture_memory_map(bs: &BootServices) -> Result<usize, Status> { | |||
| 705 | /// `image_handle` must be the valid EFI image handle this kernel was | 721 | /// `image_handle` must be the valid EFI image handle this kernel was |
| 706 | /// started with (opaque firmware pointer; never dereferenced here). | 722 | /// started with (opaque firmware pointer; never dereferenced here). |
| 707 | pub unsafe fn refresh_and_exit(bs: &BootServices, image_handle: Handle) -> Result<usize, Status> { | 723 | pub unsafe fn refresh_and_exit(bs: &BootServices, image_handle: Handle) -> Result<usize, Status> { |
| 708 | let key = capture_memory_map(bs)?; | 724 | // Fresh key WITHOUT rebuilding the heap free list: a rebuild from the |
| 725 | // raw map wipes the allocation state of every pre-EBS live allocation | ||
| 726 | // (the display shadow, the vt grids) — their spans then become free | ||
| 727 | // while still in use, and later allocations land inside the live | ||
| 728 | // shadow (the OptiPlex crash family: display writes clobber heap | ||
| 729 | // objects, zeroed back-pointers, gray-text pixels). | ||
| 730 | let key = refresh_map_key(bs)?; | ||
| 709 | let status = unsafe { (bs.exit_boot_services)(image_handle, key) }; | 731 | let status = unsafe { (bs.exit_boot_services)(image_handle, key) }; |
| 710 | if status == EFI_SUCCESS { | 732 | if status == EFI_SUCCESS { |
| 711 | Ok(key) | 733 | Ok(key) |
| @@ -833,6 +855,87 @@ pub fn heap_stats() -> HeapStats { | |||
| 833 | } | 855 | } |
| 834 | } | 856 | } |
| 835 | 857 | ||
| 858 | /// DEBUG (crash-family): which free block contains `addr`, if any. | ||
| 859 | /// Returns `(block_base, block_size, offset_within_block)`. | ||
| 860 | /// | ||
| 861 | /// The OptiPlex heap-corruption dumps land at deterministic offsets | ||
| 862 | /// inside the shadow's would-be span; this tells us whether the shadow | ||
| 863 | /// block (or any block spanning that offset) has leaked back into the | ||
| 864 | /// free list at shell entry or later. | ||
| 865 | pub fn freelist_contains(addr: usize) -> Option<(usize, usize, usize)> { | ||
| 866 | unsafe { | ||
| 867 | let mut cur = FREE_LIST; | ||
| 868 | while !cur.is_null() { | ||
| 869 | let base = cur as usize; | ||
| 870 | let size = (*cur).size; | ||
| 871 | if addr >= base && addr < base.saturating_add(size) { | ||
| 872 | return Some((base, size, addr - base)); | ||
| 873 | } | ||
| 874 | cur = (*(*cur).links()).next; | ||
| 875 | } | ||
| 876 | None | ||
| 877 | } | ||
| 878 | } | ||
| 879 | |||
| 880 | /// DEBUG (crash-family): the free list head block (base, size). | ||
| 881 | pub fn freelist_head() -> Option<(usize, usize)> { | ||
| 882 | unsafe { | ||
| 883 | if FREE_LIST.is_null() { | ||
| 884 | None | ||
| 885 | } else { | ||
| 886 | Some((FREE_LIST as usize, (*FREE_LIST).size)) | ||
| 887 | } | ||
| 888 | } | ||
| 889 | } | ||
| 890 | |||
| 891 | /// DEBUG (OptiPlex sim): force the first ExitBootServices call to fail, | ||
| 892 | /// exercising the capture_memory_map -> init_heap free-list rebuild that | ||
| 893 | /// wiped the pre-EBS shadow on real hardware. Returns true when the | ||
| 894 | /// simulation is armed (one shot). | ||
| 895 | pub static mut SIMULATE_EBS_FAILURE: bool = false; | ||
| 896 | pub fn simulate_ebs_failure() -> bool { | ||
| 897 | unsafe { | ||
| 898 | let r = SIMULATE_EBS_FAILURE; | ||
| 899 | SIMULATE_EBS_FAILURE = false; | ||
| 900 | r | ||
| 901 | } | ||
| 902 | } | ||
| 903 | |||
| 904 | /// DEBUG (crash-family): walk the free list, formatting at most `max` | ||
| 905 | /// blocks as text lines for the screen/serial. | ||
| 906 | /// | ||
| 907 | /// Snapshot the walk into a fixed stack array first: printing the lines | ||
| 908 | /// itself allocates, and an interleaved alloc/free would shift the list | ||
| 909 | /// under the walk (the shell's tty path allocates per line). | ||
| 910 | pub fn freelist_lines(max: usize) -> alloc::vec::Vec<alloc::string::String> { | ||
| 911 | let mut out = alloc::vec::Vec::new(); | ||
| 912 | unsafe { | ||
| 913 | let mut cur = FREE_LIST; | ||
| 914 | let cap = max.min(24); | ||
| 915 | let mut snap: [(usize, usize); 24] = [(0, 0); 24]; | ||
| 916 | let mut seen = 0usize; | ||
| 917 | while !cur.is_null() && seen < cap { | ||
| 918 | snap[seen] = (cur as usize, (*cur).size); | ||
| 919 | seen += 1; | ||
| 920 | cur = (*(*cur).links()).next; | ||
| 921 | } | ||
| 922 | if seen == 0 { | ||
| 923 | out.push("fl[] EMPTY".into()); | ||
| 924 | } else { | ||
| 925 | for (base, size) in snap[..seen].iter() { | ||
| 926 | out.push(alloc::format!( | ||
| 927 | "fl[{}] {:#x} size={:#x} end={:#x}", | ||
| 928 | out.len(), | ||
| 929 | base, | ||
| 930 | size, | ||
| 931 | base.wrapping_add(*size) | ||
| 932 | )); | ||
| 933 | } | ||
| 934 | } | ||
| 935 | } | ||
| 936 | out | ||
| 937 | } | ||
| 938 | |||
| 836 | /// Allocator smoke test — exercises alloc/free/coalesce without touching | 939 | /// Allocator smoke test — exercises alloc/free/coalesce without touching |
| 837 | /// the heap itself (fixed-size stack arrays only). Returns true on success. | 940 | /// the heap itself (fixed-size stack arrays only). Returns true on success. |
| 838 | pub fn self_test() -> bool { | 941 | pub fn self_test() -> bool { |
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs index 88f8d43..4ffb44d 100644 --- a/rsnix/src/vt.rs +++ b/rsnix/src/vt.rs | |||
| @@ -138,6 +138,29 @@ pub fn size() -> (usize, usize) { | |||
| 138 | unsafe { (ROWS, COLS) } | 138 | unsafe { (ROWS, COLS) } |
| 139 | } | 139 | } |
| 140 | 140 | ||
| 141 | /// DEBUG (crash-family): read one cell's raw word (ch | fg<<16 | bg<<24) | ||
| 142 | /// from the live cells grid and the rendered grid, for the given text | ||
| 143 | /// row/col. Returns (raw_cell, raw_rendered). | ||
| 144 | pub fn cell_raw(r: usize, c: usize) -> (u32, u32) { | ||
| 145 | unsafe { | ||
| 146 | if CELLS.is_null() || RENDERED.is_null() { | ||
| 147 | return (0xFFFF_FFFF, 0xFFFF_FFFF); | ||
| 148 | } | ||
| 149 | let raw = |v: &Vec<Cell>| -> u32 { | ||
| 150 | if r < ROWS && c < COLS { | ||
| 151 | let cell = v[r * COLS + c]; | ||
| 152 | cell.ch as u32 | ((cell.fg as u32) << 16) | ((cell.bg as u32) << 24) | ||
| 153 | } else { | ||
| 154 | 0xFFFF_FFFE | ||
| 155 | } | ||
| 156 | }; | ||
| 157 | ( | ||
| 158 | raw(&*(CELLS as *const Vec<Cell>)), | ||
| 159 | raw(&*(RENDERED as *const Vec<Cell>)), | ||
| 160 | ) | ||
| 161 | } | ||
| 162 | } | ||
| 163 | |||
| 141 | /// Shell-entry self-check: dump the vt grid's heap structs so a real- | 164 | /// Shell-entry self-check: dump the vt grid's heap structs so a real- |
| 142 | /// hardware boot shows whether the cells VecData / data arrays are sane | 165 | /// hardware boot shows whether the cells VecData / data arrays are sane |
| 143 | /// at shell entry (the OptiPlex's first banner write #GPs when the | 166 | /// at shell entry (the OptiPlex's first banner write #GPs when the |
| @@ -184,9 +207,12 @@ pub fn debug_verify() { | |||
| 184 | // other three probe slots as zero-initialized). A stale cells | 207 | // other three probe slots as zero-initialized). A stale cells |
| 185 | // data pointer that happens to be in-heap still passes the | 208 | // data pointer that happens to be in-heap still passes the |
| 186 | // range checks (len/cap look fine) but its "cells" read back as | 209 | // range checks (len/cap look fine) but its "cells" read back as |
| 187 | // the underlying memory's leftover fill — the OptiPlex shows | 210 | // the underlying memory's leftover fill. NOTE: the OptiPlex |
| 188 | // the firmware 0x00AAAAAA scrub there. BLANK (0x70020) vs | 211 | // dump value 0x00AAAAAA is the VGA GRAY text colour |
| 189 | // scrub (0x00AAAAAA) is cleanly distinguishable. | 212 | // (rgb(170,170,170) = PALETTE[7] = fg 7), i.e. rendered shadow |
| 213 | // pixels — the memory read as heap held display text, not a | ||
| 214 | // firmware scrub. BLANK (0x70020) vs 0x00AAAAAA is cleanly | ||
| 215 | // distinguishable. | ||
| 190 | let blank_raw = BLANK.ch as u32 | ((BLANK.fg as u32) << 16) | ((BLANK.bg as u32) << 24); | 216 | let blank_raw = BLANK.ch as u32 | ((BLANK.fg as u32) << 16) | ((BLANK.bg as u32) << 24); |
| 191 | let mut content_ok = true; | 217 | let mut content_ok = true; |
| 192 | let mut rcontent_ok = true; | 218 | let mut rcontent_ok = true; |
| @@ -269,31 +295,24 @@ pub fn debug_verify() { | |||
| 269 | r_probes_raw[2], | 295 | r_probes_raw[2], |
| 270 | r_probes_raw[3] | 296 | r_probes_raw[3] |
| 271 | )); | 297 | )); |
| 272 | // Bottom-anchored: row 0-1 get overwritten by the shell | 298 | // NOTE: debug markers are serial-only now; the crash family |
| 273 | // banner, row 2 by the prompt (and a stale crash.log | 299 | // was resolved (EBS-retry free-list rebuild) and the screen |
| 274 | // replays at rows 2+), so this marker must live at the very | 300 | // must stay clean for users. |
| 275 | // bottom to stay readable until the user can copy it. | 301 | crate::serial_println!( |
| 276 | crate::gop::boot_marker_at( | 302 | "vt: c {:#x} cap={:#x} {} | r {:#x} {}", |
| 277 | &alloc::format!( | 303 | c.as_ptr() as usize, |
| 278 | "vt: c {:#x} cap={:#x} {} | r {:#x} {}", | 304 | c.capacity(), |
| 279 | c.as_ptr() as usize, | 305 | if content_ok { "ok" } else { "GARBAGE" }, |
| 280 | c.capacity(), | 306 | r.as_ptr() as usize, |
| 281 | if content_ok { "ok" } else { "GARBAGE" }, | 307 | if rcontent_ok { "ok" } else { "GARBAGE" } |
| 282 | r.as_ptr() as usize, | ||
| 283 | if rcontent_ok { "ok" } else { "GARBAGE" } | ||
| 284 | ), | ||
| 285 | crate::gop::bottom_row(1), | ||
| 286 | ); | 308 | ); |
| 287 | } else { | 309 | } else { |
| 288 | msg("verify: ok"); | 310 | msg("verify: ok"); |
| 289 | crate::gop::boot_marker_at( | 311 | crate::serial_println!( |
| 290 | &alloc::format!( | 312 | "vt: c {:#x} cap={:#x} ok | r {:#x} ok", |
| 291 | "vt: c {:#x} cap={:#x} ok | r {:#x} ok", | 313 | c.as_ptr() as usize, |
| 292 | c.as_ptr() as usize, | 314 | c.capacity(), |
| 293 | c.capacity(), | 315 | r.as_ptr() as usize |
| 294 | r.as_ptr() as usize | ||
| 295 | ), | ||
| 296 | crate::gop::bottom_row(1), | ||
| 297 | ); | 316 | ); |
| 298 | } | 317 | } |
| 299 | } | 318 | } |
