]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sysext: move stuff around
authorZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Wed, 29 Apr 2026 22:34:19 +0000 (00:34 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@amutable.com>
Tue, 5 May 2026 11:49:51 +0000 (13:49 +0200)
The verb implementation functions are reordered to match the listing in --help.
The option are reorded a bit to have the "important" options that determine
behaviour first, and various display options and tweaks later. The cases in
parse_argv are ordered in the same way. No functional change.

src/sysext/sysext.c

index 5f97f43ffb4c22258c1f3533ff9eb11596d970ac..e4d9e7e0c355dae2b8f9b1a3e0d26c79ed58c897 100644 (file)
@@ -424,353 +424,6 @@ static int daemon_reload(void) {
         return bus_service_manager_reload(bus);
 }
 
-static int unmerge_hierarchy(ImageClass image_class, const char *p, const char *submounts_path) {
-
-        _cleanup_free_ char *dot_dir = NULL, *work_dir_info_file = NULL;
-        int n_unmerged = 0;
-        int r;
-
-        assert(p);
-
-        dot_dir = path_join(p, image_class_info[image_class].dot_directory_name);
-        if (!dot_dir)
-                return log_oom();
-
-        work_dir_info_file = path_join(dot_dir, "work_dir");
-        if (!work_dir_info_file)
-                return log_oom();
-
-        for (;;) {
-                _cleanup_free_ char *escaped_work_dir_in_root = NULL, *work_dir = NULL;
-
-                /* We only unmount /usr/ if it is a mount point and really one of ours, in order not to break
-                 * systems where /usr/ is a mount point of its own already. */
-
-                r = is_our_mount_point(image_class, p);
-                if (r < 0)
-                        return r;
-                if (r == 0)
-                        break;
-
-                r = read_one_line_file(work_dir_info_file, &escaped_work_dir_in_root);
-                if (r < 0) {
-                        if (r != -ENOENT)
-                                return log_error_errno(r, "Failed to read '%s': %m", work_dir_info_file);
-                } else {
-                        _cleanup_free_ char *work_dir_in_root = NULL;
-                        ssize_t l;
-
-                        l = cunescape_length(escaped_work_dir_in_root, r, 0, &work_dir_in_root);
-                        if (l < 0)
-                                return log_error_errno(l, "Failed to unescape work directory path: %m");
-                        work_dir = path_join(arg_root, work_dir_in_root);
-                        if (!work_dir)
-                                return log_oom();
-                }
-
-                r = umount_verbose(LOG_DEBUG, dot_dir, MNT_DETACH|UMOUNT_NOFOLLOW);
-                if (r < 0) {
-                        /* EINVAL is possibly "not a mount point". Let it slide as it's expected to occur if
-                         * the whole hierarchy was read-only, so the dot directory inside it was not
-                         * bind-mounted as read-only. */
-                        if (r != -EINVAL)
-                                return log_error_errno(r, "Failed to unmount '%s': %m", dot_dir);
-                }
-
-                /* After we've unmounted the metadata directory, save all other submounts so we can restore
-                 * them after unmerging the hierarchy. */
-                r = move_submounts(p, submounts_path);
-                if (r < 0)
-                        return r;
-
-                r = umount_verbose(LOG_ERR, p, MNT_DETACH|UMOUNT_NOFOLLOW);
-                if (r < 0)
-                        return r;
-
-                if (work_dir) {
-                        r = rm_rf(work_dir, REMOVE_ROOT | REMOVE_MISSING_OK | REMOVE_PHYSICAL);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to remove '%s': %m", work_dir);
-                }
-
-                log_info("Unmerged '%s'.", p);
-                n_unmerged++;
-        }
-
-        return n_unmerged;
-}
-
-static int unmerge_subprocess(
-                ImageClass image_class,
-                char **hierarchies,
-                const char *workspace) {
-
-        int r, ret = 0;
-
-        assert(workspace);
-        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
-         * the host otherwise. */
-        r = mount_nofollow_verbose(LOG_ERR, NULL, "/run", NULL, MS_SLAVE|MS_REC, NULL);
-        if (r < 0)
-                return r;
-
-        /* Let's create the workspace if it's missing */
-        r = mkdir_p(workspace, 0700);
-        if (r < 0)
-                return log_error_errno(r, "Failed to create '%s': %m", workspace);
-
-        STRV_FOREACH(h, hierarchies) {
-                _cleanup_free_ char *submounts_path = NULL, *resolved = NULL;
-
-                submounts_path = path_join(workspace, "submounts", *h);
-                if (!submounts_path)
-                        return log_oom();
-
-                r = chase(*h, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL);
-                if (r == -ENOENT) {
-                        log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *h);
-                        continue;
-                }
-                if (r < 0) {
-                        RET_GATHER(ret, log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *h));
-                        continue;
-                }
-
-                r = unmerge_hierarchy(image_class, resolved, submounts_path);
-                if (r < 0) {
-                        RET_GATHER(ret, r);
-                        continue;
-                }
-                if (r == 0)
-                        continue;
-
-                /* If we unmerged something, then we have to move the submounts from the hierarchy back into
-                 * place in the host's original hierarchy. */
-
-                r = move_submounts(submounts_path, resolved);
-                if (r < 0)
-                        return r;
-        }
-
-        return ret;
-}
-
-static int unmerge(
-                ImageClass image_class,
-                char **hierarchies,
-                bool no_reload) {
-
-        bool need_to_reload;
-        int r;
-
-        (void) dlopen_libmount(LOG_DEBUG);
-
-        r = need_reload(image_class, hierarchies, no_reload);
-        if (r < 0)
-                return r;
-        need_to_reload = r > 0;
-
-        r = pidref_safe_fork(
-                        "(sd-unmerge)",
-                        FORK_WAIT|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_NEW_MOUNTNS,
-                        /* ret= */ NULL);
-        if (r < 0)
-                return r;
-        if (r == 0) {
-                /* Child with its own mount namespace */
-
-                r = unmerge_subprocess(image_class, hierarchies, "/run/systemd/sysext");
-
-                /* Our namespace ceases to exist here, also implicitly detaching all temporary mounts we
-                 * created below /run. Nice! */
-
-                _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS);
-        }
-
-        if (need_to_reload) {
-                r = daemon_reload();
-                if (r < 0)
-                        return r;
-        }
-
-        return 0;
-}
-
-static int verb_unmerge(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        int r;
-
-        r = have_effective_cap(CAP_SYS_ADMIN);
-        if (r < 0)
-                return log_error_errno(r, "Failed to check if we have enough privileges: %m");
-        if (r == 0)
-                return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
-
-        return unmerge(arg_image_class,
-                       arg_hierarchies,
-                       arg_no_reload);
-}
-
-static int parse_image_class_parameter(sd_varlink *link, const char *value, ImageClass *image_class, char ***hierarchies) {
-        _cleanup_strv_free_ char **h = NULL;
-        ImageClass c;
-        int r;
-
-        assert(link);
-        assert(image_class);
-
-        if (!value)
-                return 0;
-
-        c = image_class_from_string(value);
-        if (!IN_SET(c, IMAGE_SYSEXT, IMAGE_CONFEXT))
-                return sd_varlink_error_invalid_parameter_name(link, "class");
-
-        if (hierarchies) {
-                r = parse_env_extension_hierarchies(&h, image_class_info[c].name_env);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to parse environment variable: %m");
-
-                strv_free_and_replace(*hierarchies, h);
-        }
-
-        *image_class = c;
-        return 0;
-}
-
-typedef struct MethodUnmergeParameters {
-        const char *class;
-        int no_reload;
-} MethodUnmergeParameters;
-
-static int vl_method_unmerge(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
-
-        static const sd_json_dispatch_field dispatch_table[] = {
-                { "class",    SD_JSON_VARIANT_STRING,  sd_json_dispatch_const_string, offsetof(MethodUnmergeParameters, class),     0 },
-                { "noReload", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool,      offsetof(MethodUnmergeParameters, no_reload), 0 },
-                VARLINK_DISPATCH_POLKIT_FIELD,
-                {}
-        };
-        MethodUnmergeParameters p = {
-                .no_reload = -1,
-        };
-        Hashmap **polkit_registry = ASSERT_PTR(userdata);
-        _cleanup_strv_free_ char **hierarchies = NULL;
-        ImageClass image_class = arg_image_class;
-        bool no_reload;
-        int r;
-
-        assert(link);
-
-        r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
-        if (r != 0)
-                return r;
-
-        no_reload = p.no_reload >= 0 ? p.no_reload : arg_no_reload;
-
-        r = parse_image_class_parameter(link, p.class, &image_class, &hierarchies);
-        if (r < 0)
-                return r;
-
-        r = varlink_verify_polkit_async(
-                        link,
-                        /* bus= */ NULL,
-                        image_class_info[image_class].polkit_rw_action_id,
-                        (const char**) STRV_MAKE(
-                                "verb", "unmerge",
-                                "noReload", one_zero(no_reload)),
-                        polkit_registry);
-        if (r <= 0)
-                return r;
-
-        r = unmerge(image_class, hierarchies ?: arg_hierarchies, no_reload);
-        if (r < 0)
-                return r;
-
-        return sd_varlink_reply(link, NULL);
-}
-
-static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) {
-        _cleanup_(table_unrefp) Table *t = NULL;
-        int r, ret = 0;
-
-        t = table_new("hierarchy", "extensions", "since");
-        if (!t)
-                return log_oom();
-
-        table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
-
-        STRV_FOREACH(p, arg_hierarchies) {
-                _cleanup_free_ char *resolved = NULL, *f = NULL, *buf = NULL;
-                _cleanup_strv_free_ char **l = NULL;
-                struct stat st;
-
-                r = chase(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL);
-                if (r == -ENOENT) {
-                        log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *p);
-                        continue;
-                }
-                if (r < 0) {
-                        log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p);
-                        goto inner_fail;
-                }
-
-                r = is_our_mount_point(arg_image_class, resolved);
-                if (r < 0)
-                        goto inner_fail;
-                if (r == 0) {
-                        r = table_add_many(
-                                        t,
-                                        TABLE_PATH, *p,
-                                        TABLE_STRING, "none",
-                                        TABLE_SET_COLOR, ansi_grey(),
-                                        TABLE_EMPTY);
-                        if (r < 0)
-                                return table_log_add_error(r);
-
-                        continue;
-                }
-
-                f = path_join(resolved, image_class_info[arg_image_class].dot_directory_name, image_class_info[arg_image_class].short_identifier_plural);
-                if (!f)
-                        return log_oom();
-
-                r = read_full_file(f, &buf, NULL);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to open '%s': %m", f);
-
-                l = strv_split_newlines(buf);
-                if (!l)
-                        return log_oom();
-
-                if (stat(*p, &st) < 0)
-                        return log_error_errno(errno, "Failed to stat() '%s': %m", *p);
-
-                r = table_add_many(
-                                t,
-                                TABLE_PATH, *p,
-                                TABLE_STRV, l,
-                                TABLE_TIMESTAMP, timespec_load(&st.st_mtim));
-                if (r < 0)
-                        return table_log_add_error(r);
-
-                continue;
-
-        inner_fail:
-                if (ret == 0)
-                        ret = r;
-        }
-
-        (void) table_set_sort(t, (size_t) 0);
-
-        r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
-        if (r < 0)
-                return r;
-
-        return ret;
-}
-
 static int append_overlayfs_path_option(
                 char **options,
                 const char *separator,
@@ -964,6 +617,63 @@ static OverlayFSPaths *overlayfs_paths_free(OverlayFSPaths *op) {
 }
 DEFINE_TRIVIAL_CLEANUP_FUNC(OverlayFSPaths *, overlayfs_paths_free);
 
+static int parse_env(void) {
+        const char *env_var;
+        int r;
+
+        env_var = secure_getenv(image_class_info[arg_image_class].mode_env);
+        if (env_var) {
+                r = parse_mutable_mode(env_var);
+                if (r < 0)
+                        log_warning("Failed to parse %s environment variable value '%s'. Ignoring.",
+                                    image_class_info[arg_image_class].mode_env, env_var);
+                else {
+                        arg_mutable = r;
+                        arg_mutable_set = true;
+                }
+        }
+
+        env_var = secure_getenv(image_class_info[arg_image_class].opts_env);
+        if (env_var)
+                arg_overlayfs_mount_options = env_var;
+
+        /* For debugging purposes it might make sense to do this for other hierarchies than /usr/ and
+         * /opt/, but let's make that a hacker/debugging feature, i.e. env var instead of cmdline
+         * switch. */
+        r = parse_env_extension_hierarchies(&arg_hierarchies, image_class_info[arg_image_class].name_env);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse %s environment variable: %m", image_class_info[arg_image_class].name_env);
+
+        return 0;
+}
+
+static int parse_image_class_parameter(sd_varlink *link, const char *value, ImageClass *image_class, char ***hierarchies) {
+        _cleanup_strv_free_ char **h = NULL;
+        ImageClass c;
+        int r;
+
+        assert(link);
+        assert(image_class);
+
+        if (!value)
+                return 0;
+
+        c = image_class_from_string(value);
+        if (!IN_SET(c, IMAGE_SYSEXT, IMAGE_CONFEXT))
+                return sd_varlink_error_invalid_parameter_name(link, "class");
+
+        if (hierarchies) {
+                r = parse_env_extension_hierarchies(&h, image_class_info[c].name_env);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to parse environment variable: %m");
+
+                strv_free_and_replace(*hierarchies, h);
+        }
+
+        *image_class = c;
+        return 0;
+}
+
 static int resolve_hierarchy(const char *hierarchy, char **ret_resolved_hierarchy) {
         _cleanup_free_ char *resolved_path = NULL;
         int r;
@@ -1793,7 +1503,7 @@ static int strverscmp_improvedp(char *const* a, char *const* b) {
         return strverscmp_improved(*a, *b);
 }
 
-static const ImagePolicy *pick_image_policy(const Image *img) {
+static const ImagePolicypick_image_policy(const Image *img) {
         assert(img);
         assert(img->path);
 
@@ -1817,12 +1527,185 @@ static const ImagePolicy *pick_image_policy(const Image *img) {
                 if (path_startswith(img->path, "/.extra/global_confext/"))
                         return &image_policy_confext_strict;
 
-                /* Better safe than sorry, refuse everything else passed in via the untrusted /.extra/ dir */
-                if (path_startswith(img->path, "/.extra/"))
-                        return &image_policy_deny;
+                /* Better safe than sorry, refuse everything else passed in via the untrusted /.extra/ dir */
+                if (path_startswith(img->path, "/.extra/"))
+                        return &image_policy_deny;
+        }
+
+        return image_class_info[img->class].default_image_policy;
+}
+
+static int unmerge_hierarchy(ImageClass image_class, const char *p, const char *submounts_path) {
+        _cleanup_free_ char *dot_dir = NULL, *work_dir_info_file = NULL;
+        int n_unmerged = 0;
+        int r;
+
+        assert(p);
+
+        dot_dir = path_join(p, image_class_info[image_class].dot_directory_name);
+        if (!dot_dir)
+                return log_oom();
+
+        work_dir_info_file = path_join(dot_dir, "work_dir");
+        if (!work_dir_info_file)
+                return log_oom();
+
+        for (;;) {
+                _cleanup_free_ char *escaped_work_dir_in_root = NULL, *work_dir = NULL;
+
+                /* We only unmount /usr/ if it is a mount point and really one of ours, in order not to break
+                 * systems where /usr/ is a mount point of its own already. */
+
+                r = is_our_mount_point(image_class, p);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        break;
+
+                r = read_one_line_file(work_dir_info_file, &escaped_work_dir_in_root);
+                if (r < 0) {
+                        if (r != -ENOENT)
+                                return log_error_errno(r, "Failed to read '%s': %m", work_dir_info_file);
+                } else {
+                        _cleanup_free_ char *work_dir_in_root = NULL;
+                        ssize_t l;
+
+                        l = cunescape_length(escaped_work_dir_in_root, r, 0, &work_dir_in_root);
+                        if (l < 0)
+                                return log_error_errno(l, "Failed to unescape work directory path: %m");
+                        work_dir = path_join(arg_root, work_dir_in_root);
+                        if (!work_dir)
+                                return log_oom();
+                }
+
+                r = umount_verbose(LOG_DEBUG, dot_dir, MNT_DETACH|UMOUNT_NOFOLLOW);
+                if (r < 0) {
+                        /* EINVAL is possibly "not a mount point". Let it slide as it's expected to occur if
+                         * the whole hierarchy was read-only, so the dot directory inside it was not
+                         * bind-mounted as read-only. */
+                        if (r != -EINVAL)
+                                return log_error_errno(r, "Failed to unmount '%s': %m", dot_dir);
+                }
+
+                /* After we've unmounted the metadata directory, save all other submounts so we can restore
+                 * them after unmerging the hierarchy. */
+                r = move_submounts(p, submounts_path);
+                if (r < 0)
+                        return r;
+
+                r = umount_verbose(LOG_ERR, p, MNT_DETACH|UMOUNT_NOFOLLOW);
+                if (r < 0)
+                        return r;
+
+                if (work_dir) {
+                        r = rm_rf(work_dir, REMOVE_ROOT | REMOVE_MISSING_OK | REMOVE_PHYSICAL);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to remove '%s': %m", work_dir);
+                }
+
+                log_info("Unmerged '%s'.", p);
+                n_unmerged++;
+        }
+
+        return n_unmerged;
+}
+
+static int unmerge_subprocess(
+                ImageClass image_class,
+                char **hierarchies,
+                const char *workspace) {
+
+        int r, ret = 0;
+
+        assert(workspace);
+        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
+         * the host otherwise. */
+        r = mount_nofollow_verbose(LOG_ERR, NULL, "/run", NULL, MS_SLAVE|MS_REC, NULL);
+        if (r < 0)
+                return r;
+
+        /* Let's create the workspace if it's missing */
+        r = mkdir_p(workspace, 0700);
+        if (r < 0)
+                return log_error_errno(r, "Failed to create '%s': %m", workspace);
+
+        STRV_FOREACH(h, hierarchies) {
+                _cleanup_free_ char *submounts_path = NULL, *resolved = NULL;
+
+                submounts_path = path_join(workspace, "submounts", *h);
+                if (!submounts_path)
+                        return log_oom();
+
+                r = chase(*h, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL);
+                if (r == -ENOENT) {
+                        log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *h);
+                        continue;
+                }
+                if (r < 0) {
+                        RET_GATHER(ret, log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *h));
+                        continue;
+                }
+
+                r = unmerge_hierarchy(image_class, resolved, submounts_path);
+                if (r < 0) {
+                        RET_GATHER(ret, r);
+                        continue;
+                }
+                if (r == 0)
+                        continue;
+
+                /* If we unmerged something, then we have to move the submounts from the hierarchy back into
+                 * place in the host's original hierarchy. */
+
+                r = move_submounts(submounts_path, resolved);
+                if (r < 0)
+                        return r;
+        }
+
+        return ret;
+}
+
+static int unmerge(
+                ImageClass image_class,
+                char **hierarchies,
+                bool no_reload) {
+
+        bool need_to_reload;
+        int r;
+
+        (void) dlopen_libmount(LOG_DEBUG);
+
+        r = need_reload(image_class, hierarchies, no_reload);
+        if (r < 0)
+                return r;
+        need_to_reload = r > 0;
+
+        r = pidref_safe_fork(
+                        "(sd-unmerge)",
+                        FORK_WAIT|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_NEW_MOUNTNS,
+                        /* ret= */ NULL);
+        if (r < 0)
+                return r;
+        if (r == 0) {
+                /* Child with its own mount namespace */
+
+                r = unmerge_subprocess(image_class, hierarchies, "/run/systemd/sysext");
+
+                /* Our namespace ceases to exist here, also implicitly detaching all temporary mounts we
+                 * created below /run. Nice! */
+
+                _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS);
         }
 
-        return image_class_info[img->class].default_image_policy;
+        if (need_to_reload) {
+                r = daemon_reload();
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
 }
 
 static int merge_subprocess(
@@ -2421,6 +2304,86 @@ static int merge(ImageClass image_class,
         return 1;
 }
 
+static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(table_unrefp) Table *t = NULL;
+        int r, ret = 0;
+
+        t = table_new("hierarchy", "extensions", "since");
+        if (!t)
+                return log_oom();
+
+        table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
+
+        STRV_FOREACH(p, arg_hierarchies) {
+                _cleanup_free_ char *resolved = NULL, *f = NULL, *buf = NULL;
+                _cleanup_strv_free_ char **l = NULL;
+                struct stat st;
+
+                r = chase(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL);
+                if (r == -ENOENT) {
+                        log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *p);
+                        continue;
+                }
+                if (r < 0) {
+                        log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p);
+                        goto inner_fail;
+                }
+
+                r = is_our_mount_point(arg_image_class, resolved);
+                if (r < 0)
+                        goto inner_fail;
+                if (r == 0) {
+                        r = table_add_many(
+                                        t,
+                                        TABLE_PATH, *p,
+                                        TABLE_STRING, "none",
+                                        TABLE_SET_COLOR, ansi_grey(),
+                                        TABLE_EMPTY);
+                        if (r < 0)
+                                return table_log_add_error(r);
+
+                        continue;
+                }
+
+                f = path_join(resolved, image_class_info[arg_image_class].dot_directory_name, image_class_info[arg_image_class].short_identifier_plural);
+                if (!f)
+                        return log_oom();
+
+                r = read_full_file(f, &buf, NULL);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to open '%s': %m", f);
+
+                l = strv_split_newlines(buf);
+                if (!l)
+                        return log_oom();
+
+                if (stat(*p, &st) < 0)
+                        return log_error_errno(errno, "Failed to stat() '%s': %m", *p);
+
+                r = table_add_many(
+                                t,
+                                TABLE_PATH, *p,
+                                TABLE_STRV, l,
+                                TABLE_TIMESTAMP, timespec_load(&st.st_mtim));
+                if (r < 0)
+                        return table_log_add_error(r);
+
+                continue;
+
+        inner_fail:
+                if (ret == 0)
+                        ret = r;
+        }
+
+        (void) table_set_sort(t, (size_t) 0);
+
+        r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+        if (r < 0)
+                return r;
+
+        return ret;
+}
+
 static int image_discover_and_read_metadata(ImageClass image_class, Hashmap **ret_images) {
         _cleanup_hashmap_free_ Hashmap *images = NULL;
         Image *img;
@@ -2597,6 +2560,72 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var
         return sd_varlink_reply(link, NULL);
 }
 
+static int verb_unmerge(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        int r;
+
+        r = have_effective_cap(CAP_SYS_ADMIN);
+        if (r < 0)
+                return log_error_errno(r, "Failed to check if we have enough privileges: %m");
+        if (r == 0)
+                return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
+
+        return unmerge(arg_image_class,
+                       arg_hierarchies,
+                       arg_no_reload);
+}
+
+typedef struct MethodUnmergeParameters {
+        const char *class;
+        int no_reload;
+} MethodUnmergeParameters;
+
+static int vl_method_unmerge(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+
+        static const sd_json_dispatch_field dispatch_table[] = {
+                { "class",    SD_JSON_VARIANT_STRING,  sd_json_dispatch_const_string, offsetof(MethodUnmergeParameters, class),     0 },
+                { "noReload", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool,      offsetof(MethodUnmergeParameters, no_reload), 0 },
+                VARLINK_DISPATCH_POLKIT_FIELD,
+                {}
+        };
+        MethodUnmergeParameters p = {
+                .no_reload = -1,
+        };
+        Hashmap **polkit_registry = ASSERT_PTR(userdata);
+        _cleanup_strv_free_ char **hierarchies = NULL;
+        ImageClass image_class = arg_image_class;
+        bool no_reload;
+        int r;
+
+        assert(link);
+
+        r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
+        if (r != 0)
+                return r;
+
+        no_reload = p.no_reload >= 0 ? p.no_reload : arg_no_reload;
+
+        r = parse_image_class_parameter(link, p.class, &image_class, &hierarchies);
+        if (r < 0)
+                return r;
+
+        r = varlink_verify_polkit_async(
+                        link,
+                        /* bus= */ NULL,
+                        image_class_info[image_class].polkit_rw_action_id,
+                        (const char**) STRV_MAKE(
+                                "verb", "unmerge",
+                                "noReload", one_zero(no_reload)),
+                        polkit_registry);
+        if (r <= 0)
+                return r;
+
+        r = unmerge(image_class, hierarchies ?: arg_hierarchies, no_reload);
+        if (r < 0)
+                return r;
+
+        return sd_varlink_reply(link, NULL);
+}
+
 static int refresh(
                 ImageClass image_class,
                 char **hierarchies,
@@ -2816,20 +2845,20 @@ static int help(void) {
                "  -h --help               Show this help\n"
                "     --version            Show package version\n"
                "\n%3$sOptions:%4$s\n"
+               "     --root=PATH          Operate relative to root path\n"
                "     --mutable=yes|no|auto|import|ephemeral|ephemeral-import|help\n"
                "                          Specify a mutability mode of the merged hierarchy\n"
-               "     --no-pager           Do not pipe output into a pager\n"
-               "     --no-legend          Do not show the headers and footers\n"
-               "     --root=PATH          Operate relative to root path\n"
-               "     --json=pretty|short|off\n"
-               "                          Generate JSON output\n"
+               "     --image-policy=POLICY\n"
+               "                          Specify disk image dissection policy\n"
+               "     --noexec=BOOL        Whether to mount extension overlay with noexec\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"
+               "     --no-pager           Do not pipe output into a pager\n"
+               "     --no-legend          Do not show the headers and footers\n"
+               "     --json=pretty|short|off\n"
+               "                          Generate JSON output\n"
                "\nSee the %2$s for details.\n",
                program_invocation_short_name,
                link,
@@ -2892,14 +2921,6 @@ static int parse_argv(int argc, char *argv[]) {
                 case ARG_VERSION:
                         return version();
 
-                case ARG_NO_PAGER:
-                        arg_pager_flags |= PAGER_DISABLE;
-                        break;
-
-                case ARG_NO_LEGEND:
-                        arg_legend = false;
-                        break;
-
                 case ARG_ROOT:
                         r = parse_path_argument(optarg, false, &arg_root);
                         if (r < 0)
@@ -2908,15 +2929,19 @@ static int parse_argv(int argc, char *argv[]) {
                         arg_no_reload = true;
                         break;
 
-                case ARG_JSON:
-                        r = parse_json_argument(optarg, &arg_json_format_flags);
-                        if (r <= 0)
-                                return r;
+                case ARG_MUTABLE:
+                        if (streq(optarg, "help")) {
+                                if (arg_legend)
+                                        puts("Known mutability modes:");
 
-                        break;
+                                return DUMP_STRING_TABLE(mutable_mode, MutableMode, _MUTABLE_MAX);
+                        }
 
-                case ARG_FORCE:
-                        arg_force = true;
+                        r = parse_mutable_mode(optarg);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse argument to --mutable=: %s", optarg);
+                        arg_mutable = r;
+                        arg_mutable_set = true;
                         break;
 
                 case ARG_IMAGE_POLICY:
@@ -2936,6 +2961,10 @@ static int parse_argv(int argc, char *argv[]) {
                         arg_noexec = r;
                         break;
 
+                case ARG_FORCE:
+                        arg_force = true;
+                        break;
+
                 case ARG_NO_RELOAD:
                         arg_no_reload = true;
                         break;
@@ -2946,19 +2975,19 @@ static int parse_argv(int argc, char *argv[]) {
                                 return r;
                         break;
 
-                case ARG_MUTABLE:
-                        if (streq(optarg, "help")) {
-                                if (arg_legend)
-                                        puts("Known mutability modes:");
+                case ARG_NO_PAGER:
+                        arg_pager_flags |= PAGER_DISABLE;
+                        break;
 
-                                return DUMP_STRING_TABLE(mutable_mode, MutableMode, _MUTABLE_MAX);
-                        }
+                case ARG_NO_LEGEND:
+                        arg_legend = false;
+                        break;
+
+                case ARG_JSON:
+                        r = parse_json_argument(optarg, &arg_json_format_flags);
+                        if (r <= 0)
+                                return r;
 
-                        r = parse_mutable_mode(optarg);
-                        if (r < 0)
-                                return log_error_errno(r, "Failed to parse argument to --mutable=: %s", optarg);
-                        arg_mutable = r;
-                        arg_mutable_set = true;
                         break;
 
                 case '?':
@@ -2977,36 +3006,6 @@ static int parse_argv(int argc, char *argv[]) {
         return 1;
 }
 
-static int parse_env(void) {
-        const char *env_var;
-        int r;
-
-        env_var = secure_getenv(image_class_info[arg_image_class].mode_env);
-        if (env_var) {
-                r = parse_mutable_mode(env_var);
-                if (r < 0)
-                        log_warning("Failed to parse %s environment variable value '%s'. Ignoring.",
-                                    image_class_info[arg_image_class].mode_env, env_var);
-                else {
-                        arg_mutable = r;
-                        arg_mutable_set = true;
-                }
-        }
-
-        env_var = secure_getenv(image_class_info[arg_image_class].opts_env);
-        if (env_var)
-                arg_overlayfs_mount_options = env_var;
-
-        /* For debugging purposes it might make sense to do this for other hierarchies than /usr/ and
-         * /opt/, but let's make that a hacker/debugging feature, i.e. env var instead of cmdline
-         * switch. */
-        r = parse_env_extension_hierarchies(&arg_hierarchies, image_class_info[arg_image_class].name_env);
-        if (r < 0)
-                return log_error_errno(r, "Failed to parse %s environment variable: %m", image_class_info[arg_image_class].name_env);
-
-        return 0;
-}
-
 static int sysext_main(int argc, char *argv[]) {
 
         static const Verb verbs[] = {