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 @@ Examples: SYSEXT_LEVEL=2, SYSEXT_LEVEL=15.14. + + + SYSEXT_SCOPE= + Takes a space-separated list of one or more of the strings + system, initrd and portable. This field is + only supported in extension-release.d/ 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, SYSEXT_SCOPE=system + portable 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. + + + + PORTABLE_PREFIXES= + Takes a space-separated list of one or more valid prefix match strings for the + Portable Services 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. + 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_FOREACH_PAIR(p, q, m->machine_info) - printf("%s %s=%s\n", - p == m->machine_info ? "Mach. Info:" : " ", - *p, *q); - } - - if (!strv_isempty(m->os_release)) { - char **p, **q; - - STRV_FOREACH_PAIR(p, q, m->os_release) - printf("%s %s=%s\n", - p == m->os_release ? "OS Release:" : " ", - *p, *q); - } - - if (!strv_isempty(m->extension_release)) { - char **p, **q; - - STRV_FOREACH_PAIR(p, q, m->extension_release) - printf("%s %s=%s\n", - p == m->extension_release ? "Extension Release:" : " ", - *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.:"); 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)) + !strv_isempty(m->os_release) || + !strv_isempty(m->extension_release)) putc('\n', stdout); + + printf(" Use As: %s bootable system for UEFI\n", COLOR_MARK_BOOL(m->partitions[PARTITION_ESP].found)); + + if (m->has_init_system >= 0) + printf(" %s bootable system for container\n", COLOR_MARK_BOOL(m->has_init_system)); + + printf(" %s portable service\n", + COLOR_MARK_BOOL(strv_env_pairs_get(m->os_release, "PORTABLE_PREFIXES"))); + + r = get_sysext_scopes(m, &sysext_scopes); + if (r < 0) + return log_error_errno(r, "Failed to parse SYSEXT_SCOPE: %m"); + + 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 (hashmap_isempty(unit_files)) { - _cleanup_free_ char *extensions = strv_join(extension_image_paths, ", "); - if (!extensions) + 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; - 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); + 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_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_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" </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