summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO11
-rw-r--r--docs/PORTABLE_SERVICES.md14
-rw-r--r--man/os-release.xml23
-rw-r--r--src/boot/bootctl.c6
-rw-r--r--src/core/namespace.c2
-rw-r--r--src/dissect/dissect.c111
-rw-r--r--src/portable/portable.c168
-rw-r--r--src/portable/portable.h2
-rw-r--r--src/portable/portabled-image-bus.c1
-rw-r--r--src/shared/dissect-image.c89
-rw-r--r--src/shared/dissect-image.h3
-rw-r--r--src/shared/extension-release.c23
-rw-r--r--src/shared/extension-release.h1
-rw-r--r--src/shared/mount-util.c2
-rw-r--r--src/shared/pretty-print.h23
-rw-r--r--src/sysext/sysext.c7
-rw-r--r--src/test/test-gpt.c5
-rw-r--r--src/test/test-pretty-print.c14
-rw-r--r--test/test-functions5
-rwxr-xr-xtest/units/testsuite-29.sh18
20 files changed, 429 insertions, 99 deletions
diff --git a/TODO b/TODO
index 7786166c35..3c6d76266c 100644
--- a/TODO
+++ b/TODO
@@ -94,6 +94,17 @@ Features:
magic into a string. Then use that to replace fstype_magic_to_name() in homed
sources, and similar code.
+* man: rework os-release(5), and clearly separate our extension-release.d/ and
+ initrd-release parts, i.e. list explicitly which fields are about what.
+
+* sysext: before applying a sysext, do a superficial validation run so that
+ things are not rearranged to wildy. I.e. protect against accidental fuckups,
+ such as masking out /usr/lib/ or so. We should probably refuse if existing
+ inodes are replaced by other types of inodes or so.
+
+* sysext: ensure one can build a sysext that can safely apply to *any* system
+ (because it contains only static go binaries in /opt/ or so)
+
* userdb: when synthesizing NSS records, pick "best" password from defined
passwords, not just the first. i.e. if there are multiple defined, prefer
unlocked over locked and prefer non-empty over empty.
diff --git a/docs/PORTABLE_SERVICES.md b/docs/PORTABLE_SERVICES.md
index df6eb9958e..dd9164126f 100644
--- a/docs/PORTABLE_SERVICES.md
+++ b/docs/PORTABLE_SERVICES.md
@@ -247,6 +247,20 @@ image. To facilitate 3 and 4 you also need to include a boot loader in the
image. As mentioned, `mkosi -b` takes care of all of that for you, but any
other image generator should work too.
+The
+[os-release(5)](https://www.freedesktop.org/software/systemd/man/os-release.html)
+file may optionally be extended with a `PORTABLE_PREFIXES=` field listing all
+supported portable service prefixes for the image (see above). This is useful
+for informational purposes (as it allows recognizing portable service images
+from their contents as such), but is also useful to protect the image from
+being used under a wrong name and prefix. This is particularly relevant if the
+images are cryptographically authenticated (via Verity or a similar mechanism)
+as this way the (not necessarily authenticated) image file name can be
+validated against the (authenticated) image contents. If the field is not
+specified the image will work fine, but is not necessarily recognizable as
+portable service image, and any set of units included in the image may be
+attached, there are no restrictions enforced.
+
## Extension Images
Portable services can be delivered as one or multiple images that extend the base
diff --git a/man/os-release.xml b/man/os-release.xml
index ef5ef8b2e1..1826a60d1a 100644
--- a/man/os-release.xml
+++ b/man/os-release.xml
@@ -407,6 +407,29 @@
<para>Examples: <literal>SYSEXT_LEVEL=2</literal>, <literal>SYSEXT_LEVEL=15.14</literal>.
</para></listitem>
</varlistentry>
+
+ <varlistentry>
+ <term><varname>SYSEXT_SCOPE=</varname></term>
+ <listitem><para>Takes a space-separated list of one or more of the strings
+ <literal>system</literal>, <literal>initrd</literal> and <literal>portable</literal>. This field is
+ only supported in <filename>extension-release.d/</filename> files and indicates what environments
+ the system extension is applicable to: i.e. to regular systems, to initial RAM filesystems
+ ("initrd") or to portable service images. If unspecified, <literal>SYSEXT_SCOPE=system
+ portable</literal> is implied, i.e. any system extension without this field is applicable to
+ regular systems and to portable service environments, but not to initrd
+ environments.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><varname>PORTABLE_PREFIXES=</varname></term>
+ <listitem><para>Takes a space-separated list of one or more valid prefix match strings for the
+ <ulink url="https://systemd.io/PORTABLE_SERVICES">Portable Services</ulink> logic. This field
+ serves two purposes: it's informational, identifying portable service images as such (and thus
+ allowing them to be distinguished from other OS images, such as bootable system images); whenever a
+ portable service image is attached the specified or implied portable service prefix is checked
+ against this list, to enforce restrictions how images may be attached to a
+ system.</para></listitem>
+ </varlistentry>
</variablelist>
</refsect2>
diff --git a/src/boot/bootctl.c b/src/boot/bootctl.c
index c19a51de62..07ad949a0c 100644
--- a/src/boot/bootctl.c
+++ b/src/boot/bootctl.c
@@ -1240,11 +1240,9 @@ static void read_efi_var(const char *variable, char **ret) {
}
static void print_yes_no_line(bool first, bool good, const char *name) {
- printf("%s%s%s%s %s\n",
+ printf("%s%s %s\n",
first ? " Features: " : " ",
- ansi_highlight_green_red(good),
- special_glyph_check_mark(good),
- ansi_normal(),
+ COLOR_MARK_BOOL(good),
name);
}
diff --git a/src/core/namespace.c b/src/core/namespace.c
index c8e7e65e27..9393a202c4 100644
--- a/src/core/namespace.c
+++ b/src/core/namespace.c
@@ -1149,7 +1149,7 @@ static int mount_image(const MountEntry *m, const char *root_directory) {
r = verity_dissect_and_mount(
mount_entry_source(m), mount_entry_path(m), m->image_options,
- host_os_release_id, host_os_release_version_id, host_os_release_sysext_level);
+ host_os_release_id, host_os_release_version_id, host_os_release_sysext_level, NULL);
if (r == -ENOENT && m->ignore)
return 0;
if (r == -ESTALE && host_os_release_id)
diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c
index 7b4769d1b0..753d4aefd7 100644
--- a/src/dissect/dissect.c
+++ b/src/dissect/dissect.c
@@ -11,6 +11,7 @@
#include "chase-symlinks.h"
#include "copy.h"
#include "dissect-image.h"
+#include "env-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "format-table.h"
@@ -369,6 +370,46 @@ static int strv_pair_to_json(char **l, JsonVariant **ret) {
return json_variant_new_array_strv(ret, jl);
}
+static void strv_pair_print(char **l, const char *prefix) {
+ char **p, **q;
+
+ assert(prefix);
+
+ STRV_FOREACH_PAIR(p, q, l) {
+ if (p == l)
+ printf("%s %s=%s\n", prefix, *p, *q);
+ else
+ printf("%*s %s=%s\n", (int) strlen(prefix), "", *p, *q);
+ }
+}
+
+static int get_sysext_scopes(DissectedImage *m, char ***ret_scopes) {
+ _cleanup_strv_free_ char **l = NULL;
+ const char *e;
+
+ assert(m);
+ assert(ret_scopes);
+
+ /* If there's no extension-release file its not a system extension. Otherwise the SYSEXT_SCOPE field
+ * indicates which scope it is for — and it defaults to "system" + "portable" if unset. */
+
+ if (!m->extension_release) {
+ *ret_scopes = NULL;
+ return 0;
+ }
+
+ e = strv_env_pairs_get(m->extension_release, "SYSEXT_SCOPE");
+ if (e)
+ l = strv_split(e, WHITESPACE);
+ else
+ l = strv_new("system", "portable");
+ if (!l)
+ return -ENOMEM;
+
+ *ret_scopes = TAKE_PTR(l);
+ return 1;
+}
+
static int action_dissect(DissectedImage *m, LoopDevice *d) {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_(table_unrefp) Table *t = NULL;
@@ -406,47 +447,51 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) {
else if (r < 0)
return log_error_errno(r, "Failed to acquire image metadata: %m");
else if (arg_json_format_flags & JSON_FORMAT_OFF) {
+ _cleanup_strv_free_ char **sysext_scopes = NULL;
+
if (m->hostname)
printf(" Hostname: %s\n", m->hostname);
if (!sd_id128_is_null(m->machine_id))
printf("Machine ID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(m->machine_id));
- if (!strv_isempty(m->machine_info)) {
- char **p, **q;
+ strv_pair_print(m->machine_info,
+ "Mach. Info:");
+ strv_pair_print(m->os_release,
+ "OS Release:");
+ strv_pair_print(m->extension_release,
+ " Ext. Rel.:");
- STRV_FOREACH_PAIR(p, q, m->machine_info)
- printf("%s %s=%s\n",
- p == m->machine_info ? "Mach. Info:" : " ",
- *p, *q);
- }
+ if (m->hostname ||
+ !sd_id128_is_null(m->machine_id) ||
+ !strv_isempty(m->machine_info) ||
+ !strv_isempty(m->os_release) ||
+ !strv_isempty(m->extension_release))
+ putc('\n', stdout);
- if (!strv_isempty(m->os_release)) {
- char **p, **q;
+ printf(" Use As: %s bootable system for UEFI\n", COLOR_MARK_BOOL(m->partitions[PARTITION_ESP].found));
- STRV_FOREACH_PAIR(p, q, m->os_release)
- printf("%s %s=%s\n",
- p == m->os_release ? "OS Release:" : " ",
- *p, *q);
- }
+ if (m->has_init_system >= 0)
+ printf(" %s bootable system for container\n", COLOR_MARK_BOOL(m->has_init_system));
- if (!strv_isempty(m->extension_release)) {
- char **p, **q;
+ printf(" %s portable service\n",
+ COLOR_MARK_BOOL(strv_env_pairs_get(m->os_release, "PORTABLE_PREFIXES")));
- STRV_FOREACH_PAIR(p, q, m->extension_release)
- printf("%s %s=%s\n",
- p == m->extension_release ? "Extension Release:" : " ",
- *p, *q);
- }
+ r = get_sysext_scopes(m, &sysext_scopes);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse SYSEXT_SCOPE: %m");
- if (m->hostname ||
- !sd_id128_is_null(m->machine_id) ||
- !strv_isempty(m->machine_info) ||
- !strv_isempty(m->extension_release) ||
- !strv_isempty(m->os_release))
- putc('\n', stdout);
+ printf(" %s extension for system\n",
+ COLOR_MARK_BOOL(strv_contains(sysext_scopes, "system")));
+ printf(" %s extension for initrd\n",
+ COLOR_MARK_BOOL(strv_contains(sysext_scopes, "initrd")));
+ printf(" %s extension for portable service\n",
+ COLOR_MARK_BOOL(strv_contains(sysext_scopes, "portable")));
+
+ putc('\n', stdout);
} else {
_cleanup_(json_variant_unrefp) JsonVariant *mi = NULL, *osr = NULL, *exr = NULL;
+ _cleanup_strv_free_ char **sysext_scopes = NULL;
if (!strv_isempty(m->machine_info)) {
r = strv_pair_to_json(m->machine_info, &mi);
@@ -466,6 +511,10 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) {
return log_oom();
}
+ r = get_sysext_scopes(m, &sysext_scopes);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse SYSEXT_SCOPE: %m");
+
r = json_build(&v, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("name", JSON_BUILD_STRING(basename(arg_image))),
JSON_BUILD_PAIR("size", JSON_BUILD_INTEGER(size)),
@@ -473,7 +522,13 @@ static int action_dissect(DissectedImage *m, LoopDevice *d) {
JSON_BUILD_PAIR_CONDITION(!sd_id128_is_null(m->machine_id), "machineId", JSON_BUILD_ID128(m->machine_id)),
JSON_BUILD_PAIR_CONDITION(mi, "machineInfo", JSON_BUILD_VARIANT(mi)),
JSON_BUILD_PAIR_CONDITION(osr, "osRelease", JSON_BUILD_VARIANT(osr)),
- JSON_BUILD_PAIR_CONDITION(exr, "extensionRelease", JSON_BUILD_VARIANT(exr))));
+ JSON_BUILD_PAIR_CONDITION(exr, "extensionRelease", JSON_BUILD_VARIANT(exr)),
+ JSON_BUILD_PAIR("useBootableUefi", JSON_BUILD_BOOLEAN(m->partitions[PARTITION_ESP].found)),
+ JSON_BUILD_PAIR_CONDITION(m->has_init_system >= 0, "useBootableContainer", JSON_BUILD_BOOLEAN(m->has_init_system)),
+ JSON_BUILD_PAIR("usePortableService", JSON_BUILD_BOOLEAN(strv_env_pairs_get(m->os_release, "PORTABLE_MATCHES"))),
+ JSON_BUILD_PAIR("useSystemExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes, "system"))),
+ JSON_BUILD_PAIR("useInitRDExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes, "initrd"))),
+ JSON_BUILD_PAIR("usePortableExtension", JSON_BUILD_BOOLEAN(strv_contains(sysext_scopes, "portable")))));
if (r < 0)
return log_oom();
}
diff --git a/src/portable/portable.c b/src/portable/portable.c
index 8ccb8f5228..2d1006eabd 100644
--- a/src/portable/portable.c
+++ b/src/portable/portable.c
@@ -13,6 +13,7 @@
#include "discover-image.h"
#include "dissect-image.h"
#include "env-file.h"
+#include "env-util.h"
#include "errno-list.h"
#include "escape.h"
#include "extension-release.h"
@@ -509,20 +510,20 @@ static int extract_image_and_extensions(
OrderedHashmap **ret_extension_images,
PortableMetadata **ret_os_release,
Hashmap **ret_unit_files,
+ char ***ret_valid_prefixes,
sd_bus_error *error) {
_cleanup_free_ char *id = NULL, *version_id = NULL, *sysext_level = NULL;
_cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
_cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL;
_cleanup_hashmap_free_ Hashmap *unit_files = NULL;
+ _cleanup_strv_free_ char **valid_prefixes = NULL;
_cleanup_(image_unrefp) Image *image = NULL;
Image *ext;
int r;
assert(name_or_path);
assert(matches);
- assert(ret_image);
- assert(ret_extension_images);
r = image_find_harder(IMAGE_PORTABLE, name_or_path, NULL, &image);
if (r < 0)
@@ -553,10 +554,12 @@ static int extract_image_and_extensions(
if (r < 0)
return r;
- /* If we are layering extension images on top of a runtime image, check that the os-release and extension-release metadata
- * match, otherwise reject it immediately as invalid, or it will fail when the units are started. */
- if (validate_sysext) {
+ /* If we are layering extension images on top of a runtime image, check that the os-release and
+ * extension-release metadata match, otherwise reject it immediately as invalid, or it will fail when
+ * the units are started. Also, collect valid portable prefixes if caller requested that. */
+ if (validate_sysext || ret_valid_prefixes) {
_cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *prefixes = NULL;
r = take_fdopen_unlocked(&os_release->fd, "r", &f);
if (r < 0)
@@ -565,9 +568,16 @@ static int extract_image_and_extensions(
r = parse_env_file(f, os_release->name,
"ID", &id,
"VERSION_ID", &version_id,
- "SYSEXT_LEVEL", &sysext_level);
+ "SYSEXT_LEVEL", &sysext_level,
+ "PORTABLE_PREFIXES", &prefixes);
if (r < 0)
return r;
+
+ if (prefixes) {
+ valid_prefixes = strv_split(prefixes, WHITESPACE);
+ if (!valid_prefixes)
+ return -ENOMEM;
+ }
}
ORDERED_HASHMAP_FOREACH(ext, extension_images) {
@@ -575,6 +585,7 @@ static int extract_image_and_extensions(
_cleanup_hashmap_free_ Hashmap *extra_unit_files = NULL;
_cleanup_strv_free_ char **extension_release = NULL;
_cleanup_fclose_ FILE *f = NULL;
+ const char *e;
r = portable_extract_by_path(ext->path, /* path_is_extension= */ true, matches, &extension_release_meta, &extra_unit_files, error);
if (r < 0)
@@ -584,7 +595,7 @@ static int extract_image_and_extensions(
if (r < 0)
return r;
- if (!validate_sysext)
+ if (!validate_sysext && !ret_valid_prefixes)
continue;
r = take_fdopen_unlocked(&extension_release_meta->fd, "r", &f);
@@ -595,19 +606,40 @@ static int extract_image_and_extensions(
if (r < 0)
return r;
- r = extension_release_validate(ext->path, id, version_id, sysext_level, extension_release);
- if (r == 0)
- return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", ext->path);
- if (r < 0)
- return sd_bus_error_set_errnof(error, r, "Failed to compare image %s extension-release metadata with the root's os-release: %m", ext->path);
+ if (validate_sysext) {
+ r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release);
+ if (r == 0)
+ return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", ext->path);
+ if (r < 0)
+ return sd_bus_error_set_errnof(error, r, "Failed to compare image %s extension-release metadata with the root's os-release: %m", ext->path);
+ }
+
+ e = strv_env_pairs_get(extension_release, "PORTABLE_PREFIXES");
+ if (e) {
+ _cleanup_strv_free_ char **l = NULL;
+
+ l = strv_split(e, WHITESPACE);
+ if (!l)
+ return -ENOMEM;
+
+ r = strv_extend_strv(&valid_prefixes, l, true);
+ if (r < 0)
+ return r;
+ }
}
- *ret_image = TAKE_PTR(image);
- *ret_extension_images = TAKE_PTR(extension_images);
+ strv_sort(valid_prefixes);
+
+ if (ret_image)
+ *ret_image = TAKE_PTR(image);
+ if (ret_extension_images)
+ *ret_extension_images = TAKE_PTR(extension_images);
if (ret_os_release)
*ret_os_release = TAKE_PTR(os_release);
if (ret_unit_files)
*ret_unit_files = TAKE_PTR(unit_files);
+ if (ret_valid_prefixes)
+ *ret_valid_prefixes = TAKE_PTR(valid_prefixes);
return 0;
}
@@ -618,23 +650,29 @@ int portable_extract(
char **extension_image_paths,
PortableMetadata **ret_os_release,
Hashmap **ret_unit_files,
+ char ***ret_valid_prefixes,
sd_bus_error *error) {
_cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
_cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL;
_cleanup_hashmap_free_ Hashmap *unit_files = NULL;
+ _cleanup_(strv_freep) char **valid_prefixes = NULL;
_cleanup_(image_unrefp) Image *image = NULL;
int r;
- r = extract_image_and_extensions(name_or_path,
- matches,
- extension_image_paths,
- /* validate_sysext= */ false,
- &image,
- &extension_images,
- &os_release,
- &unit_files,
- error);
+ assert(name_or_path);
+
+ r = extract_image_and_extensions(
+ name_or_path,
+ matches,
+ extension_image_paths,
+ /* validate_sysext= */ false,
+ &image,
+ &extension_images,
+ &os_release,
+ &unit_files,
+ ret_valid_prefixes ? &valid_prefixes : NULL,
+ error);
if (r < 0)
return r;
@@ -651,8 +689,12 @@ int portable_extract(
isempty(extensions) ? "" : extensions);
}
- *ret_os_release = TAKE_PTR(os_release);
- *ret_unit_files = TAKE_PTR(unit_files);
+ if (ret_os_release)
+ *ret_os_release = TAKE_PTR(os_release);
+ if (ret_unit_files)
+ *ret_unit_files = TAKE_PTR(unit_files);
+ if (ret_valid_prefixes)
+ *ret_valid_prefixes = TAKE_PTR(valid_prefixes);
return 0;
}
@@ -1211,6 +1253,18 @@ static int install_image_and_extensions_symlinks(
return 0;
}
+static bool prefix_matches_compatible(char **matches, char **valid_prefixes) {
+ char **m;
+
+ /* Checks if all 'matches' are included in the list of 'valid_prefixes' */
+
+ STRV_FOREACH(m, matches)
+ if (!strv_contains(valid_prefixes, *m))
+ return false;
+
+ return true;
+}
+
int portable_attach(
sd_bus *bus,
const char *name_or_path,
@@ -1225,33 +1279,63 @@ int portable_attach(
_cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL;
_cleanup_hashmap_free_ Hashmap *unit_files = NULL;
_cleanup_(lookup_paths_free) LookupPaths paths = {};
+ _cleanup_strv_free_ char **valid_prefixes = NULL;
_cleanup_(image_unrefp) Image *image = NULL;
PortableMetadata *item;
int r;
- r = extract_image_and_extensions(name_or_path,
- matches,
- extension_image_paths,
- /* validate_sysext= */ true,
- &image,
- &extension_images,
- /* os_release= */ NULL,
- &unit_files,
- error);
+ r = extract_image_and_extensions(
+ name_or_path,
+ matches,
+ extension_image_paths,
+ /* validate_sysext= */ true,
+ &image,
+ &extension_images,
+ /* os_release= */ NULL,
+ &unit_files,
+ &valid_prefixes,
+ error);
if (r < 0)
return r;
+ if (valid_prefixes && !prefix_matches_compatible(matches, valid_prefixes)) {
+ _cleanup_free_ char *matches_joined = NULL, *extensions_joined = NULL, *valid_prefixes_joined = NULL;
+
+ matches_joined = strv_join(matches, "', '");
+ if (!matches_joined)
+ return -ENOMEM;
+
+ extensions_joined = strv_join(extension_image_paths, ", ");
+ if (!extensions_joined)
+ return -ENOMEM;
+
+ valid_prefixes_joined = strv_join(valid_prefixes, ", ");
+ if (!valid_prefixes_joined)
+ return -ENOMEM;
+
+ return sd_bus_error_setf(
+ error,
+ SD_BUS_ERROR_INVALID_ARGS,
+ "Selected matches '%s' are not compatible with portable service image '%s%s%s', refusing. (Acceptable prefix matches are: %s)",
+ matches_joined,
+ image->path,
+ isempty(extensions_joined) ? "" : "' or any of its extensions '",
+ strempty(extensions_joined),
+ valid_prefixes_joined);
+ }
+
if (hashmap_isempty(unit_files)) {
- _cleanup_free_ char *extensions = strv_join(extension_image_paths, ", ");
- if (!extensions)
+ _cleanup_free_ char *extensions_joined = strv_join(extension_image_paths, ", ");
+ if (!extensions_joined)
return -ENOMEM;
- return sd_bus_error_setf(error,
- SD_BUS_ERROR_INVALID_ARGS,
- "Couldn't find any matching unit files in image '%s%s%s', refusing.",
- image->path,
- isempty(extensions) ? "" : "' or any of its extensions '",
- isempty(extensions) ? "" : extensions);
+ return sd_bus_error_setf(
+ error,
+ SD_BUS_ERROR_INVALID_ARGS,
+ "Couldn't find any matching unit files in image '%s%s%s', refusing.",
+ image->path,
+ isempty(extensions_joined) ? "" : "' or any of its extensions '",
+ strempty(extensions_joined));
}
r = lookup_paths_init(&paths, UNIT_FILE_SYSTEM, LOOKUP_PATHS_SPLIT_USR, NULL);
diff --git a/src/portable/portable.h b/src/portable/portable.h
index 077ab3333f..2837e8b286 100644
--- a/src/portable/portable.h
+++ b/src/portable/portable.h
@@ -65,7 +65,7 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(PortableMetadata*, portable_metadata_unref);
int portable_metadata_hashmap_to_sorted_array(Hashmap *unit_files, PortableMetadata ***ret);
-int portable_extract(const char *image, char **matches, char **extension_image_paths, PortableMetadata **ret_os_release, Hashmap **ret_unit_files, sd_bus_error *error);
+int portable_extract(const char *image, char **matches, char **extension_image_paths, PortableMetadata **ret_os_release, Hashmap **ret_unit_files, char ***ret_valid_prefixes, sd_bus_error *error);
int portable_attach(sd_bus *bus, const char *name_or_path, char **matches, const char *profile, char **extension_images, PortableFlags flags, PortableChange **changes, size_t *n_changes, sd_bus_error *error);
int portable_detach(sd_bus *bus, const char *name_or_path, char **extension_image_paths, PortableFlags flags, PortableChange **changes, size_t *n_changes, sd_bus_error *error);
diff --git a/src/portable/portabled-image-bus.c b/src/portable/portabled-image-bus.c
index 23c6e2633a..ede062dbfb 100644
--- a/src/portable/portabled-image-bus.c
+++ b/src/portable/portabled-image-bus.c
@@ -162,6 +162,7 @@ int bus_image_common_get_metadata(
extension_images,
&os_release,
&unit_files,
+ NULL,
error);
if (r < 0)
return r;
diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c
index 18c7991570..06d0319f75 100644
--- a/src/shared/dissect-image.c
+++ b/src/shared/dissect-image.c
@@ -46,6 +46,7 @@
#include "hostname-setup.h"
#include "id128-util.h"
#include "import-util.h"
+#include "io-util.h"
#include "mkdir-label.h"
#include "mount-util.h"
#include "mountpoint-util.h"
@@ -748,10 +749,14 @@ int dissect_image(
if (r != 0)
return errno_or_else(EIO);
- m = new0(DissectedImage, 1);
+ m = new(DissectedImage, 1);
if (!m)
return -ENOMEM;
+ *m = (DissectedImage) {
+ .has_init_system = -1,
+ };
+
r = sd_device_get_sysname(d, &sysname);
if (r < 0)
return log_debug_errno(r, "Failed to get device sysname: %m");
@@ -3012,6 +3017,7 @@ int dissected_image_acquire_metadata(DissectedImage *m) {
META_MACHINE_INFO,
META_OS_RELEASE,
META_EXTENSION_RELEASE,
+ META_HAS_INIT_SYSTEM,
_META_MAX,
};
@@ -3021,7 +3027,8 @@ int dissected_image_acquire_metadata(DissectedImage *m) {
[META_MACHINE_INFO] = "/etc/machine-info\0",
[META_OS_RELEASE] = ("/etc/os-release\0"
"/usr/lib/os-release\0"),
- [META_EXTENSION_RELEASE] = "extension-release\0", /* Used only for logging. */
+ [META_EXTENSION_RELEASE] = "extension-release\0", /* Used only for logging. */
+ [META_HAS_INIT_SYSTEM] = "has-init-system\0", /* ditto */
};
_cleanup_strv_free_ char **machine_info = NULL, **os_release = NULL, **extension_release = NULL;
@@ -3032,6 +3039,7 @@ int dissected_image_acquire_metadata(DissectedImage *m) {
_cleanup_free_ char *hostname = NULL;
unsigned n_meta_initialized = 0;
int fds[2 * _META_MAX], r, v;
+ int has_init_system = -1;
ssize_t n;
BLOCK_SIGNALS(SIGCHLD);
@@ -3063,6 +3071,7 @@ int dissected_image_acquire_metadata(DissectedImage *m) {
if (r < 0)
goto finish;
if (r == 0) {
+ /* Child in a new mount namespace */
error_pipe[0] = safe_close(error_pipe[0]);
r = dissected_image_mount(
@@ -3092,7 +3101,9 @@ int dissected_image_acquire_metadata(DissectedImage *m) {
fds[2*k] = safe_close(fds[2*k]);
- if (k == META_EXTENSION_RELEASE) {
+ switch (k) {
+
+ case META_EXTENSION_RELEASE:
/* As per the os-release spec, if the image is an extension it will have a file
* named after the image name in extension-release.d/ - we use the image name
* and try to resolve it with the extension-release helpers, as sometimes
@@ -3105,12 +3116,42 @@ int dissected_image_acquire_metadata(DissectedImage *m) {
r = open_extension_release(t, m->image_name, NULL, &fd);
if (r < 0)
fd = r; /* Propagate the error. */
- } else
+ break;
+
+ case META_HAS_INIT_SYSTEM: {
+ bool found = false;
+ const char *init;
+
+ FOREACH_STRING(init,
+ "/usr/lib/systemd/systemd", /* systemd on /usr merged system */
+ "/lib/systemd/systemd", /* systemd on /usr non-merged systems */
+ "/sbin/init") { /* traditional path the Linux kernel invokes */
+
+ r = chase_symlinks(init, t, CHASE_PREFIX_ROOT, NULL, NULL);
+ if (r < 0) {
+ if (r != -ENOENT)
+ log_debug_errno(r, "Failed to resolve %s, ignoring: %m", init);
+ } else {
+ found = true;
+ break;
+ }
+ }
+
+ r = loop_write(fds[2*k+1], &found, sizeof(found), false);
+ if (r < 0)
+ goto inner_fail;
+
+ continue;
+ }
+
+ default:
NULSTR_FOREACH(p, paths[k]) {
fd = chase_symlinks_and_open(p, t, CHASE_PREFIX_ROOT, O_RDONLY|O_CLOEXEC|O_NOCTTY, NULL);
if (fd >= 0)
break;
}
+ }
+
if (fd < 0) {
log_debug_errno(fd, "Failed to read %s file of image, ignoring: %m", paths[k]);
fds[2*k+1] = safe_close(fds[2*k+1]);
@@ -3118,15 +3159,17 @@ int dissected_image_acquire_metadata(DissectedImage *m) {
}
r = copy_bytes(fd, fds[2*k+1], UINT64_MAX, 0);
- if (r < 0) {
- (void) write(error_pipe[1], &r, sizeof(r));
- _exit(EXIT_FAILURE);
- }
+ if (r < 0)
+ goto inner_fail;
fds[2*k+1] = safe_close(fds[2*k+1]);
}
_exit(EXIT_SUCCESS);
+
+ inner_fail:
+ (void) write(error_pipe[1], &r, sizeof(r));
+ _exit(EXIT_FAILURE);
}
error_pipe[1] = safe_close(error_pipe[1]);
@@ -3194,7 +3237,20 @@ int dissected_image_acquire_metadata(DissectedImage *m) {
log_debug_errno(r, "Failed to read extension release file: %m");
break;
- }
+
+ case META_HAS_INIT_SYSTEM: {
+ bool b = false;
+ size_t nr;
+
+ errno = 0;
+ nr = fread(&b, 1, sizeof(b), f);
+ if (nr != sizeof(b))
+ log_debug_errno(errno_or_else(EIO), "Failed to read has-init-system boolean: %m");
+ else
+ has_init_system = b;
+
+ break;
+ }}
}
r = wait_for_terminate_and_check("(sd-dissect)", child, 0);
@@ -3218,6 +3274,7 @@ int dissected_image_acquire_metadata(DissectedImage *m) {
strv_free_and_replace(m->machine_info, machine_info);
strv_free_and_replace(m->os_release, os_release);
strv_free_and_replace(m->extension_release, extension_release);
+ m->has_init_system = has_init_system;
finish:
for (unsigned k = 0; k < n_meta_initialized; k++)
@@ -3468,7 +3525,8 @@ int verity_dissect_and_mount(
const MountOptions *options,
const char *required_host_os_release_id,
const char *required_host_os_release_version_id,
- const char *required_host_os_release_sysext_level) {
+ const char *required_host_os_release_sysext_level,
+ const char *required_sysext_scope) {
_cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL;
_cleanup_(decrypted_image_unrefp) DecryptedImage *decrypted_image = NULL;
@@ -3554,11 +3612,12 @@ int verity_dissect_and_mount(
return log_debug_errno(r, "Failed to parse image %s extension-release metadata: %m", dissected_image->image_name);
r = extension_release_validate(
- dissected_image->image_name,
- required_host_os_release_id,
- required_host_os_release_version_id,
- required_host_os_release_sysext_level,
- extension_release);
+ dissected_image->image_name,
+ required_host_os_release_id,
+ required_host_os_release_version_id,
+ required_host_os_release_sysext_level,
+ required_sysext_scope,
+ extension_release);
if (r == 0)
return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", dissected_image->image_name);
if (r < 0)
diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h
index 635ca5b76c..8ad26bc45b 100644
--- a/src/shared/dissect-image.h
+++ b/src/shared/dissect-image.h
@@ -163,6 +163,7 @@ struct DissectedImage {
char **machine_info;
char **os_release;
char **extension_release;
+ int has_init_system;
};
struct MountOptions {
@@ -227,4 +228,4 @@ bool dissected_image_verity_sig_ready(const DissectedImage *image, PartitionDesi
int mount_image_privately_interactively(const char *path, DissectImageFlags flags, char **ret_directory, LoopDevice **ret_loop_device, DecryptedImage **ret_decrypted_image);
-int verity_dissect_and_mount(const char *src, const char *dest, const MountOptions *options, const char *required_host_os_release_id, const char *required_host_os_release_version_id, const char *required_host_os_release_sysext_level);
+int verity_dissect_and_mount(const char *src, const char *dest, const MountOptions *options, const char *required_host_os_release_id, const char *required_host_os_release_version_id, const char *required_host_os_release_sysext_level, const char *required_sysext_scope);
diff --git a/src/shared/extension-release.c b/src/shared/extension-release.c
index 29cbecbf57..dccc999907 100644
--- a/src/shared/extension-release.c
+++ b/src/shared/extension-release.c
@@ -12,6 +12,7 @@ int extension_release_validate(
const char *host_os_release_id,
const char *host_os_release_version_id,
const char *host_os_release_sysext_level,
+ const char *host_sysext_scope,
char **extension_release) {
const char *extension_release_id = NULL, *extension_release_sysext_level = NULL;
@@ -25,6 +26,28 @@ int extension_release_validate(
return 0;
}
+ if (host_sysext_scope) {
+ _cleanup_strv_free_ char **extension_sysext_scope_list = NULL;
+ const char *extension_sysext_scope;
+ bool valid;
+
+ extension_sysext_scope = strv_env_pairs_get(extension_release, "SYSEXT_SCOPE");
+ if (extension_sysext_scope) {
+ extension_sysext_scope_list = strv_split(extension_sysext_scope, WHITESPACE);
+ if (!extension_sysext_scope_list)
+ return -ENOMEM;
+ }
+
+ /* by default extension are good for attachment in portable service and on the system */
+ valid = strv_contains(
+ extension_sysext_scope_list ?: STRV_MAKE("system", "portable"),
+ host_sysext_scope);
+ if (!valid) {
+ log_debug("Extension '%s' is not suitable for scope %s, ignoring extension.", name, host_sysext_scope);
+ return 0;
+ }
+ }
+
extension_release_id = strv_env_pairs_get(extension_release, "ID");
if (isempty(extension_release_id)) {
log_debug("Extension '%s' does not contain ID in extension-release but requested to match '%s'",
diff --git a/src/shared/extension-release.h b/src/shared/extension-release.h
index d026a9b225..5c3fee24be 100644
--- a/src/shared/extension-release.h
+++ b/src/shared/extension-release.h
@@ -9,6 +9,7 @@ int extension_release_validate(
const char *host_os_release_id,
const char *host_os_release_version_id,
const char *host_os_release_sysext_level,
+ const char *host_sysext_scope,
char **extension_release);
/* Parse SYSTEMD_SYSEXT_HIERARCHIES and if not set, return "/usr /opt" */
diff --git a/src/shared/mount-util.c b/src/shared/mount-util.c
index 8d4a6cd25a..c75c02f5be 100644
--- a/src/shared/mount-util.c
+++ b/src/shared/mount-util.c
@@ -874,7 +874,7 @@ static int mount_in_namespace(
mount_tmp_created = true;
if (is_image)
- r = verity_dissect_and_mount(FORMAT_PROC_FD_PATH(chased_src_fd), mount_tmp, options, NULL, NULL, NULL);
+ r = verity_dissect_and_mount(FORMAT_PROC_FD_PATH(chased_src_fd), mount_tmp, options, NULL, NULL, NULL, NULL);
else
r = mount_follow_verbose(LOG_DEBUG, FORMAT_PROC_FD_PATH(chased_src_fd), mount_tmp, NULL, MS_BIND, NULL);
if (r < 0)
diff --git a/src/shared/pretty-print.h b/src/shared/pretty-print.h
index 4619f4e4d7..45644da67d 100644
--- a/src/shared/pretty-print.h
+++ b/src/shared/pretty-print.h
@@ -1,6 +1,9 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
+#include "glyph-util.h"
+#include "terminal-util.h"
+
void print_separator(void);
int file_url_from_path(const char *path, char **ret);
@@ -17,3 +20,23 @@ typedef enum CatFlags {
int cat_files(const char *file, char **dropins, CatFlags flags);
int conf_files_cat(const char *root, const char *name);
+
+#define RED_CROSS_MARK_MAX (STRLEN(ANSI_HIGHLIGHT_RED) + STRLEN("✗") + STRLEN(ANSI_NORMAL) + 1)
+#define GREEN_CHECK_MARK_MAX (STRLEN(ANSI_HIGHLIGHT_GREEN) + STRLEN("✓") + STRLEN(ANSI_NORMAL) + 1)
+
+static inline const char *red_cross_mark_internal(char buffer[static RED_CROSS_MARK_MAX]) {
+ assert(buffer);
+ assert_se(stpcpy(stpcpy(stpcpy(buffer, ansi_highlight_red()), special_glyph(SPECIAL_GLYPH_CROSS_MARK)), ansi_normal()) < buffer + RED_CROSS_MARK_MAX);
+ return buffer;
+}
+
+static inline const char *green_check_mark_internal(char buffer[static GREEN_CHECK_MARK_MAX]) {
+ assert(buffer);
+ assert_se(stpcpy(stpcpy(stpcpy(buffer, ansi_highlight_green()), special_glyph(SPECIAL_GLYPH_CHECK_MARK)), ansi_normal()) < buffer + GREEN_CHECK_MARK_MAX);
+ return buffer;
+}
+
+#define RED_CROSS_MARK() red_cross_mark_internal((char[RED_CROSS_MARK_MAX]) {})
+#define GREEN_CHECK_MARK() green_check_mark_internal((char[GREEN_CHECK_MARK_MAX]) {})
+
+#define COLOR_MARK_BOOL(b) ((b) ? GREEN_CHECK_MARK() : RED_CROSS_MARK())
diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c
index b9387e904a..5abf1bb418 100644
--- a/src/sysext/sysext.c
+++ b/src/sysext/sysext.c
@@ -432,12 +432,17 @@ static int validate_version(
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Extension image contains /usr/lib/os-release file, which is not allowed (it may carry /etc/os-release), refusing.");
- return extension_release_validate(
+ r = extension_release_validate(
img->name,
host_os_release_id,
host_os_release_version_id,
host_os_release_sysext_level,
+ in_initrd() ? "initrd" : "system",
img->extension_release);
+ if (r < 0)
+ return log_error_errno(r, "Failed to validate extension release information: %m");
+
+ return r;
}
static int merge_subprocess(Hashmap *images, const char *workspace) {
diff --git a/src/test/test-gpt.c b/src/test/test-gpt.c
index 9b0eb57373..7ee044ba50 100644
--- a/src/test/test-gpt.c
+++ b/src/test/test-gpt.c
@@ -4,6 +4,7 @@
#include "glyph-util.h"
#include "gpt.h"
#include "log.h"
+#include "pretty-print.h"
#include "strv.h"
#include "terminal-util.h"
#include "tests.h"
@@ -32,11 +33,11 @@ static void test_gpt_types_against_architectures(void) {
r = gpt_partition_type_uuid_from_string(joined, &id);
if (r < 0) {
- printf("%s%s%s %s\n", ansi_highlight_red(), special_glyph(SPECIAL_GLYPH_CROSS_MARK), ansi_normal(), joined);
+ printf("%s %s\n", RED_CROSS_MARK(), joined);
continue;
}
- printf("%s%s%s %s\n", ansi_highlight_green(), special_glyph(SPECIAL_GLYPH_CHECK_MARK), ansi_normal(), joined);
+ printf("%s %s\n", GREEN_CHECK_MARK(), joined);
if (streq(prefix, "root-") && streq(suffix, ""))
assert_se(gpt_partition_type_is_root(id));
diff --git a/src/test/test-pretty-print.c b/src/test/test-pretty-print.c
index dbae34e73e..4620d39bb6 100644
--- a/src/test/test-pretty-print.c
+++ b/src/test/test-pretty-print.c
@@ -31,11 +31,25 @@ static void test_cat_files(void) {
assert_se(cat_files("/etc/fstab", STRV_MAKE("/etc/fstab", "/etc/fstab"), 0) == 0);
}
+static void test_red_green_cross_check_mark(void) {
+ bool b = false;
+
+ printf("yeah: <%s>\n", GREEN_CHECK_MARK());
+ printf("nay: <%s>\n", RED_CROSS_MARK());
+
+ printf("%s → %s → %s → %s\n",
+ COLOR_MARK_BOOL(b),
+ COLOR_MARK_BOOL(!b),
+ COLOR_MARK_BOOL(!!b),
+ COLOR_MARK_BOOL(!!!b));
+}
+
int main(int argc, char *argv[]) {
test_setup_logging(LOG_INFO);
test_terminal_urlify();
test_cat_files();
+ test_red_green_cross_check_mark();
print_separator();
diff --git a/test/test-functions b/test/test-functions
index a4b7efb5ec..0a3745de52 100644
--- a/test/test-functions
+++ b/test/test-functions
@@ -594,6 +594,7 @@ install_verity_minimal() {
touch "$initdir/etc/machine-id" "$initdir/etc/resolv.conf"
touch "$initdir/opt/some_file"
echo MARKER=1 >>"$initdir/usr/lib/os-release"
+ echo "PORTABLE_PREFIXES=app0 minimal" >>"$initdir/usr/lib/os-release"
echo -e "[Service]\nExecStartPre=cat /usr/lib/os-release\nExecStart=sleep 120" >"$initdir/usr/lib/systemd/system/app0.service"
cp "$initdir/usr/lib/systemd/system/app0.service" "$initdir/usr/lib/systemd/system/app0-foo.service"
@@ -638,7 +639,9 @@ EOF
export initdir="$TESTDIR/app1"
mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/usr/lib/systemd/system" "$initdir/opt"
grep "^ID=" "$os_release" >"$initdir/usr/lib/extension-release.d/extension-release.app2"
- echo "${version_id}" >>"$initdir/usr/lib/extension-release.d/extension-release.app2"
+ ( echo "${version_id}"
+ echo "SYSEXT_SCOPE=portable"
+ echo "PORTABLE_PREFIXES=app1" ) >>"$initdir/usr/lib/extension-release.d/extension-release.app2"
setfattr -n user.extension-release.strict -v false "$initdir/usr/lib/extension-release.d/extension-release.app2"
cat >"$initdir/usr/lib/systemd/system/app1.service" <<EOF
[Service]
diff --git a/test/units/testsuite-29.sh b/test/units/testsuite-29.sh
index 11f1832aa4..0e0c8cf41d 100755
--- a/test/units/testsuite-29.sh
+++ b/test/units/testsuite-29.sh
@@ -12,6 +12,11 @@ if [[ -v ASAN_OPTIONS || -v UBSAN_OPTIONS ]]; then
ARGS+=(--profile=trusted)
fi
+systemd-dissect --no-pager /usr/share/minimal_0.raw | grep -q '✓ portable service'
+systemd-dissect --no-pager /usr/share/minimal_1.raw | grep -q '✓ portable service'
+systemd-dissect --no-pager /usr/share/app0.raw | grep -q '✓ extension for portable service'
+systemd-dissect --no-pager /usr/share/app1.raw | grep -q '✓ extension for portable service'
+
export SYSTEMD_LOG_LEVEL=debug
mkdir -p /run/systemd/system/systemd-portabled.service.d/
cat <<EOF >/run/systemd/system/systemd-portabled.service.d/override.conf
@@ -98,10 +103,19 @@ portablectl detach --now --runtime --extension /usr/share/app1.raw /usr/share/mi
# portablectl also works with directory paths rather than images
-mkdir /tmp/rootdir /tmp/app1 /tmp/overlay
+mkdir /tmp/rootdir /tmp/app1 /tmp/overlay /tmp/os-release-fix /tmp/os-release-fix/etc
mount /usr/share/app1.raw /tmp/app1
mount /usr/share/minimal_0.raw /tmp/rootdir
-mount -t overlay overlay -o lowerdir=/tmp/app1:/tmp/rootdir /tmp/overlay
+
+# Fix up os-release to drop the valid PORTABLE_SERVICES field (because we are
+# bypassing the sysext logic in portabled here it will otherwise not see the
+# extensions additional valid prefix)
+grep -v "^PORTABLE_PREFIXES=" /tmp/rootdir/etc/os-release > /tmp/os-release-fix/etc/os-release
+
+mount -t overlay overlay -o lowerdir=/tmp/os-release-fix:/tmp/app1:/tmp/rootdir /tmp/overlay
+
+grep . /tmp/overlay/usr/lib/extension-release.d/*
+grep . /tmp/overlay/etc/os-release
portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/overlay app1