sysext: Skip refresh if no changes are found

When the extensions for the final system are already set up from the
initrd we should avoid disrupting the boot process with the remount
(which currently isn't atomic) and the daemon reload for
systemd-confext and systemd-sysext. Similarly, when sysupdate ran and
updated extensions it's best to avoid the remount and daemon reload if
no changes are found.
To do this, encode the current extension state in more detail than
before where only the names of the extensions where encoded in the
overlay mount. This can also be used to provide more details about the
extension origin in "systemd-sysext status (--json=)". During the
refresh add a check whether the old state matches the new state and in
this case skip the refresh unless the user provides a flag to always
refresh. Besides the extension name and the resolved path the best
method for identification is the verity hash but that is not available
for plain image files or directories. Therefore, also include data to
check for file/directory replacements. The creation/modification times
are not always real on reproducible images or extracted archive content.
The file handle together with the unique mount ID is the next best
identifier we can use when we have no verity hash. Fall back to an inode
when we get no handle. With the creation/modification time and the path
this should be good enough. Using a unique mount ID is important (with
a fallback to the regular non-unique mount ID) instead of st_dev because
st_dev gets reused too easily, e.g., by a loop device mount and the
mount ID helps to catch this. For the mount ID to be valid it has to be
resolved before we enter the new mount namespace. Thus, it gets provided
by the image dissect logic and handed over to the sysext subprocess
which runs in a new mount namespace.
Luckily, we can rule out online modification of directories or image
files because this is anyway not well supported with overlay mounts, so
we don't do a file checksum nor do we recurse into a directory to look
for the most recently touched files.  But, as said, with the
always-refresh flag one can force a reload.
This commit is contained in:
Kai Lueke
2026-02-04 00:05:24 +01:00
committed by Kai Lüke
parent 2e4c2f8df7
commit 23115eeaf1
10 changed files with 542 additions and 42 deletions
+102 -8
View File
@@ -50,12 +50,13 @@ int name_to_handle_at_loop(
const char *path,
struct file_handle **ret_handle,
int *ret_mnt_id,
uint64_t *ret_unique_mnt_id,
int flags) {
size_t n = ORIGINAL_MAX_HANDLE_SZ;
assert(fd >= 0 || fd == AT_FDCWD);
assert((flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH|AT_HANDLE_FID)) == 0);
assert((flags & ~(AT_SYMLINK_FOLLOW|AT_EMPTY_PATH|AT_HANDLE_FID|AT_HANDLE_MNT_ID_UNIQUE)) == 0);
/* We need to invoke name_to_handle_at() in a loop, given that it might return EOVERFLOW when the specified
* buffer is too small. Note that in contrast to what the docs might suggest, MAX_HANDLE_SZ is only good as a
@@ -71,7 +72,8 @@ int name_to_handle_at_loop(
for (;;) {
_cleanup_free_ struct file_handle *h = NULL;
int mnt_id = -1;
int mnt_id = -1, r;
uint64_t unique_mnt_id = 0;
h = malloc0(offsetof(struct file_handle, f_handle) + n);
if (!h)
@@ -79,11 +81,18 @@ int name_to_handle_at_loop(
h->handle_bytes = n;
if (name_to_handle_at(fd, path, h, &mnt_id, flags) >= 0) {
if (FLAGS_SET(flags, AT_HANDLE_MNT_ID_UNIQUE))
/* The kernel will still use this as uint64_t pointer */
r = name_to_handle_at(fd, path, h, (int *) &unique_mnt_id, flags);
else
r = name_to_handle_at(fd, path, h, &mnt_id, flags);
if (r >= 0) {
if (ret_handle)
*ret_handle = TAKE_PTR(h);
if (ret_unique_mnt_id)
*ret_unique_mnt_id = unique_mnt_id;
if (ret_mnt_id)
*ret_mnt_id = mnt_id;
@@ -92,13 +101,16 @@ int name_to_handle_at_loop(
if (errno != EOVERFLOW)
return -errno;
if (!ret_handle && ret_mnt_id && mnt_id >= 0) {
if (!ret_handle && ((ret_mnt_id && mnt_id >= 0) || (ret_unique_mnt_id && unique_mnt_id > 0))) {
/* As it appears, name_to_handle_at() fills in mnt_id even when it returns EOVERFLOW when the
* buffer is too small, but that's undocumented. Hence, let's make use of this if it appears to
* be filled in, and the caller was interested in only the mount ID an nothing else. */
*ret_mnt_id = mnt_id;
if (ret_unique_mnt_id)
*ret_unique_mnt_id = unique_mnt_id;
if (ret_mnt_id)
*ret_mnt_id = mnt_id;
return 0;
}
@@ -132,11 +144,55 @@ int name_to_handle_at_try_fid(
* we'll try without the flag, in order to support older kernels that didn't have AT_HANDLE_FID
* (i.e. older than Linux 6.5). */
r = name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, flags | AT_HANDLE_FID);
r = name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, /* ret_unique_mnt_id= */ NULL, flags | AT_HANDLE_FID);
if (r >= 0 || is_name_to_handle_at_fatal_error(r))
return r;
return name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, flags & ~AT_HANDLE_FID);
return name_to_handle_at_loop(fd, path, ret_handle, ret_mnt_id, /* ret_unique_mnt_id= */ NULL, flags & ~AT_HANDLE_FID);
}
int name_to_handle_at_try_unique_mntid_fid(
int fd,
const char *path,
struct file_handle **ret_handle,
uint64_t *ret_mnt_id,
int flags) {
int mnt_id = -1, r;
assert(fd >= 0 || fd == AT_FDCWD);
/* First issues name_to_handle_at() with AT_HANDLE_MNT_ID_UNIQUE and AT_HANDLE_FID.
* If this fails and this is not a fatal error we'll try without the
* AT_HANDLE_MNT_ID_UNIQUE flag because it's only available from Linux 6.12 onwards. */
r = name_to_handle_at_loop(fd, path, ret_handle, /* ret_mnt_id= */ NULL, ret_mnt_id, flags | AT_HANDLE_MNT_ID_UNIQUE | AT_HANDLE_FID);
if (r >= 0 || is_name_to_handle_at_fatal_error(r))
return r;
flags &= ~AT_HANDLE_MNT_ID_UNIQUE;
/* Then issues name_to_handle_at() with AT_HANDLE_FID. If this fails and this is not a fatal error
* we'll try without the flag, in order to support older kernels that didn't have AT_HANDLE_FID
* (i.e. older than Linux 6.5). */
r = name_to_handle_at_loop(fd, path, ret_handle, &mnt_id, /* ret_unique_mnt_id= */ NULL, flags | AT_HANDLE_FID);
if (r < 0 && is_name_to_handle_at_fatal_error(r))
return r;
if (r >= 0) {
if (ret_mnt_id && mnt_id >= 0) {
/* See if we can do better because statx can do unique mount IDs since Linux 6.8
* and only if this doesn't work we use the non-unique mnt_id as returned. */
if (path_get_unique_mnt_id_at(fd, path, ret_mnt_id) < 0)
*ret_mnt_id = mnt_id;
}
return r;
}
r = name_to_handle_at_loop(fd, path, ret_handle, &mnt_id, /* ret_unique_mnt_id= */ NULL, flags & ~AT_HANDLE_FID);
if (ret_mnt_id && mnt_id >= 0)
*ret_mnt_id = mnt_id;
return r;
}
int name_to_handle_at_u64(int fd, const char *path, uint64_t *ret) {
@@ -147,7 +203,7 @@ int name_to_handle_at_u64(int fd, const char *path, uint64_t *ret) {
/* This provides the first 64bit of the file handle. */
r = name_to_handle_at_loop(fd, path, &h, /* ret_mnt_id= */ NULL, /* flags= */ 0);
r = name_to_handle_at_loop(fd, path, &h, /* ret_mnt_id= */ NULL, /* ret_unique_mnt_id= */ NULL, /* flags= */ 0);
if (r < 0)
return r;
if (h->handle_bytes < sizeof(uint64_t))
@@ -171,6 +227,22 @@ bool file_handle_equal(const struct file_handle *a, const struct file_handle *b)
return memcmp_nn(a->f_handle, a->handle_bytes, b->f_handle, b->handle_bytes) == 0;
}
struct file_handle* file_handle_dup(const struct file_handle *fh) {
_cleanup_free_ struct file_handle *fh_copy = NULL;
assert(fh);
fh_copy = malloc0(offsetof(struct file_handle, f_handle) + fh->handle_bytes);
if (!fh_copy)
return NULL;
fh_copy->handle_bytes = fh->handle_bytes;
fh_copy->handle_type = fh->handle_type;
memcpy(fh_copy->f_handle, fh->f_handle, fh->handle_bytes);
return TAKE_PTR(fh_copy);
}
int is_mount_point_at(int dir_fd, const char *path, int flags) {
int r;
@@ -259,6 +331,28 @@ int path_get_mnt_id_at(int dir_fd, const char *path, int *ret) {
return 0;
}
int path_get_unique_mnt_id_at(int dir_fd, const char *path, uint64_t *ret) {
struct statx sx;
assert(dir_fd >= 0 || dir_fd == AT_FDCWD);
assert(ret);
if (statx(dir_fd,
strempty(path),
(isempty(path) ? AT_EMPTY_PATH : AT_SYMLINK_NOFOLLOW) |
AT_NO_AUTOMOUNT | /* don't trigger automounts, mnt_id is a local concept */
AT_STATX_DONT_SYNC, /* don't go to the network, mnt_id is a local concept */
STATX_MNT_ID_UNIQUE,
&sx) < 0)
return -errno;
if (!FLAGS_SET(sx.stx_mask, STATX_MNT_ID_UNIQUE))
return -EOPNOTSUPP;
*ret = sx.stx_mnt_id;
return 0;
}
bool fstype_is_network(const char *fstype) {
const char *x;
+4 -1
View File
@@ -34,8 +34,9 @@
bool is_name_to_handle_at_fatal_error(int err);
int name_to_handle_at_loop(int fd, const char *path, struct file_handle **ret_handle, int *ret_mnt_id, int flags);
int name_to_handle_at_loop(int fd, const char *path, struct file_handle **ret_handle, int *ret_mnt_id, uint64_t *ret_unique_mnt_id, int flags);
int name_to_handle_at_try_fid(int fd, const char *path, struct file_handle **ret_handle, int *ret_mnt_id, int flags);
int name_to_handle_at_try_unique_mntid_fid(int fd, const char *path, struct file_handle **ret_handle, uint64_t *ret_mnt_id, int flags);
int name_to_handle_at_u64(int fd, const char *path, uint64_t *ret);
static inline int path_to_handle_u64(const char *path, uint64_t *ret) {
return name_to_handle_at_u64(AT_FDCWD, path, ret);
@@ -45,11 +46,13 @@ static inline int fd_to_handle_u64(int fd, uint64_t *ret) {
}
bool file_handle_equal(const struct file_handle *a, const struct file_handle *b);
struct file_handle* file_handle_dup(const struct file_handle *fh);
int path_get_mnt_id_at(int dir_fd, const char *path, int *ret);
static inline int path_get_mnt_id(const char *path, int *ret) {
return path_get_mnt_id_at(AT_FDCWD, path, ret);
}
int path_get_unique_mnt_id_at(int dir_fd, const char *path, uint64_t *ret);
int is_mount_point_at(int dir_fd, const char *path, int flags);
int path_is_mount_point_full(const char *path, const char *root, int flags);