Skip to content

Commit

Permalink
Add support for directories instead of symbolic links in boot partition
Browse files Browse the repository at this point in the history
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.

Based on the original implementation done by Valentin David
at ostreedev#1967.

Tests were duplicated for simplicity reasons.

Upstream-Status: Pending

Signed-off-by: Ricardo Salveti <ricardo@foundries.io>

%% original patch: 0003-Add-support-for-directories-instead-of-symbolic-link.patch
  • Loading branch information
ricardosalveti authored and quaresmajose committed Jun 23, 2023
1 parent ac63067 commit 15f6700
Show file tree
Hide file tree
Showing 9 changed files with 806 additions and 26 deletions.
4 changes: 4 additions & 0 deletions Makefile-tests.am
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ _installed_or_uninstalled_test_scripts = \
tests/test-admin-deploy-grub2.sh \
tests/test-admin-deploy-nomerge.sh \
tests/test-admin-deploy-none.sh \
tests/test-admin-deploy-dir-syslinux.sh \
tests/test-admin-deploy-dir-uboot.sh \
tests/test-admin-deploy-dir-grub2.sh \
tests/test-admin-deploy-dir-none.sh \
tests/test-admin-deploy-bootid-gc.sh \
tests/test-admin-deploy-whiteouts.sh \
tests/test-osupdate-dtb.sh \
Expand Down
117 changes: 107 additions & 10 deletions src/libostree/ostree-sysroot-deploy.c
Original file line number Diff line number Diff line change
Expand Up @@ -2240,10 +2240,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);

Expand All @@ -2253,12 +2303,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:
Expand Down Expand Up @@ -2470,13 +2530,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)
Expand Down
65 changes: 50 additions & 15 deletions src/libostree/ostree-sysroot.c
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,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,
Expand All @@ -609,7 +615,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, &current_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,
Expand Down Expand Up @@ -656,7 +671,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)
Expand All @@ -669,24 +684,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;
Expand Down
2 changes: 1 addition & 1 deletion src/switchroot/ostree-prepare-root.c
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,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))
{
Expand Down
Loading

0 comments on commit 15f6700

Please sign in to comment.