-
Notifications
You must be signed in to change notification settings - Fork 331
sysroot: Support for directories instead of symbolic links in boot part #3359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -102,6 +102,21 @@ sysroot_flags_to_copy_flags (GLnxFileCopyFlags defaults, OstreeSysrootDebugFlags | |
return defaults; | ||
} | ||
|
||
static gboolean | ||
create_bootself_link (OstreeSysroot *self, GCancellable *cancellable, GError **error) | ||
{ | ||
/* 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) | ||
return FALSE; | ||
else if (errno != EEXIST) | ||
return glnx_throw_errno_prefix (error, "symlinkat"); | ||
} | ||
|
||
return TRUE; | ||
} | ||
|
||
/* Try a hardlink if we can, otherwise fall back to copying. Used | ||
* right now for kernels/initramfs/device trees in /boot, where we can just | ||
* hardlink if we're on the same partition. | ||
|
@@ -2176,6 +2191,53 @@ install_deployment_kernel (OstreeSysroot *sysroot, int new_bootversion, | |
return TRUE; | ||
} | ||
|
||
/* Determine whether an existing directory implements the semantics described in | ||
* https://uapi-group.org/specifications/specs/boot_loader_specification/#type-1-boot-loader-entry-keys | ||
*/ | ||
static gboolean | ||
is_bootconfig_type1_semantics (OstreeSysroot *sysroot, GCancellable *cancellable, GError **error) | ||
{ | ||
struct stat stbuf; | ||
|
||
if (!_ostree_sysroot_ensure_boot_fd (sysroot, error)) | ||
return FALSE; | ||
|
||
if (!glnx_fstatat_allow_noent (sysroot->boot_fd, "loader/entries.srel", &stbuf, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to create / write There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. systemd-boot also reads |
||
AT_SYMLINK_NOFOLLOW, error)) | ||
return FALSE; | ||
|
||
if (errno == ENOENT) | ||
{ | ||
g_debug ("Didn't find loader/entries.srel file"); | ||
return FALSE; | ||
} | ||
else | ||
{ | ||
/* Get semantics type by reading loader/entries.srel */ | ||
gsize len; | ||
g_autofree char *type = glnx_file_get_contents_utf8_at ( | ||
sysroot->boot_fd, "loader/entries.srel", &len, cancellable, error); | ||
if (type == NULL) | ||
{ | ||
g_debug ("Invalid loader/entries.srel file"); | ||
return FALSE; | ||
} | ||
|
||
if (g_str_has_prefix (type, "type1")) | ||
{ | ||
g_debug ("type1 semantics is found in loader/entries.srel file"); | ||
return TRUE; | ||
} | ||
else | ||
{ | ||
g_debug ("Unsupported semantics type ('%s') in loader/entries.srel file", type); | ||
return FALSE; | ||
} | ||
} | ||
|
||
return FALSE; | ||
} | ||
|
||
/* We generate the symlink 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. | ||
|
@@ -2190,9 +2252,7 @@ prepare_new_bootloader_link (OstreeSysroot *sysroot, int current_bootversion, in | |
|
||
/* This allows us to support both /boot on a seperate filesystem to / as well | ||
* as on the same filesystem. */ | ||
if (TEMP_FAILURE_RETRY (symlinkat (".", sysroot->sysroot_fd, "boot/boot")) < 0) | ||
if (errno != EEXIST) | ||
return glnx_throw_errno_prefix (error, "symlinkat"); | ||
(void)create_bootself_link (sysroot, cancellable, error); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above; let's keep checking errors here, with my above suggestion this just changes to
|
||
|
||
g_autofree char *new_target = g_strdup_printf ("loader.%d", new_bootversion); | ||
|
||
|
@@ -2204,10 +2264,57 @@ 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking about this whole thing more and it's not clear to me we need the concept of a "bootversion" at all in the case where we're doing a directory swap, do we? We could easily have
That would have been harder to do with the symlink logic but I think it'd be quite a simplification to the current code here. Especially then we can stop writing the special There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The directory name could have been random, or a hash of the content, and for the cleanup you just need to readlink before you rename, but we can't change now
Agreed, no need for bootversion when using directory swap |
||
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"); | ||
igoropaniuk marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+2285
to
+2287
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then this one is:
|
||
|
||
/* 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); | ||
|
||
|
@@ -2217,14 +2324,24 @@ swap_bootloader (OstreeSysroot *sysroot, OstreeBootloader *bootloader, int curre | |
if (!_ostree_sysroot_ensure_boot_fd (sysroot, error)) | ||
return FALSE; | ||
|
||
g_assert_cmpint (sysroot->boot_fd, !=, -1); | ||
if (loader_link) | ||
{ | ||
g_assert_cmpint (sysroot->boot_fd, !=, -1); | ||
|
||
/* 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; | ||
/* 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return FALSE; | ||
} | ||
|
||
/* As grub doesn't support replaying XFS journal, | ||
* we must fsfreeze/thaw again here: | ||
|
@@ -2447,13 +2564,47 @@ 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 symlink (original ostree model) or is a directory (e.g. EFI/vfat) | ||
* for recommended systemd type1 BLS | ||
*/ | ||
struct stat stbuf; | ||
gboolean loader_link = FALSE; | ||
gboolean force_type1_semantics = is_bootconfig_type1_semantics (self, cancellable, error); | ||
if (!glnx_fstatat_allow_noent (self->sysroot_fd, "boot/loader", &stbuf, AT_SYMLINK_NOFOLLOW, | ||
error)) | ||
return FALSE; | ||
if (errno == ENOENT) | ||
loader_link = create_bootself_link (self, cancellable, error); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. In an ideal world we wouldn't get here...it's a bit tempting to ask that for the case of a user deploying this new version of ostree targeting an ESP we ask them to pre-create the link. Or...actually another tempting thing to do is check the filesystem type for Anyways...for now I think what I'd ask is we continue to properly check errors here... OK I thought more about this and came up with Then a lot of this code can start checking There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #3405 merged! Let me know what you think! |
||
else if (S_ISLNK (stbuf.st_mode)) | ||
loader_link = TRUE; | ||
else if (S_ISDIR (stbuf.st_mode)) | ||
loader_link = FALSE; | ||
else | ||
return FALSE; | ||
|
||
if (force_type1_semantics && loader_link) | ||
return glnx_throw_errno_prefix (error, "type1 semantics, but boot/loader is symlink"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should just be |
||
|
||
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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,8 @@ | |
#include "ostree-sysroot-private.h" | ||
#include "otcore.h" | ||
|
||
#define BOOTVERSION_FILE "boot/loader/ostree_bootversion" | ||
|
||
/** | ||
* SECTION:ostree-sysroot | ||
* @title: Root partition mount point | ||
|
@@ -601,6 +603,9 @@ 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 +618,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); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we ever reading anything else than the current config ? ie |
||
gboolean entries_exists; | ||
g_auto (GLnxDirFdIterator) dfd_iter = { | ||
0, | ||
|
@@ -660,7 +674,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 +687,49 @@ 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 */ | ||
glnx_autofd int bversion_fd = -1; | ||
if (!ot_openat_ignore_enoent (self->sysroot_fd, BOOTVERSION_FILE, &bversion_fd, error)) | ||
return FALSE; | ||
|
||
if (bversion_fd == -1) | ||
{ | ||
g_debug ("File " BOOTVERSION_FILE " is not available, assuming bootversion 0"); | ||
ret_bootversion = 0; | ||
} | ||
else | ||
{ | ||
g_autofree char *version | ||
= glnx_fd_readall_utf8 (bversion_fd, NULL, cancellable, error); | ||
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 " BOOTVERSION_FILE, version); | ||
} | ||
} | ||
} | ||
|
||
*out_bootversion = ret_bootversion; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again need to adhere to GError conventions; this case returns
FALSE
but leaveserror
unset. Honestly I think it's simplest to have this helper function not use gerror i.e.Then the callers can just directly check
errno
as they see fit?