]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sysext: honor confext config during notify refresh
authordongshengyuan <545258830@qq.com>
Mon, 6 Jul 2026 02:51:28 +0000 (10:51 +0800)
committerLennart Poettering <lennart@poettering.net>
Tue, 14 Jul 2026 09:26:10 +0000 (11:26 +0200)
systemd-sysext handles the sysupdate notification hook,
but the hook refreshes both sysexts and confexts.

Build the refresh context for each image class so confext settings,
such as Mutable=yes, are applied when confexts are refreshed.

Add regression coverage for the mutable confext overlay.

Fixes #42873
Signed-off-by: dongshengyuan <dongshengyuan@uniontech.com>
src/sysext/sysext.c
test/units/TEST-50-DISSECT.dissect.sh

index 3c308bd8b6a6105d138835425efdfa817e35cb0c..0f93322af049277133ad40209e0c0fcc299bcfc7 100644 (file)
@@ -107,7 +107,6 @@ enum {
         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;
 static PagerFlags arg_pager_flags = 0;
@@ -117,11 +116,10 @@ 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 */
+static bool arg_image_policy_argv_set = false;
 static bool arg_varlink = false;
 static MutableMode arg_mutable = MUTABLE_NO;
-static bool arg_mutable_set = false; /* Tracks initialization */
-static const char *arg_overlayfs_mount_options = NULL;
+static bool arg_mutable_argv_set = false;
 
 /* Is set to IMAGE_CONFEXT when systemd is called with the confext functionality instead of the default */
 static ImageClass arg_image_class = IMAGE_SYSEXT;
@@ -132,7 +130,6 @@ static ImageClass arg_image_class = IMAGE_SYSEXT;
  * files from partial upcopies after umount, index=off allows reuse of the upper/work dirs */
 #define MUTABLE_EXTENSIONS_MOUNT_OPTIONS "redirect_dir=on,noatime,metacopy=off,index=off"
 
-STATIC_DESTRUCTOR_REGISTER(arg_hierarchies, strv_freep);
 STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
 
@@ -194,25 +191,87 @@ static int parse_mutable_mode(const char *p) {
 
 static DEFINE_CONFIG_PARSE_ENUM(config_parse_mutable_mode, mutable_mode, MutableMode);
 
-static int parse_config_file(ImageClass image_class) {
+typedef struct Context {
+        ImageClass image_class;
+        char **hierarchies;
+        char *root;
+        bool force;
+        bool no_reload;
+        bool always_refresh;
+        int noexec;
+        MutableMode mutable;
+        bool mutable_set;
+        ImagePolicy *image_policy;
+        bool image_policy_set;
+        /* image_policy is usually borrowed from arg_image_policy. When parsed from configuration, this
+         * context owns it and frees it in context_done(). */
+        bool image_policy_owned;
+        const char *overlayfs_mount_options;
+} Context;
+
+#define CONTEXT_NULL                            \
+        (Context) {                             \
+                .image_class = IMAGE_SYSEXT,    \
+                .noexec = -1,                   \
+                .mutable = MUTABLE_NO,          \
+        }
+
+static void context_done(Context *c) {
+        assert(c);
+
+        c->hierarchies = strv_free(c->hierarchies);
+        c->root = mfree(c->root);
+
+        if (c->image_policy_owned)
+                c->image_policy = image_policy_free(c->image_policy);
+}
+
+static void parse_env_image_class_config(Context *c) {
+        const char *env_var;
+        int r;
+
+        assert(c);
+
+        env_var = secure_getenv(image_class_info[c->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[c->image_class].mode_env, env_var);
+                else {
+                        c->mutable = r;
+                        c->mutable_set = true;
+                }
+        }
+
+        env_var = secure_getenv(image_class_info[c->image_class].opts_env);
+        if (env_var)
+                c->overlayfs_mount_options = env_var;
+}
+
+static int parse_config_file(Context *c) {
         _cleanup_(image_policy_freep) ImagePolicy *config_image_policy = NULL;
         MutableMode config_mutable = MUTABLE_NO;
-        const char *section = image_class == IMAGE_SYSEXT ? "SysExt" : "ConfExt";
-        const char *sections = image_class == IMAGE_SYSEXT ? "SysExt\0" : "ConfExt\0";
+        const char *section, *sections;
+        _cleanup_free_ char *config_file = NULL;
+        int r;
+
+        assert(c);
+
+        section = c->image_class == IMAGE_SYSEXT ? "SysExt" : "ConfExt";
+        sections = c->image_class == IMAGE_SYSEXT ? "SysExt\0" : "ConfExt\0";
         const ConfigTableItem items[] = {
                 { section, "Mutable",           config_parse_mutable_mode,      0,      &config_mutable            },
                 { section, "ImagePolicy",       config_parse_image_policy,      0,      &config_image_policy       },
                 {}
         };
-        _cleanup_free_ char *config_file = NULL;
-        int r;
 
-        config_file = strjoin("systemd/", image_class_info[image_class].short_identifier, ".conf");
+        config_file = strjoin("systemd/", image_class_info[c->image_class].short_identifier, ".conf");
         if (!config_file)
                 return log_oom();
 
         r = config_parse_standard_file_with_dropins_full(
-                        arg_root,
+                        c->root,
                         /* root_fd= */ -EBADF,
                         config_file,
                         sections,
@@ -224,20 +283,64 @@ static int parse_config_file(ImageClass image_class) {
         if (r < 0)
                 return r;
 
-        /* Because this runs after parse_argv we only overwrite when things aren't set yet. */
-        if (!arg_mutable_set) {
-                arg_mutable = config_mutable;
-                arg_mutable_set = true;
+        /* Configuration has the lowest priority, so only fill in values that neither the environment nor
+         * argv initialized already. */
+        if (!c->mutable_set) {
+                c->mutable = config_mutable;
+                c->mutable_set = true;
         }
 
-        if (!arg_image_policy_set) {
-                arg_image_policy = TAKE_PTR(config_image_policy);
-                arg_image_policy_set = true;
+        if (!c->image_policy_set) {
+                c->image_policy = TAKE_PTR(config_image_policy);
+                c->image_policy_set = true;
+                c->image_policy_owned = true;
         }
 
         return 0;
 }
 
+static int context_from_cmdline(Context *ret, ImageClass image_class) {
+        _cleanup_(context_done) Context c = CONTEXT_NULL;
+        int r;
+
+        assert(ret);
+
+        /* Start with command-line settings shared by both image classes. Class-specific environment and
+         * configuration are applied below, with argv values explicitly overriding both. */
+        c.image_class = image_class;
+        c.force = arg_force;
+        c.no_reload = arg_no_reload;
+        c.always_refresh = arg_always_refresh;
+        c.noexec = arg_noexec;
+
+        if (arg_root && strdup_to(&c.root, arg_root) < 0)
+                return log_oom();
+
+        r = parse_env_extension_hierarchies(&c.hierarchies, image_class_info[image_class].name_env);
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine %s hierarchies: %m", image_class_info[image_class].short_identifier);
+
+        parse_env_image_class_config(&c);
+
+        if (arg_mutable_argv_set) {
+                c.mutable = arg_mutable;
+                c.mutable_set = true;
+        }
+
+        if (arg_image_policy_argv_set) {
+                c.image_policy = arg_image_policy;
+                c.image_policy_set = true;
+                c.image_policy_owned = false;
+        }
+
+        r = parse_config_file(&c);
+        if (r < 0)
+                log_warning_errno(r, "Failed to parse %s config file, ignoring: %m", image_class_info[image_class].short_identifier);
+
+        *ret = TAKE_GENERIC(c, Context, CONTEXT_NULL);
+        return 0;
+}
+
 static int is_our_mount_point(
                 ImageClass image_class,
                 const char *p) {
@@ -326,9 +429,7 @@ static int split_unit_string(const char *s, const char *field, const char *exten
 }
 
 static int get_extension_release_metadata(
-                ImageClass image_class,
-                char **hierarchies,
-                bool no_reload,
+                const Context *c,
                 bool *ret_need_reload,
                 Set **ret_restart_units,
                 Set **ret_reload_or_restart_units) {
@@ -339,7 +440,9 @@ static int get_extension_release_metadata(
         bool need_to_reload = false;
         int r;
 
-        if (no_reload || !isempty(arg_root)) {
+        assert(c);
+
+        if (c->no_reload || !isempty(c->root)) {
                 /* With --root= we'd be talking to the host service manager about extensions merged
                  * into a foreign root, which is never what we want. */
                 if (ret_need_reload)
@@ -351,27 +454,27 @@ static int get_extension_release_metadata(
                 return 0;
         }
 
-        STRV_FOREACH(p, hierarchies) {
+        STRV_FOREACH(p, c->hierarchies) {
                 _cleanup_free_ char *f = NULL, *buf = NULL, *resolved = NULL;
                 _cleanup_strv_free_ char **mounted_extensions = NULL;
 
-                r = chase(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL);
+                r = chase(*p, c->root, CHASE_PREFIX_ROOT, &resolved, NULL);
                 if (r == -ENOENT) {
-                        log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *p);
+                        log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(c->root), *p);
                         continue;
                 }
                 if (r < 0) {
-                        log_warning_errno(r, "Failed to resolve path to hierarchy '%s%s': %m, ignoring.", strempty(arg_root), *p);
+                        log_warning_errno(r, "Failed to resolve path to hierarchy '%s%s': %m, ignoring.", strempty(c->root), *p);
                         continue;
                 }
 
-                r = is_our_mount_point(image_class, resolved);
+                r = is_our_mount_point(c->image_class, resolved);
                 if (r < 0)
                         return r;
                 if (!r)
                         continue;
 
-                f = path_join(resolved, image_class_info[image_class].dot_directory_name, image_class_info[image_class].short_identifier_plural);
+                f = path_join(resolved, image_class_info[c->image_class].dot_directory_name, image_class_info[c->image_class].short_identifier_plural);
                 if (!f)
                         return log_oom();
 
@@ -387,7 +490,7 @@ static int get_extension_release_metadata(
                         _cleanup_strv_free_ char **extension_release = NULL;
                         const char *value;
 
-                        r = load_extension_release_pairs(arg_root, image_class, *extension, /* relax_extension_release_check= */ true, &extension_release);
+                        r = load_extension_release_pairs(c->root, c->image_class, *extension, /* relax_extension_release_check= */ true, &extension_release);
                         if (r < 0) {
                                 log_debug_errno(r, "Failed to parse extension-release metadata of %s, ignoring: %m", *extension);
                                 continue;
@@ -770,40 +873,8 @@ 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;
+static int parse_image_class_parameter(sd_varlink *link, const char *value, ImageClass *image_class) {
         ImageClass c;
-        int r;
 
         assert(link);
         assert(image_class);
@@ -815,26 +886,19 @@ static int parse_image_class_parameter(sd_varlink *link, const char *value, Imag
         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) {
+static int resolve_hierarchy(const Context *c, const char *hierarchy, char **ret_resolved_hierarchy) {
         _cleanup_free_ char *resolved_path = NULL;
         int r;
 
+        assert(c);
         assert(hierarchy);
         assert(ret_resolved_hierarchy);
 
-        r = chase(hierarchy, arg_root, CHASE_PREFIX_ROOT, &resolved_path, NULL);
+        r = chase(hierarchy, c->root, CHASE_PREFIX_ROOT, &resolved_path, NULL);
         if (r < 0 && r != -ENOENT)
                 return log_error_errno(r, "Failed to resolve hierarchy '%s': %m", hierarchy);
 
@@ -871,27 +935,31 @@ static int mutable_directory_mode_matches_hierarchy(
 }
 
 static int resolve_mutable_directory(
+                const Context *c,
                 const char *hierarchy,
                 mode_t hierarchy_mode,
                 const char *workspace,
                 char **ret_resolved_mutable_directory) {
 
         _cleanup_free_ char *path = NULL, *resolved_path = NULL, *dir_name = NULL;
-        const char *root = arg_root, *base = MUTABLE_EXTENSIONS_BASE_DIR;
+        const char *root, *base = MUTABLE_EXTENSIONS_BASE_DIR;
         int r;
 
+        assert(c);
         assert(hierarchy);
         assert(ret_resolved_mutable_directory);
 
-        if (arg_mutable == MUTABLE_NO) {
+        root = c->root;
+
+        if (c->mutable == MUTABLE_NO) {
                 log_debug("Mutability for hierarchy '%s' is disabled, not resolving mutable directory.", hierarchy);
                 *ret_resolved_mutable_directory = NULL;
                 return 0;
         }
 
-        if (IN_SET(arg_mutable, MUTABLE_EPHEMERAL, MUTABLE_EPHEMERAL_IMPORT)) {
+        if (IN_SET(c->mutable, MUTABLE_EPHEMERAL, MUTABLE_EPHEMERAL_IMPORT)) {
                 /* We create mutable directory inside the temporary tmpfs workspace, which is a fixed
-                 * location that ignores arg_root. */
+                 * location that ignores the configured root. */
                 root = NULL;
                 base = workspace;
         }
@@ -904,7 +972,7 @@ static int resolve_mutable_directory(
         if (!path)
                 return log_oom();
 
-        if (IN_SET(arg_mutable, MUTABLE_YES, MUTABLE_AUTO)) {
+        if (IN_SET(c->mutable, MUTABLE_YES, MUTABLE_AUTO)) {
                 /* If there already is a mutable directory, check if its mode matches hierarchy. Merged
                  * hierarchy will have the same mode as the mutable directory, so we want no surprising mode
                  * changes here. */
@@ -913,7 +981,7 @@ static int resolve_mutable_directory(
                         return r;
         }
 
-        if (IN_SET(arg_mutable, MUTABLE_YES, MUTABLE_EPHEMERAL, MUTABLE_EPHEMERAL_IMPORT)) {
+        if (IN_SET(c->mutable, MUTABLE_YES, MUTABLE_EPHEMERAL, MUTABLE_EPHEMERAL_IMPORT)) {
                 _cleanup_close_ int path_fd = -EBADF, chmod_fd = -EBADF;
 
                 /* This also creates, e.g., /var/lib/extensions.mutable/usr if needed and all parent
@@ -943,20 +1011,21 @@ static int resolve_mutable_directory(
         return 0;
 }
 
-static int overlayfs_paths_new(const char *hierarchy, const char *workspace_path, OverlayFSPaths **ret_op) {
+static int overlayfs_paths_new(const Context *c, const char *hierarchy, const char *workspace_path, OverlayFSPaths **ret_op) {
         _cleanup_free_ char *hierarchy_copy = NULL, *resolved_hierarchy = NULL, *resolved_mutable_directory = NULL;
         mode_t hierarchy_mode;
 
         int r;
 
-        assert (hierarchy);
-        assert (ret_op);
+        assert(c);
+        assert(hierarchy);
+        assert(ret_op);
 
         hierarchy_copy = strdup(hierarchy);
         if (!hierarchy_copy)
                 return log_oom();
 
-        r = resolve_hierarchy(hierarchy, &resolved_hierarchy);
+        r = resolve_hierarchy(c, hierarchy, &resolved_hierarchy);
         if (r < 0)
                 return r;
 
@@ -969,7 +1038,7 @@ static int overlayfs_paths_new(const char *hierarchy, const char *workspace_path
         } else
                 hierarchy_mode = 0755;
 
-        r = resolve_mutable_directory(hierarchy, hierarchy_mode, workspace_path, &resolved_mutable_directory);
+        r = resolve_mutable_directory(c, hierarchy, hierarchy_mode, workspace_path, &resolved_mutable_directory);
         if (r < 0)
                 return r;
 
@@ -1028,15 +1097,16 @@ static int determine_used_extensions(const char *hierarchy, char **paths, char *
         return 0;
 }
 
-static int maybe_import_mutable_directory(OverlayFSPaths *op) {
+static int maybe_import_mutable_directory(const Context *c, OverlayFSPaths *op) {
         int r;
 
+        assert(c);
         assert(op);
 
         /* If importing mutable layer and it actually exists and is not a hierarchy itself, add it just below
          * the meta path */
 
-        if (arg_mutable != MUTABLE_IMPORT || !op->resolved_mutable_directory)
+        if (c->mutable != MUTABLE_IMPORT || !op->resolved_mutable_directory)
                 return 0;
 
         r = path_equal_or_inode_same_full(op->resolved_hierarchy, op->resolved_mutable_directory, 0);
@@ -1052,15 +1122,16 @@ static int maybe_import_mutable_directory(OverlayFSPaths *op) {
         return 0;
 }
 
-static int maybe_import_ignored_mutable_directory(OverlayFSPaths *op) {
+static int maybe_import_ignored_mutable_directory(const Context *c, OverlayFSPaths *op) {
         _cleanup_free_ char *dir_name = NULL, *path = NULL, *resolved_path = NULL;
         int r;
 
+        assert(c);
         assert(op);
 
         /* If importing the ignored mutable layer and it actually exists and is not a hierarchy itself, add
          * it just below the meta path */
-        if (arg_mutable != MUTABLE_EPHEMERAL_IMPORT)
+        if (c->mutable != MUTABLE_EPHEMERAL_IMPORT)
                 return 0;
 
         dir_name = hierarchy_as_single_path_component(op->hierarchy);
@@ -1071,7 +1142,7 @@ static int maybe_import_ignored_mutable_directory(OverlayFSPaths *op) {
         if (!path)
                 return log_oom();
 
-        r = chase(path, arg_root, CHASE_PREFIX_ROOT, &resolved_path, NULL);
+        r = chase(path, c->root, CHASE_PREFIX_ROOT, &resolved_path, NULL);
         if (r == -ENOENT) {
                 log_debug("Mutable directory for %s does not exist, not importing", op->hierarchy);
                 return 0;
@@ -1093,9 +1164,10 @@ static int maybe_import_ignored_mutable_directory(OverlayFSPaths *op) {
         return 0;
 }
 
-static int determine_top_lower_dirs(OverlayFSPaths *op, const char *meta_path) {
+static int determine_top_lower_dirs(const Context *c, OverlayFSPaths *op, const char *meta_path) {
         int r;
 
+        assert(c);
         assert(op);
         assert(meta_path);
 
@@ -1104,11 +1176,11 @@ static int determine_top_lower_dirs(OverlayFSPaths *op, const char *meta_path) {
         if (r < 0)
                 return log_oom();
 
-        r = maybe_import_mutable_directory(op);
+        r = maybe_import_mutable_directory(c, op);
         if (r < 0)
                 return r;
 
-        r = maybe_import_ignored_mutable_directory(op);
+        r = maybe_import_ignored_mutable_directory(c, op);
         if (r < 0)
                 return r;
 
@@ -1125,11 +1197,12 @@ static int determine_middle_lower_dirs(OverlayFSPaths *op, char **paths) {
         return 0;
 }
 
-static int hierarchy_as_lower_dir(OverlayFSPaths *op) {
+static int hierarchy_as_lower_dir(const Context *c, OverlayFSPaths *op) {
         int r;
 
         /* return 0 if hierarchy should be used as lower dir, >0, if not */
 
+        assert(c);
         assert(op);
 
         if (!op->resolved_hierarchy) {
@@ -1145,12 +1218,12 @@ static int hierarchy_as_lower_dir(OverlayFSPaths *op) {
                 return 1;
         }
 
-        if (arg_mutable == MUTABLE_IMPORT) {
+        if (c->mutable == MUTABLE_IMPORT) {
                 log_debug("Mutability for host hierarchy '%s' is disabled, so host hierarchy will be a lowerdir", op->resolved_hierarchy);
                 return 0;
         }
 
-        if (arg_mutable == MUTABLE_EPHEMERAL_IMPORT) {
+        if (c->mutable == MUTABLE_EPHEMERAL_IMPORT) {
                 log_debug("Mutability for host hierarchy '%s' is ephemeral, so host hierarchy will be a lowerdir", op->resolved_hierarchy);
                 return 0;
         }
@@ -1171,13 +1244,14 @@ static int hierarchy_as_lower_dir(OverlayFSPaths *op) {
         return 0;
 }
 
-static int determine_bottom_lower_dirs(OverlayFSPaths *op, dev_t *ret_backing_devnum) {
+static int determine_bottom_lower_dirs(const Context *c, OverlayFSPaths *op, dev_t *ret_backing_devnum) {
         int r;
 
+        assert(c);
         assert(op);
         assert(ret_backing_devnum);
 
-        r = hierarchy_as_lower_dir(op);
+        r = hierarchy_as_lower_dir(c, op);
         if (r < 0)
                 return r;
         if (!r) {
@@ -1194,6 +1268,7 @@ static int determine_bottom_lower_dirs(OverlayFSPaths *op, dev_t *ret_backing_de
 }
 
 static int determine_lower_dirs(
+                const Context *c,
                 OverlayFSPaths *op,
                 char **paths,
                 const char *meta_path,
@@ -1201,11 +1276,12 @@ static int determine_lower_dirs(
 
         int r;
 
+        assert(c);
         assert(op);
         assert(meta_path);
         assert(ret_backing_devnum);
 
-        r = determine_top_lower_dirs(op, meta_path);
+        r = determine_top_lower_dirs(c, op, meta_path);
         if (r < 0)
                 return r;
 
@@ -1213,20 +1289,21 @@ static int determine_lower_dirs(
         if (r < 0)
                 return r;
 
-        r = determine_bottom_lower_dirs(op, ret_backing_devnum);
+        r = determine_bottom_lower_dirs(c, op, ret_backing_devnum);
         if (r < 0)
                 return r;
 
         return 0;
 }
 
-static int determine_upper_dir(OverlayFSPaths *op) {
+static int determine_upper_dir(const Context *c, OverlayFSPaths *op) {
         int r;
 
+        assert(c);
         assert(op);
         assert(!op->upper_dir);
 
-        if (arg_mutable == MUTABLE_IMPORT) {
+        if (c->mutable == MUTABLE_IMPORT) {
                 log_debug("Mutability is disabled, there will be no upperdir for host hierarchy '%s'", op->hierarchy);
                 return 0;
         }
@@ -1251,17 +1328,18 @@ static int determine_upper_dir(OverlayFSPaths *op) {
         return 0;
 }
 
-static int determine_work_dir(OverlayFSPaths *op) {
+static int determine_work_dir(const Context *c, OverlayFSPaths *op) {
         _cleanup_free_ char *work_dir = NULL;
         int r;
 
+        assert(c);
         assert(op);
         assert(!op->work_dir);
 
         if (!op->upper_dir)
                 return 0;
 
-        if (arg_mutable == MUTABLE_IMPORT)
+        if (c->mutable == MUTABLE_IMPORT)
                 return 0;
 
         r = work_dir_for_hierarchy(op->hierarchy, op->upper_dir, &work_dir);
@@ -1454,25 +1532,26 @@ static int write_backing_file(ImageClass image_class, const char *meta_path, con
         return 0;
 }
 
-static int write_work_dir_file(ImageClass image_class, const char *meta_path, const char *work_dir, const char* hierarchy) {
+static int write_work_dir_file(const Context *c, const char *meta_path, const char *work_dir, const char* hierarchy) {
         _cleanup_free_ char *escaped_work_dir_in_root = NULL, *f = NULL;
         char *work_dir_in_root = NULL;
         int r;
 
+        assert(c);
         assert(meta_path);
 
         if (!work_dir)
                 return 0;
 
         /* Do not store work dir path for ephemeral mode, it will be gone once this process is done. */
-        if (IN_SET(arg_mutable, MUTABLE_EPHEMERAL, MUTABLE_EPHEMERAL_IMPORT))
+        if (IN_SET(c->mutable, MUTABLE_EPHEMERAL, MUTABLE_EPHEMERAL_IMPORT))
                 return 0;
 
-        work_dir_in_root = path_startswith(work_dir, empty_to_root(arg_root));
+        work_dir_in_root = path_startswith(work_dir, empty_to_root(c->root));
         if (!work_dir_in_root)
-                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Workdir '%s' must not be outside root '%s'", work_dir, empty_to_root(arg_root));
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Workdir '%s' must not be outside root '%s'", work_dir, empty_to_root(c->root));
 
-        f = path_join(meta_path, image_class_info[image_class].dot_directory_name, "work_dir");
+        f = path_join(meta_path, image_class_info[c->image_class].dot_directory_name, "work_dir");
         if (!f)
                 return log_oom();
 
@@ -1482,7 +1561,7 @@ static int write_work_dir_file(ImageClass image_class, const char *meta_path, co
         if (!escaped_work_dir_in_root)
                 return log_oom();
 
-        _cleanup_free_ char *hierarchy_path = path_join(hierarchy, image_class_info[image_class].dot_directory_name, "work_dir");
+        _cleanup_free_ char *hierarchy_path = path_join(hierarchy, image_class_info[c->image_class].dot_directory_name, "work_dir");
         if (!hierarchy_path)
                 return log_oom();
 
@@ -1494,7 +1573,7 @@ static int write_work_dir_file(ImageClass image_class, const char *meta_path, co
 }
 
 static int store_info_in_meta(
-                ImageClass image_class,
+                const Context *c,
                 char **extensions,
                 const char *origin_content,
                 const char *meta_path,
@@ -1505,11 +1584,12 @@ static int store_info_in_meta(
 
         int r;
 
+        assert(c);
         assert(meta_path);
         assert(overlay_path);
         /* work_dir may be NULL */
 
-        _cleanup_free_ char *f = path_join(meta_path, image_class_info[image_class].dot_directory_name);
+        _cleanup_free_ char *f = path_join(meta_path, image_class_info[c->image_class].dot_directory_name);
         if (!f)
                 return log_oom();
 
@@ -1525,23 +1605,23 @@ static int store_info_in_meta(
         if (r < 0)
                 return log_error_errno(r, "Failed to fix SELinux label for '%s': %m", hierarchy);
 
-        r = write_extensions_file(image_class, extensions, meta_path, hierarchy);
+        r = write_extensions_file(c->image_class, extensions, meta_path, hierarchy);
         if (r < 0)
                 return r;
 
-        r = write_origin_file(image_class, origin_content, meta_path, hierarchy);
+        r = write_origin_file(c->image_class, origin_content, meta_path, hierarchy);
         if (r < 0)
                 return r;
 
-        r = write_dev_file(image_class, meta_path, overlay_path, hierarchy);
+        r = write_dev_file(c->image_class, meta_path, overlay_path, hierarchy);
         if (r < 0)
                 return r;
 
-        r = write_work_dir_file(image_class, meta_path, work_dir, hierarchy);
+        r = write_work_dir_file(c, meta_path, work_dir, hierarchy);
         if (r < 0)
                 return r;
 
-        r = write_backing_file(image_class, meta_path, overlay_path, hierarchy, backing);
+        r = write_backing_file(c->image_class, meta_path, overlay_path, hierarchy, backing);
         if (r < 0)
                 return r;
 
@@ -1586,9 +1666,8 @@ static int make_mounts_read_only(ImageClass image_class, const char *overlay_pat
 }
 
 static int merge_hierarchy(
-                ImageClass image_class,
+                const Context *c,
                 const char *hierarchy,
-                int noexec,
                 char **extensions,
                 char **paths,
                 const char *origin_content,
@@ -1601,6 +1680,7 @@ static int merge_hierarchy(
         size_t extensions_used = 0;
         int r;
 
+        assert(c);
         assert(hierarchy);
         assert(paths);
         assert(meta_path);
@@ -1613,35 +1693,35 @@ static int merge_hierarchy(
         if (r < 0)
                 return r;
 
-        if (extensions_used == 0 && arg_mutable == MUTABLE_NO) /* No extension with files in this hierarchy? Then don't do anything. */
+        if (extensions_used == 0 && c->mutable == MUTABLE_NO) /* No extension with files in this hierarchy? Then don't do anything. */
                 return 0;
 
-        r = overlayfs_paths_new(hierarchy, workspace_path, &op);
+        r = overlayfs_paths_new(c, hierarchy, workspace_path, &op);
         if (r < 0)
                 return r;
 
         dev_t backing;
-        r = determine_lower_dirs(op, used_paths, meta_path, &backing);
+        r = determine_lower_dirs(c, op, used_paths, meta_path, &backing);
         if (r < 0)
                 return r;
 
-        r = determine_upper_dir(op);
+        r = determine_upper_dir(c, op);
         if (r < 0)
                 return r;
 
-        r = determine_work_dir(op);
+        r = determine_work_dir(c, op);
         if (r < 0)
                 return r;
 
-        r = mount_overlayfs_with_op(op, image_class, noexec, overlay_path, meta_path, arg_overlayfs_mount_options);
+        r = mount_overlayfs_with_op(op, c->image_class, c->noexec, overlay_path, meta_path, c->overlayfs_mount_options);
         if (r < 0)
                 return r;
 
-        r = store_info_in_meta(image_class, extensions, origin_content, meta_path, overlay_path, op->work_dir, op->hierarchy, backing);
+        r = store_info_in_meta(c, extensions, origin_content, meta_path, overlay_path, op->work_dir, op->hierarchy, backing);
         if (r < 0)
                 return r;
 
-        r = make_mounts_read_only(image_class, overlay_path, op->upper_dir && op->work_dir);
+        r = make_mounts_read_only(c->image_class, overlay_path, op->upper_dir && op->work_dir);
         if (r < 0)
                 return r;
 
@@ -1656,21 +1736,22 @@ 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 ImagePolicy* pick_image_policy(const Context *c, const Image *img) {
+        assert(c);
         assert(img);
         assert(img->path);
 
         /* Explicitly specified policy always wins */
-        if (arg_image_policy)
-                return arg_image_policy;
+        if (c->image_policy)
+                return c->image_policy;
 
         /* If located in /.extra/ in the initrd, then it was placed there by systemd-stub, and was
          * picked up from an untrusted ESP. Thus, require a stricter policy by default for them. (For the
          * other directories we assume the appropriate level of trust was already established.)
          * With --root= we default to the regular policy, though. (To change that, the check would need
-         * to prepend (or cut away) arg_root.) */
+         * to prepend (or cut away) c->root.) */
 
-        if (in_initrd() && !arg_root) {
+        if (in_initrd() && !c->root) {
                 if (path_startswith(img->path, "/.extra/sysext/"))
                         return &image_policy_sysext_strict;
                 if (path_startswith(img->path, "/.extra/global_sysext/"))
@@ -1688,14 +1769,15 @@ static const ImagePolicy* pick_image_policy(const Image *img) {
         return image_class_info[img->class].default_image_policy;
 }
 
-static int unmerge_hierarchy(ImageClass image_class, const char *p, const char *submounts_path) {
+static int unmerge_hierarchy(const Context *c, 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(c);
         assert(p);
 
-        dot_dir = path_join(p, image_class_info[image_class].dot_directory_name);
+        dot_dir = path_join(p, image_class_info[c->image_class].dot_directory_name);
         if (!dot_dir)
                 return log_oom();
 
@@ -1709,7 +1791,7 @@ static int unmerge_hierarchy(ImageClass image_class, const char *p, const char *
                 /* 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);
+                r = is_our_mount_point(c->image_class, p);
                 if (r < 0)
                         return r;
                 if (r == 0)
@@ -1726,7 +1808,7 @@ static int unmerge_hierarchy(ImageClass image_class, const char *p, const char *
                         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);
+                        work_dir = path_join(c->root, work_dir_in_root);
                         if (!work_dir)
                                 return log_oom();
                 }
@@ -1764,12 +1846,12 @@ static int unmerge_hierarchy(ImageClass image_class, const char *p, const char *
 }
 
 static int unmerge_subprocess(
-                ImageClass image_class,
-                char **hierarchies,
+                const Context *c,
                 const char *workspace) {
 
         int r, ret = 0;
 
+        assert(c);
         assert(workspace);
         assert(path_startswith(workspace, "/run/"));
 
@@ -1784,24 +1866,24 @@ static int unmerge_subprocess(
         if (r < 0)
                 return log_error_errno(r, "Failed to create '%s': %m", workspace);
 
-        STRV_FOREACH(h, hierarchies) {
+        STRV_FOREACH(h, c->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);
+                r = chase(*h, c->root, CHASE_PREFIX_ROOT, &resolved, NULL);
                 if (r == -ENOENT) {
-                        log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *h);
+                        log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(c->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));
+                        RET_GATHER(ret, log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(c->root), *h));
                         continue;
                 }
 
-                r = unmerge_hierarchy(image_class, resolved, submounts_path);
+                r = unmerge_hierarchy(c, resolved, submounts_path);
                 if (r < 0) {
                         RET_GATHER(ret, r);
                         continue;
@@ -1820,18 +1902,17 @@ static int unmerge_subprocess(
         return ret;
 }
 
-static int unmerge(
-                ImageClass image_class,
-                char **hierarchies,
-                bool no_reload) {
+static int unmerge(const Context *c) {
 
         _cleanup_set_free_ Set *units_to_restart = NULL, *units_to_reload_or_restart = NULL;
         bool need_to_reload;
         int r;
 
+        assert(c);
+
         (void) DLOPEN_LIBMOUNT(LOG_DEBUG, required);
 
-        r = get_extension_release_metadata(image_class, hierarchies, no_reload, &need_to_reload, &units_to_restart, &units_to_reload_or_restart);
+        r = get_extension_release_metadata(c, &need_to_reload, &units_to_restart, &units_to_reload_or_restart);
         if (r < 0)
                 return r;
 
@@ -1844,7 +1925,7 @@ static int unmerge(
         if (r == 0) {
                 /* Child with its own mount namespace */
 
-                r = unmerge_subprocess(image_class, hierarchies, "/run/systemd/sysext");
+                r = unmerge_subprocess(c, "/run/systemd/sysext");
 
                 /* Our namespace ceases to exist here, also implicitly detaching all temporary mounts we
                  * created below /run. Nice! */
@@ -1867,11 +1948,7 @@ static int unmerge(
 }
 
 static int merge_subprocess(
-                ImageClass image_class,
-                char **hierarchies,
-                bool force,
-                bool always_refresh,
-                int noexec,
+                const Context *c,
                 Hashmap *images,
                 const char *workspace) {
 
@@ -1887,10 +1964,12 @@ static int merge_subprocess(
         Image *img;
         int r;
 
-        if (!isempty(arg_root)) {
-                r = chase(arg_root, /* root= */ NULL, CHASE_MUST_BE_DIRECTORY, &root_resolved, /* ret_fd= */ NULL);
+        assert(c);
+
+        if (!isempty(c->root)) {
+                r = chase(c->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));
+                        return log_error_errno(r, "Failed to resolve --root='%s': %m", strempty(c->root));
         }
 
         assert(path_startswith(workspace, "/run/"));
@@ -1910,30 +1989,30 @@ static int merge_subprocess(
          * but let the kernel do that entirely automatically, once our namespace dies. Note that this file
          * system won't be visible to anyone but us, since we opened our own namespace and then made the
          * /run/ hierarchy (which our workspace is contained in) MS_SLAVE, see above. */
-        r = mount_nofollow_verbose(LOG_ERR, image_class_info[image_class].short_identifier, workspace, "tmpfs", 0, "mode=0700");
+        r = mount_nofollow_verbose(LOG_ERR, image_class_info[c->image_class].short_identifier, workspace, "tmpfs", 0, "mode=0700");
         if (r < 0)
                 return r;
 
         /* Acquire host OS release info, so that we can compare it with the extension's data */
         r = parse_os_release(
-                        arg_root,
+                        c->root,
                         "ID", &host_os_release_id,
                         "ID_LIKE", &host_os_release_id_like,
                         "VERSION_ID", &host_os_release_version_id,
-                        image_class_info[image_class].level_env, &host_os_release_api_level);
+                        image_class_info[c->image_class].level_env, &host_os_release_api_level);
         if (r < 0)
-                return log_error_errno(r, "Failed to acquire 'os-release' data of OS tree '%s': %m", empty_to_root(arg_root));
+                return log_error_errno(r, "Failed to acquire 'os-release' data of OS tree '%s': %m", empty_to_root(c->root));
         if (isempty(host_os_release_id))
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                        "'ID' field not found or empty in 'os-release' data of OS tree '%s'.",
-                                       empty_to_root(arg_root));
+                                       empty_to_root(c->root));
 
         /* Let's now mount all images */
         HASHMAP_FOREACH(img, images) {
                 _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);
+                p = path_join(workspace, image_class_info[c->image_class].short_identifier_plural, img->name);
                 if (!p)
                         return log_oom();
 
@@ -1945,7 +2024,7 @@ static int merge_subprocess(
                 case IMAGE_DIRECTORY:
                 case IMAGE_SUBVOLUME:
 
-                        if (!force) {
+                        if (!c->force) {
                                 r = extension_has_forbidden_content(p);
                                 if (r < 0)
                                         return r;
@@ -1988,7 +2067,7 @@ static int merge_subprocess(
                         if (verity_settings.data_path)
                                 flags |= DISSECT_IMAGE_NO_PARTITION_TABLE;
 
-                        if (!force)
+                        if (!c->force)
                                 flags |= DISSECT_IMAGE_VALIDATE_OS_EXT;
 
                         r = loop_device_make_by_path(
@@ -2005,7 +2084,7 @@ static int merge_subprocess(
                                         d,
                                         &verity_settings,
                                         /* mount_options= */ NULL,
-                                        pick_image_policy(img),
+                                        pick_image_policy(c, img),
                                         /* image_filter= */ NULL,
                                         flags,
                                         &m);
@@ -2036,7 +2115,7 @@ static int merge_subprocess(
                                         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);
+                        r = dissected_image_decrypt(m, c->root, /* passphrase= */ NULL, &verity_settings, pick_image_policy(c, img), flags);
                         if (r < 0)
                                 return r;
 
@@ -2049,7 +2128,7 @@ static int merge_subprocess(
                                         flags);
                         if (r < 0 && r != -ENOMEDIUM)
                                 return r;
-                        if (r == -ENOMEDIUM && !force) {
+                        if (r == -ENOMEDIUM && !c->force) {
                                 n_ignored++;
                                 continue;
                         }
@@ -2063,11 +2142,11 @@ static int merge_subprocess(
                         assert_not_reached();
                 }
 
-                if (force)
+                if (c->force)
                         log_debug("Force mode enabled, skipping version validation.");
                 else {
                         bool is_initrd;
-                        r = chase_and_access("/etc/initrd-release", arg_root, CHASE_PREFIX_ROOT, F_OK, /* ret_path= */ NULL);
+                        r = chase_and_access("/etc/initrd-release", c->root, CHASE_PREFIX_ROOT, F_OK, /* ret_path= */ NULL);
                         if (r < 0 && r != -ENOENT)
                                 return log_error_errno(r, "Failed to check for /etc/initrd-release: %m");
                         is_initrd = r >= 0;
@@ -2079,8 +2158,8 @@ static int merge_subprocess(
                                         host_os_release_version_id,
                                         host_os_release_api_level,
                                         is_initrd ? "initrd" : "system",
-                                        image_extension_release(img, image_class),
-                                        image_class);
+                                        image_extension_release(img, c->image_class),
+                                        c->image_class);
                         if (r < 0)
                                 return r;
                         if (r == 0) {
@@ -2108,7 +2187,7 @@ static int merge_subprocess(
                 /* 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)) {
+                if (!isempty(c->root)) {
                         const char *without_root = NULL;
                         without_root = path_startswith(img->path, root_resolved);
                         if (!isempty(without_root)) {
@@ -2163,7 +2242,7 @@ static int merge_subprocess(
         }
 
         /* Nothing left? Then shortcut things */
-        if (n_extensions == 0 && arg_mutable == MUTABLE_NO) {
+        if (n_extensions == 0 && c->mutable == MUTABLE_NO) {
                 if (n_ignored > 0)
                         log_info("No suitable extensions found (%u ignored due to incompatible image(s)).", n_ignored);
                 else
@@ -2175,16 +2254,16 @@ static int merge_subprocess(
         typesafe_qsort(extensions, n_extensions, strverscmp_improvedp);
         typesafe_qsort(extensions_v, n_extensions, strverscmp_improvedp);
 
-        STRV_FOREACH(h, hierarchies) {
+        STRV_FOREACH(h, c->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);
+                r = overlayfs_paths_new(c, *h, workspace, &op);
                 if (r < 0)
                         return r;
 
-                if (op->resolved_mutable_directory && !isempty(arg_root)) {
+                if (op->resolved_mutable_directory && !isempty(c->root)) {
                         const char *without_root = NULL;
                         without_root = path_startswith(op->resolved_mutable_directory, root_resolved);
                         if (!isempty(without_root)) {
@@ -2206,15 +2285,15 @@ static int merge_subprocess(
                 }
 
                 /* Find existing origin file for comparison. */
-                r = chase(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, /* ret_fd= */ NULL);
+                r = chase(*h, c->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);
+                        return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(c->root), *h);
 
-                f = path_join(resolved, image_class_info[image_class].dot_directory_name, "origin");
+                f = path_join(resolved, image_class_info[c->image_class].dot_directory_name, "origin");
                 if (!f)
                         return log_oom();
 
-                r = is_our_mount_point(image_class, resolved);
+                r = is_our_mount_point(c->image_class, resolved);
                 if (r < 0)
                         return r;
                 if (r == 0)
@@ -2234,13 +2313,13 @@ static int merge_subprocess(
 
         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_STRING("mode", mutable_mode_to_string(c->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),
+                           SD_JSON_BUILD_PAIR_CONDITION(!isempty(c->overlayfs_mount_options),
                                                         "mountOptions",
-                                                        SD_JSON_BUILD_STRING(arg_overlayfs_mount_options)),
+                                                        SD_JSON_BUILD_STRING(c->overlayfs_mount_options)),
                            SD_JSON_BUILD_PAIR_CONDITION(!!extensions_origin_entries,
                                                         "extensions",
                                                         SD_JSON_BUILD_VARIANT(extensions_origin_entries)));
@@ -2263,7 +2342,7 @@ static int merge_subprocess(
 
                 /* This works well with unordered entries. */
                 if (sd_json_variant_equal(extensions_origin_json, old_origin_json)) {
-                        if (!always_refresh) {
+                        if (!c->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;
@@ -2275,7 +2354,7 @@ static int merge_subprocess(
         }
 
         if (n_extensions == 0) {
-                assert(arg_mutable != MUTABLE_NO);
+                assert(c->mutable != MUTABLE_NO);
                 log_info("No extensions found, proceeding in mutable mode.");
         } else {
                 _cleanup_free_ char *buf = strv_join(extensions_v, "', '");
@@ -2295,7 +2374,7 @@ static int merge_subprocess(
 
                 assert_se(img = hashmap_get(images, extensions[n_extensions - 1 - k]));
 
-                p = path_join(workspace, image_class_info[image_class].short_identifier_plural, img->name);
+                p = path_join(workspace, image_class_info[c->image_class].short_identifier_plural, img->name);
                 if (!p)
                         return log_oom();
 
@@ -2304,18 +2383,18 @@ static int merge_subprocess(
 
         /* Let's now unmerge the status quo ante, since to build the new overlayfs we need a reference to the
          * underlying fs. */
-        STRV_FOREACH(h, hierarchies) {
+        STRV_FOREACH(h, c->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|CHASE_NONEXISTENT, &resolved, NULL);
+                r = chase(*h, c->root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL);
                 if (r < 0)
-                        return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root), *h);
+                        return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(c->root), *h);
 
-                r = unmerge_hierarchy(image_class, resolved, submounts_path);
+                r = unmerge_hierarchy(c, resolved, submounts_path);
                 if (r < 0)
                         return r;
                 if (r > 0)
@@ -2330,7 +2409,7 @@ static int merge_subprocess(
         }
 
         /* Create overlayfs mounts for all hierarchies */
-        STRV_FOREACH(h, hierarchies) {
+        STRV_FOREACH(h, c->hierarchies) {
                 _cleanup_free_ char *meta_path = NULL, *overlay_path = NULL, *merge_hierarchy_workspace = NULL, *submounts_path = NULL;
 
                 meta_path = path_join(workspace, "meta", *h); /* The place where to store metadata about this instance */
@@ -2351,9 +2430,8 @@ static int merge_subprocess(
                         return log_oom();
 
                 r = merge_hierarchy(
-                                image_class,
+                                c,
                                 *h,
-                                noexec,
                                 extensions,
                                 paths,
                                 extensions_origin_content,
@@ -2372,7 +2450,7 @@ static int merge_subprocess(
         }
 
         /* And move them all into place. This is where things appear in the host namespace */
-        STRV_FOREACH(h, hierarchies) {
+        STRV_FOREACH(h, c->hierarchies) {
                 _cleanup_free_ char *p = NULL, *resolved = NULL;
 
                 p = path_join(workspace, "overlay", *h);
@@ -2385,9 +2463,9 @@ static int merge_subprocess(
                 if (r < 0)
                         return log_error_errno(r, "Failed to check if '%s' exists: %m", p);
 
-                r = chase(*h, arg_root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL);
+                r = chase(*h, c->root, CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &resolved, NULL);
                 if (r < 0)
-                        return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(arg_root), *h);
+                        return log_error_errno(r, "Failed to resolve hierarchy '%s%s': %m", strempty(c->root), *h);
 
                 r = mkdir_p(resolved, 0755);
                 if (r < 0)
@@ -2404,16 +2482,12 @@ static int merge_subprocess(
         return MERGE_MOUNTED;
 }
 
-static int merge(ImageClass image_class,
-                 char **hierarchies,
-                 bool force,
-                 bool no_reload,
-                 bool always_refresh,
-                 int noexec,
-                 Hashmap *images) {
+static int merge(const Context *c, Hashmap *images) {
 
         int r;
 
+        assert(c);
+
         (void) DLOPEN_CRYPTSETUP(LOG_DEBUG, recommended);
         (void) DLOPEN_LIBBLKID(LOG_DEBUG, required);
         (void) DLOPEN_LIBMOUNT(LOG_DEBUG, required);
@@ -2425,7 +2499,7 @@ static int merge(ImageClass image_class,
         if (r == 0) {
                 /* Child with its own mount namespace */
 
-                r = merge_subprocess(image_class, hierarchies, force, always_refresh, noexec, images, "/run/systemd/sysext");
+                r = merge_subprocess(c, images, "/run/systemd/sysext");
 
                 /* Our namespace ceases to exist here, also implicitly detaching all temporary mounts we
                  * created below /run. Nice! */
@@ -2452,7 +2526,7 @@ static int merge(ImageClass image_class,
 
         _cleanup_set_free_ Set *units_to_restart = NULL, *units_to_reload_or_restart = NULL;
         bool need_to_reload;
-        r = get_extension_release_metadata(image_class, hierarchies, no_reload, &need_to_reload, &units_to_restart, &units_to_reload_or_restart);
+        r = get_extension_release_metadata(c, &need_to_reload, &units_to_restart, &units_to_reload_or_restart);
         if (r < 0)
                 return r;
         if (need_to_reload) {
@@ -2471,31 +2545,36 @@ static int merge(ImageClass image_class,
 
 VERB_DEFAULT_NOARG(verb_status, "status", "Show current merge status (default)");
 static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(context_done) Context c = CONTEXT_NULL;
         _cleanup_(table_unrefp) Table *t = NULL;
         int r, ret = 0;
 
+        r = context_from_cmdline(&c, arg_image_class);
+        if (r < 0)
+                return r;
+
         t = table_new("hierarchy", "extensions", "since");
         if (!t)
                 return log_oom();
 
         table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
 
-        STRV_FOREACH(p, arg_hierarchies) {
+        STRV_FOREACH(p, c.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);
+                r = chase(*p, c.root, CHASE_PREFIX_ROOT, &resolved, NULL);
                 if (r == -ENOENT) {
-                        log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *p);
+                        log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(c.root), *p);
                         continue;
                 }
                 if (r < 0) {
-                        log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p);
+                        log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(c.root), *p);
                         goto inner_fail;
                 }
 
-                r = is_our_mount_point(arg_image_class, resolved);
+                r = is_our_mount_point(c.image_class, resolved);
                 if (r < 0)
                         goto inner_fail;
                 if (r == 0) {
@@ -2511,7 +2590,7 @@ static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata)
                         continue;
                 }
 
-                f = path_join(resolved, image_class_info[arg_image_class].dot_directory_name, image_class_info[arg_image_class].short_identifier_plural);
+                f = path_join(resolved, image_class_info[c.image_class].dot_directory_name, image_class_info[c.image_class].short_identifier_plural);
                 if (!f)
                         return log_oom();
 
@@ -2550,19 +2629,20 @@ static int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata)
         return ret;
 }
 
-static int image_discover_and_read_metadata(ImageClass image_class, Hashmap **ret_images) {
+static int image_discover_and_read_metadata(const Context *c, Hashmap **ret_images) {
         _cleanup_hashmap_free_ Hashmap *images = NULL;
         Image *img;
         int r;
 
+        assert(c);
         assert(ret_images);
 
-        r = image_discover(RUNTIME_SCOPE_SYSTEM, image_class, arg_root, &images);
+        r = image_discover(RUNTIME_SCOPE_SYSTEM, c->image_class, c->root, &images);
         if (r < 0)
                 return log_error_errno(r, "Failed to discover images: %m");
 
         HASHMAP_FOREACH(img, images) {
-                r = image_read_metadata(img, arg_root, image_class_info[image_class].default_image_policy, RUNTIME_SCOPE_SYSTEM);
+                r = image_read_metadata(img, c->root, image_class_info[c->image_class].default_image_policy, RUNTIME_SCOPE_SYSTEM);
                 if (r < 0)
                         return log_error_errno(r, "Failed to read metadata for image %s: %m", img->name);
         }
@@ -2572,28 +2652,26 @@ static int image_discover_and_read_metadata(ImageClass image_class, Hashmap **re
         return 0;
 }
 
-static int look_for_merged_hierarchies(
-                ImageClass image_class,
-                char **hierarchies,
-                const char **ret_which) {
+static int look_for_merged_hierarchies(const Context *c, const char **ret_which) {
         int r;
 
+        assert(c);
         assert(ret_which);
 
         /* In merge mode fail if things are already merged. (In --refresh mode below we'll unmerge if we find
          * things are already merged...) */
-        STRV_FOREACH(p, hierarchies) {
+        STRV_FOREACH(p, c->hierarchies) {
                 _cleanup_free_ char *resolved = NULL;
 
-                r = chase(*p, arg_root, CHASE_PREFIX_ROOT, &resolved, NULL);
+                r = chase(*p, c->root, CHASE_PREFIX_ROOT, &resolved, NULL);
                 if (r == -ENOENT) {
-                        log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(arg_root), *p);
+                        log_debug_errno(r, "Hierarchy '%s%s' does not exist, ignoring.", strempty(c->root), *p);
                         continue;
                 }
                 if (r < 0)
-                        return log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(arg_root), *p);
+                        return log_error_errno(r, "Failed to resolve path to hierarchy '%s%s': %m", strempty(c->root), *p);
 
-                r = is_our_mount_point(image_class, resolved);
+                r = is_our_mount_point(c->image_class, resolved);
                 if (r < 0)
                         return r;
                 if (r > 0) {
@@ -2608,6 +2686,7 @@ static int look_for_merged_hierarchies(
 
 VERB_NOARG(verb_merge, "merge", "Merge extensions into relevant hierarchies");
 static int verb_merge(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(context_done) Context c = CONTEXT_NULL;
         _cleanup_hashmap_free_ Hashmap *images = NULL;
         const char *which;
         int r;
@@ -2618,23 +2697,21 @@ static int verb_merge(int argc, char *argv[], uintptr_t _data, void *userdata) {
         if (r == 0)
                 return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
 
-        r = image_discover_and_read_metadata(arg_image_class, &images);
+        r = context_from_cmdline(&c, arg_image_class);
         if (r < 0)
                 return r;
 
-        r = look_for_merged_hierarchies(arg_image_class, arg_hierarchies, &which);
+        r = image_discover_and_read_metadata(&c, &images);
+        if (r < 0)
+                return r;
+
+        r = look_for_merged_hierarchies(&c, &which);
         if (r < 0)
                 return r;
         if (r > 0)
                 return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Hierarchy '%s' is already merged.", which);
 
-        return merge(arg_image_class,
-                     arg_hierarchies,
-                     arg_force,
-                     arg_no_reload,
-                     arg_always_refresh,
-                     arg_noexec,
-                     images);
+        return merge(&c, images);
 }
 
 typedef struct MethodMergeParameters {
@@ -2666,6 +2743,7 @@ static int parse_merge_parameters(sd_varlink *link, sd_json_variant *parameters,
 
 static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
         Hashmap **polkit_registry = ASSERT_PTR(userdata);
+        _cleanup_(context_done) Context c = CONTEXT_NULL;
         _cleanup_hashmap_free_ Hashmap *images = NULL;
         MethodMergeParameters p = {
                 .force = -1,
@@ -2673,10 +2751,8 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var
                 .always_refresh = -1,
                 .noexec = -1,
         };
-        _cleanup_strv_free_ char **hierarchies = NULL;
         ImageClass image_class = arg_image_class;
-        bool force, no_reload, always_refresh;
-        int r, noexec;
+        int r;
 
         assert(link);
 
@@ -2684,43 +2760,48 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var
         if (r != 0)
                 return r;
 
-        r = parse_image_class_parameter(link, p.class, &image_class, &hierarchies);
+        r = parse_image_class_parameter(link, p.class, &image_class);
         if (r < 0)
                 return r;
 
-        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 = context_from_cmdline(&c, image_class);
+        if (r < 0)
+                return r;
+
+        if (p.force >= 0)
+                c.force = p.force;
+        if (p.no_reload >= 0)
+                c.no_reload = p.no_reload;
+        if (p.always_refresh >= 0)
+                c.always_refresh = p.always_refresh;
+        if (p.noexec >= 0)
+                c.noexec = p.noexec;
 
         r = varlink_verify_polkit_async(
                         link,
                         /* bus= */ NULL,
-                        image_class_info[image_class].polkit_rw_action_id,
+                        image_class_info[c.image_class].polkit_rw_action_id,
                         (const char**) STRV_MAKE(
                                 "verb", "merge",
-                                "force", one_zero(force),
-                                "noReload", one_zero(no_reload),
-                                "noexec", one_zero(noexec > 0)),
+                                "force", one_zero(c.force),
+                                "noReload", one_zero(c.no_reload),
+                                "noexec", one_zero(c.noexec > 0)),
                         polkit_registry);
         if (r <= 0)
                 return r;
 
-        r = image_discover_and_read_metadata(image_class, &images);
+        r = image_discover_and_read_metadata(&c, &images);
         if (r < 0)
                 return r;
 
         const char *which;
-        r = look_for_merged_hierarchies(
-                        image_class,
-                        hierarchies ?: arg_hierarchies,
-                        &which);
+        r = look_for_merged_hierarchies(&c, &which);
         if (r < 0)
                 return r;
         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, always_refresh, noexec, images);
+        r = merge(&c, images);
         if (r < 0)
                 return r;
 
@@ -2729,6 +2810,7 @@ static int vl_method_merge(sd_varlink *link, sd_json_variant *parameters, sd_var
 
 VERB_NOARG(verb_unmerge, "unmerge", "Unmerge extensions from relevant hierarchies");
 static int verb_unmerge(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(context_done) Context c = CONTEXT_NULL;
         int r;
 
         r = have_effective_cap(CAP_SYS_ADMIN);
@@ -2737,9 +2819,11 @@ static int verb_unmerge(int argc, char *argv[], uintptr_t _data, void *userdata)
         if (r == 0)
                 return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
 
-        return unmerge(arg_image_class,
-                       arg_hierarchies,
-                       arg_no_reload);
+        r = context_from_cmdline(&c, arg_image_class);
+        if (r < 0)
+                return r;
+
+        return unmerge(&c);
 }
 
 typedef struct MethodUnmergeParameters {
@@ -2759,9 +2843,8 @@ static int vl_method_unmerge(sd_varlink *link, sd_json_variant *parameters, sd_v
                 .no_reload = -1,
         };
         Hashmap **polkit_registry = ASSERT_PTR(userdata);
-        _cleanup_strv_free_ char **hierarchies = NULL;
+        _cleanup_(context_done) Context c = CONTEXT_NULL;
         ImageClass image_class = arg_image_class;
-        bool no_reload;
         int r;
 
         assert(link);
@@ -2770,42 +2853,43 @@ static int vl_method_unmerge(sd_varlink *link, sd_json_variant *parameters, sd_v
         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);
+        if (r < 0)
+                return r;
 
-        r = parse_image_class_parameter(link, p.class, &image_class, &hierarchies);
+        r = context_from_cmdline(&c, image_class);
         if (r < 0)
                 return r;
 
+        if (p.no_reload >= 0)
+                c.no_reload = p.no_reload;
+
         r = varlink_verify_polkit_async(
                         link,
                         /* bus= */ NULL,
-                        image_class_info[image_class].polkit_rw_action_id,
+                        image_class_info[c.image_class].polkit_rw_action_id,
                         (const char**) STRV_MAKE(
                                 "verb", "unmerge",
-                                "noReload", one_zero(no_reload)),
+                                "noReload", one_zero(c.no_reload)),
                         polkit_registry);
         if (r <= 0)
                 return r;
 
-        r = unmerge(image_class, hierarchies ?: arg_hierarchies, no_reload);
+        r = unmerge(&c);
         if (r < 0)
                 return r;
 
         return sd_varlink_reply(link, NULL);
 }
 
-static int refresh(
-                ImageClass image_class,
-                char **hierarchies,
-                bool force,
-                bool no_reload,
-                bool always_refresh,
-                int noexec) {
+static int refresh(const Context *c) {
 
         _cleanup_hashmap_free_ Hashmap *images = NULL;
         int r;
 
-        r = image_discover_and_read_metadata(image_class, &images);
+        assert(c);
+
+        r = image_discover_and_read_metadata(c, &images);
         if (r < 0)
                 return r;
 
@@ -2813,12 +2897,12 @@ static int refresh(
          * 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, always_refresh, noexec, images);
+        r = merge(c, images);
         if (r < 0)
                 return r;
         if (r == 0) /* No images found? Then unmerge. The goal of --refresh is after all that after having
                      * called there's a guarantee that the merge status matches the installed extensions. */
-                r = unmerge(image_class, hierarchies, no_reload);
+                r = unmerge(c);
 
         /* Net result here is that:
          *
@@ -2839,6 +2923,7 @@ static int refresh(
 
 VERB_NOARG(verb_refresh, "refresh", "Unmerge/merge extensions again");
 static int verb_refresh(int argc, char *argv[], uintptr_t _data, void *userdata) {
+        _cleanup_(context_done) Context c = CONTEXT_NULL;
         int r;
 
         r = have_effective_cap(CAP_SYS_ADMIN);
@@ -2847,16 +2932,16 @@ static int verb_refresh(int argc, char *argv[], uintptr_t _data, void *userdata)
         if (r == 0)
                 return log_error_errno(SYNTHETIC_ERRNO(EPERM), "Need to be privileged.");
 
-        return refresh(arg_image_class,
-                       arg_hierarchies,
-                       arg_force,
-                       arg_no_reload,
-                       arg_always_refresh,
-                       arg_noexec);
+        r = context_from_cmdline(&c, arg_image_class);
+        if (r < 0)
+                return r;
+
+        return refresh(&c);
 }
 
 static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
 
+        _cleanup_(context_done) Context c = CONTEXT_NULL;
         MethodMergeParameters p = {
                 .force = -1,
                 .no_reload = -1,
@@ -2864,10 +2949,8 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v
                 .noexec = -1,
         };
         Hashmap **polkit_registry = ASSERT_PTR(userdata);
-        _cleanup_strv_free_ char **hierarchies = NULL;
         ImageClass image_class = arg_image_class;
-        bool force, no_reload, always_refresh;
-        int r, noexec;
+        int r;
 
         assert(link);
 
@@ -2875,29 +2958,37 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v
         if (r != 0)
                 return r;
 
-        r = parse_image_class_parameter(link, p.class, &image_class, &hierarchies);
+        r = parse_image_class_parameter(link, p.class, &image_class);
+        if (r < 0)
+                return r;
+
+        r = context_from_cmdline(&c, image_class);
         if (r < 0)
                 return r;
 
-        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;
+        if (p.force >= 0)
+                c.force = p.force;
+        if (p.no_reload >= 0)
+                c.no_reload = p.no_reload;
+        if (p.always_refresh >= 0)
+                c.always_refresh = p.always_refresh;
+        if (p.noexec >= 0)
+                c.noexec = p.noexec;
 
         r = varlink_verify_polkit_async(
                         link,
                         /* bus= */ NULL,
-                        image_class_info[image_class].polkit_rw_action_id,
+                        image_class_info[c.image_class].polkit_rw_action_id,
                         (const char**) STRV_MAKE(
                                 "verb", "refresh",
-                                "force", one_zero(force),
-                                "noReload", one_zero(no_reload),
-                                "noexec", one_zero(noexec > 0)),
+                                "force", one_zero(c.force),
+                                "noReload", one_zero(c.no_reload),
+                                "noexec", one_zero(c.noexec > 0)),
                         polkit_registry);
         if (r <= 0)
                 return r;
 
-        r = refresh(image_class, hierarchies ?: arg_hierarchies, force, no_reload, always_refresh, noexec);
+        r = refresh(&c);
         if (r < 0)
                 return r;
 
@@ -2905,14 +2996,16 @@ static int vl_method_refresh(sd_varlink *link, sd_json_variant *parameters, sd_v
 }
 
 static int refresh_class(ImageClass image_class) {
-        _cleanup_strv_free_ char **hierarchies = NULL;
+        _cleanup_(context_done) Context c = CONTEXT_NULL;
         int r;
 
-        r = parse_env_extension_hierarchies(&hierarchies, image_class_info[image_class].name_env);
+        /* The sysupdate notification service is systemd-sysext, but it refreshes both sysexts and
+         * confexts. Apply the relevant class-specific environment and configuration for each refresh. */
+        r = context_from_cmdline(&c, image_class);
         if (r < 0)
-                return log_error_errno(r, "Failed to determine %s hierarchies: %m", image_class_info[image_class].short_identifier);
+                return r;
 
-        return refresh(image_class, hierarchies, arg_force, arg_no_reload, arg_always_refresh, arg_noexec);
+        return refresh(&c);
 }
 
 static int vl_method_on_completed_update(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
@@ -2995,7 +3088,7 @@ static int vl_method_list(sd_varlink *link, sd_json_variant *parameters, sd_varl
                 return r;
 
         ImageClass image_class = arg_image_class;
-        r = parse_image_class_parameter(link, class, &image_class, NULL);
+        r = parse_image_class_parameter(link, class, &image_class);
         if (r < 0)
                 return r;
 
@@ -3113,16 +3206,14 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
                         if (r < 0)
                                 return log_error_errno(r, "Failed to parse argument to --mutable=: %s", opts.arg);
                         arg_mutable = r;
-                        arg_mutable_set = true;
+                        arg_mutable_argv_set = true;
                         break;
 
                 OPTION_LONG("image-policy", "POLICY", "Specify disk image dissection policy"):
                         r = parse_image_policy_argument(opts.arg, &arg_image_policy);
                         if (r < 0)
                                 return r;
-                        /* When the CLI flag is given we initialize even if NULL
-                         * so that the config file entry won't overwrite it */
-                        arg_image_policy_set = true;
+                        arg_image_policy_argv_set = true;
                         break;
 
                 OPTION_LONG("noexec", "BOOL", "Whether to mount extension overlay with noexec"):
@@ -3183,11 +3274,6 @@ static int run(int argc, char *argv[]) {
 
         arg_image_class = invoked_as(argv, "systemd-confext") ? IMAGE_CONFEXT : IMAGE_SYSEXT;
 
-        /* Parse environment variables first */
-        r = parse_env();
-        if (r < 0)
-                return r;
-
         /* Parse command line */
         r = parse_argv(argc, argv, &args);
         if (r <= 0)
@@ -3209,13 +3295,6 @@ static int run(int argc, char *argv[]) {
                 return 0;
         }
 
-        /* Parse configuration file after argv because it needs --root=.
-         * The config entries will not overwrite values set already by
-         * env/argv because we track initialization. */
-        r = parse_config_file(arg_image_class);
-        if (r < 0)
-                log_warning_errno(r, "Failed to parse global config file, ignoring: %m");
-
         if (arg_varlink) {
                 _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL;
                 _cleanup_hashmap_free_ Hashmap *polkit_registry = NULL;
index 2fe6bf19d95a87e76198a77844b15688a570830b..9fc15d04da7647d89f313d595e3f9152974a7a9a 100755 (executable)
@@ -1082,6 +1082,55 @@ systemd-confext status
 systemd-confext unmerge
 rm -rf /run/confexts/
 
+cleanup_sysupdate_notify_confext_mutable() {
+    local mutable_marker="$1"
+    local notify_socket_was_active="$2"
+
+    if [[ "$notify_socket_was_active" == "yes" ]]; then
+        systemctl restart systemd-sysupdate-notify-sysext.socket || :
+    else
+        systemctl stop systemd-sysupdate-notify-sysext.socket || :
+    fi
+
+    rm -f "$mutable_marker"
+    rm -f "/var/lib/extensions.mutable/etc/${mutable_marker#/etc/}"
+    systemd-confext unmerge || :
+    rm -f /run/systemd/confext.conf.d/50-test-mutable.conf
+    rm -rf /run/confexts/
+    rmdir /var/lib/extensions.mutable/etc /var/lib/extensions.mutable 2>/dev/null || :
+}
+
+test_sysupdate_notify_confext_mutable() {
+    local mutable_marker="/etc/test-confext-mutable-$RANDOM"
+    local notify_socket_was_active=no
+
+    if systemctl is-active --quiet systemd-sysupdate-notify-sysext.socket; then
+        notify_socket_was_active=yes
+    fi
+
+    trap 'cleanup_sysupdate_notify_confext_mutable "$mutable_marker" "$notify_socket_was_active"' RETURN ERR
+
+    # Check that the sysupdate notification refresh honors confext-specific configuration.
+    mkdir -p /run/systemd/confext.conf.d
+    cat >/run/systemd/confext.conf.d/50-test-mutable.conf <<EOF
+[ConfExt]
+Mutable=yes
+EOF
+    mkdir -p /run/confexts/test/etc/extension-release.d
+    echo "ID=_any" >/run/confexts/test/etc/extension-release.d/extension-release.test
+    echo "ARCHITECTURE=_any" >>/run/confexts/test/etc/extension-release.d/extension-release.test
+    echo "MARKER_CONFEXT_123" >/run/confexts/test/etc/testfile
+    systemd-confext merge
+    echo "MARKER_CONFEXT_MUTABLE" >"$mutable_marker"
+    systemctl start systemd-sysupdate-notify-sysext.socket
+    varlinkctl call /run/systemd/sysupdate/notify/io.systemd.sysext io.systemd.SysUpdate.Notify.OnCompletedUpdate '{}'
+    grep -F "MARKER_CONFEXT_MUTABLE" "$mutable_marker" >/dev/null
+    trap - RETURN ERR
+    cleanup_sysupdate_notify_confext_mutable "$mutable_marker" "$notify_socket_was_active"
+}
+
+test_sysupdate_notify_confext_mutable
+
 unsquashfs -force -no-xattrs -d /tmp/img "$MINIMAL_IMAGE.raw"
 systemd-run --unit=test-root-ephemeral \
     -p RootDirectory=/tmp/img \