diff --git a/man/bootctl.xml b/man/bootctl.xml index c038c4686d..6db048b63b 100644 --- a/man/bootctl.xml +++ b/man/bootctl.xml @@ -102,7 +102,7 @@ - VALUE + STRING When called without the optional argument, prints the current value of the SystemdOptions EFI variable. When called with an argument, sets the @@ -111,6 +111,17 @@ for the meaning of that variable. + + BOOL + + Query or set the "Reboot-Into-Firmware-Setup" flag of the EFI firmware. Takes a + boolean argument which controls whether to show the firmware setup on next system reboot. If the + argument is omitted shows the current status of the flag, or whether the flag is supported. This + controls the same flag as systemctl reboot --firmware-setup, but is more + low-level and allows setting the flag independently from actually requesting a + reboot. + + diff --git a/src/basic/efivars.c b/src/basic/efivars.c index 6b6f461446..7cff804544 100644 --- a/src/basic/efivars.c +++ b/src/basic/efivars.c @@ -69,19 +69,21 @@ int efi_get_variable( return 0; } - if (DEBUG_LOGGING) + if (DEBUG_LOGGING) { + log_debug("Reading EFI variable %s.", p); begin = now(CLOCK_MONOTONIC); + } fd = open(p, O_RDONLY|O_NOCTTY|O_CLOEXEC); if (fd < 0) return log_debug_errno(errno, "open(\"%s\") failed: %m", p); if (fstat(fd, &st) < 0) - return -errno; + return log_debug_errno(errno, "fstat(\"%s\") failed: %m", p); if (st.st_size < 4) - return -ENODATA; + return log_debug_errno(SYNTHETIC_ERRNO(ENODATA), "EFI variable %s is shorter than 4 bytes, refusing.", p); if (st.st_size > 4*1024*1024 + 4) - return -E2BIG; + return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "EFI variable %s is ridiculously large, refusing.", p); if (ret_value || ret_attribute) { /* The kernel ratelimits reads from the efivarfs because EFI is inefficient, and we'll @@ -96,31 +98,34 @@ int efi_get_variable( n = read(fd, &a, sizeof(a)); if (n >= 0) break; - log_debug_errno(errno, "read from \"%s\" failed: %m", p); + log_debug_errno(errno, "Reading from \"%s\" failed: %m", p); if (errno != EINTR) return -errno; if (try >= EFI_N_RETRIES) return -EBUSY; - usleep(EFI_RETRY_DELAY); + + (void) usleep(EFI_RETRY_DELAY); } if (n != sizeof(a)) - return -EIO; + return log_debug_errno(SYNTHETIC_ERRNO(EIO), + "Read %zi bytes from EFI variable %s, expected %zu.", n, p, sizeof(a)); } if (ret_value) { - buf = malloc(st.st_size - 4 + 2); + buf = malloc(st.st_size - 4 + 3); if (!buf) return -ENOMEM; n = read(fd, buf, (size_t) st.st_size - 4); if (n < 0) - return -errno; + return log_debug_errno(errno, "Failed to read value of EFI variable %s: %m", p); assert(n <= st.st_size - 4); - /* Always NUL terminate (2 bytes, to protect UTF-16) */ + /* Always NUL terminate (3 bytes, to properly protect UTF-16, even if truncated in the middle of a character) */ ((char*) buf)[n] = 0; ((char*) buf)[n + 1] = 0; + ((char*) buf)[n + 2] = 0; } else /* Assume that the reported size is accurate */ n = st.st_size - 4; @@ -229,6 +234,14 @@ int efi_set_variable( if (r < 0) goto finish; + /* For some reason efivarfs doesn't update mtime automatically. Let's do it manually then. This is + * useful for processes that cache EFI variables to detect when changes occurred. */ + if (futimens(fd, (struct timespec[2]) { + { .tv_nsec = UTIME_NOW }, + { .tv_nsec = UTIME_NOW } + }) < 0) + log_debug_errno(errno, "Failed to update mtime/atime on %s, ignoring: %m", p); + r = 0; finish: diff --git a/src/boot/bootctl.c b/src/boot/bootctl.c index a197668ce9..a663fc5c2d 100644 --- a/src/boot/bootctl.c +++ b/src/boot/bootctl.c @@ -1041,7 +1041,10 @@ static int help(int argc, char *argv[], void *userdata) { " remove Remove systemd-boot from the ESP and EFI variables\n" " is-installed Test whether systemd-boot is installed in the ESP\n" " random-seed Initialize random seed in ESP and EFI variables\n" - " systemd-efi-options Query or set system options string in EFI variable\n" + " systemd-efi-options [STRING]\n" + " Query or set system options string in EFI variable\n" + " reboot-to-firmware [BOOL]\n" + " Query or set reboot-to-firmware EFI flag\n" "\nBoot Loader Entries Commands:\n" " list List boot loader entries\n" " set-default ID Set default boot loader entry\n" @@ -1245,6 +1248,18 @@ static int verb_status(int argc, char *argv[], void *userdata) { printf(" Firmware: %s%s (%s)%s\n", ansi_highlight(), strna(fw_type), strna(fw_info), ansi_normal()); printf(" Secure Boot: %sd\n", enable_disable(is_efi_secure_boot())); printf(" Setup Mode: %s\n", is_efi_secure_boot_setup_mode() ? "setup" : "user"); + + r = efi_get_reboot_to_firmware(); + if (r > 0) + printf(" Boot into FW: %sactive%s\n", ansi_highlight_yellow(), ansi_normal()); + else if (r == 0) + printf(" Boot into FW: supported\n"); + else if (r == -EOPNOTSUPP) + printf(" Boot into FW: not supported\n"); + else { + errno = -r; + printf(" Boot into FW: %sfailed%s (%m)\n", ansi_highlight_red(), ansi_normal()); + } printf("\n"); printf("Current Boot Loader:\n"); @@ -1311,6 +1326,7 @@ static int verb_status(int argc, char *argv[], void *userdata) { static int verb_list(int argc, char *argv[], void *userdata) { _cleanup_(boot_config_free) BootConfig config = {}; + _cleanup_strv_free_ char **efi_entries = NULL; int r; /* If we lack privileges we invoke find_esp_and_warn() in "unprivileged mode" here, which does two things: turn @@ -1333,7 +1349,13 @@ static int verb_list(int argc, char *argv[], void *userdata) { if (r < 0) return r; - (void) boot_entries_augment_from_loader(&config, false); + r = efi_loader_get_entries(&efi_entries); + if (r == -ENOENT || ERRNO_IS_NOT_SUPPORTED(r)) + log_debug_errno(r, "Boot loader reported no entries."); + else if (r < 0) + log_warning_errno(r, "Failed to determine entries reported by boot loader, ignoring: %m"); + else + (void) boot_entries_augment_from_loader(&config, efi_entries, false); if (config.n_entries == 0) log_info("No boot loader entries found."); @@ -1766,6 +1788,39 @@ static int verb_systemd_efi_options(int argc, char *argv[], void *userdata) { return 0; } +static int verb_reboot_to_firmware(int argc, char *argv[], void *userdata) { + int r; + + if (argc < 2) { + r = efi_get_reboot_to_firmware(); + if (r > 0) { + puts("active"); + return EXIT_SUCCESS; /* success */ + } + if (r == 0) { + puts("supported"); + return 1; /* recognizable error #1 */ + } + if (r == -EOPNOTSUPP) { + puts("not supported"); + return 2; /* recognizable error #2 */ + } + + log_error_errno(r, "Failed to query reboot-to-firmware state: %m"); + return 3; /* other kind of error */ + } else { + r = parse_boolean(argv[1]); + if (r < 0) + return log_error_errno(r, "Failed to parse argument: %s", argv[1]); + + r = efi_set_reboot_to_firmware(r); + if (r < 0) + return log_error_errno(r, "Failed to set reboot-to-firmware option: %m"); + + return 0; + } +} + static int bootctl_main(int argc, char *argv[]) { static const Verb verbs[] = { { "help", VERB_ANY, VERB_ANY, 0, help }, @@ -1779,6 +1834,7 @@ static int bootctl_main(int argc, char *argv[]) { { "set-oneshot", 2, 2, 0, verb_set_default }, { "random-seed", VERB_ANY, 1, 0, verb_random_seed }, { "systemd-efi-options", VERB_ANY, 2, 0, verb_systemd_efi_options }, + { "reboot-to-firmware", VERB_ANY, 2, 0, verb_reboot_to_firmware }, {} }; @@ -1802,4 +1858,4 @@ static int run(int argc, char *argv[]) { return bootctl_main(argc, argv); } -DEFINE_MAIN_FUNCTION(run); +DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/src/login/logind-core.c b/src/login/logind-core.c index c160f546bc..1375f438e4 100644 --- a/src/login/logind-core.c +++ b/src/login/logind-core.c @@ -16,6 +16,7 @@ #include "cgroup-util.h" #include "conf-parser.h" #include "device-util.h" +#include "efi-loader.h" #include "errno-util.h" #include "fd-util.h" #include "limits-util.h" @@ -817,3 +818,27 @@ void manager_reconnect_utmp(Manager *m) { manager_connect_utmp(m); #endif } + +int manager_read_efi_boot_loader_entries(Manager *m) { +#if ENABLE_EFI + int r; + + assert(m); + if (m->efi_boot_loader_entries_set) + return 0; + + r = efi_loader_get_entries(&m->efi_boot_loader_entries); + if (r == -ENOENT || ERRNO_IS_NOT_SUPPORTED(r)) { + log_debug_errno(r, "Boot loader reported no entries."); + m->efi_boot_loader_entries_set = true; + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to determine entries reported by boot loader: %m"); + + m->efi_boot_loader_entries_set = true; + return 1; +#else + return 0; +#endif +} diff --git a/src/login/logind-dbus.c b/src/login/logind-dbus.c index a88605232c..7fed32c3b4 100644 --- a/src/login/logind-dbus.c +++ b/src/login/logind-dbus.c @@ -2969,17 +2969,20 @@ static int property_get_reboot_to_boot_loader_entry( return sd_bus_message_append(reply, "s", v); } -static int boot_loader_entry_exists(const char *id) { +static int boot_loader_entry_exists(Manager *m, const char *id) { _cleanup_(boot_config_free) BootConfig config = {}; int r; + assert(m); assert(id); r = boot_entries_load_config_auto(NULL, NULL, &config); if (r < 0 && r != -ENOKEY) /* don't complain if no GPT is found, hence skip ENOKEY */ return r; - (void) boot_entries_augment_from_loader(&config, true); + r = manager_read_efi_boot_loader_entries(m); + if (r >= 0) + (void) boot_entries_augment_from_loader(&config, m->efi_boot_loader_entries, true); return boot_config_has_entry(&config, id); } @@ -3004,7 +3007,7 @@ static int method_set_reboot_to_boot_loader_entry( if (isempty(v)) v = NULL; else if (efi_loader_entry_name_valid(v)) { - r = boot_loader_entry_exists(v); + r = boot_loader_entry_exists(m, v); if (r < 0) return r; if (r == 0) @@ -3123,18 +3126,21 @@ static int property_get_boot_loader_entries( sd_bus_error *error) { _cleanup_(boot_config_free) BootConfig config = {}; + Manager *m = userdata; size_t i; int r; assert(bus); assert(reply); - assert(userdata); + assert(m); r = boot_entries_load_config_auto(NULL, NULL, &config); if (r < 0 && r != -ENOKEY) /* don't complain if there's no GPT found */ return r; - (void) boot_entries_augment_from_loader(&config, true); + r = manager_read_efi_boot_loader_entries(m); + if (r >= 0) + (void) boot_entries_augment_from_loader(&config, m->efi_boot_loader_entries, true); r = sd_bus_message_open_container(reply, 'a', "s"); if (r < 0) diff --git a/src/login/logind.c b/src/login/logind.c index d167080c26..5a556f9ea4 100644 --- a/src/login/logind.c +++ b/src/login/logind.c @@ -166,6 +166,8 @@ static Manager* manager_unref(Manager *m) { free(m->wall_message); free(m->action_job); + strv_free(m->efi_boot_loader_entries); + return mfree(m); } diff --git a/src/login/logind.h b/src/login/logind.h index e1d57277fa..7dbf0c28e1 100644 --- a/src/login/logind.h +++ b/src/login/logind.h @@ -123,6 +123,9 @@ struct Manager { uint64_t runtime_dir_inodes; uint64_t sessions_max; uint64_t inhibitors_max; + + char **efi_boot_loader_entries; + bool efi_boot_loader_entries_set; }; void manager_reset_config(Manager *m); @@ -168,3 +171,5 @@ CONFIG_PARSER_PROTOTYPE(config_parse_tmpfs_size); int manager_setup_wall_message_timer(Manager *m); bool logind_wall_tty_filter(const char *tty, void *userdata); + +int manager_read_efi_boot_loader_entries(Manager *m); diff --git a/src/shared/bootspec.c b/src/shared/bootspec.c index 13d7b2f160..11bba2c7ed 100644 --- a/src/shared/bootspec.c +++ b/src/shared/bootspec.c @@ -735,9 +735,12 @@ int boot_entries_load_config_auto( return boot_entries_load_config(esp_where, xbootldr_where, config); } -#if ENABLE_EFI -int boot_entries_augment_from_loader(BootConfig *config, bool only_auto) { - static const char * const title_table[] = { +int boot_entries_augment_from_loader( + BootConfig *config, + char **found_by_loader, + bool only_auto) { + + static const char *const title_table[] = { /* Pretty names for a few well-known automatically discovered entries. */ "auto-osx", "macOS", "auto-windows", "Windows Boot Manager", @@ -746,22 +749,14 @@ int boot_entries_augment_from_loader(BootConfig *config, bool only_auto) { "auto-reboot-to-firmware-setup", "Reboot Into Firmware Interface", }; - _cleanup_strv_free_ char **found_by_loader = NULL; size_t n_allocated; char **i; - int r; assert(config); /* Let's add the entries discovered by the boot loader to the end of our list, unless they are * already included there. */ - r = efi_loader_get_entries(&found_by_loader); - if (IN_SET(r, -ENOENT, -EOPNOTSUPP)) - return log_debug_errno(r, "Boot loader reported no entries."); - if (r < 0) - return log_error_errno(r, "Failed to determine entries reported by boot loader: %m"); - n_allocated = config->n_entries; STRV_FOREACH(i, found_by_loader) { @@ -803,7 +798,6 @@ int boot_entries_augment_from_loader(BootConfig *config, bool only_auto) { return 0; } -#endif /********************************************************************************/ diff --git a/src/shared/bootspec.h b/src/shared/bootspec.h index b40680b643..1075a41d54 100644 --- a/src/shared/bootspec.h +++ b/src/shared/bootspec.h @@ -76,13 +76,7 @@ static inline BootEntry* boot_config_default_entry(BootConfig *config) { void boot_config_free(BootConfig *config); int boot_entries_load_config(const char *esp_path, const char *xbootldr_path, BootConfig *config); int boot_entries_load_config_auto(const char *override_esp_path, const char *override_xbootldr_path, BootConfig *config); -#if ENABLE_EFI -int boot_entries_augment_from_loader(BootConfig *config, bool only_auto); -#else -static inline int boot_entries_augment_from_loader(BootConfig *config, bool only_auto) { - return -EOPNOTSUPP; -} -#endif +int boot_entries_augment_from_loader(BootConfig *config, char **list, bool only_auto); static inline const char* boot_entry_title(const BootEntry *entry) { return entry->show_title ?: entry->title ?: entry->id; diff --git a/src/shared/efi-loader.c b/src/shared/efi-loader.c index c288176fd1..b6ad43b856 100644 --- a/src/shared/efi-loader.c +++ b/src/shared/efi-loader.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1+ */ #include +#include #include #include "alloc-util.h" @@ -11,6 +12,7 @@ #include "io-util.h" #include "parse-util.h" #include "sort-util.h" +#include "stat-util.h" #include "stdio-util.h" #include "string-util.h" #include "utf8.h" @@ -28,10 +30,11 @@ #define END_ENTIRE_DEVICE_PATH_SUBTYPE 0xff #define EFI_OS_INDICATIONS_BOOT_TO_FW_UI 0x0000000000000001 -#define boot_option__contents { \ - uint32_t attr; \ - uint16_t path_len; \ - uint16_t title[]; \ +#define boot_option__contents \ + { \ + uint32_t attr; \ + uint16_t path_len; \ + uint16_t title[]; \ } struct boot_option boot_option__contents; @@ -49,33 +52,39 @@ struct drive_path { uint8_t signature_type; } _packed_; -#define device_path__contents { \ - uint8_t type; \ - uint8_t sub_type; \ - uint16_t length; \ - union { \ - uint16_t path[0]; \ - struct drive_path drive; \ - }; \ +#define device_path__contents \ + { \ + uint8_t type; \ + uint8_t sub_type; \ + uint16_t length; \ + union { \ + uint16_t path[0]; \ + struct drive_path drive; \ + }; \ } struct device_path device_path__contents; struct device_path__packed device_path__contents _packed_; assert_cc(sizeof(struct device_path) == sizeof(struct device_path__packed)); - int efi_reboot_to_firmware_supported(void) { _cleanup_free_ void *v = NULL; + static int cache = -1; uint64_t b; size_t s; int r; - if (!is_efi_boot()) + if (cache > 0) + return 0; + if (cache == 0) return -EOPNOTSUPP; + if (!is_efi_boot()) + goto not_supported; + r = efi_get_variable(EFI_VENDOR_GLOBAL, "OsIndicationsSupported", NULL, &v, &s); - if (r == -ENOENT) /* variable doesn't exist? it's not supported then */ - return -EOPNOTSUPP; + if (r == -ENOENT) + goto not_supported; /* variable doesn't exist? it's not supported then */ if (r < 0) return r; if (s != sizeof(uint64_t)) @@ -83,36 +92,68 @@ int efi_reboot_to_firmware_supported(void) { b = *(uint64_t*) v; if (!(b & EFI_OS_INDICATIONS_BOOT_TO_FW_UI)) - return -EOPNOTSUPP; /* bit unset? it's not supported then */ + goto not_supported; /* bit unset? it's not supported then */ + cache = 1; return 0; + +not_supported: + cache = 0; + return -EOPNOTSUPP; } -static int get_os_indications(uint64_t *os_indication) { +static int get_os_indications(uint64_t *ret) { + static struct stat cache_stat = {}; _cleanup_free_ void *v = NULL; + _cleanup_free_ char *fn = NULL; + static uint64_t cache; + struct stat new_stat; size_t s; int r; + assert(ret); + /* Let's verify general support first */ r = efi_reboot_to_firmware_supported(); if (r < 0) return r; + fn = efi_variable_path(EFI_VENDOR_GLOBAL, "OsIndications"); + if (!fn) + return -ENOMEM; + + /* stat() the EFI variable, to see if the mtime changed. If it did we need to cache again. */ + if (stat(fn, &new_stat) < 0) { + if (errno != ENOENT) + return -errno; + + /* Doesn't exist? Then we can exit early (also see below) */ + *ret = 0; + return 0; + + } else if (stat_inode_unmodified(&new_stat, &cache_stat)) { + /* inode didn't change, we can return the cached value */ + *ret = cache; + return 0; + } + r = efi_get_variable(EFI_VENDOR_GLOBAL, "OsIndications", NULL, &v, &s); if (r == -ENOENT) { /* Some firmware implementations that do support OsIndications and report that with - * OsIndicationsSupported will remove the OsIndications variable when it is unset. Let's pretend it's 0 - * then, to hide this implementation detail. Note that this call will return -ENOENT then only if the - * support for OsIndications is missing entirely, as determined by efi_reboot_to_firmware_supported() - * above. */ - *os_indication = 0; + * OsIndicationsSupported will remove the OsIndications variable when it is unset. Let's + * pretend it's 0 then, to hide this implementation detail. Note that this call will return + * -ENOENT then only if the support for OsIndications is missing entirely, as determined by + * efi_reboot_to_firmware_supported() above. */ + *ret = 0; return 0; - } else if (r < 0) + } + if (r < 0) return r; - else if (s != sizeof(uint64_t)) + if (s != sizeof(uint64_t)) return -EINVAL; - *os_indication = *(uint64_t *)v; + cache_stat = new_stat; + *ret = cache = *(uint64_t *)v; return 0; }