diff --git a/man/systemd-sysext.xml b/man/systemd-sysext.xml index b5ab6826a2..d6bbc0141d 100644 --- a/man/systemd-sysext.xml +++ b/man/systemd-sysext.xml @@ -371,6 +371,19 @@ + + + + When refreshing system extensions on /usr/ and /opt/ + for sysext and /etc/ for confext, ignore when the existing merged extensions + already match what would be merged. + By default the refresh is skipped when no changes are found. Note that changes done to an extension + directory while it's merged are ignored without this flag (unless an other extension got changed). + Note that changing the contents while merged is also undefined behavior in overlayfs. + + + + diff --git a/shell-completion/bash/systemd-sysext b/shell-completion/bash/systemd-sysext index c605237ed6..69d786c33e 100644 --- a/shell-completion/bash/systemd-sysext +++ b/shell-completion/bash/systemd-sysext @@ -36,6 +36,7 @@ _systemd-sysext() { [ARG]='--root --json --noexec + --always-refresh --image-policy --mutable' ) diff --git a/src/basic/mountpoint-util.c b/src/basic/mountpoint-util.c index 547183395e..d8f57c6b7a 100644 --- a/src/basic/mountpoint-util.c +++ b/src/basic/mountpoint-util.c @@ -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; diff --git a/src/basic/mountpoint-util.h b/src/basic/mountpoint-util.h index 8799224f3d..82eb1516c3 100644 --- a/src/basic/mountpoint-util.h +++ b/src/basic/mountpoint-util.h @@ -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); diff --git a/src/include/override/fcntl.h b/src/include/override/fcntl.h index f2b40a6a17..b41f364534 100644 --- a/src/include/override/fcntl.h +++ b/src/include/override/fcntl.h @@ -17,3 +17,8 @@ #ifndef AT_HANDLE_FID #define AT_HANDLE_FID AT_REMOVEDIR #endif + +/* This is defined since glibc-2.42. */ +#ifndef AT_HANDLE_MNT_ID_UNIQUE +#define AT_HANDLE_MNT_ID_UNIQUE 0x001 /* Return the u64 unique mount ID. */ +#endif diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 5bd495ee16..d80ecdaf7f 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -35,6 +35,7 @@ #include "lock-util.h" #include "log.h" #include "loop-util.h" +#include "mountpoint-util.h" #include "namespace-util.h" #include "nsresource.h" #include "nulstr-util.h" @@ -148,6 +149,8 @@ static Image* image_free(Image *i) { free(i->name); free(i->path); + free(i->fh); + free(i->hostname); strv_free(i->machine_info); strv_free(i->os_release); @@ -249,6 +252,9 @@ static int image_new( bool read_only, usec_t crtime, usec_t mtime, + struct file_handle *fh, + uint64_t on_mount_id, + uint64_t inode, Image **ret) { _cleanup_(image_unrefp) Image *i = NULL; @@ -270,12 +276,20 @@ static int image_new( .read_only = read_only, .crtime = crtime, .mtime = mtime, + .on_mount_id = on_mount_id, + .inode = inode, .usage = UINT64_MAX, .usage_exclusive = UINT64_MAX, .limit = UINT64_MAX, .limit_exclusive = UINT64_MAX, }; + if (fh) { + i->fh = file_handle_dup(fh); + if (!i->fh) + return -ENOMEM; + } + i->name = strdup(pretty); if (!i->name) return -ENOMEM; @@ -437,6 +451,28 @@ static int image_make( path_startswith(path, "/usr") || (faccessat(fd, "", W_OK, AT_EACCESS|AT_EMPTY_PATH) < 0 && errno == EROFS); + uint64_t on_mount_id = 0; + _cleanup_free_ struct file_handle *fh = NULL; + + r = name_to_handle_at_try_unique_mntid_fid(fd, /* path= */ NULL, &fh, &on_mount_id, /* flags= */ 0); + if (r < 0) { + if (is_name_to_handle_at_fatal_error(r)) + return r; + + r = path_get_unique_mnt_id_at(fd, /* path= */ NULL, &on_mount_id); + if (r < 0) { + if (!ERRNO_IS_NEG_NOT_SUPPORTED(r)) + return r; + + int on_mount_id_fallback = -1; + r = path_get_mnt_id_at(fd, /* path= */ NULL, &on_mount_id_fallback); + if (r < 0) + return r; + + on_mount_id = on_mount_id_fallback; + } + } + if (S_ISDIR(st->st_mode)) { unsigned file_attr = 0; usec_t crtime = 0; @@ -478,6 +514,9 @@ static int image_make( info.read_only || read_only, info.otime, info.ctime, + fh, + on_mount_id, + (uint64_t) st->st_ino, ret); if (r < 0) return r; @@ -503,6 +542,9 @@ static int image_make( read_only || (file_attr & FS_IMMUTABLE_FL), crtime, 0, /* we don't use mtime of stat() here, since it's not the time of last change of the tree, but only of the top-level dir */ + fh, + on_mount_id, + (uint64_t) st->st_ino, ret); if (r < 0) return r; @@ -540,6 +582,9 @@ static int image_make( !(st->st_mode & 0222) || read_only, crtime, timespec_load(&st->st_mtim), + fh, + on_mount_id, + (uint64_t) st->st_ino, ret); if (r < 0) return r; @@ -597,6 +642,9 @@ static int image_make( !(st->st_mode & 0222) || read_only, 0, 0, + fh, + on_mount_id, + (uint64_t) st->st_ino, ret); if (r < 0) return r; diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h index 1ed8b335aa..c2c1a5e93d 100644 --- a/src/shared/discover-image.h +++ b/src/shared/discover-image.h @@ -27,6 +27,10 @@ typedef struct Image { usec_t crtime; usec_t mtime; + struct file_handle *fh; + uint64_t on_mount_id; + uint64_t inode; + uint64_t usage; uint64_t usage_exclusive; uint64_t limit; diff --git a/src/shared/varlink-io.systemd.sysext.c b/src/shared/varlink-io.systemd.sysext.c index 13d39a89ff..337769c3dd 100644 --- a/src/shared/varlink-io.systemd.sysext.c +++ b/src/shared/varlink-io.systemd.sysext.c @@ -34,6 +34,7 @@ static SD_VARLINK_DEFINE_METHOD( SD_VARLINK_DEFINE_INPUT_BY_TYPE(class, ImageClass, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_INPUT(force, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_INPUT(noReload, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), + SD_VARLINK_DEFINE_INPUT(alwaysRefresh, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), SD_VARLINK_DEFINE_INPUT(noexec, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), VARLINK_DEFINE_POLKIT_INPUT); diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index ea3ebceab5..92431262f3 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -8,6 +8,7 @@ #include #include +#include "sd-json.h" #include "sd-varlink.h" #include "argv-util.h" @@ -86,6 +87,17 @@ static const char* const mutable_mode_table[_MUTABLE_MAX] = { DEFINE_PRIVATE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(mutable_mode, MutableMode, MUTABLE_YES); +enum { + MERGE_NOTHING_FOUND, + MERGE_MOUNTED, + MERGE_SKIP_REFRESH, +}; + +enum { + MERGE_EXIT_NOTHING_FOUND = 123, + MERGE_EXIT_SKIP_REFRESH = 124, +}; + static char **arg_hierarchies = NULL; /* "/usr" + "/opt" by default for sysext and /etc by default for confext */ static char *arg_root = NULL; static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF; @@ -93,6 +105,7 @@ static PagerFlags arg_pager_flags = 0; static bool arg_legend = true; static bool arg_force = false; static bool arg_no_reload = false; +static bool arg_always_refresh = false; static int arg_noexec = -1; static ImagePolicy *arg_image_policy = NULL; static bool arg_image_policy_set = false; /* Tracks initialization */ @@ -1037,23 +1050,25 @@ static int resolve_mutable_directory( } if (IN_SET(arg_mutable, MUTABLE_YES, MUTABLE_EPHEMERAL, MUTABLE_EPHEMERAL_IMPORT)) { - _cleanup_free_ char *path_in_root = NULL; + _cleanup_close_ int path_fd = -EBADF, chmod_fd = -EBADF; - path_in_root = path_join(root, path); - if (!path_in_root) - return log_oom(); - - r = mkdir_p(path_in_root, 0700); + /* This also creates, e.g., /var/lib/extensions.mutable/usr if needed and all parent + * directories plus it also works when the last part is a symlink to the real /usr but we + * can't use chase_and_open here because it does not behave the same. */ + r = chase(path, root, CHASE_AT_RESOLVE_IN_ROOT|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY|CHASE_PREFIX_ROOT, /* ret_path */ NULL, &path_fd); if (r < 0) - return log_error_errno(r, "Failed to create a directory '%s': %m", path_in_root); + return log_error_errno(r, "Failed to chase/create base directory '%s/%s': %m", strempty(root), skip_leading_slash(path)); - _cleanup_close_ int atfd = open(path_in_root, O_DIRECTORY|O_CLOEXEC); - if (atfd < 0) - return log_error_errno(errno, "Failed to open directory '%s': %m", path_in_root); + chmod_fd = fd_reopen(path_fd, O_CLOEXEC|O_DIRECTORY); + if (chmod_fd < 0) + return log_error_errno(chmod_fd, "Failed to reopen '%s/%s': %m", strempty(root), skip_leading_slash(path)); - r = mac_selinux_fix_full(atfd, /* inode_path= */ NULL, hierarchy, /* flags= */ 0); + if (fchmod(chmod_fd, hierarchy_mode) < 0) + return log_error_errno(errno, "Failed to chmod directory '%s/%s': %m", strempty(root), skip_leading_slash(path)); + + r = mac_selinux_fix_full(chmod_fd, /* inode_path= */ NULL, hierarchy, /* flags= */ 0); if (r < 0) - return log_error_errno(r, "Failed to fix SELinux label for '%s': %m", path_in_root); + return log_error_errno(r, "Failed to fix SELinux label for '%s/%s': %m", strempty(root), skip_leading_slash(path)); } r = chase(path, root, CHASE_PREFIX_ROOT, &resolved_path, NULL); @@ -1489,6 +1504,30 @@ static int write_extensions_file(ImageClass image_class, char **extensions, cons return 0; } +static int write_origin_file(ImageClass image_class, const char *origin_content, const char *meta_path, const char *hierarchy) { + _cleanup_free_ char *f = NULL; + int r; + + assert(meta_path); + + /* The origin file is compared to know if a refresh can be skipped (opt-in, used at service startup). */ + f = path_join(meta_path, image_class_info[image_class].dot_directory_name, "origin"); + if (!f) + return log_oom(); + + _cleanup_free_ char *hierarchy_path = path_join(hierarchy, image_class_info[image_class].dot_directory_name, image_class_info[image_class].short_identifier_plural); + if (!hierarchy_path) + return log_oom(); + + r = write_string_file_full(AT_FDCWD, f, strempty(origin_content), + WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755|WRITE_STRING_FILE_LABEL|WRITE_STRING_FILE_AVOID_NEWLINE, + /* ts= */ NULL, hierarchy_path); + if (r < 0) + return log_error_errno(r, "Failed to write origin meta file '%s': %m", f); + + return 0; +} + static int write_dev_file(ImageClass image_class, const char *meta_path, const char *overlay_path, const char *hierarchy) { _cleanup_free_ char *f = NULL; struct stat st; @@ -1593,6 +1632,7 @@ static int write_work_dir_file(ImageClass image_class, const char *meta_path, co static int store_info_in_meta( ImageClass image_class, char **extensions, + const char *origin_content, const char *meta_path, const char *overlay_path, const char *work_dir, @@ -1625,6 +1665,10 @@ static int store_info_in_meta( if (r < 0) return r; + r = write_origin_file(image_class, origin_content, meta_path, hierarchy); + if (r < 0) + return r; + r = write_dev_file(image_class, meta_path, overlay_path, hierarchy); if (r < 0) return r; @@ -1683,6 +1727,7 @@ static int merge_hierarchy( int noexec, char **extensions, char **paths, + const char *origin_content, const char *meta_path, const char *overlay_path, const char *workspace_path) { @@ -1728,7 +1773,7 @@ static int merge_hierarchy( if (r < 0) return r; - r = store_info_in_meta(image_class, extensions, meta_path, overlay_path, op->work_dir, op->hierarchy, backing); + r = store_info_in_meta(image_class, extensions, origin_content, meta_path, overlay_path, op->work_dir, op->hierarchy, backing); if (r < 0) return r; @@ -1780,19 +1825,29 @@ static int merge_subprocess( ImageClass image_class, char **hierarchies, bool force, + bool always_refresh, int noexec, Hashmap *images, const char *workspace) { _cleanup_free_ char *host_os_release_id = NULL, *host_os_release_id_like = NULL, *host_os_release_version_id = NULL, *host_os_release_api_level = NULL, - *filename = NULL; + *filename = NULL, *old_origin_content = NULL, + *extensions_origin_content = NULL, *root_resolved = NULL; _cleanup_strv_free_ char **extensions = NULL, **extensions_v = NULL, **paths = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *extensions_origin_entries = NULL, + *extensions_origin_json = NULL, *mutable_dir_entries = NULL; size_t n_extensions = 0; unsigned n_ignored = 0; Image *img; int r; + if (!isempty(arg_root)) { + r = chase(arg_root, /* root= */ NULL, CHASE_MUST_BE_DIRECTORY, &root_resolved, /* ret_fd= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to resolve --root='%s': %m", strempty(arg_root)); + } + assert(path_startswith(workspace, "/run/")); /* Mark the whole of /run as MS_SLAVE, so that we can mount stuff below it that doesn't show up on @@ -1830,7 +1885,8 @@ static int merge_subprocess( /* Let's now mount all images */ HASHMAP_FOREACH(img, images) { - _cleanup_free_ char *p = NULL; + _cleanup_free_ char *p = NULL, *path_without_root = NULL; + _cleanup_(sd_json_variant_unrefp) sd_json_variant *verity_hash = NULL; p = path_join(workspace, image_class_info[image_class].short_identifier_plural, img->name); if (!p) @@ -1929,6 +1985,12 @@ static int merge_subprocess( if (r < 0) return r; + if (iovec_is_set(&verity_settings.root_hash)) { + r = sd_json_variant_new_hex(&verity_hash, verity_settings.root_hash.iov_base, verity_settings.root_hash.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to create origin verity entry for '%s': %m", img->name); + } + r = dissected_image_decrypt(m, arg_root, /* passphrase= */ NULL, &verity_settings, pick_image_policy(img), flags); if (r < 0) return r; @@ -1998,6 +2060,60 @@ static int merge_subprocess( if (r < 0) return log_oom(); + /* Encode extension image origin to check if we can skip the refresh. + * It can also be used to provide more detail in "systemd-sysext status". */ + + if (!isempty(arg_root)) { + const char *without_root = NULL; + without_root = path_startswith(img->path, root_resolved); + if (!isempty(without_root)) { + path_without_root = strjoin("/", without_root); + if (!path_without_root) + return log_oom(); + } + } + if (!path_without_root) { + path_without_root = strdup(img->path); + if (!path_without_root) + return log_oom(); + } + + /* The verity hash is not available for all extension types, thus, but only as fallback, + * also include data to check for file/directory replacements through a file handle and + * unique mount ID (or inode and mount ID as fallback). + * A unique mount ID is best because st_dev gets reused too easily, e.g., by a loop dev + * mount. For the mount ID to be valid it has to be resolved before we enter the new mount + * namespace. Thus, here it wouldn't work and so instead it gets provided by the image + * dissect logic and handed over to this subprocess we are in. + * Online modification is not well supported with overlay mounts, so we don't do a file + * checksum nor do we recurse into a directory to look for touched files. If users want + * modifications to be picked up, they need to set the --always-refresh=yes flag (as will be + * printed out). */ + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *origin_entry = NULL; + + /* We suppress inclusion of weak identifiers when a strong one is there so that, e.g., + * a confext image stored on /usr gets identified only by the verity hash instead of also + * the mount ID because that changes when a sysext overlay mount appears but since the + * verity hash is the same for the confext it can actually be reused. */ + r = sd_json_buildo(&origin_entry, + SD_JSON_BUILD_PAIR_STRING("path", path_without_root), + SD_JSON_BUILD_PAIR_CONDITION(!!verity_hash, "verityHash", SD_JSON_BUILD_VARIANT(verity_hash)), + SD_JSON_BUILD_PAIR_CONDITION(!verity_hash, "onMountId", SD_JSON_BUILD_UNSIGNED(img->on_mount_id)), + SD_JSON_BUILD_PAIR_CONDITION(!verity_hash && !!img->fh, "fileHandle", + SD_JSON_BUILD_OBJECT(SD_JSON_BUILD_PAIR_INTEGER("type", img->fh->handle_type), + SD_JSON_BUILD_PAIR_HEX("handle", img->fh->f_handle, + img->fh->handle_bytes))), + SD_JSON_BUILD_PAIR_CONDITION(!verity_hash && !img->fh, "inode", SD_JSON_BUILD_UNSIGNED(img->inode)), + SD_JSON_BUILD_PAIR_CONDITION(!verity_hash, "crtime", SD_JSON_BUILD_UNSIGNED(img->crtime)), + SD_JSON_BUILD_PAIR_CONDITION(!verity_hash, "mtime", SD_JSON_BUILD_UNSIGNED(img->mtime))); + if (r < 0) + return log_error_errno(r, "Failed to create origin entry for '%s': %m", img->name); + + r = sd_json_variant_set_field(&extensions_origin_entries, img->name, origin_entry); + if (r < 0) + return log_error_errno(r, "Failed to add origin entry for '%s': %m", img->name); + n_extensions++; } @@ -2007,13 +2123,112 @@ static int merge_subprocess( log_info("No suitable extensions found (%u ignored due to incompatible image(s)).", n_ignored); else log_info("No extensions found."); - return 0; + return MERGE_NOTHING_FOUND; } /* Order by version sort with strverscmp_improved() */ typesafe_qsort(extensions, n_extensions, strverscmp_improvedp); typesafe_qsort(extensions_v, n_extensions, strverscmp_improvedp); + STRV_FOREACH(h, hierarchies) { + _cleanup_(overlayfs_paths_freep) OverlayFSPaths *op = NULL; + _cleanup_free_ char *f = NULL, *buf = NULL, *resolved = NULL, *mutable_directory_without_root = NULL; + + /* The origin file includes the backing directories for mutable overlays. */ + r = overlayfs_paths_new(*h, workspace, &op); + if (r < 0) + return r; + + if (op->resolved_mutable_directory && !isempty(arg_root)) { + const char *without_root = NULL; + without_root = path_startswith(op->resolved_mutable_directory, root_resolved); + if (!isempty(without_root)) { + mutable_directory_without_root = strjoin("/", without_root); + if (!mutable_directory_without_root) + return log_oom(); + } + } + if (!mutable_directory_without_root && op->resolved_mutable_directory) { + mutable_directory_without_root = strdup(op->resolved_mutable_directory); + if (!mutable_directory_without_root) + return log_oom(); + } + + if (mutable_directory_without_root) { + r = sd_json_variant_set_field_string(&mutable_dir_entries, *h, mutable_directory_without_root); + if (r < 0) + return log_error_errno(r, "Failed to add mutable directory to origin JSON entry: %m"); + } + + /* Find existing origin file for comparison. */ + r = chase(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, /* ret_fd= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root), *h); + + f = path_join(resolved, image_class_info[image_class].dot_directory_name, "origin"); + if (!f) + return log_oom(); + + r = is_our_mount_point(image_class, resolved); + if (r < 0) + return r; + if (r == 0) + continue; + + if (old_origin_content) + continue; + + r = read_full_file(f, &buf, /* ret_size */ NULL); + if (r < 0) { + log_debug_errno(r, "Failed to open '%s', continuing search: %m", f); + continue; + } + + old_origin_content = TAKE_PTR(buf); + } + + r = sd_json_buildo(&extensions_origin_json, + SD_JSON_BUILD_PAIR_OBJECT("mutable", + SD_JSON_BUILD_PAIR_STRING("mode", mutable_mode_to_string(arg_mutable)), + SD_JSON_BUILD_PAIR_CONDITION(!!mutable_dir_entries, + "mutableDirs", + SD_JSON_BUILD_VARIANT(mutable_dir_entries))), + SD_JSON_BUILD_PAIR_CONDITION(!isempty(arg_overlayfs_mount_options), + "mountOptions", + SD_JSON_BUILD_STRING(arg_overlayfs_mount_options)), + SD_JSON_BUILD_PAIR_CONDITION(!!extensions_origin_entries, + "extensions", + SD_JSON_BUILD_VARIANT(extensions_origin_entries))); + if (r < 0) + return log_error_errno(r, "Failed to create extensions origin JSON object: %m"); + + r = sd_json_variant_format(extensions_origin_json, SD_JSON_FORMAT_PRETTY|SD_JSON_FORMAT_NEWLINE, &extensions_origin_content); + if (r < 0) + return log_error_errno(r, "Failed to format extension origin as JSON: %m"); + + log_debug("New extension origin entry (unordered):\n%s\n", extensions_origin_content); + + if (old_origin_content) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *old_origin_json = NULL; + + log_debug("Old extension origin entry (unordered):\n%s\n", old_origin_content); + r = sd_json_parse(old_origin_content, /* flags= */ 0, &old_origin_json, /* reterr_line= */ NULL, /* reterr_column= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to parse existing extension origin content: %m"); + + /* This works well with unordered entries. */ + if (sd_json_variant_equal(extensions_origin_json, old_origin_json)) { + if (!always_refresh) { + /* This only happens during refresh, not merge, thus talk about refresh here. */ + log_info("Skipping extension refresh because no change was found, use --always-refresh=yes to always do a refresh."); + return MERGE_SKIP_REFRESH; + } + + log_debug("No change found based on origin entry but continuing as requested by --always-refresh=yes."); + } else + log_debug("Found changes based on origin entry, continuing with the refresh."); + } + if (n_extensions == 0) { assert(arg_mutable != MUTABLE_NO); log_info("No extensions found, proceeding in mutable mode."); @@ -2096,6 +2311,7 @@ static int merge_subprocess( noexec, extensions, paths, + extensions_origin_content, meta_path, overlay_path, merge_hierarchy_workspace); @@ -2140,13 +2356,14 @@ static int merge_subprocess( log_info("Merged extensions into '%s'.", resolved); } - return 1; + return MERGE_MOUNTED; } static int merge(ImageClass image_class, char **hierarchies, bool force, bool no_reload, + bool always_refresh, int noexec, Hashmap *images) { @@ -2163,21 +2380,28 @@ static int merge(ImageClass image_class, if (r == 0) { /* Child with its own mount namespace */ - r = merge_subprocess(image_class, hierarchies, force, noexec, images, "/run/systemd/sysext"); - if (r < 0) - _exit(EXIT_FAILURE); + r = merge_subprocess(image_class, hierarchies, force, always_refresh, noexec, images, "/run/systemd/sysext"); /* Our namespace ceases to exist here, also implicitly detaching all temporary mounts we * created below /run. Nice! */ - _exit(r > 0 ? EXIT_SUCCESS : 123); /* 123 means: didn't find any extensions */ + if (r < 0) + _exit(EXIT_FAILURE); + if (r == MERGE_NOTHING_FOUND) + _exit(MERGE_EXIT_NOTHING_FOUND); + if (r == MERGE_SKIP_REFRESH) + _exit(MERGE_EXIT_SKIP_REFRESH); + + _exit(EXIT_SUCCESS); } r = pidref_wait_for_terminate_and_check("(sd-merge)", &pidref, WAIT_LOG_ABNORMAL); if (r < 0) return r; - if (r == 123) /* exit code 123 means: didn't do anything */ - return 0; + if (r == MERGE_EXIT_NOTHING_FOUND) + return 0; /* Tell refresh to unmount */ + if (r == MERGE_EXIT_SKIP_REFRESH) + return 1; /* Same return code as below when we have merged new */ if (r > 0) return log_error_errno(SYNTHETIC_ERRNO(EPROTO), "Failed to merge hierarchies"); @@ -2275,6 +2499,7 @@ static int verb_merge(int argc, char **argv, void *userdata) { arg_hierarchies, arg_force, arg_no_reload, + arg_always_refresh, arg_noexec, images); } @@ -2283,16 +2508,18 @@ typedef struct MethodMergeParameters { const char *class; int force; int no_reload; + int always_refresh; int noexec; } MethodMergeParameters; static int parse_merge_parameters(sd_varlink *link, sd_json_variant *parameters, MethodMergeParameters *p) { static const sd_json_dispatch_field dispatch_table[] = { - { "class", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodMergeParameters, class), 0 }, - { "force", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodMergeParameters, force), 0 }, - { "noReload", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodMergeParameters, no_reload), 0 }, - { "noexec", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodMergeParameters, noexec), 0 }, + { "class", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodMergeParameters, class), 0 }, + { "force", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodMergeParameters, force), 0 }, + { "noReload", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodMergeParameters, no_reload), 0 }, + { "alwaysRefresh", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodMergeParameters, always_refresh), 0 }, + { "noexec", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, offsetof(MethodMergeParameters, noexec), 0 }, VARLINK_DISPATCH_POLKIT_FIELD, {} }; @@ -2310,11 +2537,12 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var MethodMergeParameters p = { .force = -1, .no_reload = -1, + .always_refresh = -1, .noexec = -1, }; _cleanup_strv_free_ char **hierarchies = NULL; ImageClass image_class = arg_image_class; - bool force, no_reload; + bool force, no_reload, always_refresh; int r, noexec; assert(link); @@ -2329,6 +2557,7 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var force = p.force >= 0 ? p.force : arg_force; no_reload = p.no_reload >= 0 ? p.no_reload : arg_no_reload; + always_refresh = p.always_refresh >= 0 ? p.always_refresh : arg_always_refresh; noexec = p.noexec >= 0 ? p.noexec : arg_noexec; r = varlink_verify_polkit_async( @@ -2358,7 +2587,7 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var if (r > 0) return sd_varlink_errorbo(link, "io.systemd.sysext.AlreadyMerged", SD_JSON_BUILD_PAIR_STRING("hierarchy", which)); - r = merge(image_class, hierarchies ?: arg_hierarchies, force, no_reload, noexec, images); + r = merge(image_class, hierarchies ?: arg_hierarchies, force, no_reload, always_refresh, noexec, images); if (r < 0) return r; @@ -2370,6 +2599,7 @@ static int refresh( char **hierarchies, bool force, bool no_reload, + bool always_refresh, int noexec) { _cleanup_hashmap_free_ Hashmap *images = NULL; @@ -2380,9 +2610,10 @@ static int refresh( return r; /* Returns > 0 if it did something, i.e. a new overlayfs is mounted now. When it does so it - * implicitly unmounts any overlayfs placed there before. Returns == 0 if it did nothing, i.e. no + * implicitly unmounts any overlayfs placed there before. It also returns == 1 if there were + * no changes found to apply and the mount stays intact. Returns == 0 if it did nothing, i.e. no * extension images found. In this case the old overlayfs remains in place if there was one. */ - r = merge(image_class, hierarchies, force, no_reload, noexec, images); + r = merge(image_class, hierarchies, force, no_reload, always_refresh, noexec, images); if (r < 0) return r; if (r == 0) /* No images found? Then unmerge. The goal of --refresh is after all that after having @@ -2394,7 +2625,8 @@ static int refresh( * 1. If an overlayfs was mounted before and no extensions exist anymore, we'll have unmerged things. * * 2. If an overlayfs was mounted before, and there are still extensions installed' we'll have - * unmerged and then merged things again. + * unmerged and then merged things again or we have skipped the refresh because no changes + * were found. * * 3. If an overlayfs so far wasn't mounted, and there are extensions installed, we'll have it * mounted now. @@ -2418,6 +2650,7 @@ static int verb_refresh(int argc, char **argv, void *userdata) { arg_hierarchies, arg_force, arg_no_reload, + arg_always_refresh, arg_noexec); } @@ -2426,12 +2659,13 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v MethodMergeParameters p = { .force = -1, .no_reload = -1, + .always_refresh = -1, .noexec = -1, }; Hashmap **polkit_registry = ASSERT_PTR(userdata); _cleanup_strv_free_ char **hierarchies = NULL; ImageClass image_class = arg_image_class; - bool force, no_reload; + bool force, no_reload, always_refresh; int r, noexec; assert(link); @@ -2446,6 +2680,7 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v force = p.force >= 0 ? p.force : arg_force; no_reload = p.no_reload >= 0 ? p.no_reload : arg_no_reload; + always_refresh = p.always_refresh >= 0 ? p.always_refresh : arg_always_refresh; noexec = p.noexec >= 0 ? p.noexec : arg_noexec; r = varlink_verify_polkit_async( @@ -2461,7 +2696,7 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v if (r <= 0) return r; - r = refresh(image_class, hierarchies ?: arg_hierarchies, force, no_reload, noexec); + r = refresh(image_class, hierarchies ?: arg_hierarchies, force, no_reload, always_refresh, noexec); if (r < 0) return r; @@ -2590,6 +2825,8 @@ static int verb_help(int argc, char **argv, void *userdata) { " Generate JSON output\n" " --force Ignore version incompatibilities\n" " --no-reload Do not reload the service manager\n" + " --always-refresh=yes|no\n" + " Do not skip refresh when no changes were found\n" " --image-policy=POLICY\n" " Specify disk image dissection policy\n" " --noexec=BOOL Whether to mount extension overlay with noexec\n" @@ -2617,21 +2854,23 @@ static int parse_argv(int argc, char *argv[]) { ARG_IMAGE_POLICY, ARG_NOEXEC, ARG_NO_RELOAD, + ARG_ALWAYS_REFRESH, ARG_MUTABLE, }; static const struct option options[] = { - { "help", no_argument, NULL, 'h' }, - { "version", no_argument, NULL, ARG_VERSION }, - { "no-pager", no_argument, NULL, ARG_NO_PAGER }, - { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, - { "root", required_argument, NULL, ARG_ROOT }, - { "json", required_argument, NULL, ARG_JSON }, - { "force", no_argument, NULL, ARG_FORCE }, - { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, - { "noexec", required_argument, NULL, ARG_NOEXEC }, - { "no-reload", no_argument, NULL, ARG_NO_RELOAD }, - { "mutable", required_argument, NULL, ARG_MUTABLE }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + { "root", required_argument, NULL, ARG_ROOT }, + { "json", required_argument, NULL, ARG_JSON }, + { "force", no_argument, NULL, ARG_FORCE }, + { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, + { "noexec", required_argument, NULL, ARG_NOEXEC }, + { "no-reload", no_argument, NULL, ARG_NO_RELOAD }, + { "always-refresh", required_argument, NULL, ARG_ALWAYS_REFRESH }, + { "mutable", required_argument, NULL, ARG_MUTABLE }, {} }; @@ -2698,6 +2937,12 @@ static int parse_argv(int argc, char *argv[]) { arg_no_reload = true; break; + case ARG_ALWAYS_REFRESH: + r = parse_boolean_argument("--always-refresh", optarg, &arg_always_refresh); + if (r < 0) + return r; + break; + case ARG_MUTABLE: if (streq(optarg, "help")) { if (arg_legend) diff --git a/test/units/TEST-50-DISSECT.sysext.sh b/test/units/TEST-50-DISSECT.sysext.sh index 0ebc115208..60fb216ece 100755 --- a/test/units/TEST-50-DISSECT.sysext.sh +++ b/test/units/TEST-50-DISSECT.sysext.sh @@ -1587,6 +1587,94 @@ rm -rf "$fake_root/var/lib/extensions/test-extension.raw.v" "$fake_root/var/othe # Done with the above vpick symlink tests for --root= and without +( init_trap +: "Check if refresh skips correctly" +fake_root=${roots_dir:+"$roots_dir/refresh-skip"} +hierarchy=/opt + +findmnt --kernel=listmount >/dev/null || { + echo >&2 "Can't run test on old kernel, skipping test." + exit 0 +} + +prepare_root "$fake_root" "$hierarchy" +prepare_extension_image "$fake_root" "$hierarchy" +prepare_hierarchy "$fake_root" "$hierarchy" + +run_systemd_sysext "$fake_root" merge +extension_verify_after_merge "$fake_root" "$hierarchy" -e -h +# The mountinfo ID gets reused and is useless here, we require a unique ID from listmount +MOUNTID1=$(findmnt --kernel=listmount -o UNIQ-ID --raw --noheadings --target "$fake_root$hierarchy") +run_systemd_sysext "$fake_root" refresh +extension_verify_after_merge "$fake_root" "$hierarchy" -e -h +MOUNTID2=$(findmnt --kernel=listmount -o UNIQ-ID --raw --noheadings --target "$fake_root$hierarchy") +if [ "$MOUNTID1" != "$MOUNTID2" ]; then + echo >&2 "Unexpected remount with 'refresh'" + exit 1 +fi +rm -rf "$fake_root/var/lib/extensions/test-extension2" +cp -ar "$fake_root/var/lib/extensions/test-extension" "$fake_root/var/lib/extensions/test-extension2" +rm -rf "$fake_root/var/lib/extensions/test-extension" +mv "$fake_root/var/lib/extensions/test-extension2" "$fake_root/var/lib/extensions/test-extension" +run_systemd_sysext "$fake_root" refresh +extension_verify_after_merge "$fake_root" "$hierarchy" -e -h +MOUNTID3=$(findmnt --kernel=listmount -o UNIQ-ID --raw --noheadings --target "$fake_root$hierarchy") +if [ "$MOUNTID2" = "$MOUNTID3" ]; then + echo >&2 "Unexpected skip with 'refresh'" + exit 1 +fi + +run_systemd_sysext "$fake_root" unmerge +extension_verify_after_unmerge "$fake_root" "$hierarchy" -h +) + +( init_trap +: "Check that refresh does a skip if verity image changes file handle but has same hash" +fake_root=${roots_dir:+"$roots_dir/refresh-skip-verity-filehandle-same-hash"} +hierarchy=/opt + +# On OpenSUSE Tumbleweed EROFS is not supported +if [ -e /usr/lib/modprobe.d/60-blacklist_fs-erofs.conf ]; then + echo >&2 "Skipping test due to missing erofs support" + exit 0 +fi + +findmnt --kernel=listmount >/dev/null || { + echo >&2 "Can't run test on old kernel, skipping test." + exit 0 +} + +prepare_root "$fake_root" "$hierarchy" +prepare_extension_image_raw_verity "$fake_root" "$hierarchy" +prepare_hierarchy "$fake_root" "$hierarchy" + +run_systemd_sysext "$fake_root" merge +extension_verify_after_merge "$fake_root" "$hierarchy" -e -h +# The mountinfo ID gets reused and is useless here, we require a unique ID from listmount +MOUNTID1=$(findmnt --kernel=listmount -o UNIQ-ID --raw --noheadings --target "$fake_root$hierarchy") +run_systemd_sysext "$fake_root" refresh +extension_verify_after_merge "$fake_root" "$hierarchy" -e -h +MOUNTID2=$(findmnt --kernel=listmount -o UNIQ-ID --raw --noheadings --target "$fake_root$hierarchy") +if [ "$MOUNTID1" != "$MOUNTID2" ]; then + echo >&2 "Unexpected remount with 'refresh'" + exit 1 +fi +# Force a new file handle (get a new inode) +mv "$fake_root/var/lib/extensions/test-extension.raw" "$fake_root/var/lib/extensions/test-extension2.raw" +cp "$fake_root/var/lib/extensions/test-extension2.raw" "$fake_root/var/lib/extensions/test-extension.raw" +rm "$fake_root/var/lib/extensions/test-extension2.raw" +run_systemd_sysext "$fake_root" refresh +extension_verify_after_merge "$fake_root" "$hierarchy" -e -h +MOUNTID3=$(findmnt --kernel=listmount -o UNIQ-ID --raw --noheadings --target "$fake_root$hierarchy") +if [ "$MOUNTID2" != "$MOUNTID3" ]; then + echo >&2 "Unexpected remount with 'refresh' after verity image file handle changed" + exit 1 +fi + +run_systemd_sysext "$fake_root" unmerge +extension_verify_after_unmerge "$fake_root" "$hierarchy" -h +) + } # End of run_sysext_tests