uefi: copy the kernel home and jump from a page it cannot reach
The staged segments were copied home after ExitBootServices by the loader itself -- on code, a stack and a copy list the firmware had placed wherever it liked. On an HP all-in-one some of that was in the kernel's range: "Placing the kernel", then a triple fault and a reset. The copy and the jump now run from one page below the kernel's lowest address, allocated as loader code before the firmware exits: a short position-independent routine, the copy list, and a stack. It switches to the loader's page tables and its own stack before copying anything. Called sysv_abi, because UEFI code is the Microsoft convention and the first try loaded the framebuffer address into rsp. The framebuffer address handed over in rbx is 64-bit now too; the kernel already read the full one from the hand-off block. Also write.sh: writes schos.iso to a USB stick, after showing the device and asking for its name to be typed back; refuses partitions, the system disk and non-removable disks; reads the result back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
a4fbdb7437
commit
726091000e
2 changed files with 239 additions and 45 deletions
144
boot/uefi/main.c
144
boot/uefi/main.c
|
|
@ -582,15 +582,97 @@ claim_covers(UINT64 lo, UINT64 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. */
|
||||
/* ---------------------------------------------------------------------------
|
||||
* The last jump, from somewhere the kernel cannot land
|
||||
*
|
||||
* The staged segments are copied home after ExitBootServices -- and this
|
||||
* loader is still running then, on code, a stack and a copy list the firmware
|
||||
* placed wherever it liked. On an HP all-in-one some of that was inside the
|
||||
* kernel's range, so the copy overwrote the loader while it ran: the screen
|
||||
* said "Placing the kernel" and the machine reset, a triple fault, with
|
||||
* "Starting the kernel" never drawn.
|
||||
*
|
||||
* So the copy and the jump run from one page of their own, below the kernel's
|
||||
* lowest address and so outside anything it occupies: this routine at the start of it,
|
||||
* the copy list after it, a stack at the end. It is position independent --
|
||||
* every address it uses arrives in a register -- so it runs wherever it is
|
||||
* copied. It switches to the loader's page tables (which map all memory 1:1)
|
||||
* and its own stack first, so nothing it needs afterwards is anywhere the
|
||||
* copies can reach. The buffers being copied *from* are outside the kernel's
|
||||
* range too: claim_span() took every free page inside it.
|
||||
*
|
||||
* rdi copy list: { dst, src, len } x n rsi n
|
||||
* rdx kernel entry rcx framebuffer, for rbx
|
||||
* r8 page tables r9 stack top
|
||||
*
|
||||
* RBX carries the framebuffer address into kmain and must never be 1 -- that
|
||||
* value marks an application processor in entry64high. A physical address or
|
||||
* zero cannot collide. (The jump this replaces was inline asm, where leaving
|
||||
* rbx out of the clobbers once let clang put the jump target in it; written
|
||||
* out in full here, every register is spelled.)
|
||||
* ------------------------------------------------------------------------- */
|
||||
__asm__(
|
||||
".text\n"
|
||||
".globl schos_tramp\n"
|
||||
".globl schos_tramp_end\n"
|
||||
"schos_tramp:\n"
|
||||
" mov %r9, %rsp\n"
|
||||
" mov %r8, %cr3\n"
|
||||
" mov %rdx, %r10\n"
|
||||
" mov %rcx, %rbx\n"
|
||||
" mov %rdi, %r11\n"
|
||||
" mov %rsi, %r9\n"
|
||||
" cld\n"
|
||||
"1: test %r9, %r9\n"
|
||||
" jz 2f\n"
|
||||
" mov 0(%r11), %rdi\n"
|
||||
" mov 8(%r11), %rsi\n"
|
||||
" mov 16(%r11), %rcx\n"
|
||||
" rep movsb\n"
|
||||
" add $24, %r11\n"
|
||||
" dec %r9\n"
|
||||
" jmp 1b\n"
|
||||
"2: jmp *%r10\n"
|
||||
"schos_tramp_end:\n");
|
||||
|
||||
extern const UINT8 schos_tramp[], schos_tramp_end[];
|
||||
|
||||
#define TRAMP_LIST 256 /* the copy list's offset in the page */
|
||||
static EFI_PHYSICAL_ADDRESS tramp_page;
|
||||
static UINT64 kern_lo; /* the kernel's lowest address; set by load_elf() */
|
||||
|
||||
/* Before ExitBootServices: the page, below the kernel and above the first
|
||||
* megabyte -- the hand-off block and the page tables are written down there
|
||||
* after the firmware exits. Loader *code*, not data: a firmware that maps
|
||||
* data pages no-execute (OVMF does) would fault on the first instruction. */
|
||||
static void
|
||||
place_staged(void)
|
||||
tramp_reserve(void)
|
||||
{
|
||||
for (UINTN i = 0; i < nlater; i++)
|
||||
copy((void *)(UINTN)later[i].dst, (void *)(UINTN)later[i].src,
|
||||
(UINTN)later[i].len);
|
||||
EFI_PHYSICAL_ADDRESS a = kern_lo - 1;
|
||||
if (EFI_ERROR(BS->AllocatePages(AllocateMaxAddress, EfiLoaderCode, 1, &a)) ||
|
||||
a < 0x100000)
|
||||
die(L"no page below 16 MB for the final jump", 0);
|
||||
tramp_page = a;
|
||||
}
|
||||
|
||||
/* After ExitBootServices, and the last thing this loader does. */
|
||||
static void __attribute__((noreturn))
|
||||
tramp_go(UINT64 entry, UINT64 fb)
|
||||
{
|
||||
UINT8 *page = (UINT8 *)(UINTN)tramp_page;
|
||||
copy(page, schos_tramp, (UINTN)(schos_tramp_end - schos_tramp));
|
||||
copy(page + TRAMP_LIST, later, nlater * sizeof later[0]);
|
||||
|
||||
// sysv_abi: the registers above are System V's argument order. This
|
||||
// loader is compiled for UEFI, which is the *Microsoft* convention (rcx,
|
||||
// rdx, r8, r9, then the stack), and without the attribute the first try
|
||||
// loaded the framebuffer address into rsp and a kernel address into cr3.
|
||||
typedef void __attribute__((sysv_abi)) tramp_fn(void *, UINT64, UINT64,
|
||||
UINT64, UINT64, UINT64);
|
||||
tramp_fn *go = (tramp_fn *)page;
|
||||
go(page + TRAMP_LIST, nlater, entry, fb, PML4_ADDR, tramp_page + 4096);
|
||||
for (;;)
|
||||
__asm__ volatile("hlt");
|
||||
}
|
||||
|
||||
// Load PT_LOAD segments to p_paddr. Physical addresses, not virtual: the
|
||||
|
|
@ -639,6 +721,7 @@ load_elf(EFI_FILE_PROTOCOL *file)
|
|||
|
||||
if (span_hi)
|
||||
claim_span(lowest_paddr & ~0xfffULL, (span_hi + 0xfff) & ~0xfffULL);
|
||||
kern_lo = lowest_paddr & ~0xfffULL;
|
||||
|
||||
for (UINT16 i = 0; i < eh.e_phnum; i++) {
|
||||
struct elf64_phdr ph;
|
||||
|
|
@ -1758,7 +1841,7 @@ f8_window(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
|||
return 0;
|
||||
}
|
||||
|
||||
static UINT32
|
||||
static UINT64
|
||||
setup_framebuffer(void)
|
||||
{
|
||||
EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
|
||||
|
|
@ -2033,10 +2116,11 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
|
|||
// and the display is the only thing that can report a failure afterwards.
|
||||
open_boot_volume(image);
|
||||
|
||||
UINT32 fb = setup_framebuffer();
|
||||
UINT64 fb = setup_framebuffer();
|
||||
|
||||
EFI_FILE_PROTOCOL *file = open_kernel(image);
|
||||
UINT64 entry = load_elf(file);
|
||||
tramp_reserve();
|
||||
ui_progress_end();
|
||||
file->Close(file);
|
||||
|
||||
|
|
@ -2166,11 +2250,6 @@ 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;
|
||||
|
|
@ -2220,35 +2299,10 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
|
|||
for (int i = 0; i < 512; i++)
|
||||
pdpt[i] = ((UINT64)i << 30) | PTE_P | PTE_PS | PTE_W;
|
||||
|
||||
ui_progress_label("Starting the kernel");
|
||||
|
||||
// RBX carries the framebuffer address into kmain, and must never be 1 --
|
||||
// that value marks an application processor in entry64high. A physical
|
||||
// MMIO address or zero, so it cannot collide.
|
||||
//
|
||||
// "rbx" is in the clobber list because this writes it, and leaving it out
|
||||
// is not a tidiness problem -- it is a jump to the wrong address. Without
|
||||
// it the compiler may allocate an *input* into rbx, and it did: clang 21
|
||||
// put `entry` there, so the second instruction overwrote the jump target
|
||||
// with the framebuffer address and the third jumped into video memory. The
|
||||
// machine then executed pixels until KVM gave up.
|
||||
//
|
||||
// movq %rax, %cr3 <- %0, the page tables
|
||||
// movq %rcx, %rbx <- %1, the framebuffer, into rbx
|
||||
// jmpq *%rbx <- %2 was rbx, and now holds the framebuffer
|
||||
//
|
||||
// It had worked for as long as the compiler happened to choose differently.
|
||||
// That is the same failure as the BIOS loader's EAX hand-off in
|
||||
// docs/history/kernel.md section 4: a register the code depends on, that
|
||||
// nothing in the source actually asks for.
|
||||
__asm__ volatile(
|
||||
"mov %0, %%cr3\n\t"
|
||||
"mov %1, %%rbx\n\t"
|
||||
"jmp *%2\n\t"
|
||||
:
|
||||
: "r"((UINT64)PML4_ADDR), "r"((UINT64)fb), "r"(entry)
|
||||
: "rbx", "memory");
|
||||
|
||||
for (;;)
|
||||
__asm__ volatile("hlt");
|
||||
// The kernel segments that could not be read straight home are copied
|
||||
// there now, by tramp_go(), from a page they cannot overwrite -- and the
|
||||
// jump into the kernel is made from the same page.
|
||||
ui_progress_label(nlater ? "Placing and starting the kernel"
|
||||
: "Starting the kernel");
|
||||
tramp_go(entry, fb);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue