From a0f03d2352cbc0b58cb238b43e80c34ce24aee7b Mon Sep 17 00:00:00 2001 From: Igor Opaniuk Date: Wed, 11 Sep 2024 18:03:10 +0200 Subject: [PATCH] sysroot: Support boot counting for boot entries Add support for boot counting for bootloader entries [1]. The boot counting data is stored in the name of the boot loader entry. A boot loader entry file name may contain a plus (+) followed by a number. This may optionally be followed by a minus (-) followed by a second number. The dot (.) and file name suffix (conf or efi) must immediately follow. The feature is enabled via sysroot configuration: [sysroot] boot-counting-tries=3 Testing: $ ostree admin deploy 91fc19319be9e79d07159303dff125f40f10e5c25614630dcbed23d95e36f907 Copying /etc changes: 2 modified, 3 removed, 4 added bootfs is sufficient for calculated new size: 0 bytes Transaction complete; bootconfig swap: yes; bootversion: boot.0.1, deployment count change: 1 $ ls /boot/loader/entries ostree-1.conf ostree-2+3.conf [1] https://uapi-group.org/specifications/specs/boot_loader_specification/#boot-counting Signed-off-by: Igor Opaniuk --- man/ostree.repo-config.xml | 12 +++ src/libostree/ostree-bootconfig-parser.c | 93 ++++++++++++++++++++++++ src/libostree/ostree-bootconfig-parser.h | 9 +++ src/libostree/ostree-repo-private.h | 1 + src/libostree/ostree-repo.c | 14 ++++ src/libostree/ostree-sysroot-deploy.c | 83 ++++++++++++++++++--- 6 files changed, 203 insertions(+), 9 deletions(-) diff --git a/man/ostree.repo-config.xml b/man/ostree.repo-config.xml index 181d7ba9e2..70fef35aee 100644 --- a/man/ostree.repo-config.xml +++ b/man/ostree.repo-config.xml @@ -416,6 +416,18 @@ License along with this library. If not, see . + + boot-counting-tries + Integer value controlling the number of maximum boot attempts. The boot + counting data is stored in the name of the boot loader entry. A boot loader entry file name + may contain a plus (+) followed by a number. This may optionally be followed by + a minus (-) followed by a second number. The dot (.) and file name suffix (conf or efi) must + immediately follow. More details in the + + The Boot Loader Specification + + + bls-append-except-default A semicolon separated string list of key-value pairs. For example: diff --git a/src/libostree/ostree-bootconfig-parser.c b/src/libostree/ostree-bootconfig-parser.c index 4c3e80d083..5abc13ab52 100644 --- a/src/libostree/ostree-bootconfig-parser.c +++ b/src/libostree/ostree-bootconfig-parser.c @@ -27,6 +27,9 @@ struct _OstreeBootconfigParser gboolean parsed; const char *separators; + guint64 tries_left; + guint64 tries_done; + GHashTable *options; /* Additional initrds; the primary initrd is in options. */ @@ -56,6 +59,94 @@ ostree_bootconfig_parser_clone (OstreeBootconfigParser *self) return parser; } +/** + * ostree_bootconfig_parser_parse_tries: + * @self: Parser + * @path: File path + * + * Parses a suffix of two counters in the form "+LEFT-DONE" from the end of the + * filename (excluding file extension). + */ +static void +ostree_bootconfig_parser_parse_tries (OstreeBootconfigParser *self, const char *filename) +{ + gchar *counter = NULL; + gchar *old_counter = NULL; + self->tries_left = 0; + self->tries_done = 0; + + for (;;) + { + char *plus = strchr (counter ?: filename, '+'); + if (plus) + { + /* Look for the last "+" symbol in the filename */ + counter = plus + 1; + continue; + } + if (counter) + break; + + /* No boot counter found */ + return; + } + + guint64 tries_left, tries_done = 0; + + old_counter = counter; + tries_left = g_ascii_strtoull (old_counter, &counter, 10); + if ((old_counter == counter) || (tries_left > INT_MAX)) + return; + + /* Parse done counter only if present */ + if (*counter == '-') + { + old_counter = counter; + tries_done = g_ascii_strtoull (counter + 1, &counter, 10); + if ((old_counter == counter) || (tries_left > INT_MAX)) + return; + } + + self->tries_left = tries_left; + self->tries_done = tries_done; +} + +/** + * ostree_bootconfig_parser_get_tries_left: + * @self: Parser + * + * Returns: Amount of boot tries left + */ +guint64 +ostree_bootconfig_parser_get_tries_left (OstreeBootconfigParser *self) +{ + return self->tries_left; +} + +/** + * ostree_bootconfig_parser_get_tries_done: + * @self: Parser + * + * Returns: Amount of boot tries + */ +guint64 +ostree_bootconfig_parser_get_tries_done (OstreeBootconfigParser *self) +{ + return self->tries_done; +} + +/** + * ostree_bootconfig_parser_get_tries_done: + * @self: Parser + * + * Returns: TRUE if the boot config was parsed from existing boot config file + */ +gboolean +ostree_bootconfig_parser_is_parsed (OstreeBootconfigParser *self) +{ + return self->parsed; +} + /** * ostree_bootconfig_parser_parse_at: * @self: Parser @@ -116,6 +207,8 @@ ostree_bootconfig_parser_parse_at (OstreeBootconfigParser *self, int dfd, const self->overlay_initrds = (char **)g_ptr_array_free (g_steal_pointer (&overlay_initrds), FALSE); } + ostree_bootconfig_parser_parse_tries(self, glnx_basename(path)); + self->parsed = TRUE; return TRUE; diff --git a/src/libostree/ostree-bootconfig-parser.h b/src/libostree/ostree-bootconfig-parser.h index 5fdad72eed..c2628a90ab 100644 --- a/src/libostree/ostree-bootconfig-parser.h +++ b/src/libostree/ostree-bootconfig-parser.h @@ -67,4 +67,13 @@ void ostree_bootconfig_parser_set_overlay_initrds (OstreeBootconfigParser *self, _OSTREE_PUBLIC char **ostree_bootconfig_parser_get_overlay_initrds (OstreeBootconfigParser *self); +_OSTREE_PUBLIC +guint64 ostree_bootconfig_parser_get_tries_left (OstreeBootconfigParser *self); + +_OSTREE_PUBLIC +guint64 ostree_bootconfig_parser_get_tries_done (OstreeBootconfigParser *self); + +_OSTREE_PUBLIC +gboolean ostree_bootconfig_parser_is_parsed (OstreeBootconfigParser *self); + G_END_DECLS diff --git a/src/libostree/ostree-repo-private.h b/src/libostree/ostree-repo-private.h index 1e27fc772d..3ea1ff65a6 100644 --- a/src/libostree/ostree-repo-private.h +++ b/src/libostree/ostree-repo-private.h @@ -247,6 +247,7 @@ struct OstreeRepo GHashTable *bls_append_values; /* Parsed key-values from bls-append-except-default key in config. */ gboolean enable_bootprefix; /* If true, prepend bootloader entries with /boot */ + guint boot_counting; OstreeRepo *parent_repo; }; diff --git a/src/libostree/ostree-repo.c b/src/libostree/ostree-repo.c index 2f4e836c65..e3e179a174 100644 --- a/src/libostree/ostree-repo.c +++ b/src/libostree/ostree-repo.c @@ -3297,6 +3297,20 @@ reload_remote_config (OstreeRepo *self, GCancellable *cancellable, GError **erro static gboolean reload_sysroot_config (OstreeRepo *self, GCancellable *cancellable, GError **error) { + { + g_autofree char *boot_counting_str = NULL; + + (void)ot_keyfile_get_value_with_default_group_optional (self->config, "sysroot", "boot-counting-tries", "0", + &boot_counting_str, NULL); + + if (boot_counting_str) + /* Ensure boot count value is in [0,5] */ + self->boot_counting + = MAX (0, MIN (INT_MAX, g_ascii_strtoull (boot_counting_str, NULL, 10))); + else + self->boot_counting = 0; + } + g_autofree char *bootloader = NULL; if (!ot_keyfile_get_value_with_default_group_optional (self->config, "sysroot", "bootloader", diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 43f380f68c..d66b215ae5 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -1801,14 +1802,60 @@ parse_os_release (const char *contents, const char *split) return ret; } -/* Generate the filename we will use in /boot/loader/entries for this deployment. +/* Generate the entry name we will use in /boot/loader/entries for this deployment. + * The provided n_deployments should be the total number of target deployments (which + * might be different from the cached value in the sysroot). + */ +static char * +bootloader_entry_name (OstreeSysroot *sysroot, guint n_deployments, + OstreeDeployment *deployment) +{ + guint index = n_deployments - ostree_deployment_get_index (deployment); + // Allow opt-out to dropping the stateroot in case of compatibility issues. + // As of 2024.5, we have a new naming scheme because grub2 parses the *filename* and ignores + // the version field. xref https://github.com/ostreedev/ostree/issues/2961 + bool use_old_naming = (sysroot->opt_flags & OSTREE_SYSROOT_GLOBAL_OPT_BOOTLOADER_NAMING_1) > 0; + if (use_old_naming) + { + const char *stateroot = ostree_deployment_get_osname (deployment); + return g_strdup_printf ("ostree-%d-%s", index, stateroot); + } + else + { + return g_strdup_printf ("ostree-%d", index); + } +} + +static guint +bootloader_get_max_boot_tries (OstreeSysroot *self, GCancellable *cancellable, GError **error) +{ + g_autoptr (OstreeRepo) repo = NULL; + if (!ostree_sysroot_get_repo (self, &repo, cancellable, error)) + return 0; + + return repo->boot_counting; +} + +static gboolean +bootloader_is_boot_count_enabled (OstreeSysroot *self, GCancellable *cancellable, GError **error) +{ + g_autoptr (OstreeRepo) repo = NULL; + if (!ostree_sysroot_get_repo (self, &repo, cancellable, error)) + return FALSE; + + return (repo->boot_counting != 0 ? TRUE : FALSE); +} + +/* Generate the entry filename we will use in /boot/loader/entries for this deployment. * The provided n_deployments should be the total number of target deployments (which * might be different from the cached value in the sysroot). */ static char * bootloader_entry_filename (OstreeSysroot *sysroot, guint n_deployments, - OstreeDeployment *deployment) + OstreeDeployment *deployment, GCancellable *cancellable, + GError **error) { + g_autofree char *bootconf_name; guint index = n_deployments - ostree_deployment_get_index (deployment); // Allow opt-out to dropping the stateroot in case of compatibility issues. // As of 2024.5, we have a new naming scheme because grub2 parses the *filename* and ignores @@ -1817,12 +1864,28 @@ bootloader_entry_filename (OstreeSysroot *sysroot, guint n_deployments, if (use_old_naming) { const char *stateroot = ostree_deployment_get_osname (deployment); - return g_strdup_printf ("ostree-%d-%s.conf", index, stateroot); + bootconf_name = g_strdup_printf ("ostree-%d-%s", index, stateroot); } else { - return g_strdup_printf ("ostree-%d.conf", index); + bootconf_name = g_strdup_printf ("ostree-%d", index); } + + if (!bootloader_is_boot_count_enabled(sysroot, cancellable, error)) + return g_strdup_printf ("%s.conf", bootconf_name); + + guint max_tries = bootloader_get_max_boot_tries (sysroot, cancellable, error); + OstreeBootconfigParser *bootconfig = ostree_deployment_get_bootconfig (deployment); + + if (!ostree_bootconfig_parser_is_parsed (bootconfig)) + return g_strdup_printf ("%s+%u.conf", bootconf_name, max_tries); + else if (!ostree_bootconfig_parser_get_tries_left (bootconfig) + && !ostree_bootconfig_parser_get_tries_done (bootconfig)) + return g_strdup_printf ("%s.conf", bootconf_name); + else + return g_strdup_printf ("%s+%" PRIu64 "-%" PRIu64 ".conf", bootconf_name, + ostree_bootconfig_parser_get_tries_left (bootconfig), + ostree_bootconfig_parser_get_tries_done (bootconfig)); } /* Given @deployment, prepare it to be booted; basically copying its @@ -1859,7 +1922,6 @@ install_deployment_kernel (OstreeSysroot *sysroot, int new_bootversion, const char *bootcsum = ostree_deployment_get_bootcsum (deployment); g_autofree char *bootcsumdir = g_strdup_printf ("ostree/%s-%s", osname, bootcsum); g_autofree char *bootconfdir = g_strdup_printf ("loader.%d/entries", new_bootversion); - g_autofree char *bootconf_name = bootloader_entry_filename (sysroot, n_deployments, deployment); if (!glnx_shutil_mkdir_p_at (sysroot->boot_fd, bootcsumdir, 0775, cancellable, error)) return FALSE; @@ -2173,8 +2235,11 @@ install_deployment_kernel (OstreeSysroot *sysroot, int new_bootversion, if (!glnx_opendirat (sysroot->boot_fd, bootconfdir, TRUE, &bootconf_dfd, error)) return FALSE; + g_autofree char *bootconf_filename = bootloader_entry_filename (sysroot, n_deployments, deployment, + cancellable, error); + if (!ostree_bootconfig_parser_write_at (ostree_deployment_get_bootconfig (deployment), - bootconf_dfd, bootconf_name, cancellable, error)) + bootconf_dfd, bootconf_filename, cancellable, error)) return FALSE; return TRUE; @@ -4212,15 +4277,15 @@ ostree_sysroot_deployment_set_kargs_in_place (OstreeSysroot *self, OstreeDeploym OstreeBootconfigParser *new_bootconfig = ostree_deployment_get_bootconfig (deployment); ostree_bootconfig_parser_set (new_bootconfig, "options", kargs_str); - g_autofree char *bootconf_name - = bootloader_entry_filename (self, self->deployments->len, deployment); + g_autofree char *bootconf_filename + = bootloader_entry_filename (self, self->deployments->len, deployment, cancellable, error); g_autofree char *bootconfdir = g_strdup_printf ("loader.%d/entries", self->bootversion); glnx_autofd int bootconf_dfd = -1; if (!glnx_opendirat (self->boot_fd, bootconfdir, TRUE, &bootconf_dfd, error)) return FALSE; - if (!ostree_bootconfig_parser_write_at (new_bootconfig, bootconf_dfd, bootconf_name, + if (!ostree_bootconfig_parser_write_at (new_bootconfig, bootconf_dfd, bootconf_filename, cancellable, error)) return FALSE; }