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;
|
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
|
* The last jump, from somewhere the kernel cannot land
|
||||||
* -- so no copy can land on a buffer still to be copied. */
|
*
|
||||||
|
* 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
|
static void
|
||||||
place_staged(void)
|
tramp_reserve(void)
|
||||||
{
|
{
|
||||||
for (UINTN i = 0; i < nlater; i++)
|
EFI_PHYSICAL_ADDRESS a = kern_lo - 1;
|
||||||
copy((void *)(UINTN)later[i].dst, (void *)(UINTN)later[i].src,
|
if (EFI_ERROR(BS->AllocatePages(AllocateMaxAddress, EfiLoaderCode, 1, &a)) ||
|
||||||
(UINTN)later[i].len);
|
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
|
// 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)
|
if (span_hi)
|
||||||
claim_span(lowest_paddr & ~0xfffULL, (span_hi + 0xfff) & ~0xfffULL);
|
claim_span(lowest_paddr & ~0xfffULL, (span_hi + 0xfff) & ~0xfffULL);
|
||||||
|
kern_lo = lowest_paddr & ~0xfffULL;
|
||||||
|
|
||||||
for (UINT16 i = 0; i < eh.e_phnum; i++) {
|
for (UINT16 i = 0; i < eh.e_phnum; i++) {
|
||||||
struct elf64_phdr ph;
|
struct elf64_phdr ph;
|
||||||
|
|
@ -1758,7 +1841,7 @@ f8_window(EFI_GRAPHICS_OUTPUT_PROTOCOL *gop)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static UINT32
|
static UINT64
|
||||||
setup_framebuffer(void)
|
setup_framebuffer(void)
|
||||||
{
|
{
|
||||||
EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
|
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.
|
// and the display is the only thing that can report a failure afterwards.
|
||||||
open_boot_volume(image);
|
open_boot_volume(image);
|
||||||
|
|
||||||
UINT32 fb = setup_framebuffer();
|
UINT64 fb = setup_framebuffer();
|
||||||
|
|
||||||
EFI_FILE_PROTOCOL *file = open_kernel(image);
|
EFI_FILE_PROTOCOL *file = open_kernel(image);
|
||||||
UINT64 entry = load_elf(file);
|
UINT64 entry = load_elf(file);
|
||||||
|
tramp_reserve();
|
||||||
ui_progress_end();
|
ui_progress_end();
|
||||||
file->Close(file);
|
file->Close(file);
|
||||||
|
|
||||||
|
|
@ -2166,11 +2250,6 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
|
||||||
|
|
||||||
__asm__ volatile("cli");
|
__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]);
|
copy((void *)SCHBOOT_E820_MAP, e820, nent * sizeof e820[0]);
|
||||||
*(UINT32 *)SCHBOOT_E820_COUNT = nent;
|
*(UINT32 *)SCHBOOT_E820_COUNT = nent;
|
||||||
|
|
@ -2220,35 +2299,10 @@ EfiMain(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab)
|
||||||
for (int i = 0; i < 512; i++)
|
for (int i = 0; i < 512; i++)
|
||||||
pdpt[i] = ((UINT64)i << 30) | PTE_P | PTE_PS | PTE_W;
|
pdpt[i] = ((UINT64)i << 30) | PTE_P | PTE_PS | PTE_W;
|
||||||
|
|
||||||
ui_progress_label("Starting the kernel");
|
// 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
|
||||||
// RBX carries the framebuffer address into kmain, and must never be 1 --
|
// jump into the kernel is made from the same page.
|
||||||
// that value marks an application processor in entry64high. A physical
|
ui_progress_label(nlater ? "Placing and starting the kernel"
|
||||||
// MMIO address or zero, so it cannot collide.
|
: "Starting the kernel");
|
||||||
//
|
tramp_go(entry, fb);
|
||||||
// "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");
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
140
write.sh
Normal file
140
write.sh
Normal file
|
|
@ -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 <device> [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
|
||||||
Loading…
Reference in a new issue