diff --git a/src/boot/boot.c b/src/boot/boot.c index 91c82b3536d..76c9f8fb47c 100644 --- a/src/boot/boot.c +++ b/src/boot/boot.c @@ -10,6 +10,7 @@ #include "efi-efivars.h" #include "efi-log.h" #include "efi-string-table.h" +#include "efivar-erase.h" #include "efivars.h" #include "export-vars.h" #include "graphics.h" @@ -3287,6 +3288,7 @@ static void export_loader_variables( EFI_LOADER_FEATURE_TPM2_ACTIVE_PCR_BANKS | EFI_LOADER_FEATURE_KEYBOARD_LAYOUT | EFI_LOADER_FEATURE_SMBIOS_MEASURED | + EFI_LOADER_FEATURE_FACTORY_RESET_VARIABLES | 0; assert(loaded_image); @@ -3465,6 +3467,10 @@ static EFI_STATUS run(EFI_HANDLE image) { config_load_all_entries(&config, loaded_image, loaded_image_path, root_dir); (void) sysfail_process(&config); + /* If a factory reset of our EFI variables has been requested, do so now, after asking the user. This + * reboots if the variables are erased. */ + (void) factory_reset_variables_process(); + if (config.n_entries == 0) return log_error_status( EFI_NOT_FOUND, diff --git a/src/boot/efi-efivars.h b/src/boot/efi-efivars.h index 53440a0b483..9976c431280 100644 --- a/src/boot/efi-efivars.h +++ b/src/boot/efi-efivars.h @@ -12,6 +12,11 @@ #define LOADER_GUID \ { 0x4a67b082, 0x0a4c, 0x41cf, { 0xb6, 0xc7, 0x44, 0x0b, 0x29, 0xbb, 0x8c, 0x4f } } +/* Allocated random UUID, for systemd's own EFI variables that are not part of the Boot Loader Interface, + * such as FactoryResetRequest. */ +#define SYSTEMD_GUID \ + { 0x8cf2644b, 0x4b0b, 0x428f, { 0x93, 0x87, 0x6d, 0x87, 0x60, 0x50, 0xdc, 0x67 } } + EFI_STATUS efivar_set_str16(const EFI_GUID *vendor, const char16_t *name, const char16_t *value, uint32_t flags); EFI_STATUS efivar_set_raw(const EFI_GUID *vendor, const char16_t *name, const void *buf, size_t size, uint32_t flags); EFI_STATUS efivar_set_uint64_str16(const EFI_GUID *vendor, const char16_t *name, uint64_t i, uint32_t flags); diff --git a/src/boot/efi.h b/src/boot/efi.h index 6f08bacb2db..d932f3dfdcb 100644 --- a/src/boot/efi.h +++ b/src/boot/efi.h @@ -409,7 +409,10 @@ typedef struct { uint32_t *Attributes, size_t *DataSize, void *Data); - void *GetNextVariableName; + EFI_STATUS (EFIAPI *GetNextVariableName)( + size_t *VariableNameSize, + char16_t *VariableName, + EFI_GUID *VendorGuid); EFI_STATUS (EFIAPI *SetVariable)( char16_t *VariableName, EFI_GUID *VendorGuid, diff --git a/src/boot/efivar-erase.c b/src/boot/efivar-erase.c new file mode 100644 index 00000000000..07c0bcff54a --- /dev/null +++ b/src/boot/efivar-erase.c @@ -0,0 +1,210 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "console.h" +#include "efi-efivars.h" +#include "efi-log.h" +#include "efi-string.h" +#include "efivar-erase.h" +#include "proto/simple-text-io.h" +#include "strv.h" +#include "util.h" + +/* Upper limit on the number of variables we enumerate, as protection against firmware that never returns + * EFI_NOT_FOUND from GetNextVariableName() */ +#define ENUMERATE_VARIABLES_MAX 65536U + +static EFI_STATUS collect_variables(const EFI_GUID *vendor, char16_t ***ret) { + _cleanup_strv_free_ char16_t **l = NULL; + size_t n = 0, buf_size = 256 * sizeof(char16_t); + _cleanup_free_ char16_t *buf = NULL; + EFI_GUID guid = {}; + EFI_STATUS err; + + assert(vendor); + assert(ret); + + /* Note that the UEFI spec leaves undefined what happens if variables are deleted while they are + * enumerated. Hence we first collect all names, and only delete them afterwards. */ + + buf = xmalloc(buf_size); + buf[0] = 0; /* Empty string starts enumeration */ + + for (unsigned i = 0;; i++) { + if (i >= ENUMERATE_VARIABLES_MAX) + return log_error_status(EFI_ABORTED, "Too many EFI variables, refusing to enumerate further."); + + size_t size = buf_size; + err = RT->GetNextVariableName(&size, buf, &guid); + if (err == EFI_NOT_FOUND) + break; + if (err == EFI_BUFFER_TOO_SMALL) { + if (size <= buf_size) + return log_error_status(err, "Firmware reported invalid EFI variable name size."); + + /* Retain the current name, enumeration continues from it */ + buf = xrealloc(buf, buf_size, size); + buf_size = size; + continue; + } + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to enumerate EFI variables: %m"); + + if (!efi_guid_equal(&guid, vendor)) + continue; + + l = xrealloc(l, n == 0 ? 0 : (n + 1) * sizeof(char16_t*), (n + 2) * sizeof(char16_t*)); + l[n++] = xstrdup16(buf); + l[n] = NULL; + } + + *ret = TAKE_PTR(l); + return EFI_SUCCESS; +} + +static EFI_STATUS erase_variable(const EFI_GUID *vendor, const char16_t *name) { + uint32_t attributes; + EFI_STATUS err; + + assert(vendor); + assert(name); + + /* Some firmware implementations refuse deletion if the attributes don't match the existing + * variable, hence query them first, and pass them along. */ + err = efivar_get_raw_full(vendor, name, &attributes, /* ret_data= */ NULL, /* ret_size= */ NULL); + if (err == EFI_NOT_FOUND) + return EFI_SUCCESS; + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to query EFI variable %ls: %m", name); + + err = RT->SetVariable((char16_t *) name, (EFI_GUID *) vendor, attributes, 0, NULL); + if (err != EFI_SUCCESS) + return log_error_status(err, "Failed to erase EFI variable %ls: %m", name); + + log_debug("Erased EFI variable %ls.", name); + return EFI_SUCCESS; +} + +EFI_STATUS efivar_erase_namespace(const EFI_GUID *vendor, const char16_t *const *keep) { + _cleanup_strv_free_ char16_t **l = NULL; + EFI_STATUS err, ret = EFI_SUCCESS; + + assert(vendor); + + err = collect_variables(vendor, &l); + if (err != EFI_SUCCESS) + return err; + + STRV_FOREACH(i, l) { + if (strv_contains((char16_t**) keep, *i)) + continue; + + err = erase_variable(vendor, *i); + if (err != EFI_SUCCESS && ret == EFI_SUCCESS) + ret = err; + } + + return ret; +} + +static EFI_STATUS ask_confirmation(bool *ret) { + EFI_STATUS err; + + assert(ret); + + clear_screen(COLOR_NORMAL); + + printf("A factory reset has been requested.\n" + "\n" + "All EFI variables owned by the boot loader and by systemd are about to be erased,\n" + "including boot menu defaults, one-shot entries and the system token. The system\n" + "will reboot afterwards.\n" + "\n" + "Erase these EFI variables? (y/n) "); + + for (;;) { + uint64_t key; + + err = console_key_read(&key, /* timeout_usec= */ UINT64_MAX); + if (err == EFI_NOT_READY) + continue; + if (err != EFI_SUCCESS) { + printf("\n"); + return log_error_status(err, "Error waiting for user input: %m"); + } + + switch (key) { + + case KEYPRESS(0, 0, 'y'): + case KEYPRESS(0, 0, 'Y'): + printf("y\n"); + *ret = true; + return EFI_SUCCESS; + + case KEYPRESS(0, 0, 'n'): + case KEYPRESS(0, 0, 'N'): + case KEYPRESS(0, SCAN_ESC, 0): + printf("n\n"); + *ret = false; + return EFI_SUCCESS; + + default: + /* Ignore anything else, the user has to make an explicit choice */ + break; + } + } +} + +EFI_STATUS factory_reset_variables_process(void) { + static const char16_t *const keep_loader[] = { + u"LoaderFactoryResetVariables", /* erased last, see below */ + NULL, + }; + static const char16_t *const keep_systemd[] = { + /* The OS needs this on the next boot to reset its partitions, hence don't erase it */ + u"FactoryResetRequest", + NULL, + }; + bool b, confirmed; + EFI_STATUS err; + + err = efivar_get_boolean_str16(MAKE_GUID_PTR(LOADER), u"LoaderFactoryResetVariables", &b); + if (err == EFI_NOT_FOUND) + return EFI_SUCCESS; + if (err == EFI_INVALID_PARAMETER) { + /* Remove the invalid request, so that we don't complain about it on every boot */ + log_warning("Invalid LoaderFactoryResetVariables EFI variable, removing."); + (void) erase_variable(MAKE_GUID_PTR(LOADER), u"LoaderFactoryResetVariables"); + return EFI_SUCCESS; + } + if (err != EFI_SUCCESS) { + log_warning_status(err, "Failed to read LoaderFactoryResetVariables EFI variable, ignoring: %m"); + return EFI_SUCCESS; + } + if (!b) + return EFI_SUCCESS; + + err = ask_confirmation(&confirmed); + if (err != EFI_SUCCESS) + /* Leave the request in place, so that we ask again on the next boot */ + return err; + if (!confirmed) { + log_info("Erasing EFI variables declined by user."); + + /* Drop the request, so that we don't ask again on every boot */ + (void) erase_variable(MAKE_GUID_PTR(LOADER), u"LoaderFactoryResetVariables"); + return EFI_SUCCESS; + } + + log_info("Erasing EFI variables of boot loader and systemd."); + + (void) efivar_erase_namespace(MAKE_GUID_PTR(LOADER), keep_loader); + (void) efivar_erase_namespace(MAKE_GUID_PTR(SYSTEMD), keep_systemd); + + /* Erase the request itself last, so that if we are interrupted half-way we'll try again on the next + * boot. Do this also if erasing some variables failed, so that we don't end up in a reboot loop. */ + (void) erase_variable(MAKE_GUID_PTR(LOADER), u"LoaderFactoryResetVariables"); + + log_wait(); + RT->ResetSystem(EfiResetCold, EFI_SUCCESS, 0, NULL); + assert_not_reached(); +} diff --git a/src/boot/efivar-erase.h b/src/boot/efivar-erase.h new file mode 100644 index 00000000000..29724663672 --- /dev/null +++ b/src/boot/efivar-erase.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "efi.h" + +EFI_STATUS efivar_erase_namespace(const EFI_GUID *vendor, const char16_t *const *keep); + +/* Checks the LoaderFactoryResetVariables EFI variable, and if set erases all EFI variables in the LOADER + * and SYSTEMD namespaces (with a few exceptions) and reboots, after asking the user for confirmation. Returns + * only if no erasing took place. */ +EFI_STATUS factory_reset_variables_process(void); diff --git a/src/boot/meson.build b/src/boot/meson.build index 79ec26795d2..c82340205c1 100644 --- a/src/boot/meson.build +++ b/src/boot/meson.build @@ -320,6 +320,7 @@ libefi_sources = files( 'efi-firmware.c', 'efi-log.c', 'efi-string.c', + 'efivar-erase.c', 'export-vars.c', 'graphics.c', 'hii.c', diff --git a/src/bootctl/bootctl-status.c b/src/bootctl/bootctl-status.c index bd8d47a2e90..b6f1d2dead9 100644 --- a/src/bootctl/bootctl-status.c +++ b/src/bootctl/bootctl-status.c @@ -411,6 +411,7 @@ int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) { { EFI_LOADER_FEATURE_TPM2_ACTIVE_PCR_BANKS, "Loader reports active TPM2 PCR banks" }, { EFI_LOADER_FEATURE_KEYBOARD_LAYOUT, "Loader reports firmware keyboard layout" }, { EFI_LOADER_FEATURE_SMBIOS_MEASURED, "Loader measures SMBIOS information" }, + { EFI_LOADER_FEATURE_FACTORY_RESET_VARIABLES, "Loader erases EFI variables on request" }, }; static const struct { uint64_t flag; diff --git a/src/fundamental/efivars.h b/src/fundamental/efivars.h index 88bc97d75d6..d859b201761 100644 --- a/src/fundamental/efivars.h +++ b/src/fundamental/efivars.h @@ -31,6 +31,7 @@ #define EFI_LOADER_FEATURE_ENTRY_PREFERRED (UINT64_C(1) << 19) #define EFI_LOADER_FEATURE_KEYBOARD_LAYOUT (UINT64_C(1) << 20) #define EFI_LOADER_FEATURE_SMBIOS_MEASURED (UINT64_C(1) << 21) +#define EFI_LOADER_FEATURE_FACTORY_RESET_VARIABLES (UINT64_C(1) << 22) /* Features of the stub, i.e. systemd-stub */ #define EFI_STUB_FEATURE_REPORT_BOOT_PARTITION (UINT64_C(1) << 0)