diff --git a/boot/uefi/main.c b/boot/uefi/main.c index 0c40804..e8f0b5a 100644 --- a/boot/uefi/main.c +++ b/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;