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/memory.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/memory.rs | 107 |
1 files changed, 105 insertions, 2 deletions
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 { |
