sd-boot: add support for a sysfail entry (#34856)

Add support for a sysfail boot entry. Sysfail boot entries can be used
for optional tweaking the automatic selection order in case a failure
state of the system in some form is detected (boot firmware failure
etc).

The EFI variable `LoaderEntrySysFail` contains the sysfail boot loader
entry to use. It can be set using bootctl:
```
$ bootctl set-sysfail sysfail.conf
```

The `LoaderEntrySysFail` EFI variable would be unset automatically
during next boot by `systemd-boot-clear-sysfail.service` if no system
failure occured, otherwise it would be kept as it is and a system
failure reason will be saved to `LoaderSysFailReason` EFI variable.

`sysfail_check()` expected to be extented to support possibleconditions
when we should boot sysfail("recovery") boot entry.

Also add support for using a sysfail boot entry in case of UEFI firmware
capsule update failure [1]. The status of a firmware update is obtained
from the EFI System Resource Table (ESRT), which provides an optional
mechanism for identifying device and system firmware resources for the
purposes of targeting firmware updates to those resources.
    
Current implementation uses the value of LastAttemptStatus field from
ESRT, which describes the result of the last firmware update attempt for
the firmware resource entry. The field is updated each time an
`UpdateCapsule()` is attempted for an ESRT entry and is preserved across
reboots (non-volatile).
    
This can be be used in setups with support for A/B OTA updates, where
the boot firmware and Linux/RootFS might be updated synchronously.
    
The check is activated by adding "sysfail-firmware-upd" to loader.conf
    
[1]
https://uefi.org/specs/UEFI/2.10/23_Firmware_Update_and_Reporting.html
This commit is contained in:
Lennart Poettering
2025-05-13 11:23:26 +02:00
committed by GitHub
19 changed files with 260 additions and 12 deletions
+36
View File
@@ -30,6 +30,7 @@
#include "shim.h"
#include "smbios.h"
#include "strv-fundamental.h"
#include "sysfail.h"
#include "ticks.h"
#include "tpm2-pcr.h"
#include "uki.h"
@@ -136,6 +137,7 @@ typedef struct {
char16_t *entry_default_efivar;
char16_t *entry_oneshot;
char16_t *entry_saved;
char16_t *entry_sysfail;
bool editor;
bool auto_entries;
bool auto_firmware;
@@ -149,6 +151,7 @@ typedef struct {
bool use_saved_entry;
bool use_saved_entry_efivar;
bool beep;
bool sysfail_occured;
int64_t console_mode;
int64_t console_mode_efivar;
} Config;
@@ -328,6 +331,8 @@ static void print_status(Config *config, char16_t *loaded_image_path) {
printf(" default (EFI var): %ls\n", config->entry_default_efivar);
if (config->entry_oneshot)
printf(" default (one-shot): %ls\n", config->entry_oneshot);
if (config->entry_sysfail)
printf(" sysfail: %ls\n", config->entry_sysfail);
if (config->entry_saved)
printf(" saved entry: %ls\n", config->entry_saved);
printf(" editor: %ls\n", yes_no(config->editor));
@@ -1499,11 +1504,13 @@ static void config_load_defaults(Config *config, EFI_FILE *root_dir) {
(void) efivar_unset(MAKE_GUID_PTR(LOADER), u"LoaderEntryOneShot", EFI_VARIABLE_NON_VOLATILE);
(void) efivar_get_str16(MAKE_GUID_PTR(LOADER), u"LoaderEntryDefault", &config->entry_default_efivar);
(void) efivar_get_str16(MAKE_GUID_PTR(LOADER), u"LoaderEntrySysFail", &config->entry_sysfail);
strtolower16(config->entry_default_config);
strtolower16(config->entry_default_efivar);
strtolower16(config->entry_oneshot);
strtolower16(config->entry_saved);
strtolower16(config->entry_sysfail);
config->use_saved_entry = streq16(config->entry_default_config, u"@saved");
config->use_saved_entry_efivar = streq16(config->entry_default_efivar, u"@saved");
@@ -1686,11 +1693,38 @@ static size_t config_find_entry(Config *config, const char16_t *pattern) {
return IDX_INVALID;
}
static bool sysfail_process(Config *config) {
SysFailType sysfail_type;
assert(config);
sysfail_type = sysfail_check();
if (sysfail_type == SYSFAIL_NO_FAILURE)
return false;
/* Store reason string in LoaderSysFailReason EFI variable */
const char16_t *reason_str = sysfail_get_error_str(sysfail_type);
if (reason_str)
(void) efivar_set_str16(MAKE_GUID_PTR(LOADER), u"LoaderSysFailReason", reason_str, 0);
config->sysfail_occured = true;
return true;
}
static void config_select_default_entry(Config *config) {
size_t i;
assert(config);
if (config->sysfail_occured) {
i = config_find_entry(config, config->entry_sysfail);
if (i != IDX_INVALID) {
config->idx_default = i;
return;
}
}
i = config_find_entry(config, config->entry_oneshot);
if (i != IDX_INVALID) {
config->idx_default = i;
@@ -2685,6 +2719,7 @@ static void config_free(Config *config) {
free(config->entry_default_efivar);
free(config->entry_oneshot);
free(config->entry_saved);
free(config->entry_sysfail);
}
static void config_write_entries_to_variable(Config *config) {
@@ -2983,6 +3018,7 @@ static EFI_STATUS run(EFI_HANDLE image) {
_cleanup_free_ char16_t *loaded_image_path = NULL;
(void) device_path_to_str(loaded_image->FilePath, &loaded_image_path);
config_load_all_entries(&config, loaded_image, loaded_image_path, root_dir);
(void) sysfail_process(&config);
if (config.n_entries == 0)
return log_error_status(
+27
View File
@@ -126,6 +126,16 @@ typedef uint64_t EFI_PHYSICAL_ADDRESS;
#define EFI_CUSTOM_MODE_ENABLE_GUID \
GUID_DEF(0xc076ec0c, 0x7028, 0x4399, 0xa0, 0x72, 0x71, 0xee, 0x5c, 0x44, 0x8b, 0x9f)
#define EFI_SYSTEM_RESOURCE_TABLE_GUID \
GUID_DEF(0xb122a263, 0x3661, 0x4f68, 0x99, 0x29, 0x78, 0xf8, 0xb0, 0xd6, 0x21, 0x80)
/* EFI System Resource Table (ESRT) Firmware Type Definitions */
#define ESRT_FW_TYPE_UNKNOWN 0x00000000U
#define ESRT_FW_TYPE_SYSTEMFIRMWARE 0x00000001U
#define ESRT_FW_TYPE_DEVICEFIRMWARE 0x00000002U
#define ESRT_FW_TYPE_UEFIDRIVER 0x00000003U
#define LAST_ATTEMPT_STATUS_SUCCESS 0x00000000U
#define EVT_TIMER 0x80000000U
#define EVT_RUNTIME 0x40000000U
@@ -426,6 +436,23 @@ typedef struct {
} *ConfigurationTable;
} EFI_SYSTEM_TABLE;
typedef struct {
EFI_GUID FwClass;
uint32_t FwType;
uint32_t FwVersion;
uint32_t LowestSupportedFwVersion;
uint32_t CapsuleFlags;
uint32_t LastAttemptVersion;
uint32_t LastAttemptStatus;
} EFI_SYSTEM_RESOURCE_ENTRY;
typedef struct {
uint32_t FwResourceCount;
uint32_t FwResourceCountMax;
uint64_t FwResourceVersion;
EFI_SYSTEM_RESOURCE_ENTRY Entries[];
} EFI_SYSTEM_RESOURCE_TABLE;
extern EFI_SYSTEM_TABLE *ST;
extern EFI_BOOT_SERVICES *BS;
extern EFI_RUNTIME_SERVICES *RT;
+1
View File
@@ -308,6 +308,7 @@ libefi_sources = files(
'secure-boot.c',
'shim.c',
'smbios.c',
'sysfail.c',
'ticks.c',
'url-discovery.c',
'util.c',
+39
View File
@@ -0,0 +1,39 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sysfail.h"
#include "util.h"
static bool firmware_update_has_failed(void) {
const EFI_SYSTEM_RESOURCE_TABLE *esrt_table;
const EFI_SYSTEM_RESOURCE_ENTRY *esrt_entries;
esrt_table = find_configuration_table(MAKE_GUID_PTR(EFI_SYSTEM_RESOURCE_TABLE));
if (!esrt_table)
return false;
esrt_entries = esrt_table->Entries;
FOREACH_ARRAY(esrt_entry, esrt_entries, esrt_table->FwResourceCount)
if (esrt_entry->FwType == ESRT_FW_TYPE_SYSTEMFIRMWARE)
return esrt_entry->LastAttemptStatus != LAST_ATTEMPT_STATUS_SUCCESS;
return false;
}
SysFailType sysfail_check(void) {
if (firmware_update_has_failed())
return SYSFAIL_FIRMWARE_UPDATE;
return SYSFAIL_NO_FAILURE;
}
const char16_t* sysfail_get_error_str(SysFailType fail_type) {
switch (fail_type) {
case SYSFAIL_NO_FAILURE:
return NULL;
case SYSFAIL_FIRMWARE_UPDATE:
return u"firmware-updare-failure";
default:
assert_not_reached();
}
}
+13
View File
@@ -0,0 +1,13 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "efivars-fundamental.h"
typedef enum SysFailType {
SYSFAIL_NO_FAILURE,
SYSFAIL_FIRMWARE_UPDATE,
_SYSFAIL_MAX,
} SysFailType;
SysFailType sysfail_check(void);
const char16_t* sysfail_get_error_str(SysFailType fail_type);
+1
View File
@@ -1224,6 +1224,7 @@ static int remove_loader_variables(void) {
EFI_LOADER_VARIABLE_STR("LoaderConfigTimeout"),
EFI_LOADER_VARIABLE_STR("LoaderConfigTimeoutOneShot"),
EFI_LOADER_VARIABLE_STR("LoaderEntryDefault"),
EFI_LOADER_VARIABLE_STR("LoaderEntrySysFail"),
EFI_LOADER_VARIABLE_STR("LoaderEntryLastBooted"),
EFI_LOADER_VARIABLE_STR("LoaderEntryOneShot"),
EFI_LOADER_VARIABLE_STR("LoaderSystemToken")) {
+8
View File
@@ -89,6 +89,11 @@ static int parse_loader_entry_target_arg(const char *arg1, char16_t **ret_target
if (r < 0)
return log_error_errno(r, "Failed to get EFI variable 'LoaderEntryDefault': %m");
} else if (streq(arg1, "@sysfail")) {
r = efi_get_variable(EFI_LOADER_VARIABLE_STR("LoaderEntrySysFail"), NULL, (void *) ret_target, ret_target_size);
if (r < 0)
return log_error_errno(r, "Failed to get EFI variable 'LoaderEntrySysFail': %m");
} else if (arg1[0] == '@' && !streq(arg1, "@saved"))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unsupported special entry identifier: %s", arg1);
else {
@@ -144,6 +149,9 @@ int verb_set_efivar(int argc, char *argv[], void *userdata) {
if (streq(argv[0], "set-default")) {
variable = EFI_LOADER_VARIABLE_STR("LoaderEntryDefault");
arg_parser = parse_loader_entry_target_arg;
} else if (streq(argv[0], "set-sysfail")) {
variable = EFI_LOADER_VARIABLE_STR("LoaderEntrySysFail");
arg_parser = parse_loader_entry_target_arg;
} else if (streq(argv[0], "set-oneshot")) {
variable = EFI_LOADER_VARIABLE_STR("LoaderEntryOneShot");
arg_parser = parse_loader_entry_target_arg;
+16 -9
View File
@@ -420,7 +420,7 @@ int verb_status(int argc, char *argv[], void *userdata) {
{ EFI_STUB_FEATURE_MULTI_PROFILE_UKI, "Stub understands profile selector" },
};
_cleanup_free_ char *fw_type = NULL, *fw_info = NULL, *loader = NULL, *loader_path = NULL, *stub = NULL, *stub_path = NULL,
*current_entry = NULL, *oneshot_entry = NULL, *default_entry = NULL;
*current_entry = NULL, *oneshot_entry = NULL, *default_entry = NULL, *sysfail_entry = NULL, *sysfail_reason = NULL;
uint64_t loader_features = 0, stub_features = 0;
int have;
@@ -435,6 +435,8 @@ int verb_status(int argc, char *argv[], void *userdata) {
(void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderEntrySelected"), &current_entry);
(void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderEntryOneShot"), &oneshot_entry);
(void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderEntryDefault"), &default_entry);
(void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderEntrySysFail"), &sysfail_entry);
(void) efi_get_variable_string_and_warn(EFI_LOADER_VARIABLE_STR("LoaderSysFailReason"), &sysfail_reason);
SecureBootMode secure = efi_get_secure_boot_mode();
printf("%sSystem:%s\n", ansi_underline(), ansi_normal());
@@ -484,7 +486,7 @@ int verb_status(int argc, char *argv[], void *userdata) {
if (loader) {
printf("%sCurrent Boot Loader:%s\n", ansi_underline(), ansi_normal());
printf(" Product: %s%s%s\n", ansi_highlight(), loader, ansi_normal());
printf(" Product: %s%s%s\n", ansi_highlight(), loader, ansi_normal());
for (size_t i = 0; i < ELEMENTSOF(loader_flags); i++)
print_yes_no_line(i == 0, FLAGS_SET(loader_features, loader_flags[i].flag), loader_flags[i].name);
@@ -502,23 +504,28 @@ int verb_status(int argc, char *argv[], void *userdata) {
SD_ID128_FORMAT_VAL(loader_partition_uuid),
SD_ID128_FORMAT_VAL(esp_uuid));
printf(" Partition: /dev/disk/by-partuuid/" SD_ID128_UUID_FORMAT_STR "\n",
printf(" Partition: /dev/disk/by-partuuid/" SD_ID128_UUID_FORMAT_STR "\n",
SD_ID128_FORMAT_VAL(loader_partition_uuid));
} else if (loader_path)
printf(" Partition: n/a\n");
printf(" Partition: n/a\n");
if (loader_path)
printf(" Loader: %s%s\n", glyph(GLYPH_TREE_RIGHT), strna(loader_path));
printf(" Loader: %s%s\n", glyph(GLYPH_TREE_RIGHT), strna(loader_path));
if (loader_url)
printf(" Net Boot URL: %s\n", loader_url);
printf(" Net Boot URL: %s\n", loader_url);
if (sysfail_entry)
printf("SysFail Reason: %s\n", sysfail_reason);
if (current_entry)
printf("Current Entry: %s\n", current_entry);
printf(" Current Entry: %s\n", current_entry);
if (default_entry)
printf("Default Entry: %s\n", default_entry);
printf(" Default Entry: %s\n", default_entry);
if (oneshot_entry && !streq_ptr(oneshot_entry, default_entry))
printf("OneShot Entry: %s\n", oneshot_entry);
printf(" OneShot Entry: %s\n", oneshot_entry);
if (sysfail_entry)
printf(" SysFail Entry: %s\n", sysfail_entry);
printf("\n");
}
+2
View File
@@ -259,6 +259,7 @@ static int help(int argc, char *argv[], void *userdata) {
"\n%3$sBoot Loader Interface Commands:%4$s\n"
" set-default ID Set default boot loader entry\n"
" set-oneshot ID Set default boot loader entry, for next boot only\n"
" set-sysfail ID Set boot loader entry used in case of a system failure\n"
" set-timeout SECONDS Set the menu timeout\n"
" set-timeout-oneshot SECONDS\n"
" Set the menu timeout for the next boot only\n"
@@ -660,6 +661,7 @@ static int bootctl_main(int argc, char *argv[]) {
{ "set-oneshot", 2, 2, 0, verb_set_efivar },
{ "set-timeout", 2, 2, 0, verb_set_efivar },
{ "set-timeout-oneshot", 2, 2, 0, verb_set_efivar },
{ "set-sysfail", 2, 2, 0, verb_set_efivar },
{ "random-seed", VERB_ANY, 1, 0, verb_random_seed },
{ "reboot-to-firmware", VERB_ANY, 2, 0, verb_reboot_to_firmware },
{}
+7
View File
@@ -457,6 +457,7 @@ void boot_config_free(BootConfig *config) {
free(config->entry_oneshot);
free(config->entry_default);
free(config->entry_selected);
free(config->entry_sysfail);
FOREACH_ARRAY(i, config->entries, config->n_entries)
boot_entry_free(i);
@@ -1437,6 +1438,12 @@ static int boot_load_efi_entry_pointers(BootConfig *config, bool skip_efivars) {
if (r < 0 && !IN_SET(r, -ENOENT, -ENODATA))
log_warning_errno(r, "Failed to read EFI variable \"LoaderEntrySelected\", ignoring: %m");
r = efi_get_variable_string(EFI_LOADER_VARIABLE_STR("LoaderEntrySysFail"), &config->entry_sysfail);
if (r == -ENOMEM)
return log_oom();
if (r < 0 && !IN_SET(r, -ENOENT, -ENODATA))
log_warning_errno(r, "Failed to read EFI variable \"LoaderEntrySysFail\", ignoring: %m");
return 1;
}
+1
View File
@@ -80,6 +80,7 @@ typedef struct BootConfig {
char *entry_oneshot;
char *entry_default;
char *entry_selected;
char *entry_sysfail;
BootEntry *entries;
size_t n_entries;