boot/uefi: the memory map the kernel gets is the one the firmware agreed to

Two bugs on the ExitBootServices path, both invisible under OVMF and both
aimed squarely at real firmware.

The retry did not re-translate. If ExitBootServices returns an error the
loader re-reads the memory map and tries again -- correct, and the
specification expects it -- but the e820 table handed to the kernel was
still the one built from the *previous* map. The retry exists precisely
because the map changed, so the kernel was being given a description of
memory the firmware had already rearranged. QEMU never takes this path,
which is why it survived. Translation is now a function called on both.

Truncation was silent. phystop comes from the largest usable region, so
dropping the tail of the map can leave the kernel believing a large
machine has a fraction of its RAM -- and nothing said so, because the
loop simply stopped at the cap. It now warns while there is still a
console to warn on; after ExitBootServices there is nowhere to report
anything.

SCHBOOT_E820_MAX goes from 32 to 40. The map sits at BASE+0x208 and the
next field is at BASE+0x600, so 1016 bytes are reserved and 42 entries of
24 bytes fit. Real firmware emits far more descriptors than QEMU's; the
headroom was already allocated and unused.

Coalescing adjacent same-type runs is what keeps any of this in budget,
and it works because boot services memory becomes usable at this point --
that is most of the fragmentation in a UEFI map.

110 [ok], no failures, and the map still reads 7 regions under OVMF.
This commit is contained in:
Nexoniarz 2026-08-11 11:42:22 +02:00
commit c4dd76cc66
2 changed files with 56 additions and 21 deletions

View file

@ -570,6 +570,9 @@ res_choose(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
} }
} }
static UINT32 build_e820(const UINT8 *mapbuf, UINTN mapsize, UINTN descsize,
struct e820entry *out);
/* Both defined below, next to the hand-off code they belong to. The system /* Both defined below, next to the hand-off code they belong to. The system
* information panel reads the same two facts the kernel will be handed, on * information panel reads the same two facts the kernel will be handed, on
* purpose: a panel with its own idea of what counts as usable memory, or its * purpose: a panel with its own idea of what counts as usable memory, or its
@ -957,6 +960,41 @@ guid_eq(const EFI_GUID *a, const EFI_GUID *b)
return 1; return 1;
} }
// Translate a UEFI memory map into E820 entries.
//
// Adjacent runs of the same type are coalesced: a real UEFI map is routinely
// over a hundred descriptors and the kernel accepts E820_MAXENTRIES, so
// without this it would see a truncated map and believe the machine has less
// memory than it does. Boot services memory becomes usable here, which is what
// makes the coalescing effective -- it is most of the fragmentation.
//
// A function rather than inline code because it has to run twice: once before
// ExitBootServices, and again if that fails and the map is re-read.
static UINT32
build_e820(const UINT8 *mapbuf, UINTN mapsize, UINTN descsize,
struct e820entry *out)
{
UINT32 nent = 0;
for (UINTN off = 0; off + descsize <= mapsize && nent < E820_MAXENTRIES;
off += descsize) {
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off);
UINT32 type = efi_type_to_e820(d->Type);
if (nent > 0 && out[nent-1].type == type &&
out[nent-1].base + out[nent-1].length == d->PhysicalStart) {
out[nent-1].length += d->NumberOfPages * 0x1000;
continue;
}
out[nent].base = d->PhysicalStart;
out[nent].length = d->NumberOfPages * 0x1000;
out[nent].type = type;
out[nent].acpi_ext = 1;
nent++;
}
return nent;
}
// The ACPI RSDP, from the UEFI configuration table. // The ACPI RSDP, from the UEFI configuration table.
// //
// The kernel's own search scans the EBDA and 0xE0000-0x100000. That is a BIOS // The kernel's own search scans the EBDA and 0xE0000-0x100000. That is a BIOS
@ -1033,27 +1071,15 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
// printing still works, but write it to low memory only after the firmware // printing still works, but write it to low memory only after the firmware
// has let go -- 0x30000 may well be firmware data until then. // has let go -- 0x30000 may well be firmware data until then.
static struct e820entry e820[E820_MAXENTRIES]; static struct e820entry e820[E820_MAXENTRIES];
UINT32 nent = 0; UINT32 nent = build_e820(mapbuf, mapsize, descsize, e820);
for (UINTN off = 0; off + descsize <= mapsize && nent < E820_MAXENTRIES;
off += descsize) {
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off);
UINT32 type = efi_type_to_e820(d->Type);
// Coalesce adjacent runs of the same type. A UEFI map is routinely // Truncation is silent data loss: phystop comes from the largest usable
// over a hundred descriptors and the kernel accepts 32; without this // region, so dropping the tail of the map can leave the kernel believing a
// it would see a truncated map and believe the machine has less // 16 GB machine has a fraction of that. Say so while there is still a
// memory than it does. // console to say it on -- after ExitBootServices there is nowhere to
if (nent > 0 && e820[nent-1].type == type && // report anything at all.
e820[nent-1].base + e820[nent-1].length == d->PhysicalStart) { if (nent >= E820_MAXENTRIES)
e820[nent-1].length += d->NumberOfPages * 0x1000; print(L"schos: WARNING: memory map truncated, some RAM will be unused\r\n");
continue;
}
e820[nent].base = d->PhysicalStart;
e820[nent].length = d->NumberOfPages * 0x1000;
e820[nent].type = type;
e820[nent].acpi_ext = 1;
nent++;
}
print(L"schos: exiting boot services\r\n"); print(L"schos: exiting boot services\r\n");
@ -1066,6 +1092,11 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
st = BS->GetMemoryMap(&mapsize, (EFI_MEMORY_DESCRIPTOR *)mapbuf, st = BS->GetMemoryMap(&mapsize, (EFI_MEMORY_DESCRIPTOR *)mapbuf,
&mapkey, &descsize, &descver); &mapkey, &descsize, &descver);
if (EFI_ERROR(st)) die(L"GetMemoryMap failed on retry", st); if (EFI_ERROR(st)) die(L"GetMemoryMap failed on retry", st);
// Translate again. The retry exists because the map *changed*, so
// handing the kernel the table built from the previous one describes
// memory the firmware has already rearranged. This path is normal on
// real firmware and never taken under OVMF, which is why it survived.
nent = build_e820(mapbuf, mapsize, descsize, e820);
st = BS->ExitBootServices(image, mapkey); st = BS->ExitBootServices(image, mapkey);
if (EFI_ERROR(st)) die(L"ExitBootServices failed", st); if (EFI_ERROR(st)) die(L"ExitBootServices failed", st);
} }

View file

@ -71,7 +71,11 @@
// 0x508, which is what leaves 0x600 free below. // 0x508, which is what leaves 0x600 free below.
#define SCHBOOT_E820_COUNT (SCHBOOT_BASE + 0x200) #define SCHBOOT_E820_COUNT (SCHBOOT_BASE + 0x200)
#define SCHBOOT_E820_MAP (SCHBOOT_BASE + 0x208) #define SCHBOOT_E820_MAP (SCHBOOT_BASE + 0x208)
#define SCHBOOT_E820_MAX 32 /* 40, not 32. The map lives at SCHBOOT_E820_MAP (BASE+0x208) and the next
* field is SCHBOOT_RSDP_ADDR at BASE+0x600, so there are 1016 bytes and each
* entry is 24 -- 42 fit. Real UEFI firmware produces far more descriptors than
* QEMU's, and the headroom costs nothing that was being used. */
#define SCHBOOT_E820_MAX 40
// Where the loader leaves the ACPI RSDP, as a little-endian 64-bit physical // Where the loader leaves the ACPI RSDP, as a little-endian 64-bit physical
// address. Zero means "not provided, go and look for it yourself". // address. Zero means "not provided, go and look for it yourself".