From b5bea21f15a2f1e5b4a2e247bf49afc683aee683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Paulo=20Rechi=20Vita?= Date: Mon, 20 Sep 2021 23:18:29 -0400 Subject: [PATCH] fallback: Detect and break out of reboot loops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The firmware on some Acer machines (and maybe others) always resets the boot entries and BootOrder variable to what was defined in the firmware setup program, overriding any external changes (including the changes made by fallback). Before shim cared about TPMs this was not a problem in practice, as fallback would create and chain-load a boot entry for the OS on every boot. However, since commit 431b8a2e75 the system is restarted if a TPM is detected on the system, triggering an infinite reboot loop in systems with such firmware. This is a known problem which has been previously reported on https://github.com/rhboot/shim/issues/128 More recently, the problem has been addressed by commit a5db51a52e, which presents a screen with a countdown to the user, where they can interrupt boot and choose to have fallback always chain-load the new entry instead of restarting the system, to break out of the reboot loop. While this solution works, it has a few shortcomings: 1. It makes an otherwise glitch-free boot process not smooth anymore. 2. The message presented is not accessible / potentially scary for non-technical users: if they press a key to interrupt the boot process, the meaning of each option is not really clear for users not familiar with how shim and fallback work. 3. The whole experience is made a bit worse by the fact that after selecting "Continue boot" / "Always continue boot", the screen will remain frozen until something else draw on the framebuffer. If GRUB is configured to be quiet, for a glitch-free boot, this may last several seconds until the kernel has started and loaded the manufacturer logo from BGRT, which gives the impression that the whole boot process froze. 4. This Boot Option Restoration screen overwrites all the debug information printed before it is displayed, essentially neutering FALLBACK_VERBOSE or SHIM_VERBOSE and making it impossible to enable debug without rebuilding fallback. This commit tries to automatically detect and break out of the reboot loop without requiring any user interaction. To achieve this, a boot counter is stored in an EFI variable and incremented every time fallback is about to reboot the system. If the counter ever reaches a maximum value configurable at build time (currently default to 3), another EFI variable is set to tell fallback to always chain-load the new entry (FB_NO_REBOOT, to make is backwards compatible with the previous solution). The counter is then reset when shim is started and knows it is not going to load fallback. Fixes: https://github.com/rhboot/shim/issues/418 Signed-off-by: João Paulo Rechi Vita --- Makefile | 6 ++++++ fallback.c | 41 ++++++++++++++++++++++++++++++++++++++++- shim.c | 5 +++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3b59fc693..193a235a1 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,12 @@ ifneq ($(origin FALLBACK_VERBOSE_WAIT), undefined) CFLAGS += -DFALLBACK_VERBOSE_WAIT=$(FALLBACK_VERBOSE_WAIT) endif +ifneq ($(origin FALLBACK_MAX_BOOT_ATTEMPTS), undefined) + CFLAGS += -DFALLBACK_MAX_BOOT_ATTEMPTS=$(FALLBACK_MAX_BOOT_ATTEMPTS) +else + CFLAGS += -DFALLBACK_MAX_BOOT_ATTEMPTS=3 +endif + all: confcheck $(TARGETS) confcheck: diff --git a/fallback.c b/fallback.c index c7805cb84..2d2a17fd0 100644 --- a/fallback.c +++ b/fallback.c @@ -5,6 +5,7 @@ */ #include "shim.h" +#define BOOT_COUNT L"FB_BOOT_COUNT" #define NO_REBOOT L"FB_NO_REBOOT" EFI_LOADED_IMAGE *this_image = NULL; @@ -1026,6 +1027,31 @@ try_start_first_option(EFI_HANDLE parent_image_handle) return efi_status; } +static UINT32 +get_boot_count(void) +{ + EFI_STATUS efi_status; + UINT32 boot_count; + UINTN size = sizeof(UINT32); + + efi_status = RT->GetVariable(BOOT_COUNT, &SHIM_LOCK_GUID, + NULL, &size, &boot_count); + if (!EFI_ERROR(efi_status)) { + return boot_count; + } + return 0; +} + +static EFI_STATUS +set_boot_count(UINT32 boot_count) +{ + return RT->SetVariable(BOOT_COUNT, &SHIM_LOCK_GUID, + EFI_VARIABLE_NON_VOLATILE | + EFI_VARIABLE_BOOTSERVICE_ACCESS | + EFI_VARIABLE_RUNTIME_ACCESS, + sizeof(UINT32), &boot_count); +} + static UINT32 get_fallback_no_reboot(void) { @@ -1041,7 +1067,6 @@ get_fallback_no_reboot(void) return 0; } -#ifndef FALLBACK_NONINTERACTIVE static EFI_STATUS set_fallback_no_reboot(void) { @@ -1055,6 +1080,7 @@ set_fallback_no_reboot(void) return efi_status; } +#ifndef FALLBACK_NONINTERACTIVE static int draw_countdown(void) { @@ -1121,6 +1147,7 @@ EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) { EFI_STATUS efi_status; + UINT32 boot_count = 0; InitializeLib(image, systab); @@ -1153,6 +1180,17 @@ efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) VerbosePrint(L"tpm not present, starting the first image\n"); try_start_first_option(image); } else { + boot_count = get_boot_count(); + boot_count++; + + VerbosePrint(L"fallback boot attempt number %d\n", boot_count); + + if (boot_count >= FALLBACK_MAX_BOOT_ATTEMPTS) { + VerbosePrint(L"reboot loop detected: booted fallback %d times in a row, setting %s\n", + FALLBACK_MAX_BOOT_ATTEMPTS, NO_REBOOT); + set_fallback_no_reboot(); + } + if (get_fallback_no_reboot() == 1) { VerbosePrint(L"%s is set, starting the first image\n", NO_REBOOT); @@ -1180,6 +1218,7 @@ efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *systab) } console_print(L"Reset System\n"); + set_boot_count(boot_count); if (get_fallback_verbose()) { int fallback_verbose_wait = 500000; /* default to 0.5s */ diff --git a/shim.c b/shim.c index 1e95e6df1..f9cb39250 100644 --- a/shim.c +++ b/shim.c @@ -1102,6 +1102,11 @@ EFI_STATUS init_grub(EFI_HANDLE image_handle) EFI_STATUS efi_status; int use_fb = should_use_fallback(image_handle); + if (!use_fb) + /* If we are not going through fallback, + * clear the fallback reboot counter */ + LibDeleteVariable(L"FB_BOOT_COUNT", &SHIM_LOCK_GUID); + efi_status = start_image(image_handle, use_fb ? FALLBACK :second_stage); if (efi_status == EFI_SECURITY_VIOLATION || efi_status == EFI_ACCESS_DENIED) {