From: Lennart Poettering Date: Wed, 20 Jun 2018 16:25:01 +0000 (+0200) Subject: sd-boot: add boot counting mechanism X-Git-Tag: v240~522^2~4 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f538cc65487afa62a1add23d729a90cd3138f9f1;p=thirdparty%2Fsystemd.git sd-boot: add boot counting mechanism --- diff --git a/src/boot/efi/boot.c b/src/boot/efi/boot.c index b1111062e56..909dad69b12 100644 --- a/src/boot/efi/boot.c +++ b/src/boot/efi/boot.c @@ -24,7 +24,7 @@ static const EFI_GUID global_guid = EFI_GLOBAL_VARIABLE; enum loader_type { LOADER_UNDEFINED, LOADER_EFI, - LOADER_LINUX + LOADER_LINUX, }; typedef struct { @@ -41,6 +41,11 @@ typedef struct { EFI_STATUS (*call)(VOID); BOOLEAN no_autoselect; BOOLEAN non_unique; + UINTN tries_done; + UINTN tries_left; + CHAR16 *path; + CHAR16 *current_name; + CHAR16 *next_name; } ConfigEntry; typedef struct { @@ -445,6 +450,17 @@ static VOID print_status(Config *config, CHAR16 *loaded_image_path) { if (entry->call) Print(L"internal call yes\n"); + if (entry->tries_left != (UINTN) -1) + Print(L"counting boots yes\n" + "tries done %u\n" + "tries left %u\n" + "current path %s\\%s\n" + "next path %s\\%s\n", + entry->tries_done, + entry->tries_left, + entry->path, entry->current_name, + entry->path, entry->next_name); + Print(L"\n--- press key ---\n\n"); console_key_read(&key, TRUE); } @@ -861,6 +877,9 @@ static VOID config_entry_free(ConfigEntry *entry) { FreePool(entry->machine_id); FreePool(entry->loader); FreePool(entry->options); + FreePool(entry->path); + FreePool(entry->current_name); + FreePool(entry->next_name); FreePool(entry); } @@ -1055,9 +1074,195 @@ static VOID config_defaults_load_from_file(Config *config, CHAR8 *content) { } } +static VOID config_entry_parse_tries( + ConfigEntry *entry, + CHAR16 *path, + CHAR16 *file, + CHAR16 *suffix) { + + UINTN left = (UINTN) -1, done = (UINTN) -1, factor = 1, i, next_left, next_done; + _cleanup_freepool_ CHAR16 *prefix = NULL; + + /* + * Parses a suffix of two counters (one going down, one going up) in the form "+LEFT-DONE" from the end of the + * filename (but before the .efi/.conf suffix), where the "-DONE" part is optional and may be left out (in + * which case that counter as assumed to be zero, i.e. the missing part is synonymous to "-0"). + * + * Names we grok, and the series they result in: + * + * foobar+3.efi → foobar+2-1.efi → foobar+1-2.efi → foobar+0-3.efi → STOP! + * foobar+4-0.efi → foobar+3-1.efi → foobar+2-2.efi → foobar+1-3.efi → foobar+0-4.efi → STOP! + */ + + i = StrLen(file); + + /* Chop off any suffix such as ".conf" or ".efi" */ + if (suffix) { + UINTN suffix_length; + + suffix_length = StrLen(suffix); + if (i < suffix_length) + return; + + i -= suffix_length; + } + + /* Go backwards through the string and parse everything we encounter */ + for (;;) { + if (i == 0) + return; + + i--; + + switch (file[i]) { + + case '+': + if (left == (UINTN) -1) /* didn't read at least one digit for 'left'? */ + return; + + if (done == (UINTN) -1) /* no 'done' counter? If so, it's equivalent to 0 */ + done = 0; + + goto good; + + case '-': + if (left == (UINTN) -1) /* didn't parse any digit yet? */ + return; + + if (done != (UINTN) -1) /* already encountered a dash earlier? */ + return; + + /* So we encountered a dash. This means this counter is of the form +LEFT-DONE. Let's assign + * what we already parsed to 'done', and start fresh for the 'left' part. */ + + done = left; + left = (UINTN) -1; + factor = 1; + break; + + case '0'...'9': { + UINTN new_factor; + + if (left == (UINTN) -1) + left = file[i] - '0'; + else { + UINTN new_left, digit; + + digit = file[i] - '0'; + if (digit > (UINTN) -1 / factor) /* overflow check */ + return; + + new_left = left + digit * factor; + if (new_left < left) /* overflow check */ + return; + + if (new_left == (UINTN) -1) /* don't allow us to be confused */ + return; + } + + new_factor = factor * 10; + if (new_factor < factor) /* overflow chck */ + return; + + factor = new_factor; + break; + } + + default: + return; + } + } + +good: + entry->tries_left = left; + entry->tries_done = done; + + entry->path = StrDuplicate(path); + entry->current_name = StrDuplicate(file); + + next_left = left <= 0 ? 0 : left - 1; + next_done = done >= (UINTN) -2 ? (UINTN) -2 : done + 1; + + prefix = StrDuplicate(file); + prefix[i] = 0; + + entry->next_name = PoolPrint(L"%s+%u-%u%s", prefix, next_left, next_done, suffix ?: L""); +} + +static VOID config_entry_bump_counters( + ConfigEntry *entry, + EFI_FILE_HANDLE root_dir) { + + _cleanup_freepool_ CHAR16* old_path = NULL, *new_path = NULL; + _cleanup_(FileHandleClosep) EFI_FILE_HANDLE handle = NULL; + static EFI_GUID EfiFileInfoGuid = EFI_FILE_INFO_ID; + _cleanup_freepool_ EFI_FILE_INFO *file_info = NULL; + UINTN file_info_size, a, b; + EFI_STATUS r; + + if (entry->tries_left == (UINTN) -1) + return; + + if (!entry->path || !entry->current_name || !entry->next_name) + return; + + old_path = PoolPrint(L"%s\\%s", entry->path, entry->current_name); + + r = uefi_call_wrapper(root_dir->Open, 5, root_dir, &handle, old_path, EFI_FILE_MODE_READ|EFI_FILE_MODE_WRITE, 0ULL); + if (EFI_ERROR(r)) + return; + + a = StrLen(entry->current_name); + b = StrLen(entry->next_name); + + file_info_size = OFFSETOF(EFI_FILE_INFO, FileName) + (a > b ? a : b) + 1; + + for (;;) { + file_info = AllocatePool(file_info_size); + + r = uefi_call_wrapper(handle->GetInfo, 4, handle, &EfiFileInfoGuid, &file_info_size, file_info); + if (!EFI_ERROR(r)) + break; + + if (r != EFI_BUFFER_TOO_SMALL || file_info_size * 2 < file_info_size) { + Print(L"\nFailed to get file info for '%s': %r\n", old_path, r); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + return; + } + + file_info_size *= 2; + FreePool(file_info); + } + + /* And rename the file */ + StrCpy(file_info->FileName, entry->next_name); + r = uefi_call_wrapper(handle->SetInfo, 4, handle, &EfiFileInfoGuid, file_info_size, file_info); + if (EFI_ERROR(r)) { + Print(L"\nFailed to rename '%s' to '%s', ignoring: %r\n", old_path, entry->next_name, r); + uefi_call_wrapper(BS->Stall, 1, 3 * 1000 * 1000); + return; + } + + /* Flush everything to disk, just in case… */ + (void) uefi_call_wrapper(handle->Flush, 1, handle); + + /* Let's tell the OS that we renamed this file, so that it knows what to rename to the counter-less name on + * success */ + new_path = PoolPrint(L"%s\\%s", entry->path, entry->next_name); + efivar_set(L"LoaderBootCountPath", new_path, FALSE); + + /* If the file we just renamed is the loader path, then let's update that. */ + if (StrCmp(entry->loader, old_path) == 0) { + FreePool(entry->loader); + entry->loader = new_path; + new_path = NULL; + } +} + static VOID config_entry_add_from_file( Config *config, EFI_HANDLE *device, + CHAR16 *path, CHAR16 *file, CHAR8 *content, CHAR16 *loaded_image_path) { @@ -1069,7 +1274,12 @@ static VOID config_entry_add_from_file( UINTN len; _cleanup_freepool_ CHAR16 *initrd = NULL; - entry = AllocateZeroPool(sizeof(ConfigEntry)); + entry = AllocatePool(sizeof(ConfigEntry)); + + *entry = (ConfigEntry) { + .tries_done = (UINTN) -1, + .tries_left = (UINTN) -1, + }; while ((line = line_get_key_value(content, (CHAR8 *)" \t", &pos, &key, &value))) { if (strcmpa((CHAR8 *)"title", key) == 0) { @@ -1183,6 +1393,8 @@ static VOID config_entry_add_from_file( StrLwr(entry->id); config_add_entry(config, entry); + + config_entry_parse_tries(entry, path, file, L".conf"); } static VOID config_load_defaults(Config *config, EFI_FILE *root_dir) { @@ -1245,12 +1457,44 @@ static VOID config_load_entries( err = file_read(entries_dir, f->FileName, 0, 0, &content, NULL); if (!EFI_ERROR(err)) - config_entry_add_from_file(config, device, f->FileName, content, loaded_image_path); + config_entry_add_from_file(config, device, L"\\loader\\entries", f->FileName, content, loaded_image_path); } uefi_call_wrapper(entries_dir->Close, 1, entries_dir); } } +static INTN config_entry_compare(ConfigEntry *a, ConfigEntry *b) { + INTN r; + + /* Order entries that have no tries left to the end of the list */ + if (a->tries_left != 0 && b->tries_left == 0) + return -1; + if (a->tries_left == 0 && b->tries_left != 0) + return 1; + + r = str_verscmp(a->id, b->id); + if (r != 0) + return r; + + if (a->tries_left == (UINTN) -1 || + b->tries_left == (UINTN) -1) + return 0; + + /* If both items have boot counting, and otherwise are identical, put the entry with more tries left first */ + if (a->tries_left > b->tries_left) + return -1; + if (a->tries_left < b->tries_left) + return 1; + + /* If they have the same number of tries left, then let the one win which was tried fewer times so far */ + if (a->tries_done < b->tries_done) + return -1; + if (a->tries_done > b->tries_done) + return 1; + + return 0; +} + static VOID config_sort_entries(Config *config) { UINTN i; @@ -1262,8 +1506,9 @@ static VOID config_sort_entries(Config *config) { for (k = 0; k < config->entry_count - i; k++) { ConfigEntry *entry; - if (str_verscmp(config->entries[k]->id, config->entries[k+1]->id) <= 0) + if (config_entry_compare(config->entries[k], config->entries[k+1]) <= 0) continue; + entry = config->entries[k]; config->entries[k] = config->entries[k+1]; config->entries[k+1] = entry; @@ -1444,10 +1689,15 @@ static BOOLEAN config_entry_add_call( ConfigEntry *entry; - entry = AllocateZeroPool(sizeof(ConfigEntry)); - entry->title = StrDuplicate(title); - entry->call = call; - entry->no_autoselect = TRUE; + entry = AllocatePool(sizeof(ConfigEntry)); + *entry = (ConfigEntry) { + .title = StrDuplicate(title), + .call = call, + .no_autoselect = TRUE, + .tries_done = (UINTN) -1, + .tries_left = (UINTN) -1, + }; + config_add_entry(config, entry); return TRUE; } @@ -1463,16 +1713,21 @@ static ConfigEntry *config_entry_add_loader( ConfigEntry *entry; - entry = AllocateZeroPool(sizeof(ConfigEntry)); - entry->type = type; - entry->title = StrDuplicate(title); - entry->device = device; - entry->loader = StrDuplicate(loader); - entry->id = StrDuplicate(id); + entry = AllocatePool(sizeof(ConfigEntry)); + *entry = (ConfigEntry) { + .type = type, + .title = StrDuplicate(title), + .device = device, + .loader = StrDuplicate(loader), + .id = StrDuplicate(id), + .key = key, + .tries_done = (UINTN) -1, + .tries_left = (UINTN) -1, + }; + StrLwr(entry->id); - entry->key = key; - config_add_entry(config, entry); + config_add_entry(config, entry); return entry; } @@ -1664,6 +1919,8 @@ static VOID config_entry_add_linux( entry->options = stra_to_str(content); } + + config_entry_parse_tries(entry, L"\\EFI\\Linux", f->FileName, L".efi"); } FreePool(os_name); @@ -1909,6 +2166,8 @@ EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { } } + config_entry_bump_counters(entry, root_dir); + /* export the selected boot entry to the system */ efivar_set(L"LoaderEntrySelected", entry->id, FALSE);