diff --git a/boot/uefi/main.c b/boot/uefi/main.c index 48c2e07..c7a0021 100644 --- a/boot/uefi/main.c +++ b/boot/uefi/main.c @@ -1747,11 +1747,40 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) // static buffer and the retry. ui_progress_label("Reading the memory map"); - static UINT8 mapbuf[32768]; - UINTN mapsize = sizeof mapbuf, mapkey = 0, descsize = 0; + // A quarter of a megabyte, and the size is not a guess. + // + // This was 32 KB, which is about 680 descriptors, and on real firmware + // that is not enough: GetMemoryMap answered EFI_BUFFER_TOO_SMALL (0x5) and + // the loader stopped there. A modern machine's map is not a handful of + // regions -- it is every reserved range the firmware has ever carved out, + // and the count is a property of the vendor rather than of the RAM. + // + // Asking first is what makes the size defensible rather than a bigger + // guess: a call with a zero size fails on purpose and fills in how much is + // actually needed. It allocates nothing, so the key from the real call + // below is still valid -- which is the whole constraint this code is + // written around. + static UINT8 mapbuf[262144]; + UINTN mapsize = 0, mapkey = 0, descsize = 0; UINT32 descver = 0; EFI_STATUS st; + st = BS->GetMemoryMap(&mapsize, (EFI_MEMORY_DESCRIPTOR *)mapbuf, + &mapkey, &descsize, &descver); + if (st == EFI_BUFFER_TOO_SMALL && mapsize > sizeof mapbuf) { + // Say the number. A loader that dies on "too small" without saying how + // small is a loader that costs somebody a rebuild to find out. + print(L"schos: the firmware's memory map needs "); + print_dec(mapsize); + print(L" bytes and this loader has "); + print_dec(sizeof mapbuf); + print(L"\r\n"); + die(L"memory map larger than the loader's buffer", st); + } + + // Two descriptors of slack, as the specification advises: the firmware is + // allowed to need a little more between the question and the answer. + mapsize = sizeof mapbuf; st = BS->GetMemoryMap(&mapsize, (EFI_MEMORY_DESCRIPTOR *)mapbuf, &mapkey, &descsize, &descver); if (EFI_ERROR(st)) die(L"GetMemoryMap failed", st); @@ -1767,8 +1796,17 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) // 16 GB machine has a fraction of that. Say so while there is still a // console to say it on -- after ExitBootServices there is nowhere to // report anything at all. - if (nent >= E820_MAXENTRIES) + if (nent >= E820_MAXENTRIES) { print(L"schos: WARNING: memory map truncated, some RAM will be unused\r\n"); + // And on the screen, because by this point print() goes to a serial + // port that a laptop does not have. The entries that get dropped are + // the high ones -- the map arrives sorted, and phystop is capped below + // 4 GB anyway -- so this is a warning rather than a failure. It is + // still not a thing to discover months later from a memory total that + // never looked quite right. + ui_progress_label("Memory map truncated -- some RAM unused"); + BS->Stall(2000000); + } ui_progress_label("Leaving the firmware"); vprint(L"schos: exiting boot services\r\n"); diff --git a/docs/architecture/boot.md b/docs/architecture/boot.md index 849eb8d..829fe3a 100644 --- a/docs/architecture/boot.md +++ b/docs/architecture/boot.md @@ -122,6 +122,38 @@ when the early console is set up. So a low framebuffer is attached exactly as before, and a high one is attached a few dozen instructions later, immediately after `kvmalloc()` — still thousands of lines before anything else happens. +### The memory map does not fit in a guess + +`GetMemoryMap` answered **`EFI_BUFFER_TOO_SMALL` (0x5)** on real hardware, and +the loader stopped there. The buffer was 32 KB — about 680 descriptors — which +is generous next to QEMU's map and not generous at all next to a firmware that +has carved out every reserved range it has ever needed. The count is a property +of the vendor, not of the RAM. + +The fix is not a bigger guess. A call with a **zero size** fails on purpose and +fills in how much is actually needed; it allocates nothing, so the map key from +the real call immediately after is still valid — which is the constraint this +whole sequence is written around, and the reason the buffer is static in the +first place. The buffer is now 256 KB, and if a machine ever exceeds even that, +it says so with both numbers rather than dying on a status code: + +``` +schos: the firmware's memory map needs 5664 bytes and this loader has 512 +schos: memory map larger than the loader's buffer (status 0x8000000000000005) +``` + +(That is the failure path deliberately provoked by shrinking the buffer to 512. +QEMU's own map needs 5664 bytes — 118 descriptors — so the real number on +hardware is larger still, and 256 KB is 5461 descriptors.) + +**A second limit is nearby and is only a warning.** `SCHBOOT_E820_MAX` is 40 +entries, fixed by the hand-off block's layout. `build_e820()` merges adjacent +regions of the same type, so 40 survives a long map — but if it ever truncates, +what gets dropped is the *high* end (the map arrives sorted, and `phystop` is +capped below 4 GB regardless). It now says so on screen as well as on the +serial line, because by that point `print()` goes to a port a laptop does not +have. + ### Two signals for a machine that cannot speak A laptop has no serial port. When it stops with the loader's splash still on