boot: the memory map does not fit in a guess
EFI status 0x5 is EFI_BUFFER_TOO_SMALL, and that is the whole bug: the loader
asked the firmware for its memory map with a 32 KB buffer, which is about 680
descriptors -- generous next to QEMU's map and not generous at all next to a
machine whose firmware has carved out every reserved range it has ever needed.
The descriptor count is a property of the vendor, not of the RAM, which is why
this was invisible in every test here and fatal on two real machines.
The fix is not a bigger guess. A call with a **zero size** fails on purpose and
fills in the size actually needed, and 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 has to be
static. 256 KB now, which is 5461 descriptors, and a machine that somehow
exceeds even that is told in numbers rather than in 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 0x...0005)
That line is real: the failure path was provoked by shrinking the buffer to 512
and booting, which is also how the 5664 was measured -- QEMU needs 118
descriptors, so hardware needs more.
**A second limit is nearby, and it 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. If it ever does truncate,
what is dropped is the high end -- the map arrives sorted and phystop is capped
below 4 GB regardless -- so it stays a warning. But it now appears on screen as
well as on the serial line, because after the splash print() goes to a port a
laptop does not have, and "some RAM was quietly unused" is not a thing to
discover months later from a total that never looked right.
Both supported configurations: one boot each, no traps, no panics, socktest and
memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3c5212a7c7
commit
96e10adfcb
2 changed files with 73 additions and 3 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue