uefi: stage what the firmware still holds; copy it home after it exits
The kernel goes at fixed physical addresses, 1 MB to about 34 MB with the RAM disk inside it. When AllocatePages refused part of that, the loader said "could not reserve" and read the kernel there anyway -- over the firmware's own memory, with the firmware still running on it. An HP all-in-one from 2012 froze at the start of the loading bar. Now every free page of the range is claimed first. A segment wholly on claimed pages is read straight home; one that is not is read into a buffer elsewhere and copied home after ExitBootServices, when boot-services memory is ours. Memory under the kernel that outlives the firmware (ACPI NVS, runtime services) is named, and so is the loader or its stack sitting in the range. Segments are read a megabyte at a time, so the bar moves through the RAM disk and shows where a machine stopped. Under OVMF the data segment (over ACPI NVS and boot-services data at 8 MB) is now staged, and it boots both as a CD and as a USB disk. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
c3561f9d56
commit
93d3b123c5
1 changed files with 171 additions and 46 deletions
215
boot/uefi/main.c
215
boot/uefi/main.c
|
|
@ -91,6 +91,9 @@ static void mirrors_show(void); /* see "the other displays", further down */
|
|||
static void mirrors_match(EFI_GRAPHICS_OUTPUT_PROTOCOL *primary);
|
||||
static void describe_gop(EFI_GRAPHICS_OUTPUT_PROTOCOL *g, const CHAR16 *tag);
|
||||
static void explain_range(UINT64 lo, UINT64 hi);
|
||||
static EFI_STATUS map_read(UINTN *size, UINTN *key, UINTN *dsize, UINT32 *dver);
|
||||
#define MAPBUF_BYTES 262144
|
||||
static UINT8 mapbuf[MAPBUF_BYTES];
|
||||
|
||||
/* Once the splash is up, nothing may write to the console.
|
||||
*
|
||||
|
|
@ -337,6 +340,10 @@ copy(void *dst, const void *src, UINTN n)
|
|||
// read from the same place the kernel was. Set by open_kernel().
|
||||
static EFI_FILE_PROTOCOL *boot_root;
|
||||
|
||||
// Where the firmware put this loader, for load_elf() to check the kernel is
|
||||
// not about to be copied over it. Set by open_boot_volume().
|
||||
static UINT64 self_base, self_size;
|
||||
|
||||
// Opened before the display is touched, because what is on the volume decides
|
||||
// some of what the display does -- see nomirror in mirrors_find().
|
||||
static void
|
||||
|
|
@ -353,6 +360,8 @@ open_boot_volume(EFI_HANDLE image)
|
|||
// is not a thing to do by accident.
|
||||
st = BS->HandleProtocol(image, &li_guid, (void **)&li);
|
||||
if (EFI_ERROR(st)) die(L"cannot get LOADED_IMAGE", st);
|
||||
self_base = (UINT64)(UINTN)li->ImageBase;
|
||||
self_size = li->ImageSize;
|
||||
|
||||
st = BS->HandleProtocol(li->DeviceHandle, &fs_guid, (void **)&fs);
|
||||
if (EFI_ERROR(st)) die(L"boot volume has no filesystem", st);
|
||||
|
|
@ -484,6 +493,106 @@ load_console_font(void)
|
|||
vprint(L" bytes at "); vprint_hex((UINT32)at); vprint(L"\r\n");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
* Where the kernel goes, while the firmware is still using it
|
||||
*
|
||||
* The kernel is placed at fixed physical addresses, 1 MB up to about 34 MB
|
||||
* with the RAM disk inside it, and nothing promises that range is free while
|
||||
* boot services run. On OVMF most of it is; on an HP all-in-one from 2012 it
|
||||
* was not, and the loader used to say "could not reserve" and write the
|
||||
* kernel there anyway -- over the firmware's own data, with the firmware
|
||||
* still running on it. The machine froze at the start of the loading bar.
|
||||
*
|
||||
* So: every free page of the range is claimed first, which also stops the
|
||||
* firmware handing any of it out as a staging buffer. A segment wholly on
|
||||
* claimed pages is read straight home. One that is not is read into a buffer
|
||||
* anywhere else and copied home in place_staged(), after ExitBootServices,
|
||||
* when boot-services memory is the loader's to overwrite.
|
||||
* ------------------------------------------------------------------------- */
|
||||
#define NCLAIM 64
|
||||
static struct { UINT64 lo, hi; } claims[NCLAIM];
|
||||
static UINTN nclaim;
|
||||
|
||||
#define NLATER 16
|
||||
static struct { UINT64 dst, src, len; } later[NLATER];
|
||||
static UINTN nlater;
|
||||
|
||||
static void
|
||||
claim_span(UINT64 lo, UINT64 hi)
|
||||
{
|
||||
UINTN size, key, dsize;
|
||||
UINT32 dver;
|
||||
int foreign = 0;
|
||||
|
||||
if (EFI_ERROR(map_read(&size, &key, &dsize, &dver)) || !dsize)
|
||||
return;
|
||||
/* A copy, because every AllocatePages below changes the map. */
|
||||
static UINT8 snap[MAPBUF_BYTES];
|
||||
copy(snap, mapbuf, size);
|
||||
|
||||
for (UINTN off = 0; off + dsize <= size; off += dsize) {
|
||||
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(snap + off);
|
||||
UINT64 a = d->PhysicalStart, b = a + d->NumberOfPages * 4096;
|
||||
if (b <= lo || a >= hi)
|
||||
continue;
|
||||
if (a < lo) a = lo;
|
||||
if (b > hi) b = hi;
|
||||
if (d->Type != EfiConventionalMemory) {
|
||||
/* Boot-services memory is free once the firmware exits; anything
|
||||
* else under the kernel outlives it, and is worth saying. */
|
||||
if (d->Type != EfiBootServicesCode && d->Type != EfiBootServicesData)
|
||||
foreign = 1;
|
||||
continue;
|
||||
}
|
||||
EFI_PHYSICAL_ADDRESS at = a;
|
||||
if (nclaim < NCLAIM &&
|
||||
!EFI_ERROR(BS->AllocatePages(AllocateAddress, EfiLoaderData,
|
||||
(UINTN)((b - a) / 4096), &at))) {
|
||||
claims[nclaim].lo = a;
|
||||
claims[nclaim].hi = b;
|
||||
nclaim++;
|
||||
}
|
||||
}
|
||||
|
||||
if (foreign) {
|
||||
print(L"schos: warning: firmware memory that outlives it lies under the kernel:\r\n");
|
||||
explain_range(lo, hi);
|
||||
}
|
||||
/* This loader, or the stack it runs on, inside the range would be copied
|
||||
* over by place_staged() -- the firmware chose both, and there is no
|
||||
* recovering from it here. Said, so a machine that dies there says why. */
|
||||
UINT64 sp = (UINT64)(UINTN)&size;
|
||||
if ((self_base < hi && self_base + self_size > lo) || (sp >= lo && sp < hi))
|
||||
print(L"schos: warning: the loader itself sits where the kernel goes\r\n");
|
||||
}
|
||||
|
||||
/* Whether every page of [lo, hi) is one claim_span() took. */
|
||||
static int
|
||||
claim_covers(UINT64 lo, UINT64 hi)
|
||||
{
|
||||
while (lo < hi) {
|
||||
UINTN i;
|
||||
for (i = 0; i < nclaim; i++)
|
||||
if (claims[i].lo <= lo && lo < claims[i].hi)
|
||||
break;
|
||||
if (i == nclaim)
|
||||
return 0;
|
||||
lo = claims[i].hi;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* After ExitBootServices: the staged segments, home. The buffers came from
|
||||
* outside the kernel's range -- claim_span() took every free page inside it
|
||||
* -- so no copy can land on a buffer still to be copied. */
|
||||
static void
|
||||
place_staged(void)
|
||||
{
|
||||
for (UINTN i = 0; i < nlater; i++)
|
||||
copy((void *)(UINTN)later[i].dst, (void *)(UINTN)later[i].src,
|
||||
(UINTN)later[i].len);
|
||||
}
|
||||
|
||||
// Load PT_LOAD segments to p_paddr. Physical addresses, not virtual: the
|
||||
// kernel is linked at 0xffff800000100000 but must be *placed* at 0x100000, and
|
||||
// p_paddr is where the linker script says each segment belongs.
|
||||
|
|
@ -506,6 +615,8 @@ load_elf(EFI_FILE_PROTOCOL *file)
|
|||
die(L"kernel is not 64-bit", 0);
|
||||
|
||||
UINT64 lowest = ~0ULL, highest = 0;
|
||||
UINT64 lowest_paddr = ~0ULL, span_hi = 0;
|
||||
UINT8 *first = 0; /* the lowest segment's bytes, wherever they are */
|
||||
|
||||
// Two passes' worth of information from one: the loop below both loads and
|
||||
// reports, so the bar tracks bytes actually written rather than a segment
|
||||
|
|
@ -518,12 +629,16 @@ load_elf(EFI_FILE_PROTOCOL *file)
|
|||
break;
|
||||
if (EFI_ERROR(file->Read(file, &m, &ph)) || m != sizeof ph)
|
||||
break;
|
||||
if (ph.p_type == PT_LOAD)
|
||||
if (ph.p_type == PT_LOAD && ph.p_memsz) {
|
||||
total += ph.p_memsz;
|
||||
if (ph.p_paddr < lowest_paddr) lowest_paddr = ph.p_paddr;
|
||||
if (ph.p_paddr + ph.p_memsz > span_hi) span_hi = ph.p_paddr + ph.p_memsz;
|
||||
}
|
||||
}
|
||||
ui_progress_begin("Loading SchOS", (ui_u32)(total ? total : 1));
|
||||
|
||||
EFI_PHYSICAL_ADDRESS claimed_end = 0; /* see the AllocatePages below */
|
||||
if (span_hi)
|
||||
claim_span(lowest_paddr & ~0xfffULL, (span_hi + 0xfff) & ~0xfffULL);
|
||||
|
||||
for (UINT16 i = 0; i < eh.e_phnum; i++) {
|
||||
struct elf64_phdr ph;
|
||||
|
|
@ -536,50 +651,54 @@ load_elf(EFI_FILE_PROTOCOL *file)
|
|||
if (ph.p_type != PT_LOAD || ph.p_memsz == 0)
|
||||
continue;
|
||||
|
||||
// Claim the pages before writing them. The firmware is still running
|
||||
// and this is its memory until it says otherwise; writing to
|
||||
// conventional memory it has not handed over is how a loader corrupts
|
||||
// the very services it is about to call.
|
||||
//
|
||||
// From the end of what the previous segment claimed, not from this
|
||||
// one's first page. The linker packs segments without page-aligning
|
||||
// them -- .text ending at 0x1dc9fe and .rodata starting at 0x1dca00 --
|
||||
// so two segments routinely share a page, and asking for that page a
|
||||
// second time is refused because it is ours already. That was the
|
||||
// "could not reserve" warning on every boot, about memory that was
|
||||
// reserved all along.
|
||||
// Where this segment's bytes go now: home, when every page of it is
|
||||
// ours, and a buffer anywhere else when it is not -- see claim_span()
|
||||
// for why writing home regardless froze an HP all-in-one.
|
||||
EFI_PHYSICAL_ADDRESS at = ph.p_paddr & ~0xfffULL;
|
||||
EFI_PHYSICAL_ADDRESS end = (ph.p_paddr + ph.p_memsz + 0xfff) & ~0xfffULL;
|
||||
if (at < claimed_end)
|
||||
at = claimed_end;
|
||||
UINTN pages = at < end ? (UINTN)((end - at) / 0x1000) : 0;
|
||||
st = pages ? BS->AllocatePages(AllocateAddress, EfiLoaderData, pages, &at)
|
||||
: EFI_SUCCESS;
|
||||
if (end > claimed_end)
|
||||
claimed_end = end;
|
||||
if (EFI_ERROR(st)) {
|
||||
// Already allocated by the firmware for something of its own, or
|
||||
// simply not conventional memory. Not fatal on its own -- the
|
||||
// range may still be usable -- but worth saying, because a kernel
|
||||
// that boots and then corrupts itself starts here.
|
||||
print(L"schos: warning: could not reserve "); print_hex(at);
|
||||
print(L"\r\n");
|
||||
explain_range(at, end);
|
||||
UINT8 *dst = (UINT8 *)(UINTN)ph.p_paddr;
|
||||
if (!claim_covers(at, end)) {
|
||||
EFI_PHYSICAL_ADDRESS buf = 0;
|
||||
UINTN pages = (UINTN)((ph.p_memsz + 0xfff) / 0x1000);
|
||||
if (nlater == NLATER)
|
||||
die(L"too many kernel segments to stage", 0);
|
||||
st = BS->AllocatePages(AllocateAnyPages, EfiLoaderData, pages, &buf);
|
||||
if (EFI_ERROR(st))
|
||||
die(L"no memory to stage a kernel segment in", st);
|
||||
vprint(L"schos: staging "); vprint_hex(ph.p_paddr);
|
||||
vprint(L" at "); vprint_hex(buf); vprint(L"\r\n");
|
||||
later[nlater].dst = ph.p_paddr;
|
||||
later[nlater].src = buf;
|
||||
later[nlater].len = ph.p_memsz;
|
||||
nlater++;
|
||||
dst = (UINT8 *)(UINTN)buf;
|
||||
}
|
||||
if (ph.p_paddr == lowest_paddr)
|
||||
first = dst;
|
||||
|
||||
if (ph.p_filesz) {
|
||||
n = (UINTN)ph.p_filesz;
|
||||
// A megabyte at a time, so the bar moves through a 30 MB segment
|
||||
// instead of sitting at its start: on a machine that stops here, where
|
||||
// it stopped is the only clue there is. Old firmware's USB stacks are
|
||||
// also happier with reads that size than with one of 30 MB.
|
||||
UINT64 left = ph.p_filesz, off = 0;
|
||||
if (left) {
|
||||
st = file->SetPosition(file, ph.p_offset);
|
||||
if (EFI_ERROR(st)) die(L"cannot seek a segment", st);
|
||||
st = file->Read(file, &n, (void *)(UINTN)ph.p_paddr);
|
||||
if (EFI_ERROR(st) || n != ph.p_filesz)
|
||||
}
|
||||
while (left) {
|
||||
n = (UINTN)(left < 0x100000 ? left : 0x100000);
|
||||
UINTN want = n;
|
||||
st = file->Read(file, &n, dst + off);
|
||||
if (EFI_ERROR(st) || n != want)
|
||||
die(L"short read loading a segment", st);
|
||||
off += n;
|
||||
left -= n;
|
||||
ui_progress_tick((ui_u32)(done + off));
|
||||
}
|
||||
// .bss: present in memsz, absent from filesz, and the kernel assumes
|
||||
// it is zeroed.
|
||||
if (ph.p_memsz > ph.p_filesz)
|
||||
zero((void *)(UINTN)(ph.p_paddr + ph.p_filesz),
|
||||
(UINTN)(ph.p_memsz - ph.p_filesz));
|
||||
zero(dst + ph.p_filesz, (UINTN)(ph.p_memsz - ph.p_filesz));
|
||||
|
||||
done += ph.p_memsz;
|
||||
ui_progress_tick((ui_u32)done);
|
||||
|
|
@ -594,7 +713,7 @@ load_elf(EFI_FILE_PROTOCOL *file)
|
|||
// of what was just loaded -- the same way the BIOS loader finds the
|
||||
// multiboot header. A magic in the image beats a symbol table: no ELF
|
||||
// symbol parsing, and no fixed offset to drift.
|
||||
UINT8 *img = (UINT8 *)(UINTN)lowest;
|
||||
UINT8 *img = first ? first : (UINT8 *)(UINTN)lowest;
|
||||
for (UINTN off = 0; off + sizeof(struct sch64_header) <= 16384; off += 8) {
|
||||
struct sch64_header *h = (struct sch64_header *)(img + off);
|
||||
if (h->magic0 == SCH64_MAGIC0 && h->magic1 == SCH64_MAGIC1)
|
||||
|
|
@ -1043,9 +1162,6 @@ cpu_brand(char *out, int cap)
|
|||
// 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)
|
||||
{
|
||||
|
|
@ -1064,11 +1180,12 @@ map_read(UINTN *size, UINTN *key, UINTN *dsize, UINT32 *dver)
|
|||
|
||||
/* Who owns [lo, hi), when it was not ours to take.
|
||||
*
|
||||
* "could not reserve" alone says where and not why, and the why decides
|
||||
* whether it matters: boot-services memory is the firmware's until
|
||||
* ExitBootServices and harmless to overwrite after it, while runtime services
|
||||
* or ACPI tables under the kernel are a machine that will corrupt itself. So
|
||||
* every descriptor in the range that is not free memory is named. */
|
||||
* claim_span() calls this when something under the kernel outlives the
|
||||
* firmware. Which thing decides whether it matters: boot-services memory is
|
||||
* the firmware's until ExitBootServices and the kernel's after it, while
|
||||
* runtime services or ACPI tables under the kernel are a machine that will
|
||||
* corrupt itself. So every descriptor in the range that is not free, and not
|
||||
* the loader's own, is named. */
|
||||
static void
|
||||
explain_range(UINT64 lo, UINT64 hi)
|
||||
{
|
||||
|
|
@ -1080,7 +1197,9 @@ explain_range(UINT64 lo, UINT64 hi)
|
|||
for (UINTN off = 0; off + dsize <= size; off += dsize) {
|
||||
EFI_MEMORY_DESCRIPTOR *d = (EFI_MEMORY_DESCRIPTOR *)(mapbuf + off);
|
||||
UINT64 a = d->PhysicalStart, b = a + d->NumberOfPages * 4096;
|
||||
if (b <= lo || a >= hi || d->Type == EfiConventionalMemory)
|
||||
/* Free memory, and what claim_span() just took, are not in the way. */
|
||||
if (b <= lo || a >= hi || d->Type == EfiConventionalMemory ||
|
||||
d->Type == EfiLoaderData)
|
||||
continue;
|
||||
print(L"schos: type "); print_dec(d->Type);
|
||||
print(L" at "); print_hex(a); print(L".."); print_hex(b);
|
||||
|
|
@ -2047,6 +2166,12 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
|
|||
|
||||
__asm__ volatile("cli");
|
||||
|
||||
// The kernel segments that could not be read straight home: the firmware
|
||||
// has let go of that memory now. See claim_span().
|
||||
if (nlater)
|
||||
ui_progress_label("Placing the kernel");
|
||||
place_staged();
|
||||
|
||||
copy((void *)SCHBOOT_E820_MAP, e820, nent * sizeof e820[0]);
|
||||
*(UINT32 *)SCHBOOT_E820_COUNT = nent;
|
||||
*(UINT64 *)SCHBOOT_RSDP_ADDR = rsdp;
|
||||
|
|
|
|||
Loading…
Reference in a new issue