From 52b6b35643f0eb2ec272c5b773b47c81fe5a1e57 Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Thu, 16 Sep 2021 10:25:10 +0200 Subject: [PATCH 1/2] sd-boot: Allow disabling timeout --- man/loader.conf.xml | 7 ++- src/boot/efi/boot.c | 136 ++++++++++++++++++++++++++------------------ 2 files changed, 86 insertions(+), 57 deletions(-) diff --git a/man/loader.conf.xml b/man/loader.conf.xml index ffbd897a1fd..26e83dfcab8 100644 --- a/man/loader.conf.xml +++ b/man/loader.conf.xml @@ -106,9 +106,10 @@ will be stored as an EFI variable in that case, overriding this option. - If the timeout is disabled, the default entry will be booted - immediately. The menu can be shown by pressing and holding a key before - systemd-boot is launched. + If set to menu-hidden or 0 no menu + is shown and the default entry will be booted immediately. The menu can be shown + by pressing and holding a key before systemd-boot is launched. Setting this to + menu-force disables the timeout while always showing the menu. diff --git a/src/boot/efi/boot.c b/src/boot/efi/boot.c index d82d679a47d..e1d8c935c4c 100644 --- a/src/boot/efi/boot.c +++ b/src/boot/efi/boot.c @@ -60,9 +60,9 @@ typedef struct { UINTN entry_count; INTN idx_default; INTN idx_default_efivar; - UINTN timeout_sec; - UINTN timeout_sec_config; - INTN timeout_sec_efivar; + UINT32 timeout_sec; /* Actual timeout used (efi_main() override > efivar > config). */ + UINT32 timeout_sec_config; + UINT32 timeout_sec_efivar; CHAR16 *entry_default_pattern; CHAR16 *entry_oneshot; CHAR16 *options_edit; @@ -75,6 +75,18 @@ typedef struct { RandomSeedMode random_seed_mode; } Config; +/* These values have been chosen so that the transitions the user sees could + * employ unsigned over-/underflow like this: + * efivar unset ↔ force menu ↔ no timeout/skip menu ↔ 1 s ↔ 2 s ↔ … */ +enum { + TIMEOUT_MIN = 1, + TIMEOUT_MAX = UINT32_MAX - 2U, + TIMEOUT_UNSET = UINT32_MAX - 1U, + TIMEOUT_MENU_FORCE = UINT32_MAX, + TIMEOUT_MENU_HIDDEN = 0, + TIMEOUT_TYPE_MAX = UINT32_MAX, +}; + static VOID cursor_left(UINTN *cursor, UINTN *first) { assert(cursor); assert(first); @@ -366,6 +378,38 @@ static UINTN entry_lookup_key(Config *config, UINTN start, CHAR16 key) { return -1; } +static CHAR16 *update_timeout_efivar(UINT32 *t, BOOLEAN inc) { + assert(t); + + switch (*t) { + case TIMEOUT_MAX: + *t = inc ? TIMEOUT_MAX : (*t - 1); + break; + case TIMEOUT_UNSET: + *t = inc ? TIMEOUT_MENU_FORCE : TIMEOUT_UNSET; + break; + case TIMEOUT_MENU_FORCE: + *t = inc ? TIMEOUT_MENU_HIDDEN : TIMEOUT_UNSET; + break; + case TIMEOUT_MENU_HIDDEN: + *t = inc ? TIMEOUT_MIN : TIMEOUT_MENU_FORCE; + break; + default: + *t += inc ? 1 : -1; + } + + switch (*t) { + case TIMEOUT_UNSET: + return StrDuplicate(L"Menu timeout defined by configuration file."); + case TIMEOUT_MENU_FORCE: + return StrDuplicate(L"Timeout disabled, menu will always be shown."); + case TIMEOUT_MENU_HIDDEN: + return StrDuplicate(L"Menu disabled. Hold down key at bootup to show menu."); + default: + return PoolPrint(L"Menu timeout set to %u s.", *t); + } +} + static VOID print_status(Config *config, CHAR16 *loaded_image_path) { UINT64 key; UINTN timeout; @@ -400,10 +444,11 @@ static VOID print_status(Config *config, CHAR16 *loaded_image_path) { Print(L"\n--- press key ---\n\n"); console_key_read(&key, 0); - Print(L"timeout: %u\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): %u\n", config->timeout_sec_config); + Print(L"timeout: %u s\n", config->timeout_sec); + if (config->timeout_sec_efivar != TIMEOUT_UNSET) + Print(L"timeout (EFI var): %u s\n", config->timeout_sec_efivar); + if (config->timeout_sec_config != TIMEOUT_UNSET) + Print(L"timeout (config): %u s\n", config->timeout_sec_config); if (config->entry_default_pattern) Print(L"default pattern: '%s'\n", config->entry_default_pattern); Print(L"editor: %s\n", yes_no(config->editor)); @@ -519,7 +564,8 @@ static BOOLEAN menu_run( UINTN x_max, y_max; CHAR16 **lines = NULL; _cleanup_freepool_ CHAR16 *clearline = NULL, *status = NULL; - UINTN timeout_remain = config->timeout_sec; + UINT32 timeout_efivar_saved = config->timeout_sec_efivar; + UINT32 timeout_remain = config->timeout_sec == TIMEOUT_MENU_FORCE ? 0 : config->timeout_sec; INT16 idx; BOOLEAN exit = FALSE, run = TRUE; INT64 console_mode_initial = ST->ConOut->Mode->Mode, console_mode_efivar_saved = config->console_mode_efivar; @@ -640,7 +686,7 @@ static BOOLEAN menu_run( if (timeout_remain > 0) { FreePool(status); - status = PoolPrint(L"Boot in %d s.", timeout_remain); + status = PoolPrint(L"Boot in %u s.", timeout_remain); } /* print status at last line of screen */ @@ -768,44 +814,12 @@ static BOOLEAN menu_run( case KEYPRESS(0, 0, '-'): case KEYPRESS(0, 0, 'T'): - if (config->timeout_sec_efivar > 0) { - config->timeout_sec_efivar--; - efivar_set_uint_string( - LOADER_GUID, - L"LoaderConfigTimeout", - config->timeout_sec_efivar, - EFI_VARIABLE_NON_VOLATILE); - if (config->timeout_sec_efivar > 0) - status = PoolPrint(L"Menu timeout set to %d s.", 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( - LOADER_GUID, L"LoaderConfigTimeout", NULL, EFI_VARIABLE_NON_VOLATILE); - if (config->timeout_sec_config > 0) - status = PoolPrint(L"Menu timeout of %d s is defined by configuration file.", - config->timeout_sec_config); - else - status = StrDuplicate(L"Menu disabled. Hold down key at bootup to show menu."); - } + status = update_timeout_efivar(&config->timeout_sec_efivar, FALSE); 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_uint_string( - LOADER_GUID, - L"LoaderConfigTimeout", - config->timeout_sec_efivar, - EFI_VARIABLE_NON_VOLATILE); - if (config->timeout_sec_efivar > 0) - status = PoolPrint(L"Menu timeout set to %d s.", - config->timeout_sec_efivar); - else - status = StrDuplicate(L"Menu disabled. Hold down key at bootup to show menu."); + status = update_timeout_efivar(&config->timeout_sec_efivar, TRUE); break; case KEYPRESS(0, 0, 'e'): @@ -888,9 +902,8 @@ static BOOLEAN menu_run( *chosen_entry = config->entries[idx_highlight]; - /* The user is likely to cycle through several modes before - * deciding to keep one. Therefore, we update the EFI var after - * we left the menu to reduce nvram writes. */ + /* Update EFI vars after we left the menu to reduce NVRAM writes. */ + if (console_mode_efivar_saved != config->console_mode_efivar) { if (config->console_mode_efivar == CONSOLE_MODE_KEEP) efivar_set(LOADER_GUID, L"LoaderConfigConsoleMode", NULL, EFI_VARIABLE_NON_VOLATILE); @@ -899,6 +912,14 @@ static BOOLEAN menu_run( config->console_mode_efivar, EFI_VARIABLE_NON_VOLATILE); } + if (timeout_efivar_saved != config->timeout_sec_efivar) { + if (config->timeout_sec_efivar == TIMEOUT_UNSET) + efivar_set(LOADER_GUID, L"LoaderConfigTimeout", NULL, EFI_VARIABLE_NON_VOLATILE); + else + efivar_set_uint_string(LOADER_GUID, L"LoaderConfigTimeout", + config->timeout_sec_efivar, EFI_VARIABLE_NON_VOLATILE); + } + for (UINTN i = 0; i < config->entry_count; i++) FreePool(lines[i]); FreePool(lines); @@ -1023,10 +1044,16 @@ static VOID config_defaults_load_from_file(Config *config, CHAR8 *content) { while ((line = line_get_key_value(content, (CHAR8 *)" \t", &pos, &key, &value))) { if (strcmpa((CHAR8 *)"timeout", key) == 0) { - _cleanup_freepool_ CHAR16 *s = NULL; + if (strcmpa((CHAR8*) "menu-force", value) == 0) + config->timeout_sec_config = TIMEOUT_MENU_FORCE; + else if (strcmpa((CHAR8*) "menu-hidden", value) == 0) + config->timeout_sec_config = TIMEOUT_MENU_HIDDEN; + else { + _cleanup_freepool_ CHAR16 *s = NULL; - s = stra_to_str(value); - config->timeout_sec_config = Atoi(s); + s = stra_to_str(value); + config->timeout_sec_config = MIN(Atoi(s), TIMEOUT_TYPE_MAX); + } config->timeout_sec = config->timeout_sec_config; continue; } @@ -1439,6 +1466,8 @@ static VOID config_load_defaults(Config *config, EFI_FILE *root_dir) { .idx_default_efivar = -1, .console_mode = CONSOLE_MODE_KEEP, .console_mode_efivar = CONSOLE_MODE_KEEP, + .timeout_sec_config = TIMEOUT_UNSET, + .timeout_sec_efivar = TIMEOUT_UNSET, }; err = file_read(root_dir, L"\\loader\\loader.conf", 0, 0, &content, NULL); @@ -1447,17 +1476,16 @@ static VOID config_load_defaults(Config *config, EFI_FILE *root_dir) { err = efivar_get_uint_string(LOADER_GUID, L"LoaderConfigTimeout", &value); if (!EFI_ERROR(err)) { - config->timeout_sec_efivar = value > INTN_MAX ? INTN_MAX : value; - config->timeout_sec = value; - } else - config->timeout_sec_efivar = -1; + config->timeout_sec_efivar = MIN(value, TIMEOUT_TYPE_MAX); + config->timeout_sec = config->timeout_sec_efivar; + } err = efivar_get_uint_string(LOADER_GUID, L"LoaderConfigTimeoutOneShot", &value); if (!EFI_ERROR(err)) { /* Unset variable now, after all it's "one shot". */ (void) efivar_set(LOADER_GUID, L"LoaderConfigTimeoutOneShot", NULL, EFI_VARIABLE_NON_VOLATILE); - config->timeout_sec = value; + config->timeout_sec = MIN(value, TIMEOUT_TYPE_MAX); config->force_menu = TRUE; /* force the menu when this is set */ } From 39ddc32a86d4aff15b07c8993d1cff0fe1fa4123 Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Mon, 4 Oct 2021 11:25:50 +0200 Subject: [PATCH 2/2] bootctl: Add set-timeout verb Fixes: #18766 --- man/bootctl.xml | 15 ++++++++ shell-completion/bash/bootctl | 2 +- shell-completion/zsh/_bootctl | 2 + src/boot/bootctl.c | 71 ++++++++++++++++++++++++++++++----- 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/man/bootctl.xml b/man/bootctl.xml index a958cde7df8..73cacc6107a 100644 --- a/man/bootctl.xml +++ b/man/bootctl.xml @@ -116,6 +116,21 @@ + + TIMEOUT + TIMEOUT + + Sets the boot loader menu timeout in seconds. The + command will set the timeout only for the next boot. See + systemd.time7 + for details about the syntax of time spans. + + If this is set to or no menu is shown and + the default entry will be booted immediately, while setting this to + disables the timeout while always showing the menu. When an empty string ("") is specified the + bootloader will revert to its default menu timeout. + + diff --git a/shell-completion/bash/bootctl b/shell-completion/bash/bootctl index e61188fee94..190e3d33f5e 100644 --- a/shell-completion/bash/bootctl +++ b/shell-completion/bash/bootctl @@ -57,7 +57,7 @@ _bootctl() { local -A VERBS=( # systemd-efi-options takes an argument, but it is free-form, so we cannot complete it - [STANDALONE]='help status install update remove is-installed random-seed systemd-efi-options list' + [STANDALONE]='help status install update remove is-installed random-seed systemd-efi-options list set-timeout set-timeout-oneshot' [BOOTENTRY]='set-default set-oneshot' [BOOLEAN]='reboot-to-firmware' ) diff --git a/shell-completion/zsh/_bootctl b/shell-completion/zsh/_bootctl index 2b50f307f1f..87ecbe37c3f 100644 --- a/shell-completion/zsh/_bootctl +++ b/shell-completion/zsh/_bootctl @@ -46,6 +46,8 @@ _bootctl_reboot-to-firmware() { "list:List boot loader entries" "set-default:Set the default boot loader entry" "set-oneshot:Set the default boot loader entry only for the next boot" + "set-timeout:Set the menu timeout" + "set-timeout-oneshot:Set the menu timeout for the next boot only" ) if (( CURRENT == 1 )); then _describe -t commands 'bootctl command' _bootctl_cmds || compadd "$@" diff --git a/src/boot/bootctl.c b/src/boot/bootctl.c index bb3627ed6ed..99297fc0722 100644 --- a/src/boot/bootctl.c +++ b/src/boot/bootctl.c @@ -1100,6 +1100,9 @@ static int help(int argc, char *argv[], void *userdata) { " list List boot loader entries\n" " set-default ID Set default boot loader entry\n" " set-oneshot ID Set default boot loader entry, for next boot only\n" + " set-timeout SECONDS Set the menu timeout\n" + " set-timeout-oneshot SECONDS\n" + " Set the menu timeout for the next boot only\n" "\n%3$ssystemd-boot Commands:%4$s\n" " install Install systemd-boot to the ESP and EFI variables\n" " update Update systemd-boot in the ESP and EFI variables\n" @@ -1774,6 +1777,37 @@ static int verb_is_installed(int argc, char *argv[], void *userdata) { return EXIT_SUCCESS; } +static int parse_timeout(const char *arg1, char16_t **ret_timeout, size_t *ret_timeout_size) { + char utf8[DECIMAL_STR_MAX(usec_t)]; + char16_t *encoded; + usec_t timeout; + int r; + + assert(arg1); + assert(ret_timeout); + assert(ret_timeout_size); + + if (streq(arg1, "menu-force")) + timeout = USEC_INFINITY; + else if (streq(arg1, "menu-hidden")) + timeout = 0; + else { + r = parse_time(arg1, &timeout, USEC_PER_SEC); + if (r < 0) + return log_error_errno(r, "Failed to parse timeout '%s': %m", arg1); + if (timeout != USEC_INFINITY && timeout > UINT32_MAX * USEC_PER_SEC) + log_warning("Timeout is too long and will be treated as 'menu-force' instead."); + } + + xsprintf(utf8, USEC_FMT, MIN(timeout / USEC_PER_SEC, UINT32_MAX)); + encoded = utf8_to_utf16(utf8, strlen(utf8)); + if (!encoded) + return log_oom(); + *ret_timeout = encoded; + *ret_timeout_size = char16_strlen(encoded) * 2 + 2; + return 0; +} + static int parse_loader_entry_target_arg(const char *arg1, char16_t **ret_target, size_t *ret_target_size) { int r; if (streq(arg1, "@current")) { @@ -1799,7 +1833,7 @@ static int parse_loader_entry_target_arg(const char *arg1, char16_t **ret_target return 0; } -static int verb_set_default(int argc, char *argv[], void *userdata) { +static int verb_set_efivar(int argc, char *argv[], void *userdata) { int r; if (!is_efi_boot()) @@ -1822,24 +1856,39 @@ static int verb_set_default(int argc, char *argv[], void *userdata) { if (!arg_touch_variables) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "'%s' operation cannot be combined with --touch-variables=no.", + "'%s' operation cannot be combined with --no-variables.", argv[0]); - const char *variable = streq(argv[0], "set-default") ? - EFI_LOADER_VARIABLE(LoaderEntryDefault) : EFI_LOADER_VARIABLE(LoaderEntryOneShot); + const char *variable; + int (* arg_parser)(const char *, char16_t **, size_t *); + + if (streq(argv[0], "set-default")) { + variable = EFI_LOADER_VARIABLE(LoaderEntryDefault); + arg_parser = parse_loader_entry_target_arg; + } else if (streq(argv[0], "set-oneshot")) { + variable = EFI_LOADER_VARIABLE(LoaderEntryOneShot); + arg_parser = parse_loader_entry_target_arg; + } else if (streq(argv[0], "set-timeout")) { + variable = EFI_LOADER_VARIABLE(LoaderConfigTimeout); + arg_parser = parse_timeout; + } else if (streq(argv[0], "set-timeout-oneshot")) { + variable = EFI_LOADER_VARIABLE(LoaderConfigTimeoutOneShot); + arg_parser = parse_timeout; + } else + assert_not_reached(); if (isempty(argv[1])) { r = efi_set_variable(variable, NULL, 0); if (r < 0 && r != -ENOENT) return log_error_errno(r, "Failed to remove EFI variable '%s': %m", variable); } else { - _cleanup_free_ char16_t *target = NULL; - size_t target_size = 0; + _cleanup_free_ char16_t *value = NULL; + size_t value_size = 0; - r = parse_loader_entry_target_arg(argv[1], &target, &target_size); + r = arg_parser(argv[1], &value, &value_size); if (r < 0) return r; - r = efi_set_variable(variable, target, target_size); + r = efi_set_variable(variable, value, value_size); if (r < 0) return log_error_errno(r, "Failed to update EFI variable '%s': %m", variable); } @@ -1944,8 +1993,10 @@ static int bootctl_main(int argc, char *argv[]) { { "remove", VERB_ANY, 1, 0, verb_remove }, { "is-installed", VERB_ANY, 1, 0, verb_is_installed }, { "list", VERB_ANY, 1, 0, verb_list }, - { "set-default", 2, 2, 0, verb_set_default }, - { "set-oneshot", 2, 2, 0, verb_set_default }, + { "set-default", 2, 2, 0, verb_set_efivar }, + { "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 }, { "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 },