From bb7a7cb3d6574855ff4bc5dab75c12d79b9e2ad2 Mon Sep 17 00:00:00 2001 From: Ricardo Salveti Date: Wed, 1 Jun 2022 17:18:06 -0300 Subject: [PATCH] sysroot: Support for directories instead of symbolic links in boot part Allow manipulating and updating /boot/loader entries under a normal directory, as well as using symbolic links. For directories this uses `renameat2` to do atomic swap of the loader directory in the boot partition. It fallsback to non-atomic rename. This stays atomic on filesystems supporting links but also provide a non-atomic behavior when filesystem does not provide any atomic alternative. /boot/loader as a normal directory is needed by systemd-boot support, and can be stored under the EFI ESP vfat partition. Tests were duplicated for simplicity reasons. Based on the original implementation done by Valentin David [1]. [1] https://github.com/ostreedev/ostree/pull/1967 Signed-off-by: Ricardo Salveti Signed-off-by: Jose Quaresma Signed-off-by: Igor Opaniuk --- src/libostree/ostree-sysroot-deploy.c | 117 +++++++++++++++++++++++--- src/libostree/ostree-sysroot.c | 65 ++++++++++---- src/switchroot/ostree-prepare-root.c | 2 +- 3 files changed, 158 insertions(+), 26 deletions(-) diff --git a/src/libostree/ostree-sysroot-deploy.c b/src/libostree/ostree-sysroot-deploy.c index 953b6523d0..dc1f1cc203 100644 --- a/src/libostree/ostree-sysroot-deploy.c +++ b/src/libostree/ostree-sysroot-deploy.c @@ -2212,10 +2212,60 @@ prepare_new_bootloader_link (OstreeSysroot *sysroot, int current_bootversion, in return TRUE; } +/* We generate the directory on disk, then potentially do a syncfs() to ensure + * that it (and everything else we wrote) has hit disk. Only after that do we + * rename it into place (via renameat2 RENAME_EXCHANGE). + */ +static gboolean +prepare_new_bootloader_dir (OstreeSysroot *sysroot, + int current_bootversion, + int new_bootversion, + GCancellable *cancellable, + GError **error) +{ + GLNX_AUTO_PREFIX_ERROR ("Preparing bootloader directory", error); + g_assert ((current_bootversion == 0 && new_bootversion == 1) || + (current_bootversion == 1 && new_bootversion == 0)); + + if (!_ostree_sysroot_ensure_boot_fd (sysroot, error)) + return FALSE; + + /* This allows us to support both /boot on a seperate filesystem to / as well + * as on the same filesystem. Allowed to fail with EPERM on ESP/vfat. + */ + if (TEMP_FAILURE_RETRY (symlinkat (".", sysroot->sysroot_fd, "boot/boot")) < 0) + if (errno != EPERM && errno != EEXIST) + return glnx_throw_errno_prefix (error, "symlinkat"); + + /* As the directory gets swapped with glnx_renameat2_exchange, the new bootloader + * deployment needs to first be moved to the 'old' path, as the 'current' one will + * become the older deployment after the exchange. + */ + g_autofree char *loader_new = g_strdup_printf ("loader.%d", new_bootversion); + g_autofree char *loader_old = g_strdup_printf ("loader.%d", current_bootversion); + + /* Tag boot version under an ostree specific file */ + g_autofree char *version_name = g_strdup_printf ("%s/ostree_bootversion", loader_new); + if (!glnx_file_replace_contents_at (sysroot->boot_fd, version_name, + (guint8*)loader_new, strlen(loader_new), + 0, cancellable, error)) + return FALSE; + + /* It is safe to remove older loader version as it wasn't really deployed */ + if (!glnx_shutil_rm_rf_at (sysroot->boot_fd, loader_old, cancellable, error)) + return FALSE; + + /* Rename new deployment to the older path before the exchange */ + if (!glnx_renameat2_noreplace (sysroot->boot_fd, loader_new, sysroot->boot_fd, loader_old)) + return FALSE; + + return TRUE; +} + /* Update the /boot/loader symlink to point to /boot/loader.$new_bootversion */ static gboolean -swap_bootloader (OstreeSysroot *sysroot, OstreeBootloader *bootloader, int current_bootversion, - int new_bootversion, GCancellable *cancellable, GError **error) +swap_bootloader (OstreeSysroot *sysroot, OstreeBootloader *bootloader, gboolean loader_link, + int current_bootversion, int new_bootversion, GCancellable *cancellable, GError **error) { GLNX_AUTO_PREFIX_ERROR ("Final bootloader swap", error); @@ -2225,12 +2275,22 @@ swap_bootloader (OstreeSysroot *sysroot, OstreeBootloader *bootloader, int curre if (!_ostree_sysroot_ensure_boot_fd (sysroot, error)) return FALSE; - /* The symlink was already written, and we used syncfs() to ensure - * its data is in place. Renaming now should give us atomic semantics; - * see https://bugzilla.gnome.org/show_bug.cgi?id=755595 - */ - if (!glnx_renameat (sysroot->boot_fd, "loader.tmp", sysroot->boot_fd, "loader", error)) - return FALSE; + if (loader_link) + { + /* The symlink was already written, and we used syncfs() to ensure + * its data is in place. Renaming now should give us atomic semantics; + * see https://bugzilla.gnome.org/show_bug.cgi?id=755595 + */ + if (!glnx_renameat (sysroot->boot_fd, "loader.tmp", sysroot->boot_fd, "loader", error)) + return FALSE; + } + else + { + /* New target is currently under the old/current version */ + g_autofree char *new_target = g_strdup_printf ("loader.%d", current_bootversion); + if (glnx_renameat2_exchange (sysroot->boot_fd, new_target, sysroot->boot_fd, "loader") != 0) + return FALSE; + } /* Now we explicitly fsync this directory, even though it * isn't required for atomicity, for two reasons: @@ -2448,13 +2508,50 @@ write_deployments_bootswap (OstreeSysroot *self, GPtrArray *new_deployments, return glnx_prefix_error (error, "Bootloader write config"); } - if (!prepare_new_bootloader_link (self, self->bootversion, new_bootversion, cancellable, error)) + /* Handle when boot/loader is a link (normal deployment) and as a normal directory (e.g. EFI/vfat) */ + struct stat stbuf; + gboolean loader_link = FALSE; + if (!glnx_fstatat_allow_noent (self->sysroot_fd, "boot/loader", &stbuf, AT_SYMLINK_NOFOLLOW, error)) return FALSE; + if (errno == ENOENT) + { + /* When there is no loader, check if the fs supports symlink or not */ + if (TEMP_FAILURE_RETRY (symlinkat (".", self->sysroot_fd, "boot/boot")) < 0) + { + if (errno == EPERM) + loader_link = FALSE; + else if (errno != EEXIST) + return glnx_throw_errno_prefix (error, "symlinkat"); + } + else + loader_link = TRUE; + } + else if (S_ISLNK (stbuf.st_mode)) + loader_link = TRUE; + else if (S_ISDIR (stbuf.st_mode)) + loader_link = FALSE; + else + return FALSE; + + if (loader_link) + { + /* Default and when loader is a link is to swap links */ + if (!prepare_new_bootloader_link (self, self->bootversion, new_bootversion, + cancellable, error)) + return FALSE; + } + else + { + /* Handle boot/loader as a directory, and swap with renameat2 RENAME_EXCHANGE */ + if (!prepare_new_bootloader_dir (self, self->bootversion, new_bootversion, + cancellable, error)) + return FALSE; + } if (!full_system_sync (self, out_syncstats, cancellable, error)) return FALSE; - if (!swap_bootloader (self, bootloader, self->bootversion, new_bootversion, cancellable, error)) + if (!swap_bootloader (self, bootloader, loader_link, self->bootversion, new_bootversion, cancellable, error)) return FALSE; if (out_subbootdir) diff --git a/src/libostree/ostree-sysroot.c b/src/libostree/ostree-sysroot.c index 925c66a7e3..2d823b02ad 100644 --- a/src/libostree/ostree-sysroot.c +++ b/src/libostree/ostree-sysroot.c @@ -601,6 +601,12 @@ compare_loader_configs_for_sorting (gconstpointer a_pp, gconstpointer b_pp) return compare_boot_loader_configs (a, b); } +static gboolean +read_current_bootversion (OstreeSysroot *self, + int *out_bootversion, + GCancellable *cancellable, + GError **error); + /* Read all the bootconfigs from `/boot/loader/`. */ gboolean _ostree_sysroot_read_boot_loader_configs (OstreeSysroot *self, int bootversion, @@ -613,7 +619,16 @@ _ostree_sysroot_read_boot_loader_configs (OstreeSysroot *self, int bootversion, g_autoptr (GPtrArray) ret_loader_configs = g_ptr_array_new_with_free_func ((GDestroyNotify)g_object_unref); - g_autofree char *entries_path = g_strdup_printf ("boot/loader.%d/entries", bootversion); + g_autofree char *entries_path = NULL; + int current_version; + if (!read_current_bootversion (self, ¤t_version, cancellable, error)) + return FALSE; + + if (current_version == bootversion) + entries_path = g_strdup ("boot/loader/entries"); + else + entries_path = g_strdup_printf ("boot/loader.%d/entries", bootversion); + gboolean entries_exists; g_auto (GLnxDirFdIterator) dfd_iter = { 0, @@ -660,7 +675,7 @@ _ostree_sysroot_read_boot_loader_configs (OstreeSysroot *self, int bootversion, return TRUE; } -/* Get the bootversion from the `/boot/loader` symlink. */ +/* Get the bootversion from the `/boot/loader` directory or symlink. */ static gboolean read_current_bootversion (OstreeSysroot *self, int *out_bootversion, GCancellable *cancellable, GError **error) @@ -673,24 +688,44 @@ read_current_bootversion (OstreeSysroot *self, int *out_bootversion, GCancellabl return FALSE; if (errno == ENOENT) { - g_debug ("Didn't find $sysroot/boot/loader symlink; assuming bootversion 0"); + g_debug ("Didn't find $sysroot/boot/loader directory or symlink; assuming bootversion 0"); ret_bootversion = 0; } else { - if (!S_ISLNK (stbuf.st_mode)) - return glnx_throw (error, "Not a symbolic link: boot/loader"); - - g_autofree char *target - = glnx_readlinkat_malloc (self->sysroot_fd, "boot/loader", cancellable, error); - if (!target) - return FALSE; - if (g_strcmp0 (target, "loader.0") == 0) - ret_bootversion = 0; - else if (g_strcmp0 (target, "loader.1") == 0) - ret_bootversion = 1; + if (S_ISLNK (stbuf.st_mode)) + { + /* Traditional link, check version by reading link name */ + g_autofree char *target = + glnx_readlinkat_malloc (self->sysroot_fd, "boot/loader", cancellable, error); + if (!target) + return FALSE; + if (g_strcmp0 (target, "loader.0") == 0) + ret_bootversion = 0; + else if (g_strcmp0 (target, "loader.1") == 0) + ret_bootversion = 1; + else + return glnx_throw (error, "Invalid target '%s' in boot/loader", target); + } else - return glnx_throw (error, "Invalid target '%s' in boot/loader", target); + { + /* Loader is a directory, check version by reading ostree_bootversion */ + gsize len; + g_autofree char* version = + glnx_file_get_contents_utf8_at(self->sysroot_fd, "boot/loader/ostree_bootversion", + &len, cancellable, error); + if (version == NULL) + { + g_debug ("Invalid boot/loader/ostree_bootversion, assuming bootversion 0"); + ret_bootversion = 0; + } + else if (g_strcmp0 (version, "loader.0") == 0) + ret_bootversion = 0; + else if (g_strcmp0 (version, "loader.1") == 0) + ret_bootversion = 1; + else + return glnx_throw (error, "Invalid version '%s' in boot/loader/ostree_bootversion", version); + } } *out_bootversion = ret_bootversion; diff --git a/src/switchroot/ostree-prepare-root.c b/src/switchroot/ostree-prepare-root.c index 8e161be76b..b371d1c9bf 100644 --- a/src/switchroot/ostree-prepare-root.c +++ b/src/switchroot/ostree-prepare-root.c @@ -515,7 +515,7 @@ main (int argc, char *argv[]) * at /boot inside the deployment. */ if (snprintf (srcpath, sizeof (srcpath), "%s/boot/loader", root_mountpoint) < 0) err (EXIT_FAILURE, "failed to assemble /boot/loader path"); - if (lstat (srcpath, &stbuf) == 0 && S_ISLNK (stbuf.st_mode)) + if (lstat (srcpath, &stbuf) == 0 && (S_ISLNK (stbuf.st_mode) || S_ISDIR (stbuf.st_mode))) { if (lstat ("boot", &stbuf) == 0 && S_ISDIR (stbuf.st_mode)) {