summaryrefslogtreecommitdiffstats
path: root/src/sysext
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 03:50:40 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 03:50:40 +0000
commitfc53809803cd2bc2434e312b19a18fa36776da12 (patch)
treeb4b43bd6538f51965ce32856e9c053d0f90919c8 /src/sysext
parentAdding upstream version 255.5. (diff)
downloadsystemd-fc53809803cd2bc2434e312b19a18fa36776da12.tar.xz
systemd-fc53809803cd2bc2434e312b19a18fa36776da12.zip
Adding upstream version 256.upstream/256
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/sysext')
-rw-r--r--src/sysext/sysext.c1007
1 files changed, 921 insertions, 86 deletions
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index 8dc515e..c55c24f 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -39,15 +39,41 @@
#include "pager.h"
#include "parse-argument.h"
#include "parse-util.h"
+#include "path-util.h"
#include "pretty-print.h"
#include "process-util.h"
+#include "rm-rf.h"
#include "sort-util.h"
+#include "string-table.h"
+#include "string-util.h"
#include "terminal-util.h"
#include "user-util.h"
#include "varlink.h"
#include "varlink-io.systemd.sysext.h"
#include "verbs.h"
+typedef enum MutableMode {
+ MUTABLE_NO,
+ MUTABLE_YES,
+ MUTABLE_AUTO,
+ MUTABLE_IMPORT,
+ MUTABLE_EPHEMERAL,
+ MUTABLE_EPHEMERAL_IMPORT,
+ _MUTABLE_MAX,
+ _MUTABLE_INVALID = -EINVAL,
+} MutableMode;
+
+static const char* const mutable_mode_table[_MUTABLE_MAX] = {
+ [MUTABLE_NO] = "no",
+ [MUTABLE_YES] = "yes",
+ [MUTABLE_AUTO] = "auto",
+ [MUTABLE_IMPORT] = "import",
+ [MUTABLE_EPHEMERAL] = "ephemeral",
+ [MUTABLE_EPHEMERAL_IMPORT] = "ephemeral-import",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN(mutable_mode, MutableMode, MUTABLE_YES);
+
static char **arg_hierarchies = NULL; /* "/usr" + "/opt" by default for sysext and /etc by default for confext */
static char *arg_root = NULL;
static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
@@ -58,50 +84,64 @@ static bool arg_no_reload = false;
static int arg_noexec = -1;
static ImagePolicy *arg_image_policy = NULL;
static bool arg_varlink = false;
+static MutableMode arg_mutable = MUTABLE_NO;
/* Is set to IMAGE_CONFEXT when systemd is called with the confext functionality instead of the default */
static ImageClass arg_image_class = IMAGE_SYSEXT;
+#define MUTABLE_EXTENSIONS_BASE_DIR "/var/lib/extensions.mutable"
+
STATIC_DESTRUCTOR_REGISTER(arg_hierarchies, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep);
/* Helper struct for naming simplicity and reusability */
static const struct {
- const char *dot_directory_name;
- const char *directory_name;
+ const char *full_identifier;
const char *short_identifier;
const char *short_identifier_plural;
+ const char *blurb;
+ const char *dot_directory_name;
+ const char *directory_name;
const char *level_env;
const char *scope_env;
const char *name_env;
+ const char *mode_env;
const ImagePolicy *default_image_policy;
unsigned long default_mount_flags;
} image_class_info[_IMAGE_CLASS_MAX] = {
[IMAGE_SYSEXT] = {
- .dot_directory_name = ".systemd-sysext",
- .directory_name = "systemd-sysext",
+ .full_identifier = "systemd-sysext",
.short_identifier = "sysext",
.short_identifier_plural = "extensions",
+ .blurb = "Merge system extension images into /usr/ and /opt/.",
+ .dot_directory_name = ".systemd-sysext",
.level_env = "SYSEXT_LEVEL",
.scope_env = "SYSEXT_SCOPE",
.name_env = "SYSTEMD_SYSEXT_HIERARCHIES",
+ .mode_env = "SYSTEMD_SYSEXT_MUTABLE_MODE",
.default_image_policy = &image_policy_sysext,
.default_mount_flags = MS_RDONLY|MS_NODEV,
},
[IMAGE_CONFEXT] = {
- .dot_directory_name = ".systemd-confext",
- .directory_name = "systemd-confext",
+ .full_identifier = "systemd-confext",
.short_identifier = "confext",
.short_identifier_plural = "confexts",
+ .blurb = "Merge configuration extension images into /etc/.",
+ .dot_directory_name = ".systemd-confext",
.level_env = "CONFEXT_LEVEL",
.scope_env = "CONFEXT_SCOPE",
.name_env = "SYSTEMD_CONFEXT_HIERARCHIES",
+ .mode_env = "SYSTEMD_CONFEXT_MUTABLE_MODE",
.default_image_policy = &image_policy_confext,
.default_mount_flags = MS_RDONLY|MS_NODEV|MS_NOSUID|MS_NOEXEC,
}
};
+static int parse_mutable_mode(const char *p) {
+ return mutable_mode_from_string(p);
+}
+
static int is_our_mount_point(
ImageClass image_class,
const char *p) {
@@ -113,7 +153,7 @@ static int is_our_mount_point(
assert(p);
- r = path_is_mount_point(p, NULL, 0);
+ r = path_is_mount_point(p);
if (r == -ENOENT) {
log_debug_errno(r, "Hierarchy '%s' doesn't exist.", p);
return false;
@@ -150,7 +190,7 @@ static int is_our_mount_point(
return log_error_errno(r, "Failed to parse device major/minor stored in '%s/dev' file on '%s': %m", image_class_info[image_class].dot_directory_name, p);
if (lstat(p, &st) < 0)
- return log_error_errno(r, "Failed to stat %s: %m", p);
+ return log_error_errno(errno, "Failed to stat %s: %m", p);
if (st.st_dev != dev) {
log_debug("Hierarchy '%s' reports a different device major/minor than what we are seeing, assuming offline copy.", p);
@@ -248,11 +288,22 @@ static int unmerge_hierarchy(
ImageClass image_class,
const char *p) {
+ _cleanup_free_ char *dot_dir = NULL, *work_dir_info_file = NULL;
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. */
@@ -262,9 +313,40 @@ static int unmerge_hierarchy(
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);
+ }
+
r = umount_verbose(LOG_ERR, p, MNT_DETACH|UMOUNT_NOFOLLOW);
if (r < 0)
- return log_error_errno(r, "Failed to unmount file system '%s': %m", p);
+ 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);
}
@@ -342,7 +424,7 @@ static int parse_image_class_parameter(Varlink *link, const char *value, ImageCl
c = image_class_from_string(value);
if (!IN_SET(c, IMAGE_SYSEXT, IMAGE_CONFEXT))
- return varlink_errorb(link, VARLINK_ERROR_INVALID_PARAMETER, JSON_BUILD_OBJECT(JSON_BUILD_PAIR_STRING("parameter", "class")));
+ return varlink_error_invalid_parameter_name(link, "class");
if (hierarchies) {
r = parse_env_extension_hierarchies(&h, image_class_info[c].name_env);
@@ -448,7 +530,7 @@ static int verb_status(int argc, char **argv, void *userdata) {
return log_oom();
if (stat(*p, &st) < 0)
- return log_error_errno(r, "Failed to stat() '%s': %m", *p);
+ return log_error_errno(errno, "Failed to stat() '%s': %m", *p);
r = table_add_many(
t,
@@ -474,11 +556,38 @@ static int verb_status(int argc, char **argv, void *userdata) {
return ret;
}
+static int append_overlayfs_path_option(
+ char **options,
+ const char *separator,
+ const char *option,
+ const char *path) {
+
+ _cleanup_free_ char *escaped = NULL;
+
+ assert(options);
+ assert(separator);
+ assert(path);
+
+ escaped = shell_escape(path, ",:");
+ if (!escaped)
+ return log_oom();
+
+ if (option) {
+ if (!strextend(options, separator, option, "=", escaped))
+ return log_oom();
+ } else if (!strextend(options, separator, escaped))
+ return log_oom();
+
+ return 0;
+}
+
static int mount_overlayfs(
ImageClass image_class,
int noexec,
const char *where,
- char **layers) {
+ char **layers,
+ const char *upper_dir,
+ const char *work_dir) {
_cleanup_free_ char *options = NULL;
bool separator = false;
@@ -486,20 +595,16 @@ static int mount_overlayfs(
int r;
assert(where);
+ assert((upper_dir && work_dir) || (!upper_dir && !work_dir));
options = strdup("lowerdir=");
if (!options)
return log_oom();
STRV_FOREACH(l, layers) {
- _cleanup_free_ char *escaped = NULL;
-
- escaped = shell_escape(*l, ",:");
- if (!escaped)
- return log_oom();
-
- if (!strextend(&options, separator ? ":" : "", escaped))
- return log_oom();
+ r = append_overlayfs_path_option(&options, separator ? ":" : "", NULL, *l);
+ if (r < 0)
+ return r;
separator = true;
}
@@ -508,6 +613,22 @@ static int mount_overlayfs(
if (noexec >= 0)
SET_FLAG(flags, MS_NOEXEC, noexec);
+ if (upper_dir && work_dir) {
+ r = append_overlayfs_path_option(&options, ",", "upperdir", upper_dir);
+ if (r < 0)
+ return r;
+
+ flags &= ~MS_RDONLY;
+
+ r = append_overlayfs_path_option(&options, ",", "workdir", work_dir);
+ if (r < 0)
+ return r;
+ /* redirect_dir=on and noatime prevent unnecessary upcopies, metacopy=off prevents broken
+ * files from partial upcopies after umount. */
+ if (!strextend(&options, ",redirect_dir=on,noatime,metacopy=off"))
+ return log_oom();
+ }
+
/* Now mount the actual overlayfs */
r = mount_nofollow_verbose(LOG_ERR, image_class_info[image_class].short_identifier, where, "overlay", flags, options);
if (r < 0)
@@ -516,62 +637,276 @@ static int mount_overlayfs(
return 0;
}
-static int merge_hierarchy(
- ImageClass image_class,
+static char *hierarchy_as_single_path_component(const char *hierarchy) {
+ /* We normally expect hierarchy to be /usr, /opt or /etc, but for debugging purposes the hierarchy
+ * could very well be like /foo/bar/baz/. So for a given hierarchy we generate a directory name by
+ * stripping the leading and trailing separators and replacing the rest of separators with dots. This
+ * makes the generated name to be the same for /foo/bar/baz and for /foo/bar.baz, but, again,
+ * specifying a different hierarchy is a debugging feature, so non-unique mapping should not be an
+ * issue in general case. */
+ const char *stripped = hierarchy;
+ _cleanup_free_ char *dir_name = NULL;
+
+ assert(hierarchy);
+
+ stripped += strspn(stripped, "/");
+
+ dir_name = strdup(stripped);
+ if (!dir_name)
+ return NULL;
+ delete_trailing_chars(dir_name, "/");
+ string_replace_char(dir_name, '/', '.');
+ return TAKE_PTR(dir_name);
+}
+
+static int paths_on_same_fs(const char *path1, const char *path2) {
+ struct stat st1, st2;
+
+ assert(path1);
+ assert(path2);
+
+ if (stat(path1, &st1) < 0)
+ return log_error_errno(errno, "Failed to stat '%s': %m", path1);
+
+ if (stat(path2, &st2) < 0)
+ return log_error_errno(errno, "Failed to stat '%s': %m", path2);
+
+ return st1.st_dev == st2.st_dev;
+}
+
+static int work_dir_for_hierarchy(
const char *hierarchy,
- int noexec,
- char **extensions,
- char **paths,
- const char *meta_path,
- const char *overlay_path) {
+ const char *resolved_upper_dir,
+ char **ret_work_dir) {
+
+ _cleanup_free_ char *parent = NULL;
+ int r;
+
+ assert(hierarchy);
+ assert(resolved_upper_dir);
+ assert(ret_work_dir);
+
+ r = path_extract_directory(resolved_upper_dir, &parent);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get parent directory of upperdir '%s': %m", resolved_upper_dir);
+
+ /* TODO: paths_in_same_superblock? partition? device? */
+ r = paths_on_same_fs(resolved_upper_dir, parent);
+ if (r < 0)
+ return r;
+ if (!r)
+ return log_error_errno(SYNTHETIC_ERRNO(EXDEV), "Unable to find a suitable workdir location for upperdir '%s' for host hierarchy '%s' - parent directory of the upperdir is in a different filesystem", resolved_upper_dir, hierarchy);
+
+ _cleanup_free_ char *f = NULL, *dir_name = NULL;
+
+ f = hierarchy_as_single_path_component(hierarchy);
+ if (!f)
+ return log_oom();
+ dir_name = strjoin(".systemd-", f, "-workdir");
+ if (!dir_name)
+ return log_oom();
+
+ free(f);
+ f = path_join(parent, dir_name);
+ if (!f)
+ return log_oom();
+
+ *ret_work_dir = TAKE_PTR(f);
+ return 0;
+}
+
+typedef struct OverlayFSPaths {
+ char *hierarchy;
+ mode_t hierarchy_mode;
+ char *resolved_hierarchy;
+ char *resolved_mutable_directory;
+
+ /* NULL if merged fs is read-only */
+ char *upper_dir;
+ /* NULL if merged fs is read-only */
+ char *work_dir;
+ /* lowest index is top lowerdir, highest index is bottom lowerdir */
+ char **lower_dirs;
+} OverlayFSPaths;
+
+static OverlayFSPaths *overlayfs_paths_free(OverlayFSPaths *op) {
+ if (!op)
+ return NULL;
+
+ free(op->hierarchy);
+ free(op->resolved_hierarchy);
+ free(op->resolved_mutable_directory);
+
+ free(op->upper_dir);
+ free(op->work_dir);
+ strv_free(op->lower_dirs);
+
+ return mfree(op);
+}
+DEFINE_TRIVIAL_CLEANUP_FUNC(OverlayFSPaths *, overlayfs_paths_free);
+
+static int resolve_hierarchy(const char *hierarchy, char **ret_resolved_hierarchy) {
+ _cleanup_free_ char *resolved_path = NULL;
+ int r;
+
+ assert(hierarchy);
+ assert(ret_resolved_hierarchy);
+
+ r = chase(hierarchy, arg_root, CHASE_PREFIX_ROOT, &resolved_path, NULL);
+ if (r < 0 && r != -ENOENT)
+ return log_error_errno(r, "Failed to resolve hierarchy '%s': %m", hierarchy);
+
+ *ret_resolved_hierarchy = TAKE_PTR(resolved_path);
+ return 0;
+}
+
+static int mutable_directory_mode_matches_hierarchy(
+ const char *root_or_null,
+ const char *path,
+ mode_t hierarchy_mode) {
- _cleanup_free_ char *resolved_hierarchy = NULL, *f = NULL, *buf = NULL;
- _cleanup_strv_free_ char **layers = NULL;
+ _cleanup_free_ char *path_in_root = NULL;
struct stat st;
+ mode_t actual_mode;
+
+ assert(path);
+
+ path_in_root = path_join(root_or_null, path);
+ if (!path_in_root)
+ return log_oom();
+
+ if (stat(path_in_root, &st) < 0) {
+ if (errno == ENOENT)
+ return 0;
+ return log_error_errno(errno, "Failed to stat mutable directory '%s': %m", path_in_root);
+ }
+
+ actual_mode = st.st_mode & 0777;
+ if (actual_mode != hierarchy_mode)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mutable directory '%s' has mode %04o, ought to have mode %04o", path_in_root, actual_mode, hierarchy_mode);
+
+ return 0;
+}
+
+static int resolve_mutable_directory(
+ 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;
int r;
assert(hierarchy);
- assert(meta_path);
- assert(overlay_path);
+ assert(ret_resolved_mutable_directory);
- /* Resolve the path of the host's version of the hierarchy, i.e. what we want to use as lowest layer
- * in the overlayfs stack. */
- r = chase(hierarchy, arg_root, CHASE_PREFIX_ROOT, &resolved_hierarchy, NULL);
- if (r == -ENOENT)
- log_debug_errno(r, "Hierarchy '%s' on host doesn't exist, not merging.", hierarchy);
- else if (r < 0)
- return log_error_errno(r, "Failed to resolve host hierarchy '%s': %m", hierarchy);
- else {
- r = dir_is_empty(resolved_hierarchy, /* ignore_hidden_or_backup= */ false);
- if (r < 0)
- return log_error_errno(r, "Failed to check if host hierarchy '%s' is empty: %m", resolved_hierarchy);
- if (r > 0) {
- log_debug("Host hierarchy '%s' is empty, not merging.", resolved_hierarchy);
- resolved_hierarchy = mfree(resolved_hierarchy);
- }
+ if (arg_mutable == MUTABLE_NO) {
+ log_debug("Mutability for hierarchy '%s' is disabled, not resolving mutable directory.", hierarchy);
+ *ret_resolved_mutable_directory = NULL;
+ return 0;
}
- /* Let's generate a metadata file that lists all extensions we took into account for this
- * hierarchy. We include this in the final fs, to make things nicely discoverable and
- * recognizable. */
- f = path_join(meta_path, image_class_info[image_class].dot_directory_name, image_class_info[image_class].short_identifier_plural);
- if (!f)
+ if (IN_SET(arg_mutable, MUTABLE_EPHEMERAL, MUTABLE_EPHEMERAL_IMPORT)) {
+ /* We create mutable directory inside the temporary tmpfs workspace, which is a fixed
+ * location that ignores arg_root. */
+ root = NULL;
+ base = workspace;
+ }
+
+ dir_name = hierarchy_as_single_path_component(hierarchy);
+ if (!dir_name)
return log_oom();
- buf = strv_join(extensions, "\n");
- if (!buf)
+ path = path_join(base, dir_name);
+ if (!path)
return log_oom();
- r = write_string_file(f, buf, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755);
+ if (IN_SET(arg_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. */
+ r = mutable_directory_mode_matches_hierarchy(root, path, hierarchy_mode);
+ if (r < 0)
+ return r;
+ }
+
+ if (IN_SET(arg_mutable, MUTABLE_YES, MUTABLE_EPHEMERAL, MUTABLE_EPHEMERAL_IMPORT)) {
+ _cleanup_free_ char *path_in_root = NULL;
+
+ path_in_root = path_join(root, path);
+ if (!path_in_root)
+ return log_oom();
+
+ r = mkdir_p(path_in_root, 0700);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create a directory '%s': %m", path_in_root);
+ }
+
+ r = chase(path, root, CHASE_PREFIX_ROOT, &resolved_path, NULL);
+ if (r < 0 && r != -ENOENT)
+ return log_error_errno(r, "Failed to resolve mutable directory '%s': %m", path);
+
+ *ret_resolved_mutable_directory = TAKE_PTR(resolved_path);
+ return 0;
+}
+
+static int overlayfs_paths_new(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);
+
+ hierarchy_copy = strdup(hierarchy);
+ if (!hierarchy_copy)
+ return log_oom();
+
+ r = resolve_hierarchy(hierarchy, &resolved_hierarchy);
if (r < 0)
- return log_error_errno(r, "Failed to write extension meta file '%s': %m", f);
+ return r;
- /* Put the meta path (i.e. our synthesized stuff) at the top of the layer stack */
- layers = strv_new(meta_path);
- if (!layers)
+ if (resolved_hierarchy) {
+ struct stat st;
+
+ if (stat(resolved_hierarchy, &st) < 0)
+ return log_error_errno(errno, "Failed to stat '%s': %m", resolved_hierarchy);
+ hierarchy_mode = st.st_mode & 0777;
+ } else
+ hierarchy_mode = 0755;
+
+ r = resolve_mutable_directory(hierarchy, hierarchy_mode, workspace_path, &resolved_mutable_directory);
+ if (r < 0)
+ return r;
+
+ OverlayFSPaths *op;
+ op = new(OverlayFSPaths, 1);
+ if (!op)
return log_oom();
- /* Put the extensions in the middle */
+ *op = (OverlayFSPaths) {
+ .hierarchy = TAKE_PTR(hierarchy_copy),
+ .hierarchy_mode = hierarchy_mode,
+ .resolved_hierarchy = TAKE_PTR(resolved_hierarchy),
+ .resolved_mutable_directory = TAKE_PTR(resolved_mutable_directory),
+ };
+
+ *ret_op = TAKE_PTR(op);
+ return 0;
+}
+
+static int determine_used_extensions(const char *hierarchy, char **paths, char ***ret_used_paths, size_t *ret_extensions_used) {
+ _cleanup_strv_free_ char **used_paths = NULL;
+ size_t n = 0;
+ int r;
+
+ assert(hierarchy);
+ assert(paths);
+ assert(ret_used_paths);
+ assert(ret_extensions_used);
+
STRV_FOREACH(p, paths) {
_cleanup_free_ char *resolved = NULL;
@@ -591,54 +926,514 @@ static int merge_hierarchy(
continue;
}
- r = strv_consume(&layers, TAKE_PTR(resolved));
+ r = strv_consume_with_size (&used_paths, &n, TAKE_PTR(resolved));
if (r < 0)
return log_oom();
}
- if (!layers[1]) /* No extension with files in this hierarchy? Then don't do anything. */
+ *ret_used_paths = TAKE_PTR(used_paths);
+ *ret_extensions_used = n;
+ return 0;
+}
+
+static int maybe_import_mutable_directory(OverlayFSPaths *op) {
+ int r;
+
+ 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)
return 0;
- if (resolved_hierarchy) {
- /* Add the host hierarchy as last (lowest) layer in the stack */
- r = strv_consume(&layers, TAKE_PTR(resolved_hierarchy));
+ r = path_equal_or_inode_same_full(op->resolved_hierarchy, op->resolved_mutable_directory, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check equality of hierarchy %s and its mutable directory %s: %m", op->resolved_hierarchy, op->resolved_mutable_directory);
+ if (r > 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ELOOP), "Not importing mutable directory for hierarchy %s as a lower dir, because it points to the hierarchy itself", op->hierarchy);
+
+ r = strv_extend(&op->lower_dirs, op->resolved_mutable_directory);
+ if (r < 0)
+ return log_oom();
+
+ return 0;
+}
+
+static int maybe_import_ignored_mutable_directory(OverlayFSPaths *op) {
+ _cleanup_free_ char *dir_name = NULL, *path = NULL, *resolved_path = NULL;
+ int r;
+
+ 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)
+ return 0;
+
+ dir_name = hierarchy_as_single_path_component(op->hierarchy);
+ if (!dir_name)
+ return log_oom();
+
+ path = path_join(MUTABLE_EXTENSIONS_BASE_DIR, dir_name);
+ if (!path)
+ return log_oom();
+
+ r = chase(path, arg_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;
+ }
+ if (r < 0)
+ return log_error_errno(r, "Failed to resolve mutable directory '%s': %m", path);
+
+ r = path_equal_or_inode_same_full(op->resolved_hierarchy, resolved_path, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check equality of hierarchy %s and its mutable directory %s: %m", op->resolved_hierarchy, op->resolved_mutable_directory);
+
+ if (r > 0)
+ return log_error_errno(SYNTHETIC_ERRNO(ELOOP), "Not importing mutable directory for hierarchy %s as a lower dir, because it points to the hierarchy itself", op->hierarchy);
+
+ r = strv_consume(&op->lower_dirs, TAKE_PTR(resolved_path));
+ if (r < 0)
+ return log_oom();
+
+ return 0;
+}
+
+static int determine_top_lower_dirs(OverlayFSPaths *op, const char *meta_path) {
+ int r;
+
+ assert(op);
+ assert(meta_path);
+
+ /* Put the meta path (i.e. our synthesized stuff) at the top of the layer stack */
+ r = strv_extend(&op->lower_dirs, meta_path);
+ if (r < 0)
+ return log_oom();
+
+ r = maybe_import_mutable_directory(op);
+ if (r < 0)
+ return r;
+
+ r = maybe_import_ignored_mutable_directory(op);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int determine_middle_lower_dirs(OverlayFSPaths *op, char **paths) {
+ int r;
+
+ assert(op);
+ assert(paths);
+
+ /* The paths were already determined in determine_used_extensions, so we just take them as is. */
+ r = strv_extend_strv(&op->lower_dirs, paths, false);
+ if (r < 0)
+ return log_oom ();
+
+ return 0;
+}
+
+static int hierarchy_as_lower_dir(OverlayFSPaths *op) {
+ int r;
+
+ /* return 0 if hierarchy should be used as lower dir, >0, if not */
+
+ assert(op);
+
+ if (!op->resolved_hierarchy) {
+ log_debug("Host hierarchy '%s' does not exist, will not be used as lowerdir", op->hierarchy);
+ return 1;
+ }
+
+ r = dir_is_empty(op->resolved_hierarchy, /* ignore_hidden_or_backup= */ false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check if host hierarchy '%s' is empty: %m", op->resolved_hierarchy);
+ if (r > 0) {
+ log_debug("Host hierarchy '%s' is empty, will not be used as lower dir.", op->resolved_hierarchy);
+ return 1;
+ }
+
+ if (arg_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) {
+ log_debug("Mutability for host hierarchy '%s' is ephemeral, so host hierarchy will be a lowerdir", op->resolved_hierarchy);
+ return 0;
+ }
+
+ if (!op->resolved_mutable_directory) {
+ log_debug("No mutable directory found, so host hierarchy '%s' will be used as lowerdir", op->resolved_hierarchy);
+ return 0;
+ }
+
+ r = path_equal_or_inode_same_full(op->resolved_hierarchy, op->resolved_mutable_directory, 0);
+ if (r < 0)
+ return log_error_errno(r, "Failed to check equality of hierarchy %s and its mutable directory %s: %m", op->resolved_hierarchy, op->resolved_mutable_directory);
+ if (r > 0) {
+ log_debug("Host hierarchy '%s' will serve as upperdir.", op->resolved_hierarchy);
+ return 1;
+ }
+
+ return 0;
+}
+
+static int determine_bottom_lower_dirs(OverlayFSPaths *op) {
+ int r;
+
+ assert(op);
+
+ r = hierarchy_as_lower_dir(op);
+ if (r < 0)
+ return r;
+ if (!r) {
+ r = strv_extend(&op->lower_dirs, op->resolved_hierarchy);
if (r < 0)
return log_oom();
}
+ return 0;
+}
+
+static int determine_lower_dirs(
+ OverlayFSPaths *op,
+ char **paths,
+ const char *meta_path) {
+
+ int r;
+
+ assert(op);
+ assert(paths);
+ assert(meta_path);
+
+ r = determine_top_lower_dirs(op, meta_path);
+ if (r < 0)
+ return r;
+
+ r = determine_middle_lower_dirs(op, paths);
+ if (r < 0)
+ return r;
+
+ r = determine_bottom_lower_dirs(op);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int determine_upper_dir(OverlayFSPaths *op) {
+ int r;
+
+ assert(op);
+ assert(!op->upper_dir);
+
+ if (arg_mutable == MUTABLE_IMPORT) {
+ log_debug("Mutability is disabled, there will be no upperdir for host hierarchy '%s'", op->hierarchy);
+ return 0;
+ }
+
+ if (!op->resolved_mutable_directory) {
+ log_debug("No mutable directory found for host hierarchy '%s', there will be no upperdir", op->hierarchy);
+ return 0;
+ }
+
+ /* Require upper dir to be on writable filesystem if it's going to be used as an actual overlayfs
+ * upperdir, instead of a lowerdir as an imported path. */
+ r = path_is_read_only_fs(op->resolved_mutable_directory);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine if mutable directory '%s' is on read-only filesystem: %m", op->resolved_mutable_directory);
+ if (r > 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EROFS), "Can't use '%s' as an upperdir as it is read-only.", op->resolved_mutable_directory);
+
+ op->upper_dir = strdup(op->resolved_mutable_directory);
+ if (!op->upper_dir)
+ return log_oom();
+
+ return 0;
+}
+
+static int determine_work_dir(OverlayFSPaths *op) {
+ _cleanup_free_ char *work_dir = NULL;
+ int r;
+
+ assert(op);
+ assert(!op->work_dir);
+
+ if (!op->upper_dir)
+ return 0;
+
+ if (arg_mutable == MUTABLE_IMPORT)
+ return 0;
+
+ r = work_dir_for_hierarchy(op->hierarchy, op->upper_dir, &work_dir);
+ if (r < 0)
+ return r;
+
+ op->work_dir = TAKE_PTR(work_dir);
+ return 0;
+}
+
+static int mount_overlayfs_with_op(
+ OverlayFSPaths *op,
+ ImageClass image_class,
+ int noexec,
+ const char *overlay_path,
+ const char *meta_path) {
+
+ int r;
+ const char *top_layer = NULL;
+
+ assert(op);
+ assert(overlay_path);
+
r = mkdir_p(overlay_path, 0700);
if (r < 0)
return log_error_errno(r, "Failed to make directory '%s': %m", overlay_path);
- r = mount_overlayfs(image_class, noexec, overlay_path, layers);
+ r = mkdir_p(meta_path, 0700);
+ if (r < 0)
+ return log_error_errno(r, "Failed to make directory '%s': %m", meta_path);
+
+ if (op->upper_dir && op->work_dir) {
+ r = mkdir_p(op->work_dir, 0700);
+ if (r < 0)
+ return log_error_errno(r, "Failed to make directory '%s': %m", op->work_dir);
+ top_layer = op->upper_dir;
+ } else {
+ assert(!strv_isempty(op->lower_dirs));
+ top_layer = op->lower_dirs[0];
+ }
+
+ /* Overlayfs merged directory has the same mode as the top layer (either first lowerdir in options in
+ * read-only case, or upperdir for mutable case. Set up top overlayfs layer to the same mode as the
+ * unmerged hierarchy, otherwise we might end up with merged hierarchy owned by root and with mode
+ * being 0700. */
+ if (chmod(top_layer, op->hierarchy_mode) < 0)
+ return log_error_errno(errno, "Failed to set permissions of '%s' to %04o: %m", top_layer, op->hierarchy_mode);
+
+ r = mount_overlayfs(image_class, noexec, overlay_path, op->lower_dirs, op->upper_dir, op->work_dir);
if (r < 0)
return r;
- /* The overlayfs superblock is read-only. Let's also mark the bind mount read-only. Extra turbo safety 😎 */
- r = bind_remount_recursive(overlay_path, MS_RDONLY, MS_RDONLY, NULL);
+ return 0;
+}
+
+static int write_extensions_file(ImageClass image_class, char **extensions, const char *meta_path) {
+ _cleanup_free_ char *f = NULL, *buf = NULL;
+ int r;
+
+ assert(extensions);
+ assert(meta_path);
+
+ /* Let's generate a metadata file that lists all extensions we took into account for this
+ * hierarchy. We include this in the final fs, to make things nicely discoverable and
+ * recognizable. */
+ f = path_join(meta_path, image_class_info[image_class].dot_directory_name, image_class_info[image_class].short_identifier_plural);
+ if (!f)
+ return log_oom();
+
+ buf = strv_join(extensions, "\n");
+ if (!buf)
+ return log_oom();
+
+ r = write_string_file(f, buf, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755);
if (r < 0)
- return log_error_errno(r, "Failed to make bind mount '%s' read-only: %m", overlay_path);
+ return log_error_errno(r, "Failed to write extension meta file '%s': %m", f);
+
+ return 0;
+}
+
+static int write_dev_file(ImageClass image_class, const char *meta_path, const char *overlay_path) {
+ _cleanup_free_ char *f = NULL;
+ struct stat st;
+ int r;
+
+ assert(meta_path);
+ assert(overlay_path);
/* Now we have mounted the new file system. Let's now figure out its .st_dev field, and make that
* available in the metadata directory. This is useful to detect whether the metadata dir actually
* belongs to the fs it is found on: if .st_dev of the top-level mount matches it, it's pretty likely
* we are looking at a live tree, and not an unpacked tar or so of one. */
if (stat(overlay_path, &st) < 0)
- return log_error_errno(r, "Failed to stat mount '%s': %m", overlay_path);
+ return log_error_errno(errno, "Failed to stat mount '%s': %m", overlay_path);
- free(f);
f = path_join(meta_path, image_class_info[image_class].dot_directory_name, "dev");
if (!f)
return log_oom();
+ /* Modifying the underlying layers while the overlayfs is mounted is technically undefined, but at
+ * least it won't crash or deadlock, as per the kernel docs about overlayfs:
+ * https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html#changes-to-underlying-filesystems */
r = write_string_file(f, FORMAT_DEVNUM(st.st_dev), WRITE_STRING_FILE_CREATE);
if (r < 0)
return log_error_errno(r, "Failed to write '%s': %m", f);
+ return 0;
+}
+
+static int write_work_dir_file(ImageClass image_class, const char *meta_path, const char *work_dir) {
+ _cleanup_free_ char *escaped_work_dir_in_root = NULL, *f = NULL;
+ char *work_dir_in_root = NULL;
+ int r;
+
+ 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))
+ return 0;
+
+ work_dir_in_root = path_startswith(work_dir, empty_to_root(arg_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));
+
+ f = path_join(meta_path, image_class_info[image_class].dot_directory_name, "work_dir");
+ if (!f)
+ return log_oom();
+
+ /* Paths can have newlines for whatever reason, so better escape them to really get a single
+ * line file. */
+ escaped_work_dir_in_root = cescape(work_dir_in_root);
+ if (!escaped_work_dir_in_root)
+ return log_oom();
+ r = write_string_file(f, escaped_work_dir_in_root, WRITE_STRING_FILE_CREATE);
+ if (r < 0)
+ return log_error_errno(r, "Failed to write '%s': %m", f);
+
+ return 0;
+}
+
+static int store_info_in_meta(
+ ImageClass image_class,
+ char **extensions,
+ const char *meta_path,
+ const char *overlay_path,
+ const char *work_dir) {
+
+ int r;
+
+ assert(extensions);
+ assert(meta_path);
+ assert(overlay_path);
+ /* work_dir may be NULL */
+
+ r = write_extensions_file(image_class, extensions, meta_path);
+ if (r < 0)
+ return r;
+
+ r = write_dev_file(image_class, meta_path, overlay_path);
+ if (r < 0)
+ return r;
+
+ r = write_work_dir_file(image_class, meta_path, work_dir);
+ if (r < 0)
+ return r;
+
/* Make sure the top-level dir has an mtime marking the point we established the merge */
if (utimensat(AT_FDCWD, meta_path, NULL, AT_SYMLINK_NOFOLLOW) < 0)
return log_error_errno(r, "Failed fix mtime of '%s': %m", meta_path);
+ return 0;
+}
+
+static int make_mounts_read_only(ImageClass image_class, const char *overlay_path, bool mutable) {
+ int r;
+
+ assert(overlay_path);
+
+ if (mutable) {
+ /* Bind mount the meta path as read-only on mutable overlays to avoid accidental
+ * modifications of the contents of meta directory, which could lead to systemd thinking that
+ * this hierarchy is not our mount. */
+ _cleanup_free_ char *f = NULL;
+
+ f = path_join(overlay_path, image_class_info[image_class].dot_directory_name);
+ if (!f)
+ return log_oom();
+
+ r = mount_nofollow_verbose(LOG_ERR, f, f, NULL, MS_BIND, NULL);
+ if (r < 0)
+ return r;
+
+ r = bind_remount_one(f, MS_RDONLY, MS_RDONLY);
+ if (r < 0)
+ return log_error_errno(r, "Failed to remount '%s' as read-only: %m", f);
+ } else {
+ /* The overlayfs superblock is read-only. Let's also mark the bind mount read-only. Extra
+ * turbo safety 😎 */
+ r = bind_remount_recursive(overlay_path, MS_RDONLY, MS_RDONLY, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to make bind mount '%s' read-only: %m", overlay_path);
+ }
+
+ return 0;
+}
+
+static int merge_hierarchy(
+ ImageClass image_class,
+ const char *hierarchy,
+ int noexec,
+ char **extensions,
+ char **paths,
+ const char *meta_path,
+ const char *overlay_path,
+ const char *workspace_path) {
+
+ _cleanup_(overlayfs_paths_freep) OverlayFSPaths *op = NULL;
+ _cleanup_strv_free_ char **used_paths = NULL;
+ size_t extensions_used = 0;
+ int r;
+
+ assert(hierarchy);
+ assert(extensions);
+ assert(paths);
+ assert(meta_path);
+ assert(overlay_path);
+ assert(workspace_path);
+
+ r = determine_used_extensions(hierarchy, paths, &used_paths, &extensions_used);
+ if (r < 0)
+ return r;
+
+ if (extensions_used == 0) /* No extension with files in this hierarchy? Then don't do anything. */
+ return 0;
+
+ r = overlayfs_paths_new(hierarchy, workspace_path, &op);
+ if (r < 0)
+ return r;
+
+ r = determine_lower_dirs(op, used_paths, meta_path);
+ if (r < 0)
+ return r;
+
+ r = determine_upper_dir(op);
+ if (r < 0)
+ return r;
+
+ r = determine_work_dir(op);
+ if (r < 0)
+ return r;
+
+ r = mount_overlayfs_with_op(op, image_class, noexec, overlay_path, meta_path);
+ if (r < 0)
+ return r;
+
+ r = store_info_in_meta(image_class, extensions, meta_path, overlay_path, op->work_dir);
+ if (r < 0)
+ return r;
+
+ r = make_mounts_read_only(image_class, overlay_path, op->upper_dir && op->work_dir);
+ if (r < 0)
+ return r;
+
return 1;
}
@@ -659,8 +1454,16 @@ static const ImagePolicy *pick_image_policy(const Image *img) {
* 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 already. */
- if (in_initrd() && path_startswith(img->path, "/.extra/sysext/"))
- return &image_policy_sysext_strict;
+ if (in_initrd()) {
+ if (path_startswith(img->path, "/.extra/sysext/"))
+ return &image_policy_sysext_strict;
+ if (path_startswith(img->path, "/.extra/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;
+ }
return image_class_info[img->class].default_image_policy;
}
@@ -761,7 +1564,8 @@ static int merge_subprocess(
DISSECT_IMAGE_MOUNT_ROOT_ONLY |
DISSECT_IMAGE_USR_NO_ROOT |
DISSECT_IMAGE_ADD_PARTITION_DEVICES |
- DISSECT_IMAGE_PIN_PARTITION_DEVICES;
+ DISSECT_IMAGE_PIN_PARTITION_DEVICES |
+ DISSECT_IMAGE_ALLOW_USERSPACE_VERITY;
r = verity_settings_load(&verity_settings, img->path, NULL, NULL);
if (r < 0)
@@ -854,7 +1658,7 @@ static int merge_subprocess(
if (r < 0)
return log_oom();
- n_extensions ++;
+ n_extensions++;
}
/* Nothing left? Then shortcut things */
@@ -908,7 +1712,7 @@ static int merge_subprocess(
/* Create overlayfs mounts for all hierarchies */
STRV_FOREACH(h, hierarchies) {
- _cleanup_free_ char *meta_path = NULL, *overlay_path = NULL;
+ _cleanup_free_ char *meta_path = NULL, *overlay_path = NULL, *merge_hierarchy_workspace = NULL;
meta_path = path_join(workspace, "meta", *h); /* The place where to store metadata about this instance */
if (!meta_path)
@@ -918,6 +1722,11 @@ static int merge_subprocess(
if (!overlay_path)
return log_oom();
+ /* Temporary directory for merge_hierarchy needs, like ephemeral directories. */
+ merge_hierarchy_workspace = path_join(workspace, "mh_workspace", *h);
+ if (!merge_hierarchy_workspace)
+ return log_oom();
+
r = merge_hierarchy(
image_class,
*h,
@@ -925,7 +1734,8 @@ static int merge_subprocess(
extensions,
paths,
meta_path,
- overlay_path);
+ overlay_path,
+ merge_hierarchy_workspace);
if (r < 0)
return r;
}
@@ -954,7 +1764,8 @@ static int merge_subprocess(
if (r < 0)
return log_error_errno(r, "Failed to create hierarchy mount point '%s': %m", resolved);
- r = mount_nofollow_verbose(LOG_ERR, p, resolved, NULL, MS_BIND, NULL);
+ /* Using MS_REC to potentially bring in our read-only bind mount of metadata. */
+ r = mount_nofollow_verbose(LOG_ERR, p, resolved, NULL, MS_BIND|MS_REC, NULL);
if (r < 0)
return r;
@@ -992,9 +1803,10 @@ static int merge(ImageClass image_class,
r = wait_for_terminate_and_check("(sd-merge)", pid, WAIT_LOG_ABNORMAL);
if (r < 0)
return r;
-
if (r == 123) /* exit code 123 means: didn't do anything */
return 0;
+ if (r > 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EPROTO), "Failed to merge hierarchies");
r = need_reload(image_class, hierarchies, no_reload);
if (r < 0)
@@ -1110,9 +1922,9 @@ static int parse_merge_parameters(Varlink *link, JsonVariant *parameters, Method
static const JsonDispatch dispatch_table[] = {
{ "class", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(MethodMergeParameters, class), 0 },
- { "force", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(MethodMergeParameters, force), 0 },
- { "noReload", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(MethodMergeParameters, no_reload), 0 },
- { "noexec", JSON_VARIANT_BOOLEAN, json_dispatch_boolean, offsetof(MethodMergeParameters, noexec), 0 },
+ { "force", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(MethodMergeParameters, force), 0 },
+ { "noReload", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(MethodMergeParameters, no_reload), 0 },
+ { "noexec", JSON_VARIANT_BOOLEAN, json_dispatch_tristate, offsetof(MethodMergeParameters, noexec), 0 },
{}
};
@@ -1207,7 +2019,7 @@ static int refresh(
* 4. If there was no overlayfs mount so far, and no extensions installed, we implement a NOP.
*/
- return 0;
+ return r;
}
static int verb_refresh(int argc, char **argv, void *userdata) {
@@ -1358,13 +2170,13 @@ static int verb_help(int argc, char **argv, void *userdata) {
_cleanup_free_ char *link = NULL;
int r;
- r = terminal_urlify_man("systemd-sysext", "8", &link);
+ r = terminal_urlify_man(image_class_info[arg_image_class].full_identifier, "8", &link);
if (r < 0)
return log_oom();
printf("%1$s [OPTIONS...] COMMAND\n"
- "\n%5$sMerge extension images into /usr/ and /opt/ hierarchies for\n"
- " sysext and into the /etc/ hierarchy for confext.%6$s\n"
+ "\n%5$s%7$s%6$s\n"
+ "\n%3$sCommands:%4$s\n"
" status Show current merge status (default)\n"
" merge Merge extensions into relevant hierarchies\n"
" unmerge Unmerge extensions from relevant hierarchies\n"
@@ -1373,6 +2185,8 @@ static int verb_help(int argc, char **argv, void *userdata) {
" -h --help Show this help\n"
" --version Show package version\n"
"\n%3$sOptions:%4$s\n"
+ " --mutable=yes|no|auto|import|ephemeral|ephemeral-import\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"
@@ -1389,7 +2203,8 @@ static int verb_help(int argc, char **argv, void *userdata) {
ansi_underline(),
ansi_normal(),
ansi_highlight(),
- ansi_normal());
+ ansi_normal(),
+ image_class_info[arg_image_class].blurb);
return 0;
}
@@ -1406,6 +2221,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_IMAGE_POLICY,
ARG_NOEXEC,
ARG_NO_RELOAD,
+ ARG_MUTABLE,
};
static const struct option options[] = {
@@ -1419,6 +2235,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "image-policy", required_argument, NULL, ARG_IMAGE_POLICY },
{ "noexec", required_argument, NULL, ARG_NOEXEC },
{ "no-reload", no_argument, NULL, ARG_NO_RELOAD },
+ { "mutable", required_argument, NULL, ARG_MUTABLE },
{}
};
@@ -1482,6 +2299,13 @@ static int parse_argv(int argc, char *argv[]) {
arg_no_reload = true;
break;
+ case ARG_MUTABLE:
+ r = parse_mutable_mode(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse argument to --mutable=: %s", optarg);
+ arg_mutable = r;
+ break;
+
case '?':
return -EINVAL;
@@ -1514,12 +2338,23 @@ static int sysext_main(int argc, char *argv[]) {
}
static int run(int argc, char *argv[]) {
+ const char *env_var;
int r;
log_setup();
arg_image_class = invoked_as(argv, "systemd-confext") ? IMAGE_CONFEXT : IMAGE_SYSEXT;
+ env_var = 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;
+ }
+
r = parse_argv(argc, argv);
if (r <= 0)
return r;