sd-boot: add boot counting mechanism
authorLennart Poettering <lennart@poettering.net>
Wed, 20 Jun 2018 16:25:01 +0000 (18:25 +0200)
committerLennart Poettering <lennart@poettering.net>
Tue, 16 Oct 2018 14:44:34 +0000 (16:44 +0200)
src/boot/efi/boot.c

index b111106..909dad6 100644 (file)
@@ -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);