mm: the loader jumped out from under itself, and 3 GB was never the limit

Three things, and the first one is the freeze.

**The loader jumped out from under itself.** A machine with a lot of RAM
stopped at "Starting the kernel" with the splash still up: no panic, no reset,
nothing. The firmware's own exception handler had the answer the whole time,
once a machine was found that printed it:

    X64 Exception Type - 0E(#PF - Page-Fault)
    ExceptionData - 0x10   I:1 P:0
    RIP - 0000000140004DEE      <- the loader's own code, at 5.4 GB
    RAX - 0000000000001000      <- the page tables it had just installed
    RCX - 00000000001000F0      <- the kernel it was about to jump to

I:1 P:0 is an instruction fetch from a page that is not present. The loader
built page tables covering four gigabytes, loaded cr3 -- and the firmware had
placed the loader image *above* four gigabytes, so the next instruction fetch
was unmapped. It reproduces exactly on `-m 8G` and never on `-m 1G`, which is
why every test here passed.

The PDPT is one page whether four entries are filled or all 512, so all 512
are: 512 GB of identity map, replaced by kvmalloc() a few hundred instructions
later.

**And 3 GB was never the limit, it was a circle.** PHYSTOP_MAX was 0xC0000000
because kvmalloc() mapped exactly four 1 GB pages, and kvmalloc mapped four
because that was the cap -- each holding the other up, throwing away thirteen
gigabytes of a sixteen gigabyte machine. kvmalloc now maps as many gigabytes as
the machine has RAM and tells ioremap where the direct map stopped, instead of
ioremap assuming it stopped at four.

Freeing memory becomes two passes, and the ordering is forced rather than
chosen: kinit1() runs *before* kvmalloc because kvmalloc calls kalloc for its
own page tables, and freeing a page means writing into it -- so kinit1 can only
touch what the loader mapped. kinit_high() adds the rest afterwards.
`phystop` and the new `lowtop` are now two different numbers: the DMA arena
stays below 4 GB because a device is not obliged to reach the top of memory,
and an RTL8139's buffer address register is thirty-two bits wide.

**And the 0 MB.** The System Information screen reported "0 MB usable" on a
machine with sixteen gigabytes in it, because memory_mb() had its own 16 KB
guess at how big a memory map gets, and returned zero when GetMemoryMap said
EFI_BUFFER_TOO_SMALL. That is the same bug as the previous commit's, in a
second copy, and this one failed silently. There is one buffer and one
map_read() now.

Verified at 1 GB, 8 GB and 16 GB: all three boot, no traps, no panics, socktest
and memfdtest green in each. 8 GB and 16 GB did not boot at all before -- and
16 GB now reports 17408 MB with "mem: 13312 MB above 4 GB added to the
allocator", where the old kernel would have used three. Both display
configurations still clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-09-25 15:23:38 +02:00
commit 3b26575b4a
9 changed files with 246 additions and 55 deletions

View file

@ -942,21 +942,50 @@ cpu_brand(char *out, int cap)
* from ExitBootServices long after the mistake. The count is deliberately of
* the types the kernel will treat as usable, so it matches what the system
* later reports rather than what the firmware owns. */
// The firmware's memory map, read into the one buffer there is.
//
// There used to be two of these, each with its own guess at how big a memory
// map gets -- 32 KB where it mattered and 16 KB here -- and both guesses were
// wrong on real hardware. The one here was the worse of the two, because it
// did not fail loudly: GetMemoryMap answered EFI_BUFFER_TOO_SMALL, this
// returned zero, and the System Information screen reported **0 MB usable** on
// a machine with sixteen gigabytes in it.
//
// So: one buffer, one function, and the size comes from the firmware rather
// than from an estimate. A call with a zero size fails on purpose and fills in
// what is needed; it allocates nothing, so a caller that needs the map key for
// ExitBootServices still has a valid one.
#define MAPBUF_BYTES 262144
static UINT8 mapbuf[MAPBUF_BYTES];
static EFI_STATUS
map_read(UINTN *size, UINTN *key, UINTN *dsize, UINT32 *dver)
{
EFI_STATUS st;
*size = 0;
st = BS->GetMemoryMap(size, (EFI_MEMORY_DESCRIPTOR *)mapbuf,
key, dsize, dver);
if (st == EFI_BUFFER_TOO_SMALL && *size > sizeof mapbuf)
return st; /* the caller decides how loudly to complain */
*size = sizeof mapbuf;
return BS->GetMemoryMap(size, (EFI_MEMORY_DESCRIPTOR *)mapbuf,
key, dsize, dver);
}
static UINT64
memory_mb(void)
{
UINTN size = 0, mapkey = 0, dsize = 0;
UINT32 dver = 0;
static char buf[16384];
UINT64 pages = 0;
size = sizeof buf;
if (EFI_ERROR(BS->GetMemoryMap(&size, (EFI_MEMORY_DESCRIPTOR *)buf,
&mapkey, &dsize, &dver)))
if (EFI_ERROR(map_read(&size, &mapkey, &dsize, &dver)))
return 0;
UINT64 pages = 0;
for (UINTN off = 0; off + dsize <= size; off += dsize) {
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(buf + off);
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off);
if (efi_type_to_e820(d->Type) == E820_USABLE)
pages += d->NumberOfPages;
}
@ -1760,13 +1789,11 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
// 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);
st = map_read(&mapsize, &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.
@ -1778,11 +1805,6 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
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);
// Translate before leaving, while the map is still readable and while
@ -1816,9 +1838,7 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
// The map changed underneath us between GetMemoryMap and here. Take it
// again and retry once; this is normal and the specification expects
// a loader to handle it.
mapsize = sizeof mapbuf;
st = BS->GetMemoryMap(&mapsize, (EFI_MEMORY_DESCRIPTOR *)mapbuf,
&mapkey, &descsize, &descver);
st = map_read(&mapsize, &mapkey, &descsize, &descver);
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
@ -1855,7 +1875,35 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
zero(pdpt, 0x1000);
pml4[0] = PDPT_ADDR | PTE_P | PTE_W; // identity
pml4[256] = PDPT_ADDR | PTE_P | PTE_W; // 0xffff800000000000
for (int i = 0; i < 4; i++) // 4 GB in 1 GB pages
// **All 512 entries, not four.**
//
// Four gigabytes was enough for as long as everything that mattered lived
// below four gigabytes -- and on a machine with a lot of RAM, one thing
// does not: *this loader*. The firmware places the image where it likes,
// and on an 8 GB machine it lands above 4 GB. The instruction after
//
// mov %rax, %cr3
//
// is then an instruction fetch from an address these tables do not map,
// and the machine dies between one instruction and the next, with the
// splash still on screen and nothing said:
//
// X64 Exception Type - 0E(#PF - Page-Fault)
// ExceptionData - 0x10 I:1 P:0
// RIP - 0000000140004DEE <- the loader, at 5.4 GB
// RAX - 0000000000001000 <- the page tables it just installed
// RCX - 00000000001000F0 <- the kernel it was about to jump to
//
// That is the whole of "it freezes at Starting the kernel". It reproduces
// on QEMU with `-m 8G` and never on `-m 1G`, which is why every test here
// passed.
//
// The fix costs nothing: the PDPT is one page whether four entries are
// filled or all of them, and 512 GB of identity map covers any machine
// whose firmware is going to load us somewhere surprising. The kernel
// replaces all of this in kvmalloc() a few hundred instructions later.
for (int i = 0; i < 512; i++)
pdpt[i] = ((UINT64)i << 30) | PTE_P | PTE_PS | PTE_W;
ui_progress_label("Starting the kernel");