From 726091000e0ce932a9b56cea3de404e4ccc4dceb Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Sat, 24 Oct 2026 11:00:02 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4 --- boot/uefi/main.c | 144 ++++++++++++++++++++++++++++++++--------------- write.sh | 140 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+), 45 deletions(-) create mode 100644 write.sh diff --git a/boot/uefi/main.c b/boot/uefi/main.c index 034c4e9..0579cef 100644 --- a/boot/uefi/main.c +++ b/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); } diff --git a/write.sh b/write.sh new file mode 100644 index 0000000..1740a71 --- /dev/null +++ b/write.sh @@ -0,0 +1,140 @@ +#!/bin/sh +# Write schos.iso to a USB stick. +# +# ./write.sh sdc or ./write.sh /dev/sdc +# ./write.sh sdc other.iso to write a different image +# +# Shows what is on the device, asks you to type the device's name back, then +# asks for your password and writes. Everything on the device is destroyed; +# there is no undo. +# +# Linux only: it reads the device list from lsblk. It refuses a partition +# (sdc1 -- the stick is written whole), anything holding a mounted system +# path, and anything that is not a removable or USB disk unless you add +# --any-disk, because the one mistake this script exists to stop is writing +# over the disk you are running from. + +set -eu + +if [ -t 1 ]; then + R=$(printf '\033[1;31m'); G=$(printf '\033[1;32m') + B=$(printf '\033[1m'); D=$(printf '\033[2m'); N=$(printf '\033[0m') +else + R='' G='' B='' D='' N='' +fi + +die() { printf '%swrite.sh:%s %s\n' "$R" "$N" "$*" >&2; exit 1; } + +usage() { + echo "usage: ./write.sh [image.iso] [--any-disk]" + echo " ./write.sh sdc writes schos.iso to /dev/sdc" + echo + echo "Removable disks on this machine:" + lsblk -d -n -o NAME,SIZE,TRAN,RM,MODEL 2>/dev/null | + awk '$3 == "usb" || $4 == "1" { printf " /dev/%-8s %8s %s\n", $1, $2, substr($0, index($0,$5)) }' + exit 2 +} + +ANY=0 +DEV= +ISO= +for a in "$@"; do + case "$a" in + --any-disk) ANY=1 ;; + -h|--help) usage ;; + *) if [ -z "$DEV" ]; then DEV=$a; elif [ -z "$ISO" ]; then ISO=$a; else usage; fi ;; + esac +done +[ -n "$DEV" ] || usage + +HERE=$(cd "$(dirname "$0")" && pwd) +[ -n "$ISO" ] || ISO=$HERE/schos.iso + +command -v lsblk >/dev/null 2>&1 || die "needs lsblk (util-linux); this script is Linux only" + +# --- the image -------------------------------------------------------------- + +[ -f "$ISO" ] || die "no image at $ISO -- run 'make iso' first" +ISO_BYTES=$(wc -c < "$ISO" | tr -d ' ') +ISO_DATE=$(date -r "$ISO" '+%Y-%m-%d %H:%M' 2>/dev/null || echo '?') + +# --- the device ------------------------------------------------------------- + +case "$DEV" in + /dev/*) ;; + *) DEV=/dev/$DEV ;; +esac +[ -b "$DEV" ] || die "$DEV is not a block device" + +NAME=$(basename "$DEV") +TYPE=$(lsblk -d -n -o TYPE "$DEV") +[ "$TYPE" = disk ] || die "$DEV is a $TYPE, not a whole disk -- give the disk (sdc), not a partition (sdc1)" + +SIZE_BYTES=$(lsblk -d -n -b -o SIZE "$DEV" | tr -d ' ') +SIZE=$(lsblk -d -n -o SIZE "$DEV" | tr -d ' ') +TRAN=$(lsblk -d -n -o TRAN "$DEV" | tr -d ' ') +RM=$(lsblk -d -n -o RM "$DEV" | tr -d ' ') +MODEL=$(lsblk -d -n -o MODEL "$DEV" | sed 's/ *$//') + +# The disk this system runs from, or anything with a system path on it. +for mp in $(lsblk -n -o MOUNTPOINTS "$DEV" 2>/dev/null); do + case "$mp" in + /|/boot|/boot/*|/efi|/home|/nix|/nix/*|/usr|/var|\[SWAP\]) + die "$DEV holds $mp -- that is this computer's own disk, not a stick" ;; + esac +done + +if [ "$TRAN" != usb ] && [ "$RM" != 1 ] && [ "$ANY" != 1 ]; then + die "$DEV ($MODEL, $SIZE, ${TRAN:-no transport}) is not a USB or removable disk. + If you are sure, run it again with --any-disk." +fi + +[ "$SIZE_BYTES" -ge "$ISO_BYTES" ] || die "$DEV is $SIZE, smaller than the image" + +# --- show, and ask ---------------------------------------------------------- + +echo +echo "${B}Image${N} $ISO" +echo " $((ISO_BYTES / 1048576)) MB, built $ISO_DATE" +echo +echo "${B}Device${N} $DEV ${B}$MODEL${N} $SIZE ${D}(${TRAN:-?}, removable=$RM)${N}" +lsblk -n -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINTS "$DEV" | sed '1d; s/^/ /' +USED=$(lsblk -n -o NAME "$DEV" | sed 1d | wc -l | tr -d ' ') +[ "$USED" -gt 0 ] || echo " ${D}(no partitions)${N}" +echo +echo "${R}Everything on $DEV will be destroyed. This cannot be undone.${N}" +printf 'Type %s%s%s to write, anything else to stop: ' "$B" "$NAME" "$N" +read -r answer +[ "$answer" = "$NAME" ] || { echo "Stopped. Nothing was written."; exit 1; } + +# --- write ------------------------------------------------------------------ + +echo +echo "Your password is needed to write to the device." +sudo -v || die "no permission; nothing was written" + +# Unmount whatever the desktop mounted, or the kernel keeps writing its own +# cached view of the old filesystem over the new image afterwards. +for part in $(lsblk -n -l -o NAME,MOUNTPOINTS "$DEV" | awk 'NF > 1 { print $1 }'); do + echo "Unmounting /dev/$part" + sudo umount "/dev/$part" || die "could not unmount /dev/$part; nothing was written" +done + +echo "Writing..." +sudo dd if="$ISO" of="$DEV" bs=4M conv=fsync oflag=direct status=progress +sync + +# --- check ------------------------------------------------------------------ + +# Read it back. A stick that takes the write and keeps none of it is common +# enough among cheap ones to be worth thirty seconds. +echo "Checking what was written..." +if sudo cmp -n "$ISO_BYTES" "$ISO" "$DEV"; then + echo "${G}Done.${N} $DEV holds $(basename "$ISO") and can be removed." +else + die "the device does not read back what was written -- the stick may be failing" +fi + +# The kernel re-reads the partition table so the new layout shows up here +# too; harmless if it cannot. +sudo partprobe "$DEV" 2>/dev/null || sudo blockdev --rereadpt "$DEV" 2>/dev/null || true