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 e8a4085a3a5..75699ca33b9 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 bf04d318403..d739445e836 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 97a29d63fd3..277addb8c32 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 00000000000..f17b4278eb3 --- /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 00000000000..55b0da46789 --- /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 00000000000..6206c803175 --- /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 00000000000..5c7808a0676 --- /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 00000000000..11305b8d06e --- /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 00000000000..8665afde977 --- /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 00000000000..809c69310e3 --- /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 00000000000..aff69a97784 --- /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 00000000000..e6fedbc929f --- /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 00000000000..ca2f9a2508e --- /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 00000000000..94039ead316 --- /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 00000000000..e18faac669a --- /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 00000000000..5678b50c310 --- /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 00000000000..efaafd74928 --- /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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 OhYlS&bm;i=#Qy-JcAf(O literal 0 Hc-jL100001 diff --git a/test/test-efi-create-disk.sh b/test/test-efi-create-disk.sh new file mode 100755 index 00000000000..07595b72148 --- /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 -- 2.47.3