From 0fa2cac4f0cdefaf1addd7f1fe0fd8113db9360b Mon Sep 17 00:00:00 2001 From: Kay Sievers Date: Sun, 8 Feb 2015 12:25:35 +0100 Subject: [PATCH] sd-boot: add EFI boot manager and stub loader --- .gitignore | 3 + Makefile.am | 121 ++ configure.ac | 82 +- m4/arch.m4 | 13 + src/sd-boot/.gitignore | 2 + src/sd-boot/console.c | 141 +++ src/sd-boot/console.h | 34 + src/sd-boot/graphics.c | 389 +++++++ src/sd-boot/graphics.h | 26 + src/sd-boot/linux.c | 130 +++ src/sd-boot/linux.h | 24 + src/sd-boot/pefile.c | 172 +++ src/sd-boot/pefile.h | 22 + src/sd-boot/sd-boot.c | 2023 ++++++++++++++++++++++++++++++++++ src/sd-boot/stub.c | 106 ++ src/sd-boot/util.c | 322 ++++++ src/sd-boot/util.h | 44 + test/splash.bmp | Bin 0 -> 289238 bytes test/test-efi-create-disk.sh | 42 + 19 files changed, 3690 insertions(+), 6 deletions(-) create mode 100644 m4/arch.m4 create mode 100644 src/sd-boot/.gitignore create mode 100644 src/sd-boot/console.c create mode 100644 src/sd-boot/console.h create mode 100644 src/sd-boot/graphics.c create mode 100644 src/sd-boot/graphics.h create mode 100644 src/sd-boot/linux.c create mode 100644 src/sd-boot/linux.h create mode 100644 src/sd-boot/pefile.c create mode 100644 src/sd-boot/pefile.h create mode 100644 src/sd-boot/sd-boot.c create mode 100644 src/sd-boot/stub.c create mode 100644 src/sd-boot/util.c create mode 100644 src/sd-boot/util.h create mode 100644 test/splash.bmp create mode 100755 test/test-efi-create-disk.sh diff --git a/.gitignore b/.gitignore index e8a4085a3a..75699ca33b 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,9 @@ /machinectl /mtd_probe /networkctl +/linuxx64.efi.stub +/sd-bootx64.efi +/test-efi-disk.img /scsi_id /systemadm /systemctl diff --git a/Makefile.am b/Makefile.am index bf04d31840..d739445e83 100644 --- a/Makefile.am +++ b/Makefile.am @@ -111,6 +111,7 @@ catalogdir=$(prefix)/lib/systemd/catalog kernelinstalldir = $(prefix)/lib/kernel/install.d factory_etcdir = $(prefix)/share/factory/etc factory_pamdir = $(prefix)/share/factory/etc/pam.d +sd_bootlibdir = $(prefix)/lib/systemd/sd-boot # And these are the special ones for / rootprefix=@rootprefix@ @@ -2497,6 +2498,126 @@ dist_bashcompletion_DATA += \ dist_zshcompletion_DATA += \ shell-completion/zsh/_bootctl +# ------------------------------------------------------------------------------ +efi_cppflags = \ + $(EFI_CPPFLAGS) \ + -I$(top_builddir) -include config.h \ + -I$(EFI_INC_DIR)/efi \ + -I$(EFI_INC_DIR)/efi/$(EFI_ARCH) \ + -DEFI_MACHINE_TYPE_NAME=\"$(EFI_MACHINE_TYPE_NAME)\" + +efi_cflags = \ + $(EFI_CFLAGS) \ + -Wall \ + -Wextra \ + -nostdinc \ + -ggdb -O0 \ + -fpic \ + -fshort-wchar \ + -nostdinc \ + -ffreestanding \ + -fno-strict-aliasing \ + -fno-stack-protector \ + -Wsign-compare \ + -mno-sse \ + -mno-mmx + +if ARCH_X86_64 +efi_cflags += \ + -mno-red-zone \ + -DEFI_FUNCTION_WRAPPER \ + -DGNU_EFI_USE_MS_ABI +endif + +efi_ldflags = \ + $(EFI_LDFLAGS) \ + -T $(EFI_LDS_DIR)/elf_$(EFI_ARCH)_efi.lds \ + -shared \ + -Bsymbolic \ + -nostdlib \ + -znocombreloc \ + -L $(EFI_LIB_DIR) \ + $(EFI_LDS_DIR)/crt0-efi-$(EFI_ARCH).o + +# ------------------------------------------------------------------------------ +sd_boot_headers = \ + src/sd-boot/util.h \ + src/sd-boot/console.h \ + src/sd-boot/graphics.h \ + src/sd-boot/pefile.h + +sd_boot_sources = \ + src/sd-boot/util.c \ + src/sd-boot/console.c \ + src/sd-boot/graphics.c \ + src/sd-boot/pefile.c \ + src/sd-boot/sd-boot.c + +sd_boot_objects = $(addprefix $(top_builddir)/,$(sd_boot_sources:.c=.o)) +sd_boot_solib = $(top_builddir)/src/sd-boot/sd_boot.so +sd_boot = sd-boot$(EFI_MACHINE_TYPE_NAME).efi + +sd_bootlib_DATA = $(sd_boot) +CLEANFILES += $(sd_boot_objects) $(sd_boot_solib) $(sd_boot) +EXTRA_DIST += $(sd_boot_sources) $(sd_boot_headers) + +$(top_builddir)/src/sd-boot/%.o: $(top_srcdir)/src/sd-boot/%.c $(addprefix $(top_srcdir)/,$(sd_boot_headers)) + @$(MKDIR_P) $(top_builddir)/src/sd-boot/ + $(AM_V_CC)$(EFI_CC) $(efi_cppflags) $(efi_cflags) -c $< -o $@ + +$(sd_boot_solib): $(sd_boot_objects) + $(AM_V_CCLD)$(LD) $(efi_ldflags) $(sd_boot_objects) \ + -o $@ -lefi -lgnuefi $(shell $(CC) -print-libgcc-file-name); \ + nm -D -u $@ | grep ' U ' && exit 1 || : + +$(sd_boot): $(sd_boot_solib) + $(AM_V_GEN) objcopy -j .text -j .sdata -j .data -j .dynamic \ + -j .dynsym -j .rel -j .rela -j .reloc \ + --target=efi-app-$(EFI_ARCH) $< $@ + +# ------------------------------------------------------------------------------ +stub_headers = \ + src/sd-boot/util.h \ + src/sd-boot/pefile.h \ + src/sd-boot/linux.h + +stub_sources = \ + src/sd-boot/util.c \ + src/sd-boot/pefile.c \ + src/sd-boot/linux.c \ + src/sd-boot/stub.c + +stub_objects = $(addprefix $(top_builddir)/,$(stub_sources:.c=.o)) +stub_solib = $(top_builddir)/src/sd-boot/stub.so +stub = linux$(EFI_MACHINE_TYPE_NAME).efi.stub + +sd_bootlib_DATA += $(stub) +CLEANFILES += $(stub_objects) $(stub_solib) $(stub) +EXTRA_DIST += $(stub_sources) $(stub_headers) + +$(top_builddir)/src/sd-boot/%.o: $(top_srcdir)/src/sd-boot/%.c $(addprefix $(top_srcdir)/,$(stub_headers)) + @$(MKDIR_P) $(top_builddir)/src/sd-boot/ + $(AM_V_CC)$(EFI_CC) $(efi_cppflags) $(efi_cflags) -c $< -o $@ + +$(stub_solib): $(stub_objects) + $(AM_V_CCLD)$(LD) $(efi_ldflags) $(stub_objects) \ + -o $@ -lefi -lgnuefi $(shell $(CC) -print-libgcc-file-name); \ + nm -D -u $@ | grep ' U ' && exit 1 || : + +$(stub): $(stub_solib) + $(AM_V_GEN) objcopy -j .text -j .sdata -j .data -j .dynamic \ + -j .dynsym -j .rel -j .rela -j .reloc \ + --target=efi-app-$(EFI_ARCH) $< $@ + +# ------------------------------------------------------------------------------ +CLEANFILES += test-efi-disk.img +EXTRA_DIST += test/test-efi-create-disk.sh + +test-efi-disk.img: $(sd_boot) $(stub) test/test-efi-create-disk.sh + $(AM_V_GEN)test/test-efi-create-disk.sh + +test-efi: test-efi-disk.img + $(QEMU) -machine accel=kvm -m 1024 -bios $(QEMU_BIOS) -snapshot test-efi-disk.img endif # ------------------------------------------------------------------------------ diff --git a/configure.ac b/configure.ac index 97a29d63fd..277addb8c3 100644 --- a/configure.ac +++ b/configure.ac @@ -38,19 +38,17 @@ AM_INIT_AUTOMAKE([foreign 1.11 -Wall -Wno-portability silent-rules tar-pax no-di AM_SILENT_RULES([yes]) AC_CANONICAL_HOST AC_DEFINE_UNQUOTED([CANONICAL_HOST], "$host", [Canonical host string.]) -AS_IF([test "x$host_cpu" = "xmips" || test "x$host_cpu" = "xmipsel" || - test "x$host_cpu" = "xmips64" || test "x$host_cpu" = "xmips64el"], - [AC_DEFINE(ARCH_MIPS, [], [Whether on mips arch])]) - LT_PREREQ(2.2) LT_INIT([disable-static]) AS_IF([test "x$enable_static" = "xyes"], [AC_MSG_ERROR([--enable-static is not supported by systemd])]) AS_IF([test "x$enable_largefile" = "xno"], [AC_MSG_ERROR([--disable-largefile is not supported by systemd])]) -# i18n stuff for the PolicyKit policy files +SET_ARCH(X86_64, x86_64*) +SET_ARCH(IA32, i*86*) +SET_ARCH(MIPS, mips*) -# Check whether intltool can be found, disable NLS otherwise +# i18n stuff for the PolicyKit policy files, heck whether intltool can be found, disable NLS otherwise AC_CHECK_PROG(intltool_found, [intltool-merge], [yes], [no]) AS_IF([test x"$intltool_found" != xyes], [AS_IF([test x"$enable_nls" = xyes], @@ -1144,6 +1142,63 @@ if test "x$enable_efi" != "xno"; then fi AM_CONDITIONAL(ENABLE_EFI, [test "x$have_efi" = "xyes"]) +# ------------------------------------------------------------------------------ +EFI_CC=gcc +AC_SUBST([EFI_CC]) + +EFI_ARCH=`echo $host | sed "s/\(-\).*$//"` + +AM_COND_IF(ARCH_IA32, [ + EFI_ARCH=ia32 + EFI_MACHINE_TYPE_NAME=ia32]) + +AM_COND_IF(ARCH_X86_64, [ + EFI_MACHINE_TYPE_NAME=x64]) + +AC_SUBST([EFI_ARCH]) +AC_SUBST([EFI_MACHINE_TYPE_NAME]) + +have_gnuefi=no +AC_ARG_ENABLE(gnuefi, AS_HELP_STRING([--enable-gnuefi], [Disable optional gnuefi support])) +AS_IF([test "x$enable_gnuefi" != "xno"], [ + AC_CHECK_HEADERS(efi/${EFI_ARCH}/efibind.h, + [AC_DEFINE(HAVE_GNUEFI, 1, [Define if gnuefi is available]) + have_gnuefi=yes], + [AS_IF([test "x$have_gnuefi" = xyes], [AC_MSG_ERROR([*** gnuefi support requested but headers not found])]) + ]) +]) +AM_CONDITIONAL(HAVE_GNUEFI, [test "$have_gnuefi" = "yes"]) + +if test "x$enable_gnuefi" != "xno"; then + efiroot=$(echo $(cd /usr/lib/$(gcc -print-multi-os-directory); pwd)) + + EFI_LIB_DIR="$efiroot" + AC_ARG_WITH(efi-libdir, + AS_HELP_STRING([--with-efi-libdir=PATH], [Path to efi lib directory]), + [EFI_LIB_DIR="$withval"], [EFI_LIB_DIR="$efiroot"] + ) + AC_SUBST([EFI_LIB_DIR]) + + AC_ARG_WITH(efi-ldsdir, + AS_HELP_STRING([--with-efi-ldsdir=PATH], [Path to efi lds directory]), + [EFI_LDS_DIR="$withval"], + [ + for EFI_LDS_DIR in "${efiroot}/gnuefi" "${efiroot}"; do + for lds in ${EFI_LDS_DIR}/elf_${EFI_ARCH}_efi.lds; do + test -f ${lds} && break 2 + done + done + ] + ) + AC_SUBST([EFI_LDS_DIR]) + + AC_ARG_WITH(efi-includedir, + AS_HELP_STRING([--with-efi-includedir=PATH], [Path to efi include directory]), + [EFI_INC_DIR="$withval"], [EFI_INC_DIR="/usr/include"] + ) + AC_SUBST([EFI_INC_DIR]) +fi + # ------------------------------------------------------------------------------ AC_ARG_WITH(unifont, AS_HELP_STRING([--with-unifont=PATH], @@ -1392,6 +1447,14 @@ AS_IF([test "x$0" != "x./configure"], [ AC_SUBST([INTLTOOL_UPDATE], [/bin/true]) ]) +# QEMU and OVMF UEFI firmware +AS_IF([test x"$cross_compiling" = "xyes"], [], [ + AC_PATH_PROG([QEMU], [qemu-system-x86_64]) + AC_CHECK_FILE([/usr/share/qemu/bios-ovmf.bin], [QEMU_BIOS=/usr/share/qemu/bios-ovmf.bin]) + AC_CHECK_FILE([/usr/share/qemu-ovmf/bios.bin], [QEMU_BIOS=/usr/share/qemu-ovmf/bios.bin]) + AC_SUBST([QEMU_BIOS]) +]) + AC_ARG_ENABLE(tests, [AC_HELP_STRING([--disable-tests], [disable tests])], enable_tests=$enableval, enable_tests=yes) @@ -1496,6 +1559,13 @@ AC_MSG_RESULT([ coredump: ${have_coredump} polkit: ${have_polkit} efi: ${have_efi} + gnuefi: ${have_gnuefi} + efi arch: ${EFI_ARCH} + EFI machine type: ${EFI_MACHINE_TYPE_NAME} + EFI CC ${EFI_CC} + EFI libdir: ${EFI_LIB_DIR} + EFI ldsdir: ${EFI_LDS_DIR} + EFI includedir: ${EFI_INC_DIR} kmod: ${have_kmod} xkbcommon: ${have_xkbcommon} blkid: ${have_blkid} diff --git a/m4/arch.m4 b/m4/arch.m4 new file mode 100644 index 0000000000..f17b4278eb --- /dev/null +++ b/m4/arch.m4 @@ -0,0 +1,13 @@ + +dnl SET_ARCH(ARCHNAME, PATTERN) +dnl +dnl Define ARCH_ condition if the pattern match with the current +dnl architecture +dnl +AC_DEFUN([SET_ARCH], [ + cpu_$1=false + case "$host" in + $2) cpu_$1=true ;; + esac + AM_CONDITIONAL(AS_TR_CPP(ARCH_$1), [test "x$cpu_$1" = xtrue]) +]) diff --git a/src/sd-boot/.gitignore b/src/sd-boot/.gitignore new file mode 100644 index 0000000000..55b0da4678 --- /dev/null +++ b/src/sd-boot/.gitignore @@ -0,0 +1,2 @@ +/sd_boot.so +/stub.so diff --git a/src/sd-boot/console.c b/src/sd-boot/console.c new file mode 100644 index 0000000000..6206c80317 --- /dev/null +++ b/src/sd-boot/console.c @@ -0,0 +1,141 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Copyright (C) 2012-2013 Kay Sievers + * Copyright (C) 2012 Harald Hoyer + */ + +#include +#include + +#include "util.h" +#include "console.h" + +#define EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID \ + { 0xdd9e7534, 0x7762, 0x4698, { 0x8c, 0x14, 0xf5, 0x85, 0x17, 0xa6, 0x25, 0xaa } } + +struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL; + +typedef EFI_STATUS (EFIAPI *EFI_INPUT_RESET_EX)( + struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This; + BOOLEAN ExtendedVerification; +); + +typedef UINT8 EFI_KEY_TOGGLE_STATE; + +typedef struct { + UINT32 KeyShiftState; + EFI_KEY_TOGGLE_STATE KeyToggleState; +} EFI_KEY_STATE; + +typedef struct { + EFI_INPUT_KEY Key; + EFI_KEY_STATE KeyState; +} EFI_KEY_DATA; + +typedef EFI_STATUS (EFIAPI *EFI_INPUT_READ_KEY_EX)( + struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This; + EFI_KEY_DATA *KeyData; +); + +typedef EFI_STATUS (EFIAPI *EFI_SET_STATE)( + struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This; + EFI_KEY_TOGGLE_STATE *KeyToggleState; +); + +typedef EFI_STATUS (EFIAPI *EFI_KEY_NOTIFY_FUNCTION)( + EFI_KEY_DATA *KeyData; +); + +typedef EFI_STATUS (EFIAPI *EFI_REGISTER_KEYSTROKE_NOTIFY)( + struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This; + EFI_KEY_DATA KeyData; + EFI_KEY_NOTIFY_FUNCTION KeyNotificationFunction; + VOID **NotifyHandle; +); + +typedef EFI_STATUS (EFIAPI *EFI_UNREGISTER_KEYSTROKE_NOTIFY)( + struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *This; + VOID *NotificationHandle; +); + +typedef struct _EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL { + EFI_INPUT_RESET_EX Reset; + EFI_INPUT_READ_KEY_EX ReadKeyStrokeEx; + EFI_EVENT WaitForKeyEx; + EFI_SET_STATE SetState; + EFI_REGISTER_KEYSTROKE_NOTIFY RegisterKeyNotify; + EFI_UNREGISTER_KEYSTROKE_NOTIFY UnregisterKeyNotify; +} EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL; + +EFI_STATUS console_key_read(UINT64 *key, BOOLEAN wait) { + EFI_GUID EfiSimpleTextInputExProtocolGuid = EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID; + static EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL *TextInputEx; + static BOOLEAN checked; + UINTN index; + EFI_INPUT_KEY k; + EFI_STATUS err; + + if (!checked) { + err = LibLocateProtocol(&EfiSimpleTextInputExProtocolGuid, (VOID **)&TextInputEx); + if (EFI_ERROR(err)) + TextInputEx = NULL; + + checked = TRUE; + } + + /* wait until key is pressed */ + if (wait) { + if (TextInputEx) + uefi_call_wrapper(BS->WaitForEvent, 3, 1, &TextInputEx->WaitForKeyEx, &index); + else + uefi_call_wrapper(BS->WaitForEvent, 3, 1, &ST->ConIn->WaitForKey, &index); + } + + if (TextInputEx) { + EFI_KEY_DATA keydata; + UINT64 keypress; + + err = uefi_call_wrapper(TextInputEx->ReadKeyStrokeEx, 2, TextInputEx, &keydata); + if (!EFI_ERROR(err)) { + UINT32 shift = 0; + + /* do not distinguish between left and right keys */ + if (keydata.KeyState.KeyShiftState & EFI_SHIFT_STATE_VALID) { + if (keydata.KeyState.KeyShiftState & (EFI_RIGHT_CONTROL_PRESSED|EFI_LEFT_CONTROL_PRESSED)) + shift |= EFI_CONTROL_PRESSED; + if (keydata.KeyState.KeyShiftState & (EFI_RIGHT_ALT_PRESSED|EFI_LEFT_ALT_PRESSED)) + shift |= EFI_ALT_PRESSED; + }; + + /* 32 bit modifier keys + 16 bit scan code + 16 bit unicode */ + keypress = KEYPRESS(shift, keydata.Key.ScanCode, keydata.Key.UnicodeChar); + if (keypress > 0) { + *key = keypress; + return 0; + } + } + } + + /* fallback for firmware which does not support SimpleTextInputExProtocol + * + * This is also called in case ReadKeyStrokeEx did not return a key, because + * some broken firmwares offer SimpleTextInputExProtocol, but never acually + * handle any key. */ + err = uefi_call_wrapper(ST->ConIn->ReadKeyStroke, 2, ST->ConIn, &k); + if (EFI_ERROR(err)) + return err; + + *key = KEYPRESS(0, k.ScanCode, k.UnicodeChar); + return 0; +} diff --git a/src/sd-boot/console.h b/src/sd-boot/console.h new file mode 100644 index 0000000000..5c7808a067 --- /dev/null +++ b/src/sd-boot/console.h @@ -0,0 +1,34 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Copyright (C) 2012-2013 Kay Sievers + * Copyright (C) 2012 Harald Hoyer + */ + +#ifndef __SDBOOT_CONSOLE_H +#define __SDBOOT_CONSOLE_H + +#define EFI_SHIFT_STATE_VALID 0x80000000 +#define EFI_RIGHT_CONTROL_PRESSED 0x00000004 +#define EFI_LEFT_CONTROL_PRESSED 0x00000008 +#define EFI_RIGHT_ALT_PRESSED 0x00000010 +#define EFI_LEFT_ALT_PRESSED 0x00000020 + +#define EFI_CONTROL_PRESSED (EFI_RIGHT_CONTROL_PRESSED|EFI_LEFT_CONTROL_PRESSED) +#define EFI_ALT_PRESSED (EFI_RIGHT_ALT_PRESSED|EFI_LEFT_ALT_PRESSED) +#define KEYPRESS(keys, scan, uni) ((((UINT64)keys) << 32) | ((scan) << 16) | (uni)) +#define KEYCHAR(k) ((k) & 0xffff) +#define CHAR_CTRL(c) ((c) - 'a' + 1) + +EFI_STATUS console_key_read(UINT64 *key, BOOLEAN wait); +#endif diff --git a/src/sd-boot/graphics.c b/src/sd-boot/graphics.c new file mode 100644 index 0000000000..11305b8d06 --- /dev/null +++ b/src/sd-boot/graphics.c @@ -0,0 +1,389 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Copyright (C) 2012-2013 Kay Sievers + * Copyright (C) 2012 Harald Hoyer + * Copyright (C) 2013 Intel Corporation + * Authored by Joonas Lahtinen + */ + +#include +#include + +#include "util.h" +#include "graphics.h" + +EFI_STATUS graphics_mode(BOOLEAN on) { + #define EFI_CONSOLE_CONTROL_PROTOCOL_GUID \ + { 0xf42f7782, 0x12e, 0x4c12, { 0x99, 0x56, 0x49, 0xf9, 0x43, 0x4, 0xf7, 0x21 } }; + + struct _EFI_CONSOLE_CONTROL_PROTOCOL; + + typedef enum { + EfiConsoleControlScreenText, + EfiConsoleControlScreenGraphics, + EfiConsoleControlScreenMaxValue, + } EFI_CONSOLE_CONTROL_SCREEN_MODE; + + typedef EFI_STATUS (EFIAPI *EFI_CONSOLE_CONTROL_PROTOCOL_GET_MODE)( + struct _EFI_CONSOLE_CONTROL_PROTOCOL *This, + EFI_CONSOLE_CONTROL_SCREEN_MODE *Mode, + BOOLEAN *UgaExists, + BOOLEAN *StdInLocked + ); + + typedef EFI_STATUS (EFIAPI *EFI_CONSOLE_CONTROL_PROTOCOL_SET_MODE)( + struct _EFI_CONSOLE_CONTROL_PROTOCOL *This, + EFI_CONSOLE_CONTROL_SCREEN_MODE Mode + ); + + typedef EFI_STATUS (EFIAPI *EFI_CONSOLE_CONTROL_PROTOCOL_LOCK_STD_IN)( + struct _EFI_CONSOLE_CONTROL_PROTOCOL *This, + CHAR16 *Password + ); + + typedef struct _EFI_CONSOLE_CONTROL_PROTOCOL { + EFI_CONSOLE_CONTROL_PROTOCOL_GET_MODE GetMode; + EFI_CONSOLE_CONTROL_PROTOCOL_SET_MODE SetMode; + EFI_CONSOLE_CONTROL_PROTOCOL_LOCK_STD_IN LockStdIn; + } EFI_CONSOLE_CONTROL_PROTOCOL; + + EFI_GUID ConsoleControlProtocolGuid = EFI_CONSOLE_CONTROL_PROTOCOL_GUID; + EFI_CONSOLE_CONTROL_PROTOCOL *ConsoleControl = NULL; + EFI_CONSOLE_CONTROL_SCREEN_MODE new; + EFI_CONSOLE_CONTROL_SCREEN_MODE current; + BOOLEAN uga_exists; + BOOLEAN stdin_locked; + EFI_STATUS err; + + err = LibLocateProtocol(&ConsoleControlProtocolGuid, (VOID **)&ConsoleControl); + if (EFI_ERROR(err)) { + /* console control protocol is nonstandard and might not exist. */ + return err == EFI_NOT_FOUND ? EFI_SUCCESS : err; + } + + /* check current mode */ + err = uefi_call_wrapper(ConsoleControl->GetMode, 4, ConsoleControl, ¤t, &uga_exists, &stdin_locked); + if (EFI_ERROR(err)) + return err; + + /* do not touch the mode */ + new = on ? EfiConsoleControlScreenGraphics : EfiConsoleControlScreenText; + if (new == current) + return EFI_SUCCESS; + + err = uefi_call_wrapper(ConsoleControl->SetMode, 2, ConsoleControl, new); + + /* some firmware enables the cursor when switching modes */ + uefi_call_wrapper(ST->ConOut->EnableCursor, 2, ST->ConOut, FALSE); + + return err; +} + +struct bmp_file { + CHAR8 signature[2]; + UINT32 size; + UINT16 reserved[2]; + UINT32 offset; +} __attribute__((packed)); + +/* we require at least BITMAPINFOHEADER, later versions are + accepted, but their features ignored */ +struct bmp_dib { + UINT32 size; + UINT32 x; + UINT32 y; + UINT16 planes; + UINT16 depth; + UINT32 compression; + UINT32 image_size; + INT32 x_pixel_meter; + INT32 y_pixel_meter; + UINT32 colors_used; + UINT32 colors_important; +} __attribute__((packed)); + +struct bmp_map { + UINT8 blue; + UINT8 green; + UINT8 red; + UINT8 reserved; +} __attribute__((packed)); + +EFI_STATUS bmp_parse_header(UINT8 *bmp, UINTN size, struct bmp_dib **ret_dib, + struct bmp_map **ret_map, UINT8 **pixmap) { + struct bmp_file *file; + struct bmp_dib *dib; + struct bmp_map *map; + UINTN row_size; + + if (size < sizeof(struct bmp_file) + sizeof(struct bmp_dib)) + return EFI_INVALID_PARAMETER; + + /* check file header */ + file = (struct bmp_file *)bmp; + if (file->signature[0] != 'B' || file->signature[1] != 'M') + return EFI_INVALID_PARAMETER; + if (file->size != size) + return EFI_INVALID_PARAMETER; + if (file->size < file->offset) + return EFI_INVALID_PARAMETER; + + /* check device-independent bitmap */ + dib = (struct bmp_dib *)(bmp + sizeof(struct bmp_file)); + if (dib->size < sizeof(struct bmp_dib)) + return EFI_UNSUPPORTED; + + switch (dib->depth) { + case 1: + case 4: + case 8: + case 24: + if (dib->compression != 0) + return EFI_UNSUPPORTED; + + break; + + case 16: + case 32: + if (dib->compression != 0 && dib->compression != 3) + return EFI_UNSUPPORTED; + + break; + + default: + return EFI_UNSUPPORTED; + } + + row_size = (((dib->depth * dib->x) + 31) / 32) * 4; + if (file->size - file->offset < dib->y * row_size) + return EFI_INVALID_PARAMETER; + if (row_size * dib->y > 64 * 1024 * 1024) + return EFI_INVALID_PARAMETER; + + /* check color table */ + map = (struct bmp_map *)(bmp + sizeof(struct bmp_file) + dib->size); + if (file->offset < sizeof(struct bmp_file) + dib->size) + return EFI_INVALID_PARAMETER; + + if (file->offset > sizeof(struct bmp_file) + dib->size) { + UINT32 map_count; + UINTN map_size; + + if (dib->colors_used) + map_count = dib->colors_used; + else { + switch (dib->depth) { + case 1: + case 4: + case 8: + map_count = 1 << dib->depth; + break; + + default: + map_count = 0; + break; + } + } + + map_size = file->offset - (sizeof(struct bmp_file) + dib->size); + if (map_size != sizeof(struct bmp_map) * map_count) + return EFI_INVALID_PARAMETER; + } + + *ret_map = map; + *ret_dib = dib; + *pixmap = bmp + file->offset; + + return EFI_SUCCESS; +} + +static VOID pixel_blend(UINT32 *dst, const UINT32 source) { + UINT32 alpha, src, src_rb, src_g, dst_rb, dst_g, rb, g; + + alpha = (source & 0xff); + + /* convert src from RGBA to XRGB */ + src = source >> 8; + + /* decompose into RB and G components */ + src_rb = (src & 0xff00ff); + src_g = (src & 0x00ff00); + + dst_rb = (*dst & 0xff00ff); + dst_g = (*dst & 0x00ff00); + + /* blend */ + rb = ((((src_rb - dst_rb) * alpha + 0x800080) >> 8) + dst_rb) & 0xff00ff; + g = ((((src_g - dst_g) * alpha + 0x008000) >> 8) + dst_g) & 0x00ff00; + + *dst = (rb | g); +} + +EFI_STATUS bmp_to_blt(EFI_GRAPHICS_OUTPUT_BLT_PIXEL *buf, + struct bmp_dib *dib, struct bmp_map *map, + UINT8 *pixmap) { + UINT8 *in; + UINTN y; + + /* transform and copy pixels */ + in = pixmap; + for (y = 0; y < dib->y; y++) { + EFI_GRAPHICS_OUTPUT_BLT_PIXEL *out; + UINTN row_size; + UINTN x; + + out = &buf[(dib->y - y - 1) * dib->x]; + for (x = 0; x < dib->x; x++, in++, out++) { + switch (dib->depth) { + case 1: { + UINTN i; + + for (i = 0; i < 8 && x < dib->x; i++) { + out->Red = map[((*in) >> (7 - i)) & 1].red; + out->Green = map[((*in) >> (7 - i)) & 1].green; + out->Blue = map[((*in) >> (7 - i)) & 1].blue; + out++; + x++; + } + out--; + x--; + break; + } + + case 4: { + UINTN i; + + i = (*in) >> 4; + out->Red = map[i].red; + out->Green = map[i].green; + out->Blue = map[i].blue; + if (x < (dib->x - 1)) { + out++; + x++; + i = (*in) & 0x0f; + out->Red = map[i].red; + out->Green = map[i].green; + out->Blue = map[i].blue; + } + break; + } + + case 8: + out->Red = map[*in].red; + out->Green = map[*in].green; + out->Blue = map[*in].blue; + break; + + case 16: { + UINT16 i = *(UINT16 *) in; + + out->Red = (i & 0x7c00) >> 7; + out->Green = (i & 0x3e0) >> 2; + out->Blue = (i & 0x1f) << 3; + in += 1; + break; + } + + case 24: + out->Red = in[2]; + out->Green = in[1]; + out->Blue = in[0]; + in += 2; + break; + + case 32: { + UINT32 i = *(UINT32 *) in; + + pixel_blend((UINT32 *)out, i); + + in += 3; + break; + } + } + } + + /* add row padding; new lines always start at 32 bit boundary */ + row_size = in - pixmap; + in += ((row_size + 3) & ~3) - row_size; + } + + return EFI_SUCCESS; +} + +EFI_STATUS graphics_splash(EFI_FILE *root_dir, CHAR16 *path, + const EFI_GRAPHICS_OUTPUT_BLT_PIXEL *background) { + EFI_GUID GraphicsOutputProtocolGuid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID; + EFI_GRAPHICS_OUTPUT_PROTOCOL *GraphicsOutput = NULL; + UINT8 *content; + INTN len; + struct bmp_dib *dib; + struct bmp_map *map; + UINT8 *pixmap; + UINT64 blt_size; + VOID *blt = NULL; + UINTN x_pos = 0; + UINTN y_pos = 0; + EFI_STATUS err; + + err = LibLocateProtocol(&GraphicsOutputProtocolGuid, (VOID **)&GraphicsOutput); + if (EFI_ERROR(err)) + return err; + + len = file_read(root_dir, path, 0, 0, &content); + if (len < 0) + return EFI_LOAD_ERROR; + + err = bmp_parse_header(content, len, &dib, &map, &pixmap); + if (EFI_ERROR(err)) + goto err; + + if(dib->x < GraphicsOutput->Mode->Info->HorizontalResolution) + x_pos = (GraphicsOutput->Mode->Info->HorizontalResolution - dib->x) / 2; + if(dib->y < GraphicsOutput->Mode->Info->VerticalResolution) + y_pos = (GraphicsOutput->Mode->Info->VerticalResolution - dib->y) / 2; + + uefi_call_wrapper(GraphicsOutput->Blt, 10, GraphicsOutput, + (EFI_GRAPHICS_OUTPUT_BLT_PIXEL *)background, + EfiBltVideoFill, 0, 0, 0, 0, + GraphicsOutput->Mode->Info->HorizontalResolution, + GraphicsOutput->Mode->Info->VerticalResolution, 0); + + /* EFI buffer */ + blt_size = dib->x * dib->y * sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL); + blt = AllocatePool(blt_size); + if (!blt) + return EFI_OUT_OF_RESOURCES; + + err = uefi_call_wrapper(GraphicsOutput->Blt, 10, GraphicsOutput, + blt, EfiBltVideoToBltBuffer, x_pos, y_pos, 0, 0, + dib->x, dib->y, 0); + if (EFI_ERROR(err)) + goto err; + + err = bmp_to_blt(blt, dib, map, pixmap); + if (EFI_ERROR(err)) + goto err; + + err = graphics_mode(TRUE); + if (EFI_ERROR(err)) + goto err; + + err = uefi_call_wrapper(GraphicsOutput->Blt, 10, GraphicsOutput, + blt, EfiBltBufferToVideo, 0, 0, x_pos, y_pos, + dib->x, dib->y, 0); +err: + FreePool(blt); + FreePool(content); + return err; +} diff --git a/src/sd-boot/graphics.h b/src/sd-boot/graphics.h new file mode 100644 index 0000000000..8665afde97 --- /dev/null +++ b/src/sd-boot/graphics.h @@ -0,0 +1,26 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Copyright (C) 2012-2013 Kay Sievers + * Copyright (C) 2012 Harald Hoyer + * Copyright (C) 2013 Intel Corporation + * Authored by Joonas Lahtinen + */ + +#ifndef __SDBOOT_GRAPHICS_H +#define __SDBOOT_GRAPHICS_H + +EFI_STATUS graphics_mode(BOOLEAN on); +EFI_STATUS graphics_splash(EFI_FILE *root_dir, CHAR16 *path, + const EFI_GRAPHICS_OUTPUT_BLT_PIXEL *background); +#endif diff --git a/src/sd-boot/linux.c b/src/sd-boot/linux.c new file mode 100644 index 0000000000..809c69310e --- /dev/null +++ b/src/sd-boot/linux.c @@ -0,0 +1,130 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Copyright (C) 2015 Kay Sievers + */ + +#include +#include + +#include "util.h" +#include "linux.h" + +#define SETUP_MAGIC 0x53726448 /* "HdrS" */ +struct SetupHeader { + UINT8 boot_sector[0x01f1]; + UINT8 setup_secs; + UINT16 root_flags; + UINT32 sys_size; + UINT16 ram_size; + UINT16 video_mode; + UINT16 root_dev; + UINT16 signature; + UINT16 jump; + UINT32 header; + UINT16 version; + UINT16 su_switch; + UINT16 setup_seg; + UINT16 start_sys; + UINT16 kernel_ver; + UINT8 loader_id; + UINT8 load_flags; + UINT16 movesize; + UINT32 code32_start; + UINT32 ramdisk_start; + UINT32 ramdisk_len; + UINT32 bootsect_kludge; + UINT16 heap_end; + UINT8 ext_loader_ver; + UINT8 ext_loader_type; + UINT32 cmd_line_ptr; + UINT32 ramdisk_max; + UINT32 kernel_alignment; + UINT8 relocatable_kernel; + UINT8 min_alignment; + UINT16 xloadflags; + UINT32 cmdline_size; + UINT32 hardware_subarch; + UINT64 hardware_subarch_data; + UINT32 payload_offset; + UINT32 payload_length; + UINT64 setup_data; + UINT64 pref_address; + UINT32 init_size; + UINT32 handover_offset; +} __attribute__((packed)); + +#ifdef __x86_64__ +typedef VOID(*handover_f)(VOID *image, EFI_SYSTEM_TABLE *table, struct SetupHeader *setup); +static inline VOID linux_efi_handover(EFI_HANDLE image, struct SetupHeader *setup) { + handover_f handover; + + asm volatile ("cli"); + handover = (handover_f)((UINTN)setup->code32_start + 512 + setup->handover_offset); + handover(image, ST, setup); +} +#else +typedef VOID(*handover_f)(VOID *image, EFI_SYSTEM_TABLE *table, struct SetupHeader *setup) __attribute__((regparm(0))); +static inline VOID linux_efi_handover(EFI_HANDLE image, struct SetupHeader *setup) { + handover_f handover; + + handover = (handover_f)((UINTN)setup->code32_start + setup->handover_offset); + handover(image, ST, setup); +} +#endif + +EFI_STATUS linux_exec(EFI_HANDLE *image, + CHAR8 *cmdline, UINTN cmdline_len, + UINTN linux_addr, + UINTN initrd_addr, UINTN initrd_size) { + struct SetupHeader *image_setup; + struct SetupHeader *boot_setup; + EFI_PHYSICAL_ADDRESS addr; + EFI_STATUS err; + + image_setup = (struct SetupHeader *)(linux_addr); + if (image_setup->signature != 0xAA55 || image_setup->header != SETUP_MAGIC) + return EFI_LOAD_ERROR; + + if (image_setup->version < 0x20b || !image_setup->relocatable_kernel) + return EFI_LOAD_ERROR; + + addr = 0x3fffffff; + err = uefi_call_wrapper(BS->AllocatePages, 4, AllocateMaxAddress, EfiLoaderData, + EFI_SIZE_TO_PAGES(0x4000), &addr); + if (EFI_ERROR(err)) + return err; + boot_setup = (struct SetupHeader *)(UINTN)addr; + ZeroMem(boot_setup, 0x4000); + CopyMem(boot_setup, image_setup, sizeof(struct SetupHeader)); + boot_setup->loader_id = 0xff; + + boot_setup->code32_start = (UINT32)linux_addr + (image_setup->setup_secs+1) * 512; + + if (cmdline) { + addr = 0xA0000; + err = uefi_call_wrapper(BS->AllocatePages, 4, AllocateMaxAddress, EfiLoaderData, + EFI_SIZE_TO_PAGES(cmdline_len + 1), &addr); + if (EFI_ERROR(err)) + return err; + CopyMem((VOID *)(UINTN)addr, cmdline, cmdline_len); + ((CHAR8 *)addr)[cmdline_len] = 0; + boot_setup->cmd_line_ptr = (UINT32)addr; + } + + boot_setup->ramdisk_start = (UINT32)initrd_addr; + boot_setup->ramdisk_len = (UINT32)initrd_size; + + linux_efi_handover(image, boot_setup); + return EFI_LOAD_ERROR; +} diff --git a/src/sd-boot/linux.h b/src/sd-boot/linux.h new file mode 100644 index 0000000000..aff69a9778 --- /dev/null +++ b/src/sd-boot/linux.h @@ -0,0 +1,24 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Copyright (C) 2015 Kay Sievers + */ + +#ifndef __SDBOOT_kernel_H +#define __SDBOOT_kernel_H + +EFI_STATUS linux_exec(EFI_HANDLE *image, + CHAR8 *cmdline, UINTN cmdline_size, + UINTN linux_addr, + UINTN initrd_addr, UINTN initrd_size); +#endif diff --git a/src/sd-boot/pefile.c b/src/sd-boot/pefile.c new file mode 100644 index 0000000000..e6fedbc929 --- /dev/null +++ b/src/sd-boot/pefile.c @@ -0,0 +1,172 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Copyright (C) 2015 Kay Sievers + */ + +#include +#include + +#include "util.h" +#include "pefile.h" + +struct DosFileHeader { + UINT8 Magic[2]; + UINT16 LastSize; + UINT16 nBlocks; + UINT16 nReloc; + UINT16 HdrSize; + UINT16 MinAlloc; + UINT16 MaxAlloc; + UINT16 ss; + UINT16 sp; + UINT16 Checksum; + UINT16 ip; + UINT16 cs; + UINT16 RelocPos; + UINT16 nOverlay; + UINT16 reserved[4]; + UINT16 OEMId; + UINT16 OEMInfo; + UINT16 reserved2[10]; + UINT32 ExeHeader; +} __attribute__((packed)); + +#define PE_HEADER_MACHINE_I386 0x014c +#define PE_HEADER_MACHINE_X64 0x8664 +struct PeFileHeader { + UINT16 Machine; + UINT16 NumberOfSections; + UINT32 TimeDateStamp; + UINT32 PointerToSymbolTable; + UINT32 NumberOfSymbols; + UINT16 SizeOfOptionalHeader; + UINT16 Characteristics; +} __attribute__((packed)); + +struct PeSectionHeader { + UINT8 Name[8]; + UINT32 VirtualSize; + UINT32 VirtualAddress; + UINT32 SizeOfRawData; + UINT32 PointerToRawData; + UINT32 PointerToRelocations; + UINT32 PointerToLinenumbers; + UINT16 NumberOfRelocations; + UINT16 NumberOfLinenumbers; + UINT32 Characteristics; +} __attribute__((packed)); + + +EFI_STATUS pefile_locate_sections(EFI_FILE *dir, CHAR16 *path, CHAR8 **sections, UINTN *addrs, UINTN *offsets, UINTN *sizes) { + EFI_FILE_HANDLE handle; + struct DosFileHeader dos; + uint8_t magic[4]; + struct PeFileHeader pe; + UINTN len; + UINTN i; + EFI_STATUS err; + + err = uefi_call_wrapper(dir->Open, 5, dir, &handle, path, EFI_FILE_MODE_READ, 0ULL); + if (EFI_ERROR(err)) + return err; + + /* MS-DOS stub */ + len = sizeof(dos); + err = uefi_call_wrapper(handle->Read, 3, handle, &len, &dos); + if (EFI_ERROR(err)) + goto out; + if (len != sizeof(dos)) { + err = EFI_LOAD_ERROR; + goto out; + } + + if (CompareMem(dos.Magic, "MZ", 2) != 0) { + err = EFI_LOAD_ERROR; + goto out; + } + + err = uefi_call_wrapper(handle->SetPosition, 2, handle, dos.ExeHeader); + if (EFI_ERROR(err)) + goto out; + + /* PE header */ + len = sizeof(magic); + err = uefi_call_wrapper(handle->Read, 3, handle, &len, &magic); + if (EFI_ERROR(err)) + goto out; + if (len != sizeof(magic)) { + err = EFI_LOAD_ERROR; + goto out; + } + + if (CompareMem(magic, "PE\0\0", 2) != 0) { + err = EFI_LOAD_ERROR; + goto out; + } + + len = sizeof(pe); + err = uefi_call_wrapper(handle->Read, 3, handle, &len, &pe); + if (EFI_ERROR(err)) + goto out; + if (len != sizeof(pe)) { + err = EFI_LOAD_ERROR; + goto out; + } + + /* PE32+ Subsystem type */ + if (pe.Machine != PE_HEADER_MACHINE_X64 && + pe.Machine != PE_HEADER_MACHINE_I386) { + err = EFI_LOAD_ERROR; + goto out; + } + + if (pe.NumberOfSections > 96) { + err = EFI_LOAD_ERROR; + goto out; + } + + /* the sections start directly after the headers */ + err = uefi_call_wrapper(handle->SetPosition, 2, handle, dos.ExeHeader + sizeof(magic) + sizeof(pe) + pe.SizeOfOptionalHeader); + if (EFI_ERROR(err)) + goto out; + + for (i = 0; i < pe.NumberOfSections; i++) { + struct PeSectionHeader sect; + UINTN j; + + len = sizeof(sect); + err = uefi_call_wrapper(handle->Read, 3, handle, &len, §); + if (EFI_ERROR(err)) + goto out; + if (len != sizeof(sect)) { + err = EFI_LOAD_ERROR; + goto out; + } + for (j = 0; sections[j]; j++) { + if (CompareMem(sect.Name, sections[j], strlena(sections[j])) != 0) + continue; + + if (addrs) + addrs[j] = (UINTN)sect.VirtualAddress; + if (offsets) + offsets[j] = (UINTN)sect.PointerToRawData; + if (sizes) + sizes[j] = (UINTN)sect.VirtualSize; + } + } + +out: + uefi_call_wrapper(handle->Close, 1, handle); + return err; +} diff --git a/src/sd-boot/pefile.h b/src/sd-boot/pefile.h new file mode 100644 index 0000000000..ca2f9a2508 --- /dev/null +++ b/src/sd-boot/pefile.h @@ -0,0 +1,22 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Copyright (C) 2015 Kay Sievers + */ + +#ifndef __SDBOOT_PEFILE_H +#define __SDBOOT_PEFILE_H + +EFI_STATUS pefile_locate_sections(EFI_FILE *dir, CHAR16 *path, + CHAR8 **sections, UINTN *addrs, UINTN *offsets, UINTN *sizes); +#endif diff --git a/src/sd-boot/sd-boot.c b/src/sd-boot/sd-boot.c new file mode 100644 index 0000000000..94039ead31 --- /dev/null +++ b/src/sd-boot/sd-boot.c @@ -0,0 +1,2023 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Copyright (C) 2012-2015 Kay Sievers + * Copyright (C) 2012-2015 Harald Hoyer + */ + +#include +#include + +#include "util.h" +#include "console.h" +#include "graphics.h" +#include "pefile.h" +#include "linux.h" + +#ifndef EFI_OS_INDICATIONS_BOOT_TO_FW_UI +#define EFI_OS_INDICATIONS_BOOT_TO_FW_UI 0x0000000000000001ULL +#endif + +/* magic string to find in the binary image */ +static const char __attribute__((used)) magic[] = "#### LoaderInfo: sd-boot " VERSION " ####"; + +static const EFI_GUID global_guid = EFI_GLOBAL_VARIABLE; + +enum loader_type { + LOADER_UNDEFINED, + LOADER_EFI, + LOADER_LINUX +}; + +typedef struct { + CHAR16 *file; + CHAR16 *title_show; + CHAR16 *title; + CHAR16 *version; + CHAR16 *machine_id; + EFI_HANDLE *device; + enum loader_type type; + CHAR16 *loader; + CHAR16 *options; + CHAR16 *splash; + CHAR16 key; + EFI_STATUS (*call)(VOID); + BOOLEAN no_autoselect; + BOOLEAN non_unique; +} ConfigEntry; + +typedef struct { + ConfigEntry **entries; + UINTN entry_count; + INTN idx_default; + INTN idx_default_efivar; + UINTN timeout_sec; + UINTN timeout_sec_config; + INTN timeout_sec_efivar; + CHAR16 *entry_default_pattern; + CHAR16 *splash; + EFI_GRAPHICS_OUTPUT_BLT_PIXEL *background; + CHAR16 *entry_oneshot; + CHAR16 *options_edit; + CHAR16 *entries_auto; +} Config; + +static VOID cursor_left(UINTN *cursor, UINTN *first) +{ + if ((*cursor) > 0) + (*cursor)--; + else if ((*first) > 0) + (*first)--; +} + +static VOID cursor_right(UINTN *cursor, UINTN *first, UINTN x_max, UINTN len) +{ + if ((*cursor)+1 < x_max) + (*cursor)++; + else if ((*first) + (*cursor) < len) + (*first)++; +} + +static BOOLEAN line_edit(CHAR16 *line_in, CHAR16 **line_out, UINTN x_max, UINTN y_pos) { + CHAR16 *line; + UINTN size; + UINTN len; + UINTN first; + CHAR16 *print; + UINTN cursor; + UINTN clear; + BOOLEAN exit; + BOOLEAN enter; + + if (!line_in) + line_in = L""; + size = StrLen(line_in) + 1024; + line = AllocatePool(size * sizeof(CHAR16)); + StrCpy(line, line_in); + len = StrLen(line); + print = AllocatePool((x_max+1) * sizeof(CHAR16)); + + uefi_call_wrapper(ST->ConOut->EnableCursor, 2, ST->ConOut, TRUE); + + first = 0; + cursor = 0; + clear = 0; + enter = FALSE; + exit = FALSE; + while (!exit) { + EFI_STATUS err; + UINT64 key; + UINTN i; + + i = len - first; + if (i >= x_max-1) + i = x_max-1; + CopyMem(print, line + first, i * sizeof(CHAR16)); + while (clear > 0 && i < x_max-1) { + clear--; + print[i++] = ' '; + } + print[i] = '\0'; + + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, 0, y_pos); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, print); + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, cursor, y_pos); + + err = console_key_read(&key, TRUE); + if (EFI_ERROR(err)) + continue; + + switch (key) { + case KEYPRESS(0, SCAN_ESC, 0): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'c'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'g'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('c')): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('g')): + exit = TRUE; + break; + + case KEYPRESS(0, SCAN_HOME, 0): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'a'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('a')): + /* beginning-of-line */ + cursor = 0; + first = 0; + continue; + + case KEYPRESS(0, SCAN_END, 0): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'e'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('e')): + /* end-of-line */ + cursor = len - first; + if (cursor+1 >= x_max) { + cursor = x_max-1; + first = len - (x_max-1); + } + continue; + + case KEYPRESS(0, SCAN_DOWN, 0): + case KEYPRESS(EFI_ALT_PRESSED, 0, 'f'): + case KEYPRESS(EFI_CONTROL_PRESSED, SCAN_RIGHT, 0): + /* forward-word */ + while (line[first + cursor] && line[first + cursor] == ' ') + cursor_right(&cursor, &first, x_max, len); + while (line[first + cursor] && line[first + cursor] != ' ') + cursor_right(&cursor, &first, x_max, len); + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, cursor, y_pos); + continue; + + case KEYPRESS(0, SCAN_UP, 0): + case KEYPRESS(EFI_ALT_PRESSED, 0, 'b'): + case KEYPRESS(EFI_CONTROL_PRESSED, SCAN_LEFT, 0): + /* backward-word */ + if ((first + cursor) > 0 && line[first + cursor-1] == ' ') { + cursor_left(&cursor, &first); + while ((first + cursor) > 0 && line[first + cursor] == ' ') + cursor_left(&cursor, &first); + } + while ((first + cursor) > 0 && line[first + cursor-1] != ' ') + cursor_left(&cursor, &first); + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, cursor, y_pos); + continue; + + case KEYPRESS(0, SCAN_RIGHT, 0): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'f'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('f')): + /* forward-char */ + if (first + cursor == len) + continue; + cursor_right(&cursor, &first, x_max, len); + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, cursor, y_pos); + continue; + + case KEYPRESS(0, SCAN_LEFT, 0): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'b'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('b')): + /* backward-char */ + cursor_left(&cursor, &first); + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, cursor, y_pos); + continue; + + case KEYPRESS(EFI_ALT_PRESSED, 0, 'd'): + /* kill-word */ + clear = 0; + for (i = first + cursor; i < len && line[i] == ' '; i++) + clear++; + for (; i < len && line[i] != ' '; i++) + clear++; + + for (i = first + cursor; i + clear < len; i++) + line[i] = line[i + clear]; + len -= clear; + line[len] = '\0'; + continue; + + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'w'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('w')): + case KEYPRESS(EFI_ALT_PRESSED, 0, CHAR_BACKSPACE): + /* backward-kill-word */ + clear = 0; + if ((first + cursor) > 0 && line[first + cursor-1] == ' ') { + cursor_left(&cursor, &first); + clear++; + while ((first + cursor) > 0 && line[first + cursor] == ' ') { + cursor_left(&cursor, &first); + clear++; + } + } + while ((first + cursor) > 0 && line[first + cursor-1] != ' ') { + cursor_left(&cursor, &first); + clear++; + } + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, cursor, y_pos); + + for (i = first + cursor; i + clear < len; i++) + line[i] = line[i + clear]; + len -= clear; + line[len] = '\0'; + continue; + + case KEYPRESS(0, SCAN_DELETE, 0): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'd'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('d')): + if (len == 0) + continue; + if (first + cursor == len) + continue; + for (i = first + cursor; i < len; i++) + line[i] = line[i+1]; + clear = 1; + len--; + continue; + + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'k'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('k')): + /* kill-line */ + line[first + cursor] = '\0'; + clear = len - (first + cursor); + len = first + cursor; + continue; + + case KEYPRESS(0, 0, CHAR_LINEFEED): + case KEYPRESS(0, 0, CHAR_CARRIAGE_RETURN): + if (StrCmp(line, line_in) != 0) { + *line_out = line; + line = NULL; + } + enter = TRUE; + exit = TRUE; + break; + + case KEYPRESS(0, 0, CHAR_BACKSPACE): + if (len == 0) + continue; + if (first == 0 && cursor == 0) + continue; + for (i = first + cursor-1; i < len; i++) + line[i] = line[i+1]; + clear = 1; + len--; + if (cursor > 0) + cursor--; + if (cursor > 0 || first == 0) + continue; + /* show full line if it fits */ + if (len < x_max) { + cursor = first; + first = 0; + continue; + } + /* jump left to see what we delete */ + if (first > 10) { + first -= 10; + cursor = 10; + } else { + cursor = first; + first = 0; + } + continue; + + case KEYPRESS(0, 0, ' ') ... KEYPRESS(0, 0, '~'): + case KEYPRESS(0, 0, 0x80) ... KEYPRESS(0, 0, 0xffff): + if (len+1 == size) + continue; + for (i = len; i > first + cursor; i--) + line[i] = line[i-1]; + line[first + cursor] = KEYCHAR(key); + len++; + line[len] = '\0'; + if (cursor+1 < x_max) + cursor++; + else if (first + cursor < len) + first++; + continue; + } + } + + uefi_call_wrapper(ST->ConOut->EnableCursor, 2, ST->ConOut, FALSE); + FreePool(print); + FreePool(line); + return enter; +} + +static UINTN entry_lookup_key(Config *config, UINTN start, CHAR16 key) { + UINTN i; + + if (key == 0) + return -1; + + /* select entry by number key */ + if (key >= '1' && key <= '9') { + i = key - '0'; + if (i > config->entry_count) + i = config->entry_count; + return i-1; + } + + /* find matching key in config entries */ + for (i = start; i < config->entry_count; i++) + if (config->entries[i]->key == key) + return i; + + for (i = 0; i < start; i++) + if (config->entries[i]->key == key) + return i; + + return -1; +} + +static VOID print_status(Config *config, EFI_FILE *root_dir, CHAR16 *loaded_image_path) { + UINT64 key; + UINTN i; + CHAR16 *s; + CHAR8 *b; + UINTN x; + UINTN y; + UINTN size; + EFI_STATUS err; + UINTN color = 0; + const EFI_GRAPHICS_OUTPUT_BLT_PIXEL *pixel = config->background; + + uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, EFI_LIGHTGRAY|EFI_BACKGROUND_BLACK); + uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut); + + /* show splash and wait for key */ + for (;;) { + static const EFI_GRAPHICS_OUTPUT_BLT_PIXEL colors[] = { + { .Red = 0xff, .Green = 0xff, .Blue = 0xff }, + { .Red = 0xc0, .Green = 0xc0, .Blue = 0xc0 }, + { .Red = 0xff, .Green = 0, .Blue = 0 }, + { .Red = 0, .Green = 0xff, .Blue = 0 }, + { .Red = 0, .Green = 0, .Blue = 0xff }, + { .Red = 0, .Green = 0, .Blue = 0 }, + }; + + err = EFI_NOT_FOUND; + if (config->splash) + err = graphics_splash(root_dir, config->splash, pixel); + if (EFI_ERROR(err)) + err = graphics_splash(root_dir, L"\\EFI\\systemd\\splash.bmp", pixel); + if (EFI_ERROR(err)) + break; + + /* 'b' rotates through background colors */ + console_key_read(&key, TRUE); + if (key == KEYPRESS(0, 0, 'b')) { + pixel = &colors[color++]; + if (color == ELEMENTSOF(colors)) + color = 0; + + continue; + } + + graphics_mode(FALSE); + uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut); + break; + } + + Print(L"sd-boot version: " VERSION "\n"); + Print(L"architecture: " EFI_MACHINE_TYPE_NAME "\n"); + Print(L"loaded image: %s\n", loaded_image_path); + Print(L"UEFI specification: %d.%02d\n", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff); + Print(L"firmware vendor: %s\n", ST->FirmwareVendor); + Print(L"firmware version: %d.%02d\n", ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff); + + if (uefi_call_wrapper(ST->ConOut->QueryMode, 4, ST->ConOut, ST->ConOut->Mode->Mode, &x, &y) == EFI_SUCCESS) + Print(L"console size: %d x %d\n", x, y); + + if (efivar_get_raw(&global_guid, L"SecureBoot", &b, &size) == EFI_SUCCESS) { + Print(L"SecureBoot: %s\n", *b > 0 ? L"enabled" : L"disabled"); + FreePool(b); + } + + if (efivar_get_raw(&global_guid, L"SetupMode", &b, &size) == EFI_SUCCESS) { + Print(L"SetupMode: %s\n", *b > 0 ? L"setup" : L"user"); + FreePool(b); + } + + if (efivar_get_raw(&global_guid, L"OsIndicationsSupported", &b, &size) == EFI_SUCCESS) { + Print(L"OsIndicationsSupported: %d\n", (UINT64)*b); + FreePool(b); + } + Print(L"\n"); + + Print(L"timeout: %d\n", config->timeout_sec); + if (config->timeout_sec_efivar >= 0) + Print(L"timeout (EFI var): %d\n", config->timeout_sec_efivar); + Print(L"timeout (config): %d\n", config->timeout_sec_config); + if (config->entry_default_pattern) + Print(L"default pattern: '%s'\n", config->entry_default_pattern); + if (config->splash) + Print(L"splash '%s'\n", config->splash); + if (config->background) + Print(L"background '#%02x%02x%02x'\n", + config->background->Red, + config->background->Green, + config->background->Blue); + Print(L"\n"); + + Print(L"config entry count: %d\n", config->entry_count); + Print(L"entry selected idx: %d\n", config->idx_default); + if (config->idx_default_efivar >= 0) + Print(L"entry EFI var idx: %d\n", config->idx_default_efivar); + Print(L"\n"); + + if (efivar_get_int(L"LoaderConfigTimeout", &i) == EFI_SUCCESS) + Print(L"LoaderConfigTimeout: %d\n", i); + if (config->entry_oneshot) + Print(L"LoaderEntryOneShot: %s\n", config->entry_oneshot); + if (efivar_get(L"LoaderDeviceIdentifier", &s) == EFI_SUCCESS) { + Print(L"LoaderDeviceIdentifier: %s\n", s); + FreePool(s); + } + if (efivar_get(L"LoaderDevicePartUUID", &s) == EFI_SUCCESS) { + Print(L"LoaderDevicePartUUID: %s\n", s); + FreePool(s); + } + if (efivar_get(L"LoaderEntryDefault", &s) == EFI_SUCCESS) { + Print(L"LoaderEntryDefault: %s\n", s); + FreePool(s); + } + + Print(L"\n--- press key ---\n\n"); + console_key_read(&key, TRUE); + + for (i = 0; i < config->entry_count; i++) { + ConfigEntry *entry; + + if (key == KEYPRESS(0, SCAN_ESC, 0) || key == KEYPRESS(0, 0, 'q')) + break; + + entry = config->entries[i]; + + if (entry->splash) { + err = graphics_splash(root_dir, entry->splash, config->background); + if (!EFI_ERROR(err)) { + console_key_read(&key, TRUE); + graphics_mode(FALSE); + } + } + + Print(L"config entry: %d/%d\n", i+1, config->entry_count); + if (entry->file) + Print(L"file '%s'\n", entry->file); + Print(L"title show '%s'\n", entry->title_show); + if (entry->title) + Print(L"title '%s'\n", entry->title); + if (entry->version) + Print(L"version '%s'\n", entry->version); + if (entry->machine_id) + Print(L"machine-id '%s'\n", entry->machine_id); + if (entry->device) { + EFI_DEVICE_PATH *device_path; + CHAR16 *str; + + device_path = DevicePathFromHandle(entry->device); + if (device_path) { + str = DevicePathToStr(device_path); + Print(L"device handle '%s'\n", str); + FreePool(str); + } + } + if (entry->loader) + Print(L"loader '%s'\n", entry->loader); + if (entry->options) + Print(L"options '%s'\n", entry->options); + if (entry->splash) + Print(L"splash '%s'\n", entry->splash); + Print(L"auto-select %s\n", entry->no_autoselect ? L"no" : L"yes"); + if (entry->call) + Print(L"internal call yes\n"); + + Print(L"\n--- press key ---\n\n"); + console_key_read(&key, TRUE); + } + + uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut); +} + +static BOOLEAN menu_run(Config *config, ConfigEntry **chosen_entry, EFI_FILE *root_dir, CHAR16 *loaded_image_path) { + EFI_STATUS err; + UINTN visible_max; + UINTN idx_highlight; + UINTN idx_highlight_prev; + UINTN idx_first; + UINTN idx_last; + BOOLEAN refresh; + BOOLEAN highlight; + UINTN i; + UINTN line_width; + CHAR16 **lines; + UINTN x_start; + UINTN y_start; + UINTN x_max; + UINTN y_max; + CHAR16 *status; + CHAR16 *clearline; + INTN timeout_remain; + INT16 idx; + BOOLEAN exit = FALSE; + BOOLEAN run = TRUE; + BOOLEAN wait = FALSE; + + graphics_mode(FALSE); + uefi_call_wrapper(ST->ConIn->Reset, 2, ST->ConIn, FALSE); + uefi_call_wrapper(ST->ConOut->EnableCursor, 2, ST->ConOut, FALSE); + uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, EFI_LIGHTGRAY|EFI_BACKGROUND_BLACK); + + /* draw a single character to make ClearScreen work on some firmware */ + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, L" "); + uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut); + + err = uefi_call_wrapper(ST->ConOut->QueryMode, 4, ST->ConOut, ST->ConOut->Mode->Mode, &x_max, &y_max); + if (EFI_ERROR(err)) { + x_max = 80; + y_max = 25; + } + + /* we check 10 times per second for a keystroke */ + if (config->timeout_sec > 0) + timeout_remain = config->timeout_sec * 10; + else + timeout_remain = -1; + + idx_highlight = config->idx_default; + idx_highlight_prev = 0; + + visible_max = y_max - 2; + + if ((UINTN)config->idx_default >= visible_max) + idx_first = config->idx_default-1; + else + idx_first = 0; + + idx_last = idx_first + visible_max-1; + + refresh = TRUE; + highlight = FALSE; + + /* length of the longest entry */ + line_width = 5; + for (i = 0; i < config->entry_count; i++) { + UINTN entry_len; + + entry_len = StrLen(config->entries[i]->title_show); + if (line_width < entry_len) + line_width = entry_len; + } + if (line_width > x_max-6) + line_width = x_max-6; + + /* offsets to center the entries on the screen */ + x_start = (x_max - (line_width)) / 2; + if (config->entry_count < visible_max) + y_start = ((visible_max - config->entry_count) / 2) + 1; + else + y_start = 0; + + /* menu entries title lines */ + lines = AllocatePool(sizeof(CHAR16 *) * config->entry_count); + for (i = 0; i < config->entry_count; i++) { + UINTN j, k; + + lines[i] = AllocatePool(((x_max+1) * sizeof(CHAR16))); + for (j = 0; j < x_start; j++) + lines[i][j] = ' '; + + for (k = 0; config->entries[i]->title_show[k] != '\0' && j < x_max; j++, k++) + lines[i][j] = config->entries[i]->title_show[k]; + + for (; j < x_max; j++) + lines[i][j] = ' '; + lines[i][x_max] = '\0'; + } + + status = NULL; + clearline = AllocatePool((x_max+1) * sizeof(CHAR16)); + for (i = 0; i < x_max; i++) + clearline[i] = ' '; + clearline[i] = 0; + + while (!exit) { + UINT64 key; + + if (refresh) { + for (i = 0; i < config->entry_count; i++) { + if (i < idx_first || i > idx_last) + continue; + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, 0, y_start + i - idx_first); + if (i == idx_highlight) + uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, + EFI_BLACK|EFI_BACKGROUND_LIGHTGRAY); + else + uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, + EFI_LIGHTGRAY|EFI_BACKGROUND_BLACK); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, lines[i]); + if ((INTN)i == config->idx_default_efivar) { + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, x_start-3, y_start + i - idx_first); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, L"=>"); + } + } + refresh = FALSE; + } else if (highlight) { + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, 0, y_start + idx_highlight_prev - idx_first); + uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, EFI_LIGHTGRAY|EFI_BACKGROUND_BLACK); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, lines[idx_highlight_prev]); + if ((INTN)idx_highlight_prev == config->idx_default_efivar) { + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, x_start-3, y_start + idx_highlight_prev - idx_first); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, L"=>"); + } + + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, 0, y_start + idx_highlight - idx_first); + uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, EFI_BLACK|EFI_BACKGROUND_LIGHTGRAY); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, lines[idx_highlight]); + if ((INTN)idx_highlight == config->idx_default_efivar) { + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, x_start-3, y_start + idx_highlight - idx_first); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, L"=>"); + } + highlight = FALSE; + } + + if (timeout_remain > 0) { + FreePool(status); + status = PoolPrint(L"Boot in %d sec.", (timeout_remain + 5) / 10); + } + + /* print status at last line of screen */ + if (status) { + UINTN len; + UINTN x; + + /* center line */ + len = StrLen(status); + if (len < x_max) + x = (x_max - len) / 2; + else + x = 0; + uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, EFI_LIGHTGRAY|EFI_BACKGROUND_BLACK); + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, 0, y_max-1); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, clearline + (x_max - x)); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, status); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, clearline+1 + x + len); + } + + err = console_key_read(&key, wait); + if (EFI_ERROR(err)) { + /* timeout reached */ + if (timeout_remain == 0) { + exit = TRUE; + break; + } + + /* sleep and update status */ + if (timeout_remain > 0) { + uefi_call_wrapper(BS->Stall, 1, 100 * 1000); + timeout_remain--; + continue; + } + + /* timeout disabled, wait for next key */ + wait = TRUE; + continue; + } + + timeout_remain = -1; + + /* clear status after keystroke */ + if (status) { + FreePool(status); + status = NULL; + uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, EFI_LIGHTGRAY|EFI_BACKGROUND_BLACK); + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, 0, y_max-1); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, clearline+1); + } + + idx_highlight_prev = idx_highlight; + + switch (key) { + case KEYPRESS(0, SCAN_UP, 0): + case KEYPRESS(0, 0, 'k'): + if (idx_highlight > 0) + idx_highlight--; + break; + + case KEYPRESS(0, SCAN_DOWN, 0): + case KEYPRESS(0, 0, 'j'): + if (idx_highlight < config->entry_count-1) + idx_highlight++; + break; + + case KEYPRESS(0, SCAN_HOME, 0): + case KEYPRESS(EFI_ALT_PRESSED, 0, '<'): + if (idx_highlight > 0) { + refresh = TRUE; + idx_highlight = 0; + } + break; + + case KEYPRESS(0, SCAN_END, 0): + case KEYPRESS(EFI_ALT_PRESSED, 0, '>'): + if (idx_highlight < config->entry_count-1) { + refresh = TRUE; + idx_highlight = config->entry_count-1; + } + break; + + case KEYPRESS(0, SCAN_PAGE_UP, 0): + if (idx_highlight > visible_max) + idx_highlight -= visible_max; + else + idx_highlight = 0; + break; + + case KEYPRESS(0, SCAN_PAGE_DOWN, 0): + idx_highlight += visible_max; + if (idx_highlight > config->entry_count-1) + idx_highlight = config->entry_count-1; + break; + + case KEYPRESS(0, 0, CHAR_LINEFEED): + case KEYPRESS(0, 0, CHAR_CARRIAGE_RETURN): + exit = TRUE; + break; + + case KEYPRESS(0, SCAN_F1, 0): + case KEYPRESS(0, 0, 'h'): + case KEYPRESS(0, 0, '?'): + status = StrDuplicate(L"(d)efault, (t/T)timeout, (e)dit, (v)ersion (Q)uit (P)rint (h)elp"); + break; + + case KEYPRESS(0, 0, 'Q'): + exit = TRUE; + run = FALSE; + break; + + case KEYPRESS(0, 0, 'd'): + if (config->idx_default_efivar != (INTN)idx_highlight) { + /* store the selected entry in a persistent EFI variable */ + efivar_set(L"LoaderEntryDefault", config->entries[idx_highlight]->file, TRUE); + config->idx_default_efivar = idx_highlight; + status = StrDuplicate(L"Default boot entry selected."); + } else { + /* clear the default entry EFI variable */ + efivar_set(L"LoaderEntryDefault", NULL, TRUE); + config->idx_default_efivar = -1; + status = StrDuplicate(L"Default boot entry cleared."); + } + refresh = TRUE; + break; + + case KEYPRESS(0, 0, '-'): + case KEYPRESS(0, 0, 'T'): + if (config->timeout_sec_efivar > 0) { + config->timeout_sec_efivar--; + efivar_set_int(L"LoaderConfigTimeout", config->timeout_sec_efivar, TRUE); + if (config->timeout_sec_efivar > 0) + status = PoolPrint(L"Menu timeout set to %d sec.", config->timeout_sec_efivar); + else + status = StrDuplicate(L"Menu disabled. Hold down key at bootup to show menu."); + } else if (config->timeout_sec_efivar <= 0){ + config->timeout_sec_efivar = -1; + efivar_set(L"LoaderConfigTimeout", NULL, TRUE); + if (config->timeout_sec_config > 0) + status = PoolPrint(L"Menu timeout of %d sec is defined by configuration file.", + config->timeout_sec_config); + else + status = StrDuplicate(L"Menu disabled. Hold down key at bootup to show menu."); + } + break; + + case KEYPRESS(0, 0, '+'): + case KEYPRESS(0, 0, 't'): + if (config->timeout_sec_efivar == -1 && config->timeout_sec_config == 0) + config->timeout_sec_efivar++; + config->timeout_sec_efivar++; + efivar_set_int(L"LoaderConfigTimeout", config->timeout_sec_efivar, TRUE); + if (config->timeout_sec_efivar > 0) + status = PoolPrint(L"Menu timeout set to %d sec.", + config->timeout_sec_efivar); + else + status = StrDuplicate(L"Menu disabled. Hold down key at bootup to show menu."); + break; + + case KEYPRESS(0, 0, 'e'): + /* only the options of configured entries can be edited */ + if (config->entries[idx_highlight]->type == LOADER_UNDEFINED) + break; + uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, EFI_LIGHTGRAY|EFI_BACKGROUND_BLACK); + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, 0, y_max-1); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, clearline+1); + if (line_edit(config->entries[idx_highlight]->options, &config->options_edit, x_max-1, y_max-1)) + exit = TRUE; + uefi_call_wrapper(ST->ConOut->SetCursorPosition, 3, ST->ConOut, 0, y_max-1); + uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, clearline+1); + break; + + case KEYPRESS(0, 0, 'v'): + status = PoolPrint(L"sd-boot " VERSION " (" EFI_MACHINE_TYPE_NAME "), UEFI Specification %d.%02d, Vendor %s %d.%02d", + ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff, + ST->FirmwareVendor, ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff); + break; + + case KEYPRESS(0, 0, 'P'): + print_status(config, root_dir, loaded_image_path); + refresh = TRUE; + break; + + case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'l'): + case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('l')): + refresh = TRUE; + break; + + default: + /* jump with a hotkey directly to a matching entry */ + idx = entry_lookup_key(config, idx_highlight+1, KEYCHAR(key)); + if (idx < 0) + break; + idx_highlight = idx; + refresh = TRUE; + } + + if (idx_highlight > idx_last) { + idx_last = idx_highlight; + idx_first = 1 + idx_highlight - visible_max; + refresh = TRUE; + } + if (idx_highlight < idx_first) { + idx_first = idx_highlight; + idx_last = idx_highlight + visible_max-1; + refresh = TRUE; + } + + idx_last = idx_first + visible_max-1; + + if (!refresh && idx_highlight != idx_highlight_prev) + highlight = TRUE; + } + + *chosen_entry = config->entries[idx_highlight]; + + for (i = 0; i < config->entry_count; i++) + FreePool(lines[i]); + FreePool(lines); + FreePool(clearline); + + uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, EFI_WHITE|EFI_BACKGROUND_BLACK); + uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut); + return run; +} + +static VOID config_add_entry(Config *config, ConfigEntry *entry) { + if ((config->entry_count & 15) == 0) { + UINTN i; + + i = config->entry_count + 16; + if (config->entry_count == 0) + config->entries = AllocatePool(sizeof(VOID *) * i); + else + config->entries = ReallocatePool(config->entries, + sizeof(VOID *) * config->entry_count, sizeof(VOID *) * i); + } + config->entries[config->entry_count++] = entry; +} + +static VOID config_entry_free(ConfigEntry *entry) { + FreePool(entry->title_show); + FreePool(entry->title); + FreePool(entry->machine_id); + FreePool(entry->loader); + FreePool(entry->options); +} + +static BOOLEAN is_digit(CHAR16 c) +{ + return (c >= '0') && (c <= '9'); +} + +static UINTN c_order(CHAR16 c) +{ + if (c == '\0') + return 0; + if (is_digit(c)) + return 0; + else if ((c >= 'a') && (c <= 'z')) + return c; + else + return c + 0x10000; +} + +static INTN str_verscmp(CHAR16 *s1, CHAR16 *s2) +{ + CHAR16 *os1 = s1; + CHAR16 *os2 = s2; + + while (*s1 || *s2) { + INTN first; + + while ((*s1 && !is_digit(*s1)) || (*s2 && !is_digit(*s2))) { + INTN order; + + order = c_order(*s1) - c_order(*s2); + if (order) + return order; + s1++; + s2++; + } + + while (*s1 == '0') + s1++; + while (*s2 == '0') + s2++; + + first = 0; + while (is_digit(*s1) && is_digit(*s2)) { + if (first == 0) + first = *s1 - *s2; + s1++; + s2++; + } + + if (is_digit(*s1)) + return 1; + if (is_digit(*s2)) + return -1; + + if (first) + return first; + } + + return StrCmp(os1, os2); +} + +static CHAR8 *line_get_key_value(CHAR8 *content, CHAR8 *sep, UINTN *pos, CHAR8 **key_ret, CHAR8 **value_ret) { + CHAR8 *line; + UINTN linelen; + CHAR8 *value; + +skip: + line = content + *pos; + if (*line == '\0') + return NULL; + + linelen = 0; + while (line[linelen] && !strchra((CHAR8 *)"\n\r", line[linelen])) + linelen++; + + /* move pos to next line */ + *pos += linelen; + if (content[*pos]) + (*pos)++; + + /* empty line */ + if (linelen == 0) + goto skip; + + /* terminate line */ + line[linelen] = '\0'; + + /* remove leading whitespace */ + while (strchra((CHAR8 *)" \t", *line)) { + line++; + linelen--; + } + + /* remove trailing whitespace */ + while (linelen > 0 && strchra(sep, line[linelen-1])) + linelen--; + line[linelen] = '\0'; + + if (*line == '#') + goto skip; + + /* split key/value */ + value = line; + while (*value && !strchra(sep, *value)) + value++; + if (*value == '\0') + goto skip; + *value = '\0'; + value++; + while (*value && strchra(sep, *value)) + value++; + + /* unquote */ + if (value[0] == '\"' && line[linelen-1] == '\"') { + value++; + line[linelen-1] = '\0'; + } + + *key_ret = line; + *value_ret = value; + return line; +} + +static VOID config_defaults_load_from_file(Config *config, CHAR8 *content) { + CHAR8 *line; + UINTN pos = 0; + CHAR8 *key, *value; + + line = content; + while ((line = line_get_key_value(content, (CHAR8 *)" \t", &pos, &key, &value))) { + if (strcmpa((CHAR8 *)"timeout", key) == 0) { + CHAR16 *s; + + s = stra_to_str(value); + config->timeout_sec_config = Atoi(s); + config->timeout_sec = config->timeout_sec_config; + FreePool(s); + continue; + } + + if (strcmpa((CHAR8 *)"default", key) == 0) { + FreePool(config->entry_default_pattern); + config->entry_default_pattern = stra_to_str(value); + StrLwr(config->entry_default_pattern); + continue; + } + + if (strcmpa((CHAR8 *)"splash", key) == 0) { + FreePool(config->splash); + config->splash = stra_to_path(value); + continue; + } + + if (strcmpa((CHAR8 *)"background", key) == 0) { + CHAR16 c[3]; + + /* accept #RRGGBB hex notation */ + if (value[0] != '#') + continue; + if (value[7] != '\0') + continue; + + FreePool(config->background); + config->background = AllocateZeroPool(sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL)); + if (!config->background) + continue; + + c[0] = value[1]; + c[1] = value[2]; + c[2] = '\0'; + config->background->Red = xtoi(c); + + c[0] = value[3]; + c[1] = value[4]; + config->background->Green = xtoi(c); + + c[0] = value[5]; + c[1] = value[6]; + config->background->Blue = xtoi(c); + continue; + } + } +} + +static VOID config_entry_add_from_file(Config *config, EFI_HANDLE *device, CHAR16 *file, CHAR8 *content, CHAR16 *loaded_image_path) { + ConfigEntry *entry; + CHAR8 *line; + UINTN pos = 0; + CHAR8 *key, *value; + UINTN len; + CHAR16 *initrd = NULL; + + entry = AllocateZeroPool(sizeof(ConfigEntry)); + + line = content; + while ((line = line_get_key_value(content, (CHAR8 *)" \t", &pos, &key, &value))) { + if (strcmpa((CHAR8 *)"title", key) == 0) { + FreePool(entry->title); + entry->title = stra_to_str(value); + continue; + } + + if (strcmpa((CHAR8 *)"version", key) == 0) { + FreePool(entry->version); + entry->version = stra_to_str(value); + continue; + } + + if (strcmpa((CHAR8 *)"machine-id", key) == 0) { + FreePool(entry->machine_id); + entry->machine_id = stra_to_str(value); + continue; + } + + if (strcmpa((CHAR8 *)"linux", key) == 0) { + FreePool(entry->loader); + entry->type = LOADER_LINUX; + entry->loader = stra_to_path(value); + entry->key = 'l'; + continue; + } + + if (strcmpa((CHAR8 *)"efi", key) == 0) { + entry->type = LOADER_EFI; + FreePool(entry->loader); + entry->loader = stra_to_path(value); + + /* do not add an entry for ourselves */ + if (StriCmp(entry->loader, loaded_image_path) == 0) { + entry->type = LOADER_UNDEFINED; + break; + } + continue; + } + + if (strcmpa((CHAR8 *)"architecture", key) == 0) { + /* do not add an entry for an EFI image of architecture not matching with that of the image */ + if (strcmpa((CHAR8 *)EFI_MACHINE_TYPE_NAME, value) != 0) { + entry->type = LOADER_UNDEFINED; + break; + } + continue; + } + + if (strcmpa((CHAR8 *)"initrd", key) == 0) { + CHAR16 *new; + + new = stra_to_path(value); + if (initrd) { + CHAR16 *s; + + s = PoolPrint(L"%s initrd=%s", initrd, new); + FreePool(initrd); + initrd = s; + } else + initrd = PoolPrint(L"initrd=%s", new); + FreePool(new); + continue; + } + + if (strcmpa((CHAR8 *)"options", key) == 0) { + CHAR16 *new; + + new = stra_to_str(value); + if (entry->options) { + CHAR16 *s; + + s = PoolPrint(L"%s %s", entry->options, new); + FreePool(entry->options); + entry->options = s; + } else { + entry->options = new; + new = NULL; + } + FreePool(new); + continue; + } + + if (strcmpa((CHAR8 *)"splash", key) == 0) { + FreePool(entry->splash); + entry->splash = stra_to_path(value); + continue; + } + } + + if (entry->type == LOADER_UNDEFINED) { + config_entry_free(entry); + FreePool(initrd); + FreePool(entry); + return; + } + + /* add initrd= to options */ + if (entry->type == LOADER_LINUX && initrd) { + if (entry->options) { + CHAR16 *s; + + s = PoolPrint(L"%s %s", initrd, entry->options); + FreePool(entry->options); + entry->options = s; + } else { + entry->options = initrd; + initrd = NULL; + } + } + FreePool(initrd); + + if (entry->machine_id) { + CHAR16 *var; + + /* append additional options from EFI variables for this machine-id */ + var = PoolPrint(L"LoaderEntryOptions-%s", entry->machine_id); + if (var) { + CHAR16 *s; + + if (efivar_get(var, &s) == EFI_SUCCESS) { + if (entry->options) { + CHAR16 *s2; + + s2 = PoolPrint(L"%s %s", entry->options, s); + FreePool(entry->options); + entry->options = s2; + } else + entry->options = s; + } + FreePool(var); + } + + var = PoolPrint(L"LoaderEntryOptionsOneShot-%s", entry->machine_id); + if (var) { + CHAR16 *s; + + if (efivar_get(var, &s) == EFI_SUCCESS) { + if (entry->options) { + CHAR16 *s2; + + s2 = PoolPrint(L"%s %s", entry->options, s); + FreePool(entry->options); + entry->options = s2; + } else + entry->options = s; + efivar_set(var, NULL, TRUE); + } + FreePool(var); + } + } + + entry->device = device; + entry->file = StrDuplicate(file); + len = StrLen(entry->file); + /* remove ".conf" */ + if (len > 5) + entry->file[len - 5] = '\0'; + StrLwr(entry->file); + + config_add_entry(config, entry); +} + +static VOID config_load(Config *config, EFI_HANDLE *device, EFI_FILE *root_dir, CHAR16 *loaded_image_path) { + EFI_FILE_HANDLE entries_dir; + EFI_STATUS err; + CHAR8 *content = NULL; + UINTN sec; + UINTN len; + UINTN i; + + len = file_read(root_dir, L"\\loader\\loader.conf", 0, 0, &content); + if (len > 0) + config_defaults_load_from_file(config, content); + FreePool(content); + + err = efivar_get_int(L"LoaderConfigTimeout", &sec); + if (!EFI_ERROR(err)) { + config->timeout_sec_efivar = sec; + config->timeout_sec = sec; + } else + config->timeout_sec_efivar = -1; + + err = uefi_call_wrapper(root_dir->Open, 5, root_dir, &entries_dir, L"\\loader\\entries", EFI_FILE_MODE_READ, 0ULL); + if (!EFI_ERROR(err)) { + for (;;) { + CHAR16 buf[256]; + UINTN bufsize; + EFI_FILE_INFO *f; + CHAR8 *content = NULL; + UINTN len; + + bufsize = sizeof(buf); + err = uefi_call_wrapper(entries_dir->Read, 3, entries_dir, &bufsize, buf); + if (bufsize == 0 || EFI_ERROR(err)) + break; + + f = (EFI_FILE_INFO *) buf; + if (f->FileName[0] == '.') + continue; + if (f->Attribute & EFI_FILE_DIRECTORY) + continue; + len = StrLen(f->FileName); + if (len < 6) + continue; + if (StriCmp(f->FileName + len - 5, L".conf") != 0) + continue; + + len = file_read(entries_dir, f->FileName, 0, 0, &content); + if (len > 0) + config_entry_add_from_file(config, device, f->FileName, content, loaded_image_path); + FreePool(content); + } + uefi_call_wrapper(entries_dir->Close, 1, entries_dir); + } + + /* sort entries after version number */ + for (i = 1; i < config->entry_count; i++) { + BOOLEAN more; + UINTN k; + + more = FALSE; + for (k = 0; k < config->entry_count - i; k++) { + ConfigEntry *entry; + + if (str_verscmp(config->entries[k]->file, config->entries[k+1]->file) <= 0) + continue; + entry = config->entries[k]; + config->entries[k] = config->entries[k+1]; + config->entries[k+1] = entry; + more = TRUE; + } + if (!more) + break; + } +} + +static VOID config_default_entry_select(Config *config) { + CHAR16 *var; + EFI_STATUS err; + UINTN i; + + /* + * The EFI variable to specify a boot entry for the next, and only the + * next reboot. The variable is always cleared directly after it is read. + */ + err = efivar_get(L"LoaderEntryOneShot", &var); + if (!EFI_ERROR(err)) { + BOOLEAN found = FALSE; + + for (i = 0; i < config->entry_count; i++) { + if (StrCmp(config->entries[i]->file, var) == 0) { + config->idx_default = i; + found = TRUE; + break; + } + } + + config->entry_oneshot = StrDuplicate(var); + efivar_set(L"LoaderEntryOneShot", NULL, TRUE); + FreePool(var); + if (found) + return; + } + + /* + * The EFI variable to select the default boot entry overrides the + * configured pattern. The variable can be set and cleared by pressing + * the 'd' key in the loader selection menu, the entry is marked with + * an '*'. + */ + err = efivar_get(L"LoaderEntryDefault", &var); + if (!EFI_ERROR(err)) { + BOOLEAN found = FALSE; + + for (i = 0; i < config->entry_count; i++) { + if (StrCmp(config->entries[i]->file, var) == 0) { + config->idx_default = i; + config->idx_default_efivar = i; + found = TRUE; + break; + } + } + FreePool(var); + if (found) + return; + } + config->idx_default_efivar = -1; + + if (config->entry_count == 0) + return; + + /* + * Match the pattern from the end of the list to the start, find last + * entry (largest number) matching the given pattern. + */ + if (config->entry_default_pattern) { + i = config->entry_count; + while (i--) { + if (config->entries[i]->no_autoselect) + continue; + if (MetaiMatch(config->entries[i]->file, config->entry_default_pattern)) { + config->idx_default = i; + return; + } + } + } + + /* select the last suitable entry */ + i = config->entry_count; + while (i--) { + if (config->entries[i]->no_autoselect) + continue; + config->idx_default = i; + return; + } + + /* no entry found */ + config->idx_default = -1; +} + +/* generate a unique title, avoiding non-distinguishable menu entries */ +static VOID config_title_generate(Config *config) { + UINTN i, k; + BOOLEAN unique; + + /* set title */ + for (i = 0; i < config->entry_count; i++) { + CHAR16 *title; + + FreePool(config->entries[i]->title_show); + title = config->entries[i]->title; + if (!title) + title = config->entries[i]->file; + config->entries[i]->title_show = StrDuplicate(title); + } + + unique = TRUE; + for (i = 0; i < config->entry_count; i++) { + for (k = 0; k < config->entry_count; k++) { + if (i == k) + continue; + if (StrCmp(config->entries[i]->title_show, config->entries[k]->title_show) != 0) + continue; + + unique = FALSE; + config->entries[i]->non_unique = TRUE; + config->entries[k]->non_unique = TRUE; + } + } + if (unique) + return; + + /* add version to non-unique titles */ + for (i = 0; i < config->entry_count; i++) { + CHAR16 *s; + + if (!config->entries[i]->non_unique) + continue; + if (!config->entries[i]->version) + continue; + + s = PoolPrint(L"%s (%s)", config->entries[i]->title_show, config->entries[i]->version); + FreePool(config->entries[i]->title_show); + config->entries[i]->title_show = s; + config->entries[i]->non_unique = FALSE; + } + + unique = TRUE; + for (i = 0; i < config->entry_count; i++) { + for (k = 0; k < config->entry_count; k++) { + if (i == k) + continue; + if (StrCmp(config->entries[i]->title_show, config->entries[k]->title_show) != 0) + continue; + + unique = FALSE; + config->entries[i]->non_unique = TRUE; + config->entries[k]->non_unique = TRUE; + } + } + if (unique) + return; + + /* add machine-id to non-unique titles */ + for (i = 0; i < config->entry_count; i++) { + CHAR16 *s; + CHAR16 *m; + + if (!config->entries[i]->non_unique) + continue; + if (!config->entries[i]->machine_id) + continue; + + m = StrDuplicate(config->entries[i]->machine_id); + m[8] = '\0'; + s = PoolPrint(L"%s (%s)", config->entries[i]->title_show, m); + FreePool(config->entries[i]->title_show); + config->entries[i]->title_show = s; + config->entries[i]->non_unique = FALSE; + FreePool(m); + } + + unique = TRUE; + for (i = 0; i < config->entry_count; i++) { + for (k = 0; k < config->entry_count; k++) { + if (i == k) + continue; + if (StrCmp(config->entries[i]->title_show, config->entries[k]->title_show) != 0) + continue; + + unique = FALSE; + config->entries[i]->non_unique = TRUE; + config->entries[k]->non_unique = TRUE; + } + } + if (unique) + return; + + /* add file name to non-unique titles */ + for (i = 0; i < config->entry_count; i++) { + CHAR16 *s; + + if (!config->entries[i]->non_unique) + continue; + s = PoolPrint(L"%s (%s)", config->entries[i]->title_show, config->entries[i]->file); + FreePool(config->entries[i]->title_show); + config->entries[i]->title_show = s; + config->entries[i]->non_unique = FALSE; + } +} + +static BOOLEAN config_entry_add_call(Config *config, CHAR16 *title, EFI_STATUS (*call)(VOID)) { + ConfigEntry *entry; + + entry = AllocateZeroPool(sizeof(ConfigEntry)); + entry->title = StrDuplicate(title); + entry->call = call; + entry->no_autoselect = TRUE; + config_add_entry(config, entry); + return TRUE; +} + +static ConfigEntry *config_entry_add_loader(Config *config, EFI_HANDLE *device, + enum loader_type type,CHAR16 *file, CHAR16 key, CHAR16 *title, CHAR16 *loader) { + ConfigEntry *entry; + + entry = AllocateZeroPool(sizeof(ConfigEntry)); + entry->type = type; + entry->title = StrDuplicate(title); + entry->device = device; + entry->loader = StrDuplicate(loader); + entry->file = StrDuplicate(file); + StrLwr(entry->file); + entry->key = key; + config_add_entry(config, entry); + + return entry; +} + +static BOOLEAN config_entry_add_loader_auto(Config *config, EFI_HANDLE *device, EFI_FILE *root_dir, CHAR16 *loaded_image_path, + CHAR16 *file, CHAR16 key, CHAR16 *title, CHAR16 *loader) { + EFI_FILE_HANDLE handle; + ConfigEntry *entry; + EFI_STATUS err; + + /* do not add an entry for ourselves */ + if (loaded_image_path && StriCmp(loader, loaded_image_path) == 0) + return FALSE; + + /* check existence */ + err = uefi_call_wrapper(root_dir->Open, 5, root_dir, &handle, loader, EFI_FILE_MODE_READ, 0ULL); + if (EFI_ERROR(err)) + return FALSE; + uefi_call_wrapper(handle->Close, 1, handle); + + entry = config_entry_add_loader(config, device, LOADER_UNDEFINED, file, key, title, loader); + if (!entry) + return FALSE; + + /* do not boot right away into auto-detected entries */ + entry->no_autoselect = TRUE; + + /* do not show a splash; they do not need one, or they draw their own */ + entry->splash = StrDuplicate(L""); + + /* export identifiers of automatically added entries */ + if (config->entries_auto) { + CHAR16 *s; + + s = PoolPrint(L"%s %s", config->entries_auto, file); + FreePool(config->entries_auto); + config->entries_auto = s; + } else + config->entries_auto = StrDuplicate(file); + + return TRUE; +} + +static VOID config_entry_add_osx(Config *config) { + EFI_STATUS err; + UINTN handle_count = 0; + EFI_HANDLE *handles = NULL; + + err = LibLocateHandle(ByProtocol, &FileSystemProtocol, NULL, &handle_count, &handles); + if (!EFI_ERROR(err)) { + UINTN i; + + for (i = 0; i < handle_count; i++) { + EFI_FILE *root; + BOOLEAN found; + + root = LibOpenRoot(handles[i]); + if (!root) + continue; + found = config_entry_add_loader_auto(config, handles[i], root, NULL, L"auto-osx", 'a', L"OS X", + L"\\System\\Library\\CoreServices\\boot.efi"); + uefi_call_wrapper(root->Close, 1, root); + if (found) + break; + } + + FreePool(handles); + } +} + +static VOID config_entry_add_linux( Config *config, EFI_LOADED_IMAGE *loaded_image, EFI_FILE *root_dir) { + EFI_FILE_HANDLE linux_dir; + EFI_STATUS err; + + err = uefi_call_wrapper(root_dir->Open, 5, root_dir, &linux_dir, L"\\EFI\\Linux", EFI_FILE_MODE_READ, 0ULL); + if (!EFI_ERROR(err)) { + for (;;) { + CHAR16 buf[256]; + UINTN bufsize; + EFI_FILE_INFO *f; + CHAR8 *sections[] = { + (UINT8 *)".osrel", + NULL + }; + UINTN offs[ELEMENTSOF(sections)-1] = {}; + UINTN szs[ELEMENTSOF(sections)-1] = {}; + UINTN addrs[ELEMENTSOF(sections)-1] = {}; + CHAR8 *content = NULL; + UINTN len; + CHAR8 *line; + UINTN pos = 0; + CHAR8 *key, *value; + CHAR16 *os_name = NULL; + CHAR16 *os_id = NULL; + CHAR16 *os_version = NULL; + + bufsize = sizeof(buf); + err = uefi_call_wrapper(linux_dir->Read, 3, linux_dir, &bufsize, buf); + if (bufsize == 0 || EFI_ERROR(err)) + break; + + f = (EFI_FILE_INFO *) buf; + if (f->FileName[0] == '.') + continue; + if (f->Attribute & EFI_FILE_DIRECTORY) + continue; + len = StrLen(f->FileName); + if (len < 5) + continue; + if (StriCmp(f->FileName + len - 4, L".efi") != 0) + continue; + + /* look for an .osrel section in the .efi binary */ + err = pefile_locate_sections(linux_dir, f->FileName, sections, addrs, offs, szs); + if (EFI_ERROR(err)) + continue; + + len = file_read(linux_dir, f->FileName, offs[0], szs[0], &content); + if (len <= 0) + continue; + + /* read properties from the embedded os-release file */ + line = content; + while ((line = line_get_key_value(content, (CHAR8 *)"=", &pos, &key, &value))) { + if (strcmpa((CHAR8 *)"PRETTY_NAME", key) == 0) { + os_name = stra_to_str(value); + continue; + } + + if (strcmpa((CHAR8 *)"ID", key) == 0) { + os_id = stra_to_str(value); + continue; + } + + if (strcmpa((CHAR8 *)"VERSION_ID", key) == 0) { + os_version = stra_to_str(value); + continue; + } + } + + if (os_name && os_id && os_version) { + CHAR16 *conf; + CHAR16 *path; + + conf = PoolPrint(L"%s-%s", os_id, os_version); + path = PoolPrint(L"\\EFI\\Linux\\%s", f->FileName); + config_entry_add_loader(config, loaded_image->DeviceHandle, LOADER_LINUX, conf, 'l', os_name, path); + FreePool(conf); + FreePool(path); + FreePool(os_name); + FreePool(os_id); + FreePool(os_version); + } + + FreePool(content); + } + uefi_call_wrapper(linux_dir->Close, 1, linux_dir); + } +} + +static EFI_STATUS image_start(EFI_HANDLE parent_image, const Config *config, const ConfigEntry *entry) { + EFI_HANDLE image; + EFI_DEVICE_PATH *path; + CHAR16 *options; + EFI_STATUS err; + + path = FileDevicePath(entry->device, entry->loader); + if (!path) { + Print(L"Error getting device path."); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + return EFI_INVALID_PARAMETER; + } + + err = uefi_call_wrapper(BS->LoadImage, 6, FALSE, parent_image, path, NULL, 0, &image); + if (EFI_ERROR(err)) { + Print(L"Error loading %s: %r", entry->loader, err); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + goto out; + } + + if (config->options_edit) + options = config->options_edit; + else if (entry->options) + options = entry->options; + else + options = NULL; + if (options) { + EFI_LOADED_IMAGE *loaded_image; + + err = uefi_call_wrapper(BS->OpenProtocol, 6, image, &LoadedImageProtocol, (VOID **)&loaded_image, + parent_image, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL); + if (EFI_ERROR(err)) { + Print(L"Error getting LoadedImageProtocol handle: %r", err); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + goto out_unload; + } + loaded_image->LoadOptions = options; + loaded_image->LoadOptionsSize = (StrLen(loaded_image->LoadOptions)+1) * sizeof(CHAR16); + } + + efivar_set_time_usec(L"LoaderTimeExecUSec", 0); + err = uefi_call_wrapper(BS->StartImage, 3, image, NULL, NULL); +out_unload: + uefi_call_wrapper(BS->UnloadImage, 1, image); +out: + FreePool(path); + return err; +} + +static EFI_STATUS reboot_into_firmware(VOID) { + CHAR8 *b; + UINTN size; + UINT64 osind; + EFI_STATUS err; + + osind = EFI_OS_INDICATIONS_BOOT_TO_FW_UI; + + err = efivar_get_raw(&global_guid, L"OsIndications", &b, &size); + if (!EFI_ERROR(err)) + osind |= (UINT64)*b; + FreePool(b); + + err = efivar_set_raw(&global_guid, L"OsIndications", (CHAR8 *)&osind, sizeof(UINT64), TRUE); + if (EFI_ERROR(err)) + return err; + + err = uefi_call_wrapper(RT->ResetSystem, 4, EfiResetCold, EFI_SUCCESS, 0, NULL); + Print(L"Error calling ResetSystem: %r", err); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + return err; +} + +static VOID config_free(Config *config) { + UINTN i; + + for (i = 0; i < config->entry_count; i++) + config_entry_free(config->entries[i]); + FreePool(config->entries); + FreePool(config->entry_default_pattern); + FreePool(config->options_edit); + FreePool(config->entry_oneshot); + FreePool(config->entries_auto); + FreePool(config->splash); + FreePool(config->background); +} + +EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { + CHAR16 *s; + CHAR8 *b; + UINTN size; + EFI_LOADED_IMAGE *loaded_image; + EFI_FILE *root_dir; + CHAR16 *loaded_image_path; + EFI_DEVICE_PATH *device_path; + EFI_STATUS err; + Config config; + UINT64 init_usec; + BOOLEAN menu = FALSE; + + InitializeLib(image, sys_table); + init_usec = time_usec(); + efivar_set_time_usec(L"LoaderTimeInitUSec", init_usec); + efivar_set(L"LoaderInfo", L"sd-boot " VERSION, FALSE); + s = PoolPrint(L"%s %d.%02d", ST->FirmwareVendor, ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff); + efivar_set(L"LoaderFirmwareInfo", s, FALSE); + FreePool(s); + s = PoolPrint(L"UEFI %d.%02d", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff); + efivar_set(L"LoaderFirmwareType", s, FALSE); + FreePool(s); + + err = uefi_call_wrapper(BS->OpenProtocol, 6, image, &LoadedImageProtocol, (VOID **)&loaded_image, + image, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL); + if (EFI_ERROR(err)) { + Print(L"Error getting a LoadedImageProtocol handle: %r ", err); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + return err; + } + + /* export the device path this image is started from */ + device_path = DevicePathFromHandle(loaded_image->DeviceHandle); + if (device_path) { + CHAR16 *str; + EFI_DEVICE_PATH *path, *paths; + + str = DevicePathToStr(device_path); + efivar_set(L"LoaderDeviceIdentifier", str, FALSE); + FreePool(str); + + paths = UnpackDevicePath(device_path); + for (path = paths; !IsDevicePathEnd(path); path = NextDevicePathNode(path)) { + HARDDRIVE_DEVICE_PATH *drive; + CHAR16 uuid[37]; + + if (DevicePathType(path) != MEDIA_DEVICE_PATH) + continue; + if (DevicePathSubType(path) != MEDIA_HARDDRIVE_DP) + continue; + drive = (HARDDRIVE_DEVICE_PATH *)path; + if (drive->SignatureType != SIGNATURE_TYPE_GUID) + continue; + + GuidToString(uuid, (EFI_GUID *)&drive->Signature); + efivar_set(L"LoaderDevicePartUUID", uuid, FALSE); + break; + } + FreePool(paths); + } + + root_dir = LibOpenRoot(loaded_image->DeviceHandle); + if (!root_dir) { + Print(L"Unable to open root directory: %r ", err); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + return EFI_LOAD_ERROR; + } + + + /* the filesystem path to this image, to prevent adding ourselves to the menu */ + loaded_image_path = DevicePathToStr(loaded_image->FilePath); + efivar_set(L"LoaderImageIdentifier", loaded_image_path, FALSE); + + /* scan "\loader\entries\*.conf" files */ + ZeroMem(&config, sizeof(Config)); + config_load(&config, loaded_image->DeviceHandle, root_dir, loaded_image_path); + + if (!config.background) { + config.background = AllocateZeroPool(sizeof(EFI_GRAPHICS_OUTPUT_BLT_PIXEL)); + if (StriCmp(L"Apple", ST->FirmwareVendor) == 0) { + config.background->Red = 0xc0; + config.background->Green = 0xc0; + config.background->Blue = 0xc0; + } + } + + /* if we find some well-known loaders, add them to the end of the list */ + config_entry_add_linux(&config, loaded_image, root_dir); + config_entry_add_loader_auto(&config, loaded_image->DeviceHandle, root_dir, loaded_image_path, + L"auto-windows", 'w', L"Windows Boot Manager", L"\\EFI\\Microsoft\\Boot\\bootmgfw.efi"); + config_entry_add_loader_auto(&config, loaded_image->DeviceHandle, root_dir, loaded_image_path, + L"auto-efi-shell", 's', L"EFI Shell", L"\\shell" EFI_MACHINE_TYPE_NAME ".efi"); + config_entry_add_loader_auto(&config, loaded_image->DeviceHandle, root_dir, loaded_image_path, + L"auto-efi-default", '\0', L"EFI Default Loader", L"\\EFI\\Boot\\boot" EFI_MACHINE_TYPE_NAME ".efi"); + config_entry_add_osx(&config); + efivar_set(L"LoaderEntriesAuto", config.entries_auto, FALSE); + + if (efivar_get_raw(&global_guid, L"OsIndicationsSupported", &b, &size) == EFI_SUCCESS) { + UINT64 osind = (UINT64)*b; + + if (osind & EFI_OS_INDICATIONS_BOOT_TO_FW_UI) + config_entry_add_call(&config, L"Reboot Into Firmware Interface", reboot_into_firmware); + FreePool(b); + } + + if (config.entry_count == 0) { + Print(L"No loader found. Configuration files in \\loader\\entries\\*.conf are needed."); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + goto out; + } + + config_title_generate(&config); + + /* select entry by configured pattern or EFI LoaderDefaultEntry= variable*/ + config_default_entry_select(&config); + + /* if no configured entry to select from was found, enable the menu */ + if (config.idx_default == -1) { + config.idx_default = 0; + if (config.timeout_sec == 0) + config.timeout_sec = 10; + } + + /* select entry or show menu when key is pressed or timeout is set */ + if (config.timeout_sec == 0) { + UINT64 key; + + err = console_key_read(&key, FALSE); + if (!EFI_ERROR(err)) { + INT16 idx; + + /* find matching key in config entries */ + idx = entry_lookup_key(&config, config.idx_default, KEYCHAR(key)); + if (idx >= 0) + config.idx_default = idx; + else + menu = TRUE; + } + } else + menu = TRUE; + + for (;;) { + ConfigEntry *entry; + + entry = config.entries[config.idx_default]; + if (menu) { + efivar_set_time_usec(L"LoaderTimeMenuUSec", 0); + uefi_call_wrapper(BS->SetWatchdogTimer, 4, 0, 0x10000, 0, NULL); + if (!menu_run(&config, &entry, root_dir, loaded_image_path)) + break; + + /* run special entry like "reboot" */ + if (entry->call) { + entry->call(); + continue; + } + } else { + err = EFI_NOT_FOUND; + + /* splash from entry file */ + if (entry->splash) { + /* some entries disable the splash because they draw their own */ + if (entry->splash[0] == '\0') + err = EFI_SUCCESS; + else + err = graphics_splash(root_dir, entry->splash, config.background); + } + + /* splash from config file */ + if (EFI_ERROR(err) && config.splash) + err = graphics_splash(root_dir, config.splash, config.background); + + /* default splash */ + if (EFI_ERROR(err)) + graphics_splash(root_dir, L"\\EFI\\systemd\\splash.bmp", config.background); + } + + /* export the selected boot entry to the system */ + efivar_set(L"LoaderEntrySelected", entry->file, FALSE); + + uefi_call_wrapper(BS->SetWatchdogTimer, 4, 5 * 60, 0x10000, 0, NULL); + err = image_start(image, &config, entry); + + if (err == EFI_ACCESS_DENIED || err == EFI_SECURITY_VIOLATION) { + /* Platform is secure boot and requested image isn't + * trusted. Need to go back to prior boot system and + * install more keys or hashes. Signal failure by + * returning the error */ + Print(L"\nImage %s gives a security error\n", entry->title); + Print(L"Please enrol the hash or signature of %s\n", entry->loader); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + goto out; + } + + menu = TRUE; + config.timeout_sec = 0; + } + err = EFI_SUCCESS; +out: + FreePool(loaded_image_path); + config_free(&config); + uefi_call_wrapper(root_dir->Close, 1, root_dir); + uefi_call_wrapper(BS->CloseProtocol, 4, image, &LoadedImageProtocol, image, NULL); + return err; +} diff --git a/src/sd-boot/stub.c b/src/sd-boot/stub.c new file mode 100644 index 0000000000..e18faac669 --- /dev/null +++ b/src/sd-boot/stub.c @@ -0,0 +1,106 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/* This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Copyright (C) 2015 Kay Sievers + */ + +#include +#include + +#include "util.h" +#include "pefile.h" +#include "linux.h" + +/* magic string to find in the binary image */ +static const char __attribute__((used)) magic[] = "#### LoaderInfo: stub " VERSION " ####"; + +static const EFI_GUID global_guid = EFI_GLOBAL_VARIABLE; + +EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { + EFI_LOADED_IMAGE *loaded_image; + EFI_FILE *root_dir; + CHAR16 *loaded_image_path; + CHAR8 *b; + UINTN size; + BOOLEAN secure = FALSE; + CHAR8 *sections[] = { + (UINT8 *)".cmdline", + (UINT8 *)".linux", + (UINT8 *)".initrd", + NULL + }; + UINTN addrs[ELEMENTSOF(sections)-1] = {}; + UINTN offs[ELEMENTSOF(sections)-1] = {}; + UINTN szs[ELEMENTSOF(sections)-1] = {}; + CHAR8 *cmdline = NULL; + UINTN cmdline_len; + EFI_STATUS err; + + InitializeLib(image, sys_table); + + err = uefi_call_wrapper(BS->OpenProtocol, 6, image, &LoadedImageProtocol, (VOID **)&loaded_image, + image, NULL, EFI_OPEN_PROTOCOL_GET_PROTOCOL); + if (EFI_ERROR(err)) { + Print(L"Error getting a LoadedImageProtocol handle: %r ", err); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + return err; + } + + root_dir = LibOpenRoot(loaded_image->DeviceHandle); + if (!root_dir) { + Print(L"Unable to open root directory: %r ", err); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + return EFI_LOAD_ERROR; + } + + loaded_image_path = DevicePathToStr(loaded_image->FilePath); + + if (efivar_get_raw(&global_guid, L"SecureBoot", &b, &size) == EFI_SUCCESS) { + if (*b > 0) + secure = TRUE; + FreePool(b); + } + + err = pefile_locate_sections(root_dir, loaded_image_path, sections, addrs, offs, szs); + if (EFI_ERROR(err)) { + Print(L"Unable to locate embedded .linux section: %r ", err); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + return err; + } + + if (szs[0] > 0) + cmdline = (CHAR8 *)(loaded_image->ImageBase + addrs[0]); + + cmdline_len = szs[0]; + + /* if we are not in secure boot mode, accept a custom command line and replace the built-in one */ + if (!secure && loaded_image->LoadOptionsSize > 0) { + CHAR16 *options; + CHAR8 *line; + UINTN i; + + options = (CHAR16 *)loaded_image->LoadOptions; + cmdline_len = (loaded_image->LoadOptionsSize / sizeof(CHAR16)) * sizeof(CHAR8); + line = AllocatePool(cmdline_len); + for (i = 0; i < cmdline_len; i++) + line[i] = options[i]; + cmdline = line; + } + + err = linux_exec(image, cmdline, cmdline_len, + (UINTN)loaded_image->ImageBase + addrs[1], + (UINTN)loaded_image->ImageBase + addrs[2], szs[2]); + + Print(L"Execution of embedded linux image failed: %r\n", err); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + return err; +} diff --git a/src/sd-boot/util.c b/src/sd-boot/util.c new file mode 100644 index 0000000000..5678b50c31 --- /dev/null +++ b/src/sd-boot/util.c @@ -0,0 +1,322 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Copyright (C) 2012-2013 Kay Sievers + * Copyright (C) 2012 Harald Hoyer + */ + +#include +#include + +#include "util.h" + +/* + * Allocated random UUID, intended to be shared across tools that implement + * the (ESP)\loader\entries\-.conf convention and the + * associated EFI variables. + */ +static const EFI_GUID loader_guid = { 0x4a67b082, 0x0a4c, 0x41cf, {0xb6, 0xc7, 0x44, 0x0b, 0x29, 0xbb, 0x8c, 0x4f} }; + +#ifdef __x86_64__ +UINT64 ticks_read(VOID) { + UINT64 a, d; + __asm__ volatile ("rdtsc" : "=a" (a), "=d" (d)); + return (d << 32) | a; +} +#else +UINT64 ticks_read(VOID) { + UINT64 val; + __asm__ volatile ("rdtsc" : "=A" (val)); + return val; +} +#endif + +/* count TSC ticks during a millisecond delay */ +UINT64 ticks_freq(VOID) { + UINT64 ticks_start, ticks_end; + + ticks_start = ticks_read(); + uefi_call_wrapper(BS->Stall, 1, 1000); + ticks_end = ticks_read(); + + return (ticks_end - ticks_start) * 1000; +} + +UINT64 time_usec(VOID) { + UINT64 ticks; + static UINT64 freq; + + ticks = ticks_read(); + if (ticks == 0) + return 0; + + if (freq == 0) { + freq = ticks_freq(); + if (freq == 0) + return 0; + } + + return 1000 * 1000 * ticks / freq; +} + +EFI_STATUS efivar_set_raw(const EFI_GUID *vendor, CHAR16 *name, CHAR8 *buf, UINTN size, BOOLEAN persistent) { + UINT32 flags; + + flags = EFI_VARIABLE_BOOTSERVICE_ACCESS|EFI_VARIABLE_RUNTIME_ACCESS; + if (persistent) + flags |= EFI_VARIABLE_NON_VOLATILE; + + return uefi_call_wrapper(RT->SetVariable, 5, name, (EFI_GUID *)vendor, flags, size, buf); +} + +EFI_STATUS efivar_set(CHAR16 *name, CHAR16 *value, BOOLEAN persistent) { + return efivar_set_raw(&loader_guid, name, (CHAR8 *)value, value ? (StrLen(value)+1) * sizeof(CHAR16) : 0, persistent); +} + +EFI_STATUS efivar_set_int(CHAR16 *name, UINTN i, BOOLEAN persistent) { + CHAR16 str[32]; + + SPrint(str, 32, L"%d", i); + return efivar_set(name, str, persistent); +} + +EFI_STATUS efivar_get(CHAR16 *name, CHAR16 **value) { + CHAR8 *buf; + CHAR16 *val; + UINTN size; + EFI_STATUS err; + + err = efivar_get_raw(&loader_guid, name, &buf, &size); + if (EFI_ERROR(err)) + return err; + + val = StrDuplicate((CHAR16 *)buf); + if (!val) { + FreePool(buf); + return EFI_OUT_OF_RESOURCES; + } + + *value = val; + return EFI_SUCCESS; +} + +EFI_STATUS efivar_get_int(CHAR16 *name, UINTN *i) { + CHAR16 *val; + EFI_STATUS err; + + err = efivar_get(name, &val); + if (!EFI_ERROR(err)) { + *i = Atoi(val); + FreePool(val); + } + return err; +} + +EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, CHAR16 *name, CHAR8 **buffer, UINTN *size) { + CHAR8 *buf; + UINTN l; + EFI_STATUS err; + + l = sizeof(CHAR16 *) * EFI_MAXIMUM_VARIABLE_SIZE; + buf = AllocatePool(l); + if (!buf) + return EFI_OUT_OF_RESOURCES; + + err = uefi_call_wrapper(RT->GetVariable, 5, name, (EFI_GUID *)vendor, NULL, &l, buf); + if (!EFI_ERROR(err)) { + *buffer = buf; + if (size) + *size = l; + } else + FreePool(buf); + return err; + +} + +VOID efivar_set_time_usec(CHAR16 *name, UINT64 usec) { + CHAR16 str[32]; + + if (usec == 0) + usec = time_usec(); + if (usec == 0) + return; + + SPrint(str, 32, L"%ld", usec); + efivar_set(name, str, FALSE); +} + +static INTN utf8_to_16(CHAR8 *stra, CHAR16 *c) { + CHAR16 unichar; + UINTN len; + UINTN i; + + if (stra[0] < 0x80) + len = 1; + else if ((stra[0] & 0xe0) == 0xc0) + len = 2; + else if ((stra[0] & 0xf0) == 0xe0) + len = 3; + else if ((stra[0] & 0xf8) == 0xf0) + len = 4; + else if ((stra[0] & 0xfc) == 0xf8) + len = 5; + else if ((stra[0] & 0xfe) == 0xfc) + len = 6; + else + return -1; + + switch (len) { + case 1: + unichar = stra[0]; + break; + case 2: + unichar = stra[0] & 0x1f; + break; + case 3: + unichar = stra[0] & 0x0f; + break; + case 4: + unichar = stra[0] & 0x07; + break; + case 5: + unichar = stra[0] & 0x03; + break; + case 6: + unichar = stra[0] & 0x01; + break; + } + + for (i = 1; i < len; i++) { + if ((stra[i] & 0xc0) != 0x80) + return -1; + unichar <<= 6; + unichar |= stra[i] & 0x3f; + } + + *c = unichar; + return len; +} + +CHAR16 *stra_to_str(CHAR8 *stra) { + UINTN strlen; + UINTN len; + UINTN i; + CHAR16 *str; + + len = strlena(stra); + str = AllocatePool((len + 1) * sizeof(CHAR16)); + + strlen = 0; + i = 0; + while (i < len) { + INTN utf8len; + + utf8len = utf8_to_16(stra + i, str + strlen); + if (utf8len <= 0) { + /* invalid utf8 sequence, skip the garbage */ + i++; + continue; + } + + strlen++; + i += utf8len; + } + str[strlen] = '\0'; + return str; +} + +CHAR16 *stra_to_path(CHAR8 *stra) { + CHAR16 *str; + UINTN strlen; + UINTN len; + UINTN i; + + len = strlena(stra); + str = AllocatePool((len + 2) * sizeof(CHAR16)); + + str[0] = '\\'; + strlen = 1; + i = 0; + while (i < len) { + INTN utf8len; + + utf8len = utf8_to_16(stra + i, str + strlen); + if (utf8len <= 0) { + /* invalid utf8 sequence, skip the garbage */ + i++; + continue; + } + + if (str[strlen] == '/') + str[strlen] = '\\'; + if (str[strlen] == '\\' && str[strlen-1] == '\\') { + /* skip double slashes */ + i += utf8len; + continue; + } + + strlen++; + i += utf8len; + } + str[strlen] = '\0'; + return str; +} + +CHAR8 *strchra(CHAR8 *s, CHAR8 c) { + do { + if (*s == c) + return s; + } while (*s++); + return NULL; +} + +INTN file_read(EFI_FILE_HANDLE dir, CHAR16 *name, UINTN off, UINTN size, CHAR8 **content) { + EFI_FILE_HANDLE handle; + CHAR8 *buf; + UINTN buflen; + EFI_STATUS err; + UINTN len; + + err = uefi_call_wrapper(dir->Open, 5, dir, &handle, name, EFI_FILE_MODE_READ, 0ULL); + if (EFI_ERROR(err)) + return err; + + if (size == 0) { + EFI_FILE_INFO *info; + + info = LibFileInfo(handle); + buflen = info->FileSize+1; + FreePool(info); + } else + buflen = size; + + if (off > 0) { + err = uefi_call_wrapper(handle->SetPosition, 2, handle, off); + if (EFI_ERROR(err)) + return err; + } + + buf = AllocatePool(buflen); + err = uefi_call_wrapper(handle->Read, 3, handle, &buflen, buf); + if (!EFI_ERROR(err)) { + buf[buflen] = '\0'; + *content = buf; + len = buflen; + } else { + len = err; + FreePool(buf); + } + + uefi_call_wrapper(handle->Close, 1, handle); + return len; +} diff --git a/src/sd-boot/util.h b/src/sd-boot/util.h new file mode 100644 index 0000000000..efaafd7492 --- /dev/null +++ b/src/sd-boot/util.h @@ -0,0 +1,44 @@ +/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ + +/* + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * Copyright (C) 2012-2013 Kay Sievers + * Copyright (C) 2012 Harald Hoyer + */ + +#ifndef __SDBOOT_UTIL_H +#define __SDBOOT_UTIL_H + +#include +#include + +#define ELEMENTSOF(x) (sizeof(x)/sizeof((x)[0])) + +UINT64 ticks_read(void); +UINT64 ticks_freq(void); +UINT64 time_usec(void); + +EFI_STATUS efivar_set(CHAR16 *name, CHAR16 *value, BOOLEAN persistent); +EFI_STATUS efivar_set_raw(const EFI_GUID *vendor, CHAR16 *name, CHAR8 *buf, UINTN size, BOOLEAN persistent); +EFI_STATUS efivar_set_int(CHAR16 *name, UINTN i, BOOLEAN persistent); +VOID efivar_set_time_usec(CHAR16 *name, UINT64 usec); + +EFI_STATUS efivar_get(CHAR16 *name, CHAR16 **value); +EFI_STATUS efivar_get_raw(const EFI_GUID *vendor, CHAR16 *name, CHAR8 **buffer, UINTN *size); +EFI_STATUS efivar_get_int(CHAR16 *name, UINTN *i); + +CHAR8 *strchra(CHAR8 *s, CHAR8 c); +CHAR16 *stra_to_path(CHAR8 *stra); +CHAR16 *stra_to_str(CHAR8 *stra); + +INTN file_read(EFI_FILE_HANDLE dir, CHAR16 *name, UINTN off, UINTN size, CHAR8 **content); +#endif diff --git a/test/splash.bmp b/test/splash.bmp new file mode 100644 index 0000000000000000000000000000000000000000..27247f7a22b61f5571ba5a6069c4fe52845d65cd GIT binary patch literal 289238 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

Kx6+UJioe$XggWrZTx7a4~LYp^VZYx&~ z+M-2Et$*eM>L91FUpCfhAkA#!@y{{MY^$xUb^RpLNb}|`rAGTwm+#ts;jPRR{;tiM zcb`?&!bdUkY##kHG3l$Z_yZq{C3cciyE-rlCx=V$zz56&Vw5zL&y{;dDb`ayN;z}I zd*T%5O-;HW7W>+LvBX_}Gt4RFaKtFjDQOpJs>r0P#<3?hd*1D6oA27n)!)LtZYu}B zf_(w|EOxWIJ-FspbkH}bt@<-twfdX3Wch8@)AtjrYkJgj3e%74IW(+|lta=!<5Rq* zt73_Jc%8S3M|v;fu77`?$9yYXf(L>JJ^&u@Yu0lrGan@_x`fK-*MMz{`tp^KdgNH0dm@3g+)ta zsXKXH7m?p@@;n+Dx9PowOYlJOzz5d@o>M*_<#STXn|n?vH>EmGu}ZTxVO23Xs_MD1 z*iARY61Uw!EJe-G{r?#q^Do1Deu@Lrv7l$nZ@Z4_LB*h7@;J|9--;#fy&{&}Ig9f_ zdCmG5=;H*mRR!kt7C~D+)PaP#dopm$!T55_b-Sgf4C?XyS>6=Mx=7EP zsf_}Ua7efW4}63@;92E!H<_%;r6p|GSwC8p)x+qVO3E9ta+IKOV^B)w|u!WSGDG zZuiUl-urp}aNpp8;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q z;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q z;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q e;DO+Q;DO+Q;DO+Q;DO+Q;DO+Q;DLX;2mUXkcAf(O literal 0 HcmV?d00001 diff --git a/test/test-efi-create-disk.sh b/test/test-efi-create-disk.sh new file mode 100755 index 0000000000..07595b7214 --- /dev/null +++ b/test/test-efi-create-disk.sh @@ -0,0 +1,42 @@ +#!/bin/bash -e + +# create GPT table with EFI System Partition +rm -f test-efi-disk.img +dd if=/dev/null of=test-efi-disk.img bs=1M seek=512 count=1 +parted --script test-efi-disk.img "mklabel gpt" "mkpart ESP fat32 1MiB 511MiB" "set 1 boot on" + +# create FAT32 file system +LOOP=$(losetup --show -f -P test-efi-disk.img) +mkfs.vfat -F32 ${LOOP}p1 +mkdir -p mnt +mount ${LOOP}p1 mnt + +mkdir -p mnt/EFI/{Boot,systemd} +cp sd-bootx64.efi mnt/EFI/Boot/bootx64.efi +cp test/splash.bmp mnt/EFI/systemd/ + +[ -e /boot/shellx64.efi ] && cp /boot/shellx64.efi mnt/ + +mkdir mnt/EFI/Linux +echo -n "foo=yes bar=no root=/dev/fakeroot debug rd.break=initqueue" > mnt/cmdline.txt +objcopy \ + --add-section .osrel=/etc/os-release --change-section-vma .osrel=0x20000 \ + --add-section .cmdline=mnt/cmdline.txt --change-section-vma .cmdline=0x30000 \ + --add-section .linux=/boot/$(cat /etc/machine-id)/$(uname -r)/linux --change-section-vma .linux=0x40000 \ + --add-section .initrd=/boot/$(cat /etc/machine-id)/$(uname -r)/initrd --change-section-vma .initrd=0x3000000 \ + linuxx64.efi.stub mnt/EFI/Linux/linux-test.efi + +# install entries +mkdir -p mnt/loader/entries +echo -e "timeout 3\nsplash /EFI/systemd/splash.bmp\n" > mnt/loader/loader.conf +echo -e "title Test\nefi /test\n" > mnt/loader/entries/test.conf +echo -e "title Test2\nlinux /test2\noptions option=yes word number=1000 more\n" > mnt/loader/entries/test2.conf +echo -e "title Test3\nlinux /test3\n" > mnt/loader/entries/test3.conf +echo -e "title Test4\nlinux /test4\n" > mnt/loader/entries/test4.conf +echo -e "title Test5\nefi /test5\n" > mnt/loader/entries/test5.conf +echo -e "title Test6\nlinux /test6\n" > mnt/loader/entries/test6.conf + +sync +umount mnt +rmdir mnt +losetup -d $LOOP