diff --git a/src/portable/portable.c b/src/portable/portable.c index 4044c523e1..3f841b9258 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -11,8 +11,10 @@ #include "dirent-util.h" #include "discover-image.h" #include "dissect-image.h" +#include "env-file.h" #include "errno-list.h" #include "escape.h" +#include "extension-release.h" #include "fd-util.h" #include "fileio.h" #include "fs-util.h" @@ -230,6 +232,8 @@ DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(portable_metadata_hash_ops, char, static int extract_now( const char *where, char **matches, + const char *image_name, + bool path_is_extension, int socket_fd, PortableMetadata **ret_os_release, Hashmap **ret_unit_files) { @@ -239,6 +243,7 @@ static int extract_now( _cleanup_(lookup_paths_free) LookupPaths paths = {}; _cleanup_close_ int os_release_fd = -1; _cleanup_free_ char *os_release_path = NULL; + const char *os_release_id; char **i; int r; @@ -253,19 +258,27 @@ static int extract_now( assert(where); - /* First, find /etc/os-release and send it upstream (or just save it). */ - r = open_os_release(where, &os_release_path, &os_release_fd); + /* First, find os-release/extension-release and send it upstream (or just save it). */ + if (path_is_extension) { + os_release_id = strjoina("/usr/lib/extension-release.d/extension-release.", image_name); + r = open_extension_release(where, image_name, &os_release_path, &os_release_fd); + } else { + os_release_id = "/etc/os-release"; + r = open_os_release(where, &os_release_path, &os_release_fd); + } if (r < 0) - log_debug_errno(r, "Couldn't acquire os-release file, ignoring: %m"); + log_debug_errno(r, + "Couldn't acquire %s file, ignoring: %m", + path_is_extension ? "extension-release " : "os-release"); else { if (socket_fd >= 0) { - r = send_item(socket_fd, "/etc/os-release", os_release_fd); + r = send_item(socket_fd, os_release_id, os_release_fd); if (r < 0) return log_debug_errno(r, "Failed to send os-release file: %m"); } if (ret_os_release) { - os_release = portable_metadata_new("/etc/os-release", NULL, os_release_fd); + os_release = portable_metadata_new(os_release_id, NULL, os_release_fd); if (!os_release) return -ENOMEM; @@ -351,7 +364,7 @@ static int extract_now( static int portable_extract_by_path( const char *path, - bool extract_os_release, + bool path_is_extension, char **matches, PortableMetadata **ret_os_release, Hashmap **ret_unit_files, @@ -369,7 +382,7 @@ static int portable_extract_by_path( /* We can't turn this into a loop-back block device, and this returns EISDIR? Then this is a directory * tree and not a raw device. It's easy then. */ - r = extract_now(path, matches, -1, &os_release, &unit_files); + r = extract_now(path, matches, NULL, path_is_extension, -1, &os_release, &unit_files); if (r < 0) return r; @@ -423,15 +436,22 @@ static int portable_extract_by_path( if (r < 0) return r; if (r == 0) { + DissectImageFlags flags = DISSECT_IMAGE_READ_ONLY; + seq[0] = safe_close(seq[0]); - r = dissected_image_mount(m, tmpdir, UID_INVALID, UID_INVALID, DISSECT_IMAGE_READ_ONLY); + if (path_is_extension) + flags |= DISSECT_IMAGE_VALIDATE_OS_EXT; + else + flags |= DISSECT_IMAGE_VALIDATE_OS; + + r = dissected_image_mount(m, tmpdir, UID_INVALID, UID_INVALID, flags); if (r < 0) { log_debug_errno(r, "Failed to mount dissected image: %m"); goto child_finish; } - r = extract_now(tmpdir, matches, seq[1], NULL, NULL); + r = extract_now(tmpdir, matches, m->image_name, path_is_extension, seq[1], NULL, NULL); child_finish: _exit(r < 0 ? EXIT_FAILURE : EXIT_SUCCESS); @@ -477,7 +497,7 @@ static int portable_extract_by_path( add = NULL; - } else if (PORTABLE_METADATA_IS_OS_RELEASE(add)) { + } else if (PORTABLE_METADATA_IS_OS_RELEASE(add) || PORTABLE_METADATA_IS_EXTENSION_RELEASE(add)) { assert(!os_release); os_release = TAKE_PTR(add); @@ -491,13 +511,12 @@ static int portable_extract_by_path( child = 0; } - /* When the portable image is layered, the image with units will not - * have a full filesystem, so no os-release - it will be in the root layer */ - if (extract_os_release && !os_release) - return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Image '%s' lacks os-release data, refusing.", path); - - if (!extract_os_release && hashmap_isempty(unit_files)) - return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Couldn't find any matching unit files in image '%s', refusing.", path); + if (!os_release) + return sd_bus_error_setf(error, + SD_BUS_ERROR_INVALID_ARGS, + "Image '%s' lacks %s data, refusing.", + path, + path_is_extension ? "extension-release" : "os-release"); if (ret_unit_files) *ret_unit_files = TAKE_PTR(unit_files); @@ -508,14 +527,18 @@ static int portable_extract_by_path( return 0; } -int portable_extract( +static int extract_image_and_extensions( const char *name_or_path, char **matches, char **extension_image_paths, + bool validate_sysext, + Image **ret_image, + OrderedHashmap **ret_extension_images, PortableMetadata **ret_os_release, Hashmap **ret_unit_files, 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; @@ -524,6 +547,9 @@ int portable_extract( 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) @@ -550,19 +576,106 @@ int portable_extract( } } - r = portable_extract_by_path(image->path, true, matches, &os_release, &unit_files, error); + r = portable_extract_by_path(image->path, /* path_is_extension= */ false, matches, &os_release, &unit_files, error); if (r < 0) return r; - ORDERED_HASHMAP_FOREACH(ext, extension_images) { - _cleanup_hashmap_free_ Hashmap *extra_unit_files = NULL; + /* 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) { + _cleanup_fclose_ FILE *f = NULL; - r = portable_extract_by_path(ext->path, false, matches, NULL, &extra_unit_files, error); + r = take_fdopen_unlocked(&os_release->fd, "r", &f); if (r < 0) return r; + + r = parse_env_file(f, os_release->name, + "ID", &id, + "VERSION_ID", &version_id, + "SYSEXT_LEVEL", &sysext_level); + if (r < 0) + return r; + } + + ORDERED_HASHMAP_FOREACH(ext, extension_images) { + _cleanup_(portable_metadata_unrefp) PortableMetadata *extension_release_meta = NULL; + _cleanup_hashmap_free_ Hashmap *extra_unit_files = NULL; + _cleanup_strv_free_ char **extension_release = NULL; + _cleanup_fclose_ FILE *f = NULL; + + r = portable_extract_by_path(ext->path, /* path_is_extension= */ true, matches, &extension_release_meta, &extra_unit_files, error); + if (r < 0) + return r; + r = hashmap_move(unit_files, extra_unit_files); if (r < 0) return r; + + if (!validate_sysext) + continue; + + r = take_fdopen_unlocked(&extension_release_meta->fd, "r", &f); + if (r < 0) + return r; + + r = load_env_file_pairs(f, extension_release_meta->name, &extension_release); + 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); + } + + *ret_image = TAKE_PTR(image); + *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); + + return 0; +} + +int portable_extract( + const char *name_or_path, + char **matches, + char **extension_image_paths, + PortableMetadata **ret_os_release, + Hashmap **ret_unit_files, + 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_(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); + if (r < 0) + return r; + + if (hashmap_isempty(unit_files)) { + _cleanup_free_ char *extensions = strv_join(extension_image_paths, ", "); + if (!extensions) + 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); } *ret_os_release = TAKE_PTR(os_release); @@ -1138,47 +1251,31 @@ int portable_attach( _cleanup_(lookup_paths_free) LookupPaths paths = {}; _cleanup_(image_unrefp) Image *image = NULL; PortableMetadata *item; - Image *ext; - char **p; int r; - assert(name_or_path); - - r = image_find_harder(IMAGE_PORTABLE, name_or_path, NULL, &image); + r = extract_image_and_extensions(name_or_path, + matches, + extension_image_paths, + /* validate_sysext= */ true, + &image, + &extension_images, + /* os_release= */ NULL, + &unit_files, + error); if (r < 0) return r; - if (!strv_isempty(extension_image_paths)) { - extension_images = ordered_hashmap_new(&image_hash_ops); - if (!extension_images) + + if (hashmap_isempty(unit_files)) { + _cleanup_free_ char *extensions = strv_join(extension_image_paths, ", "); + if (!extensions) return -ENOMEM; - STRV_FOREACH(p, extension_image_paths) { - _cleanup_(image_unrefp) Image *new = NULL; - - r = image_find_harder(IMAGE_PORTABLE, *p, NULL, &new); - if (r < 0) - return r; - - r = ordered_hashmap_put(extension_images, new->name, new); - if (r < 0) - return r; - TAKE_PTR(new); - } - } - - r = portable_extract_by_path(image->path, true, matches, NULL, &unit_files, error); - if (r < 0) - return r; - - ORDERED_HASHMAP_FOREACH(ext, extension_images) { - _cleanup_hashmap_free_ Hashmap *extra_unit_files = NULL; - - r = portable_extract_by_path(ext->path, false, matches, NULL, &extra_unit_files, error); - if (r < 0) - return r; - r = hashmap_move(unit_files, extra_unit_files); - if (r < 0) - return r; + 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); } 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 dd080edf4e..94144287ae 100644 --- a/src/portable/portable.h +++ b/src/portable/portable.h @@ -16,6 +16,7 @@ typedef struct PortableMetadata { } PortableMetadata; #define PORTABLE_METADATA_IS_OS_RELEASE(m) (streq((m)->name, "/etc/os-release")) +#define PORTABLE_METADATA_IS_EXTENSION_RELEASE(m) (startswith((m)->name, "/usr/lib/extension-release.d/extension-release.")) #define PORTABLE_METADATA_IS_UNIT(m) (!IN_SET((m)->name[0], 0, '/')) typedef enum PortableFlags { diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 99a2f62e4a..81ae0c3ffc 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -1742,17 +1742,28 @@ int dissected_image_mount( if (r < 0) return r; - if (flags & DISSECT_IMAGE_VALIDATE_OS) { - r = path_is_os_tree(where); - if (r < 0) - return r; - if (r == 0) { + if ((flags & (DISSECT_IMAGE_VALIDATE_OS|DISSECT_IMAGE_VALIDATE_OS_EXT)) != 0) { + /* If either one of the validation flags are set, ensure that the image qualifies + * as one or the other (or both). */ + bool ok = false; + + if (FLAGS_SET(flags, DISSECT_IMAGE_VALIDATE_OS)) { + r = path_is_os_tree(where); + if (r < 0) + return r; + if (r > 0) + ok = true; + } + if (!ok && FLAGS_SET(flags, DISSECT_IMAGE_VALIDATE_OS_EXT)) { r = path_is_extension_tree(where, m->image_name); if (r < 0) return r; - if (r == 0) - return -EMEDIUMTYPE; + if (r > 0) + ok = true; } + + if (!ok) + return -ENOMEDIUM; } } @@ -2623,6 +2634,7 @@ int dissected_image_acquire_metadata(DissectedImage *m) { DISSECT_IMAGE_READ_ONLY| DISSECT_IMAGE_MOUNT_ROOT_ONLY| DISSECT_IMAGE_VALIDATE_OS| + DISSECT_IMAGE_VALIDATE_OS_EXT| DISSECT_IMAGE_USR_NO_ROOT); if (r < 0) { /* Let parent know the error */ diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index cad52f2184..4c4f5ff585 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -100,19 +100,20 @@ typedef enum DissectImageFlags { DISSECT_IMAGE_MOUNT_ROOT_ONLY = 1 << 6, /* Mount only the root and /usr partitions */ DISSECT_IMAGE_MOUNT_NON_ROOT_ONLY = 1 << 7, /* Mount only the non-root and non-/usr partitions */ DISSECT_IMAGE_VALIDATE_OS = 1 << 8, /* Refuse mounting images that aren't identifiable as OS images */ - DISSECT_IMAGE_NO_UDEV = 1 << 9, /* Don't wait for udev initializing things */ - DISSECT_IMAGE_RELAX_VAR_CHECK = 1 << 10, /* Don't insist that the UUID of /var is hashed from /etc/machine-id */ - DISSECT_IMAGE_FSCK = 1 << 11, /* File system check the partition before mounting (no effect when combined with DISSECT_IMAGE_READ_ONLY) */ - DISSECT_IMAGE_NO_PARTITION_TABLE = 1 << 12, /* Only recognize single file system images */ - DISSECT_IMAGE_VERITY_SHARE = 1 << 13, /* When activating a verity device, reuse existing one if already open */ - DISSECT_IMAGE_MKDIR = 1 << 14, /* Make top-level directory to mount right before mounting, if missing */ - DISSECT_IMAGE_USR_NO_ROOT = 1 << 15, /* If no root fs is in the image, but /usr is, then allow this (so that we can mount the rootfs as tmpfs or so */ - DISSECT_IMAGE_REQUIRE_ROOT = 1 << 16, /* Don't accept disks without root partition (or at least /usr partition if DISSECT_IMAGE_USR_NO_ROOT is set) */ - DISSECT_IMAGE_MOUNT_READ_ONLY = 1 << 17, /* Make mounts read-only */ + DISSECT_IMAGE_VALIDATE_OS_EXT = 1 << 9, /* Refuse mounting images that aren't identifiable as OS extension images */ + DISSECT_IMAGE_NO_UDEV = 1 << 10, /* Don't wait for udev initializing things */ + DISSECT_IMAGE_RELAX_VAR_CHECK = 1 << 11, /* Don't insist that the UUID of /var is hashed from /etc/machine-id */ + DISSECT_IMAGE_FSCK = 1 << 12, /* File system check the partition before mounting (no effect when combined with DISSECT_IMAGE_READ_ONLY) */ + DISSECT_IMAGE_NO_PARTITION_TABLE = 1 << 13, /* Only recognize single file system images */ + DISSECT_IMAGE_VERITY_SHARE = 1 << 14, /* When activating a verity device, reuse existing one if already open */ + DISSECT_IMAGE_MKDIR = 1 << 15, /* Make top-level directory to mount right before mounting, if missing */ + DISSECT_IMAGE_USR_NO_ROOT = 1 << 16, /* If no root fs is in the image, but /usr is, then allow this (so that we can mount the rootfs as tmpfs or so */ + DISSECT_IMAGE_REQUIRE_ROOT = 1 << 17, /* Don't accept disks without root partition (or at least /usr partition if DISSECT_IMAGE_USR_NO_ROOT is set) */ + DISSECT_IMAGE_MOUNT_READ_ONLY = 1 << 18, /* Make mounts read-only */ DISSECT_IMAGE_READ_ONLY = DISSECT_IMAGE_DEVICE_READ_ONLY | DISSECT_IMAGE_MOUNT_READ_ONLY, - DISSECT_IMAGE_GROWFS = 1 << 18, /* Grow file systems in partitions marked for that to the size of the partitions after mount */ - DISSECT_IMAGE_MOUNT_IDMAPPED = 1 << 19, /* Mount mounts with kernel 5.12-style userns ID mapping, if file system type doesn't support uid=/gid= */ + DISSECT_IMAGE_GROWFS = 1 << 19, /* Grow file systems in partitions marked for that to the size of the partitions after mount */ + DISSECT_IMAGE_MOUNT_IDMAPPED = 1 << 20, /* Mount mounts with kernel 5.12-style userns ID mapping, if file system type doesn't support uid=/gid= */ } DissectImageFlags; struct DissectedImage {