diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-02 02:16:40 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-02 02:16:40 +0200 |
| commit | d9fe082ef07b6e5c599585d6911dbad2bbe4c787 (patch) | |
| tree | f54e2ff928bd23926858aba9bd44c7a6f82bb80b | |
| parent | v0.5.5: self-diagnosing boot — vt refuses 0x0 geometry, panic band shows la... (diff) | |
| download | rs-dos-d9fe082ef07b6e5c599585d6911dbad2bbe4c787.tar.gz rs-dos-d9fe082ef07b6e5c599585d6911dbad2bbe4c787.zip | |
v0.5.5: fix real-hardware boot crash — shadow back in heap as first allocation
User's machine: boot reached 'rsnix: boot done — shell' then panicked
'len is 0, index 0' at the first cell write — CELLS (the vt grid) was
valid at vt::init and empty at the first shell paint, i.e. something
stomped the heap between vt::init and the shell banner. The only
change in that window vs the working v0.5.3 was the v0.5.4 move of the
1.9 MB display shadow from the heap to the fb-aware page allocator:
on this machine's memory layout the page-allocator shadow's fill loop
(480K words) clobbered the heap's vt vectors.
- Shadow is again the *first* heap allocation, in gop::init before
vt::init builds its grids (the v0.5.3 layout that provably boots on
the user's hardware) — nothing else in the heap can collide with it.
- init_heap now splits the free list at the GOP framebuffer hole
instead of subtracting its size from one block that still *spanned*
the hole — no heap allocation of any size can land on the display.
- Kept the panic-proof try_reserve + fb-overlap rejection + no-shadow
warning band; dropped the late init_shadow() call in main.rs.
- Verified on QEMU: shadow @ 0x1980020 origin=heap, allocated before
vt::init, fb clean at 0x80000000; all 5 suites PASS; 0 warnings.
| -rw-r--r-- | rsdos/src/main.rs | 9 | ||||
| -rw-r--r-- | rsnix/src/gop.rs | 158 | ||||
| -rw-r--r-- | rsnix/src/memory.rs | 37 | ||||
| -rwxr-xr-x | scripts/build-image-release.sh | 99 |
4 files changed, 188 insertions, 115 deletions
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs index aa116e4..2717dff 100644 --- a/rsdos/src/main.rs +++ b/rsdos/src/main.rs | |||
| @@ -162,7 +162,13 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 162 | let (rows, cols) = rsnix::vt::size(); | 162 | let (rows, cols) = rsnix::vt::size(); |
| 163 | rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols); | 163 | rsnix::serial_println!("vt: terminal ready ({}x{})", rows, cols); |
| 164 | let (fw, fh) = rsnix::gop::pixel_dims(); | 164 | let (fw, fh) = rsnix::gop::pixel_dims(); |
| 165 | rsnix::gop::boot_marker(&alloc::format!("rsnix: gop {}x{} vt {}x{} ok", fw, fh, cols, rows)); | 165 | rsnix::gop::boot_marker(&alloc::format!( |
| 166 | "rsnix: gop {}x{} vt {}x{} ok", | ||
| 167 | fw, | ||
| 168 | fh, | ||
| 169 | cols, | ||
| 170 | rows | ||
| 171 | )); | ||
| 166 | 172 | ||
| 167 | // ExitBootServices — we now own the machine | 173 | // ExitBootServices — we now own the machine |
| 168 | if unsafe { (bs.exit_boot_services)(image_handle, map_key) } != EFI_SUCCESS { | 174 | if unsafe { (bs.exit_boot_services)(image_handle, map_key) } != EFI_SUCCESS { |
| @@ -212,7 +218,6 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut | |||
| 212 | } else { | 218 | } else { |
| 213 | rsnix::serial_println!("pages: init FAILED"); | 219 | rsnix::serial_println!("pages: init FAILED"); |
| 214 | } | 220 | } |
| 215 | rsnix::gop::init_shadow(); | ||
| 216 | rsnix::gop::boot_marker("rsnix: shadow ok"); | 221 | rsnix::gop::boot_marker("rsnix: shadow ok"); |
| 217 | if rsnix::memory::page_self_test() { | 222 | if rsnix::memory::page_self_test() { |
| 218 | rsnix::serial_println!("pages: self-test OK (alloc/free/runs)"); | 223 | rsnix::serial_println!("pages: self-test OK (alloc/free/runs)"); |
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs index 90c24bf..d37468f 100644 --- a/rsnix/src/gop.rs +++ b/rsnix/src/gop.rs | |||
| @@ -80,8 +80,7 @@ const STAGE_MAX: usize = 96; | |||
| 80 | /// The last boot stage string (for the panic band). | 80 | /// The last boot stage string (for the panic band). |
| 81 | pub fn last_boot_stage() -> alloc::string::String { | 81 | pub fn last_boot_stage() -> alloc::string::String { |
| 82 | unsafe { | 82 | unsafe { |
| 83 | alloc::string::String::from_utf8_lossy(&LAST_BOOT_STAGE[..LAST_BOOT_STAGE_LEN]) | 83 | alloc::string::String::from_utf8_lossy(&LAST_BOOT_STAGE[..LAST_BOOT_STAGE_LEN]).into_owned() |
| 84 | .into_owned() | ||
| 85 | } | 84 | } |
| 86 | } | 85 | } |
| 87 | 86 | ||
| @@ -210,7 +209,7 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { | |||
| 210 | let bgr = info.pixel_format == PIXEL_BGR; | 209 | let bgr = info.pixel_format == PIXEL_BGR; |
| 211 | let _ = fg; | 210 | let _ = fg; |
| 212 | 211 | ||
| 213 | let fb = Framebuffer { | 212 | let fbf = Framebuffer { |
| 214 | base, | 213 | base, |
| 215 | width, | 214 | width, |
| 216 | height, | 215 | height, |
| @@ -221,14 +220,8 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { | |||
| 221 | bgr, | 220 | bgr, |
| 222 | }; | 221 | }; |
| 223 | 222 | ||
| 224 | // The RAM backbuffer is allocated *after* ExitBootServices by | 223 | // Re-stash the post-SetMode region (base can move) for the |
| 225 | // `init_shadow()` (main.rs) — via the fb-aware page allocator so it | 224 | // allocators, and log the authoritative geometry for diagnostics. |
| 226 | // can never land on the framebuffer (see init_shadow). Nothing is | ||
| 227 | // drawn between fb.clear() here and init_shadow(), so the blank | ||
| 228 | // shadow is in sync with the cleared display. | ||
| 229 | |||
| 230 | // Re-stash the post-SetMode region (base can move) for the page | ||
| 231 | // allocator, and log the authoritative geometry for diagnostics. | ||
| 232 | unsafe { | 225 | unsafe { |
| 233 | FB_REGION_BASE = mode.framebuffer_base as usize; | 226 | FB_REGION_BASE = mode.framebuffer_base as usize; |
| 234 | FB_REGION_SIZE = mode.framebuffer_size; | 227 | FB_REGION_SIZE = mode.framebuffer_size; |
| @@ -244,104 +237,65 @@ pub fn init(gop: &mut GraphicsOutputProtocol) { | |||
| 244 | bgr | 237 | bgr |
| 245 | ); | 238 | ); |
| 246 | 239 | ||
| 247 | fb.clear(); | 240 | fbf.clear(); |
| 248 | 241 | ||
| 249 | unsafe { | 242 | unsafe { |
| 250 | FB = Some(fb); | 243 | FB = Some(fbf); |
| 251 | } | 244 | } |
| 252 | } | 245 | |
| 253 | 246 | // The RAM backbuffer is the *first* heap allocation: allocated here | |
| 254 | /// Allocate the RAM backbuffer. Must run *after* | 247 | // (pre-ExitBootServices, before vt::init builds its grids) so no |
| 255 | /// `memory::init_page_allocator` (which excludes the GOP framebuffer | 248 | // other heap object can collide with it — the v0.5.3 layout that |
| 256 | /// pages per-page), and before any text is painted. | 249 | // provably boots on real hardware. The heap free list excludes the |
| 257 | /// | 250 | // fb region (init_heap splits at the fb hole) and we additionally |
| 258 | /// The heap (pre-ExitBootServices) is excluded from the fb region only | 251 | // reject any range overlapping the fb, so the shadow can never |
| 259 | /// by a coarse overlap subtraction that still leaves one contiguous | 252 | // alias the display (an aliased shadow turns every scroll into a |
| 260 | /// free block *spanning* the fb hole — a 1.9 MB shadow allocation could | 253 | // 1.8 MB MMIO→MMIO memcpy on an uncached real fb — a wedge). |
| 261 | /// therefore land *on* the framebuffer on machines where firmware | 254 | // Allocation failure degrades to no-shadow mode, never panics. |
| 262 | /// reports it as ConventionalMemory. A shadow aliased over the display | ||
| 263 | /// makes every scroll a 1.8 MB MMIO→MMIO memcpy, which can wedge a | ||
| 264 | /// real uncached framebuffer (a stall, not a fault — hence no dump). | ||
| 265 | /// The page allocator marks those pages used, so the shadow can never | ||
| 266 | /// land there. Heap allocation is kept only as a fallback, with an | ||
| 267 | /// explicit overlap rejection. | ||
| 268 | pub fn init_shadow() { | ||
| 269 | unsafe { | 255 | unsafe { |
| 270 | let Some(fb) = fb().as_ref() else { | 256 | if let Some(fbinfo) = fb().as_ref() { |
| 271 | return; | 257 | let words = (fbinfo.pitch * fbinfo.height) / 4; |
| 272 | }; | 258 | if words > 0 { |
| 273 | let words = (fb.pitch * fb.height) / 4; | 259 | let bytes = words * 4; |
| 274 | if words == 0 { | 260 | #[allow(clippy::slow_vector_initialization)] // deliberate: try_reserve must not panic |
| 275 | return; | 261 | let mut v: alloc::vec::Vec<u32> = alloc::vec::Vec::new(); |
| 276 | } | 262 | if v.try_reserve_exact(words).is_ok() { |
| 277 | let bytes = words * 4; | 263 | v.resize(words, 0); // can't fail after the reserve |
| 278 | let pages = bytes.div_ceil(crate::memory::PAGE_SIZE); | 264 | let p = v.as_ptr() as usize; |
| 279 | let mut origin = "none"; | 265 | let overlaps = FB_REGION_SIZE > 0 |
| 280 | 266 | && p < FB_REGION_BASE + FB_REGION_SIZE | |
| 281 | // Primary: fb-aware page allocator (fb pages already marked | 267 | && p + bytes > FB_REGION_BASE; |
| 282 | // used by init_page_allocator). Defensively refuse the range if | 268 | if !overlaps { |
| 283 | // it ever overlaps the fb region anyway. | 269 | SHADOW = v.as_ptr() as *mut u32; |
| 284 | if let Some(p) = crate::memory::alloc_pages(pages) { | 270 | SHADOW_WORDS = words; |
| 285 | let overlaps = FB_REGION_SIZE > 0 | 271 | for i in 0..words { |
| 286 | && p < FB_REGION_BASE + FB_REGION_SIZE | 272 | *SHADOW.add(i) = fbinfo.bg; |
| 287 | && p + bytes > FB_REGION_BASE; | 273 | } |
| 288 | if !overlaps { | 274 | core::mem::forget(v); // shadow owns the memory now |
| 289 | SHADOW = p as *mut u32; | 275 | } |
| 290 | SHADOW_WORDS = words; | ||
| 291 | origin = "page-alloc"; | ||
| 292 | for i in 0..words { | ||
| 293 | *SHADOW.add(i) = fb.bg; | ||
| 294 | } | 276 | } |
| 295 | } | 277 | let (rbase, rsize) = (FB_REGION_BASE, FB_REGION_SIZE); |
| 296 | } | 278 | let (shadow, swords) = (SHADOW as usize, SHADOW_WORDS); |
| 297 | if origin == "none" { | 279 | crate::serial_println!( |
| 298 | // Fallback: heap — must never panic here: an OOM would halt | 280 | "gop: shadow @ {:#x} words={} fb={:#x}..{:#x} origin=heap", |
| 299 | // the kernel before the shell banner, a blank screen on real | 281 | shadow, |
| 300 | // hardware (the old `vec![0; words]` could abort). On any | 282 | swords, |
| 301 | // failure we degrade to no-shadow mode instead. | 283 | rbase, |
| 302 | #[allow(clippy::slow_vector_initialization)] // deliberate: try_reserve must not panic | 284 | rbase + rsize, |
| 303 | let mut v: alloc::vec::Vec<u32> = alloc::vec::Vec::new(); | 285 | ); |
| 304 | if v.try_reserve_exact(words).is_ok() { | 286 | if SHADOW.is_null() { |
| 305 | v.resize(words, 0); // can't fail after the reserve | 287 | let msg = |
| 306 | let p = v.as_ptr() as usize; | 288 | "gop: WARNING — shadow rejected (fb overlap); no-shadow fallback"; |
| 307 | let overlaps = FB_REGION_SIZE > 0 | 289 | crate::serial_println!("{}", msg); |
| 308 | && p < FB_REGION_BASE + FB_REGION_SIZE | 290 | let lines: alloc::vec::Vec<&str> = msg |
| 309 | && p + bytes > FB_REGION_BASE; | 291 | .as_bytes() |
| 310 | if !overlaps { | 292 | .chunks(100) |
| 311 | SHADOW = v.as_ptr() as *mut u32; | 293 | .map(|c| core::str::from_utf8(c).unwrap_or("?")) |
| 312 | SHADOW_WORDS = words; | 294 | .collect(); |
| 313 | origin = "heap"; | 295 | dump_lines(&lines); |
| 314 | core::mem::forget(v); // shadow owns the memory now | ||
| 315 | } | 296 | } |
| 316 | } | 297 | } |
| 317 | } | 298 | } |
| 318 | let (rbase, rsize) = (FB_REGION_BASE, FB_REGION_SIZE); | ||
| 319 | let (shadow, swords) = (SHADOW as usize, SHADOW_WORDS); | ||
| 320 | crate::serial_println!( | ||
| 321 | "gop: shadow @ {:#x} words={} fb={:#x}..{:#x} origin={}", | ||
| 322 | shadow, | ||
| 323 | swords, | ||
| 324 | rbase, | ||
| 325 | rbase + rsize, | ||
| 326 | origin | ||
| 327 | ); | ||
| 328 | // Real hardware has no serial console: surface a degraded shadow | ||
| 329 | // (heap fallback or rejected allocation) as a visible warning | ||
| 330 | // band so a boot on hardware shows it on the screen. | ||
| 331 | if origin != "page-alloc" { | ||
| 332 | let msg = if SHADOW.is_null() { | ||
| 333 | "gop: WARNING — shadow allocation rejected (fb overlap); no-shadow fallback" | ||
| 334 | } else { | ||
| 335 | "gop: WARNING — shadow from heap (page allocator unavailable)" | ||
| 336 | }; | ||
| 337 | crate::serial_println!("{}", msg); | ||
| 338 | let lines: alloc::vec::Vec<&str> = msg | ||
| 339 | .as_bytes() | ||
| 340 | .chunks(100) | ||
| 341 | .map(|c| core::str::from_utf8(c).unwrap_or("?")) | ||
| 342 | .collect(); | ||
| 343 | dump_lines(&lines); | ||
| 344 | } | ||
| 345 | } | 299 | } |
| 346 | } | 300 | } |
| 347 | 301 | ||
diff --git a/rsnix/src/memory.rs b/rsnix/src/memory.rs index a947c53..605125a 100644 --- a/rsnix/src/memory.rs +++ b/rsnix/src/memory.rs | |||
| @@ -366,18 +366,33 @@ fn init_heap() { | |||
| 366 | if d.kind == MEMORY_TYPE_CONVENTIONAL { | 366 | if d.kind == MEMORY_TYPE_CONVENTIONAL { |
| 367 | let s = d.physical_start as usize; | 367 | let s = d.physical_start as usize; |
| 368 | let e = s + d.number_of_pages as usize * 4096; | 368 | let e = s + d.number_of_pages as usize * 4096; |
| 369 | // Subtract any overlap with the framebuffer region. | 369 | // Split at the framebuffer hole so the free list NEVER |
| 370 | let (cs, ce) = match fb { | 370 | // contains fb pages: a coarse size subtraction leaves one |
| 371 | Some((b, sz)) if e > b && s < b + sz => (s.max(b), e.min(b + sz)), | 371 | // block *spanning* the hole, letting a large allocation |
| 372 | _ => (e, s), // no overlap → empty range | 372 | // (e.g. the 1.9 MB display shadow) straddle the display. |
| 373 | }; | 373 | let mut lo = s; |
| 374 | let mut sz = e - s; | 374 | if let Some((b, sz)) = fb |
| 375 | if ce > cs { | 375 | && e > b |
| 376 | sz -= ce - cs; | 376 | && s < b + sz |
| 377 | { | ||
| 378 | let h0 = b.max(s); | ||
| 379 | let h1 = (b + sz).min(e); | ||
| 380 | if h0 > lo { | ||
| 381 | // Segment before the hole. | ||
| 382 | let seg = h0 - lo; | ||
| 383 | if seg > best_size { | ||
| 384 | best_start = lo; | ||
| 385 | best_size = seg; | ||
| 386 | } | ||
| 387 | } | ||
| 388 | lo = h1; | ||
| 377 | } | 389 | } |
| 378 | if sz > best_size { | 390 | if e > lo { |
| 379 | best_start = s; | 391 | let seg = e - lo; |
| 380 | best_size = sz; | 392 | if seg > best_size { |
| 393 | best_start = lo; | ||
| 394 | best_size = seg; | ||
| 395 | } | ||
| 381 | } | 396 | } |
| 382 | } | 397 | } |
| 383 | } | 398 | } |
diff --git a/scripts/build-image-release.sh b/scripts/build-image-release.sh new file mode 100755 index 0000000..32f8f91 --- /dev/null +++ b/scripts/build-image-release.sh | |||
| @@ -0,0 +1,99 @@ | |||
| 1 | #!/usr/bin/env bash | ||
| 2 | # | ||
| 3 | # scripts/build-image.sh — assemble a bootable rs-dos disk image (UEFI/GPT) | ||
| 4 | # | ||
| 5 | # Layout: | ||
| 6 | # Partition 1: ESP (FAT32) — rsboot bootloader + kernel | ||
| 7 | # | ||
| 8 | # Output: | ||
| 9 | # rs-dos.img (or custom path) — bootable raw disk image for UEFI | ||
| 10 | # | ||
| 11 | # Usage: | ||
| 12 | # ./scripts/build-image.sh [output.img] | ||
| 13 | # | ||
| 14 | set -euo pipefail | ||
| 15 | |||
| 16 | SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | ||
| 17 | PROJECT_DIR="$(dirname "$SCRIPT_DIR")" | ||
| 18 | |||
| 19 | cd "$PROJECT_DIR" | ||
| 20 | |||
| 21 | BUILD_DIR="$PROJECT_DIR/build" | ||
| 22 | ESP_DIR="$PROJECT_DIR/esp" | ||
| 23 | |||
| 24 | IMAGE="${1:-$PROJECT_DIR/rs-dos.img}" | ||
| 25 | TARGET=x86_64-unknown-uefi | ||
| 26 | PROFILE=release | ||
| 27 | OUT_DIR="target/${TARGET}/${PROFILE}" | ||
| 28 | |||
| 29 | BOOTLOADER="$BUILD_DIR/bootloader/BOOTX64.EFI" | ||
| 30 | KERNEL="$OUT_DIR/rsdos.efi" | ||
| 31 | BZIMAGE="$ESP_DIR/bzImage" | ||
| 32 | INITRD="$ESP_DIR/initramfs.cpio.gz" | ||
| 33 | BOOT_EFI="$ESP_DIR/EFI/BOOT/BOOTX64.EFI" | ||
| 34 | |||
| 35 | ESP_START=2048 # 1 MiB offset (standard alignment) | ||
| 36 | ESP_SIZE_MB=64 | ||
| 37 | |||
| 38 | IMAGE_SIZE_MB=$((ESP_SIZE_MB + 2)) # ESP + GPT gap + backup | ||
| 39 | |||
| 40 | # ---- checks ---- | ||
| 41 | for cmd in sfdisk mkfs.vfat mcopy truncate dd; do | ||
| 42 | if ! command -v "$cmd" &>/dev/null; then | ||
| 43 | echo "Error: $cmd not found" | ||
| 44 | exit 1 | ||
| 45 | fi | ||
| 46 | done | ||
| 47 | |||
| 48 | # ---- build kernel ---- | ||
| 49 | echo "[1/4] Building kernel..." | ||
| 50 | cargo build --release --package rsdos --target x86_64-unknown-uefi ${CARGO_FLAGS:-} | ||
| 51 | |||
| 52 | # ---- build bootloader ---- | ||
| 53 | echo "[2/4] Building bootloader..." | ||
| 54 | if [ ! -f "$BOOTLOADER" ]; then | ||
| 55 | "$SCRIPT_DIR/build-bootloader.sh" | ||
| 56 | fi | ||
| 57 | |||
| 58 | # ---- assemble ESP ---- | ||
| 59 | echo "[3/4] Assembling ESP..." | ||
| 60 | mkdir -p "$ESP_DIR/EFI/BOOT" | ||
| 61 | cp "$KERNEL" "$BZIMAGE" | ||
| 62 | cp "$BOOTLOADER" "$BOOT_EFI" | ||
| 63 | if [ ! -f "$INITRD" ]; then | ||
| 64 | printf '' | gzip >"$INITRD" | ||
| 65 | fi | ||
| 66 | |||
| 67 | # ---- create disk image ---- | ||
| 68 | echo "[4/4] Writing disk image..." | ||
| 69 | rm -f "$IMAGE" | ||
| 70 | truncate -s "${IMAGE_SIZE_MB}M" "$IMAGE" | ||
| 71 | |||
| 72 | # GPT partition table: single EFI System partition | ||
| 73 | sfdisk --quiet "$IMAGE" <<EOF | ||
| 74 | label: gpt | ||
| 75 | type=U, start=$ESP_START, size=+ | ||
| 76 | EOF | ||
| 77 | |||
| 78 | # Build FAT32 image with ESP contents | ||
| 79 | ESP_IMG=$(mktemp) | ||
| 80 | truncate -s "${ESP_SIZE_MB}M" "$ESP_IMG" | ||
| 81 | mkfs.vfat -F 32 -n "RS-DOS" "$ESP_IMG" >/dev/null 2>&1 | ||
| 82 | |||
| 83 | # Copy files into FAT | ||
| 84 | mmd -i "$ESP_IMG" ::/EFI 2>/dev/null || true | ||
| 85 | mmd -i "$ESP_IMG" ::/EFI/BOOT 2>/dev/null || true | ||
| 86 | mcopy -i "$ESP_IMG" "$BOOT_EFI" ::/EFI/BOOT/BOOTX64.EFI | ||
| 87 | mcopy -i "$ESP_IMG" "$BZIMAGE" ::/bzImage 2>/dev/null || true | ||
| 88 | mcopy -i "$ESP_IMG" "$INITRD" ::/initramfs.cpio.gz 2>/dev/null || true | ||
| 89 | |||
| 90 | # Inject FAT into image at partition offset | ||
| 91 | dd if="$ESP_IMG" of="$IMAGE" bs=512 seek="$ESP_START" conv=notrunc 2>/dev/null | ||
| 92 | rm -f "$ESP_IMG" | ||
| 93 | |||
| 94 | echo "" | ||
| 95 | echo "=== Done: $IMAGE ===" | ||
| 96 | ls -lh "$IMAGE" | ||
| 97 | echo "" | ||
| 98 | echo "Boot with: ./scripts/boot-image.sh" | ||
| 99 | echo "USB: sudo dd if=$IMAGE of=/dev/sdX bs=4M status=progress" | ||
