summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-02 02:16:40 +0200
committersalmonik <salmonik@tuta.io>2026-08-02 02:16:40 +0200
commitd9fe082ef07b6e5c599585d6911dbad2bbe4c787 (patch)
treef54e2ff928bd23926858aba9bd44c7a6f82bb80b
parentv0.5.5: self-diagnosing boot — vt refuses 0x0 geometry, panic band shows la... (diff)
downloadrs-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.rs9
-rw-r--r--rsnix/src/gop.rs158
-rw-r--r--rsnix/src/memory.rs37
-rwxr-xr-xscripts/build-image-release.sh99
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).
81pub fn last_boot_stage() -> alloc::string::String { 81pub 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.
268pub 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#
14set -euo pipefail
15
16SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
18
19cd "$PROJECT_DIR"
20
21BUILD_DIR="$PROJECT_DIR/build"
22ESP_DIR="$PROJECT_DIR/esp"
23
24IMAGE="${1:-$PROJECT_DIR/rs-dos.img}"
25TARGET=x86_64-unknown-uefi
26PROFILE=release
27OUT_DIR="target/${TARGET}/${PROFILE}"
28
29BOOTLOADER="$BUILD_DIR/bootloader/BOOTX64.EFI"
30KERNEL="$OUT_DIR/rsdos.efi"
31BZIMAGE="$ESP_DIR/bzImage"
32INITRD="$ESP_DIR/initramfs.cpio.gz"
33BOOT_EFI="$ESP_DIR/EFI/BOOT/BOOTX64.EFI"
34
35ESP_START=2048 # 1 MiB offset (standard alignment)
36ESP_SIZE_MB=64
37
38IMAGE_SIZE_MB=$((ESP_SIZE_MB + 2)) # ESP + GPT gap + backup
39
40# ---- checks ----
41for 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
46done
47
48# ---- build kernel ----
49echo "[1/4] Building kernel..."
50cargo build --release --package rsdos --target x86_64-unknown-uefi ${CARGO_FLAGS:-}
51
52# ---- build bootloader ----
53echo "[2/4] Building bootloader..."
54if [ ! -f "$BOOTLOADER" ]; then
55 "$SCRIPT_DIR/build-bootloader.sh"
56fi
57
58# ---- assemble ESP ----
59echo "[3/4] Assembling ESP..."
60mkdir -p "$ESP_DIR/EFI/BOOT"
61cp "$KERNEL" "$BZIMAGE"
62cp "$BOOTLOADER" "$BOOT_EFI"
63if [ ! -f "$INITRD" ]; then
64 printf '' | gzip >"$INITRD"
65fi
66
67# ---- create disk image ----
68echo "[4/4] Writing disk image..."
69rm -f "$IMAGE"
70truncate -s "${IMAGE_SIZE_MB}M" "$IMAGE"
71
72# GPT partition table: single EFI System partition
73sfdisk --quiet "$IMAGE" <<EOF
74label: gpt
75type=U, start=$ESP_START, size=+
76EOF
77
78# Build FAT32 image with ESP contents
79ESP_IMG=$(mktemp)
80truncate -s "${ESP_SIZE_MB}M" "$ESP_IMG"
81mkfs.vfat -F 32 -n "RS-DOS" "$ESP_IMG" >/dev/null 2>&1
82
83# Copy files into FAT
84mmd -i "$ESP_IMG" ::/EFI 2>/dev/null || true
85mmd -i "$ESP_IMG" ::/EFI/BOOT 2>/dev/null || true
86mcopy -i "$ESP_IMG" "$BOOT_EFI" ::/EFI/BOOT/BOOTX64.EFI
87mcopy -i "$ESP_IMG" "$BZIMAGE" ::/bzImage 2>/dev/null || true
88mcopy -i "$ESP_IMG" "$INITRD" ::/initramfs.cpio.gz 2>/dev/null || true
89
90# Inject FAT into image at partition offset
91dd if="$ESP_IMG" of="$IMAGE" bs=512 seek="$ESP_START" conv=notrunc 2>/dev/null
92rm -f "$ESP_IMG"
93
94echo ""
95echo "=== Done: $IMAGE ==="
96ls -lh "$IMAGE"
97echo ""
98echo "Boot with: ./scripts/boot-image.sh"
99echo "USB: sudo dd if=$IMAGE of=/dev/sdX bs=4M status=progress"