diff --git a/man/os-release.xml b/man/os-release.xml index 17165071ec..ca5ab0fce3 100644 --- a/man/os-release.xml +++ b/man/os-release.xml @@ -108,8 +108,9 @@ follows the syntax and rules as described in the Portable Services page. The purpose of this file is to identify the extension and to allow the operating system to verify that the extension image - matches the base OS. This is typically implemented by checking that the ID= options - match, and either SYSEXT_LEVEL= exists and matches too, or if it is not present, + matches the base OS. This is typically implemented by checking that the extension ID= + option either matches the host ID= option or is included the host ID_LIKE= + option, and either SYSEXT_LEVEL= exists and matches too, or if it is not present, VERSION_ID= exists and matches. This ensures ABI/API compatibility between the layers and prevents merging of an incompatible image in an overlay. diff --git a/src/core/namespace.c b/src/core/namespace.c index 8ced4ead2e..8226f89493 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1596,6 +1596,7 @@ static int mount_image( r = parse_os_release( empty_to_root(root_directory), "ID", &rdata.os_release_id, + "ID_LIKE", &rdata.os_release_id_like, "VERSION_ID", &rdata.os_release_version_id, image_class_info[IMAGE_SYSEXT].level_env, &rdata.os_release_sysext_level, image_class_info[IMAGE_CONFEXT].level_env, &rdata.os_release_confext_level, @@ -1623,9 +1624,10 @@ static int mount_image( return 0; if (r == -ESTALE && rdata.os_release_id) return log_error_errno(r, // FIXME: this should not be logged ad LOG_ERR, as it will result in duplicate logging. - "Failed to mount image %s, extension-release metadata does not match the lower layer's: ID=%s%s%s%s%s%s%s", + "Failed to mount image %s, extension-release metadata does not match the lower layer's: ID=%s ID_LIKE='%s'%s%s%s%s%s%s", mount_entry_source(m), rdata.os_release_id, + strempty(rdata.os_release_id_like), rdata.os_release_version_id ? " VERSION_ID=" : "", strempty(rdata.os_release_version_id), rdata.os_release_sysext_level ? image_class_info[IMAGE_SYSEXT].level_env_print : "", @@ -1804,8 +1806,9 @@ static int apply_one_mount( break; case MOUNT_EXTENSION_DIRECTORY: { - _cleanup_free_ char *host_os_release_id = NULL, *host_os_release_version_id = NULL, - *host_os_release_level = NULL, *extension_name = NULL; + _cleanup_free_ char *host_os_release_id = NULL, *host_os_release_id_like = NULL, + *host_os_release_version_id = NULL, *host_os_release_level = NULL, + *extension_name = NULL; _cleanup_strv_free_ char **extension_release = NULL; ImageClass class = IMAGE_SYSEXT; @@ -1840,6 +1843,7 @@ static int apply_one_mount( r = parse_os_release( empty_to_root(root_directory), "ID", &host_os_release_id, + "ID_LIKE", &host_os_release_id_like, "VERSION_ID", &host_os_release_version_id, image_class_info[class].level_env, &host_os_release_level, NULL); @@ -1851,6 +1855,7 @@ static int apply_one_mount( r = extension_release_validate( extension_name, host_os_release_id, + host_os_release_id_like, host_os_release_version_id, host_os_release_level, /* host_extension_scope = */ NULL, /* Leave empty, we need to accept both system and portable */ diff --git a/src/portable/portable.c b/src/portable/portable.c index 75e32abcce..1e19096174 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -570,7 +570,7 @@ static int extract_image_and_extensions( char ***ret_valid_prefixes, sd_bus_error *error) { - _cleanup_free_ char *id = NULL, *version_id = NULL, *sysext_level = NULL, *confext_level = NULL; + _cleanup_free_ char *id = NULL, *id_like = NULL, *version_id = NULL, *sysext_level = NULL, *confext_level = NULL; _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL; _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL; _cleanup_(pick_result_done) PickResult result = PICK_RESULT_NULL; @@ -672,6 +672,7 @@ static int extract_image_and_extensions( r = parse_env_file_fd(os_release->fd, os_release->name, "ID", &id, + "ID_LIKE", &id_like, "VERSION_ID", &version_id, "SYSEXT_LEVEL", &sysext_level, "CONFEXT_LEVEL", &confext_level, @@ -719,9 +720,9 @@ static int extract_image_and_extensions( return r; if (validate_extension) { - r = extension_release_validate(ext->path, id, version_id, sysext_level, "portable", extension_release, IMAGE_SYSEXT); + r = extension_release_validate(ext->path, id, id_like, version_id, sysext_level, "portable", extension_release, IMAGE_SYSEXT); if (r < 0) - r = extension_release_validate(ext->path, id, version_id, confext_level, "portable", extension_release, IMAGE_CONFEXT); + r = extension_release_validate(ext->path, id, id_like, version_id, confext_level, "portable", extension_release, IMAGE_CONFEXT); if (r == 0) return sd_bus_error_set_errnof(error, ESTALE, "Image %s extension-release metadata does not match the root's", ext->path); diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 646bbfc633..34877ffa56 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -4421,6 +4421,7 @@ int verity_dissect_and_mount( r = extension_release_validate( dissected_image->image_name, extension_release_data->os_release_id, + extension_release_data->os_release_id_like, extension_release_data->os_release_version_id, class == IMAGE_SYSEXT ? extension_release_data->os_release_sysext_level : extension_release_data->os_release_confext_level, extension_release_data->os_release_extension_scope, @@ -4447,6 +4448,7 @@ void extension_release_data_done(ExtensionReleaseData *data) { assert(data); data->os_release_id = mfree(data->os_release_id); + data->os_release_id_like = mfree(data->os_release_id_like); data->os_release_version_id = mfree(data->os_release_version_id); data->os_release_sysext_level = mfree(data->os_release_sysext_level); data->os_release_confext_level = mfree(data->os_release_confext_level); diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 54fdd8da0d..97431bca67 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -145,6 +145,7 @@ typedef struct ImageFilter { typedef struct ExtensionReleaseData { char *os_release_id; + char *os_release_id_like; char *os_release_version_id; char *os_release_sysext_level; char *os_release_confext_level; diff --git a/src/shared/extension-util.c b/src/shared/extension-util.c index afaea7e3ae..f2361b3fd1 100644 --- a/src/shared/extension-util.c +++ b/src/shared/extension-util.c @@ -13,6 +13,7 @@ int extension_release_validate( const char *name, const char *host_os_release_id, + const char *host_os_release_id_like, const char *host_os_release_version_id, const char *host_os_extension_release_level, const char *host_extension_scope, @@ -22,6 +23,7 @@ int extension_release_validate( const char *extension_release_id = NULL, *extension_release_level = NULL, *extension_architecture = NULL; const char *extension_level = image_class == IMAGE_CONFEXT ? "CONFEXT_LEVEL" : "SYSEXT_LEVEL"; const char *extension_scope = image_class == IMAGE_CONFEXT ? "CONFEXT_SCOPE" : "SYSEXT_SCOPE"; + _cleanup_strv_free_ char **id_like_l = NULL; assert(name); assert(!isempty(host_os_release_id)); @@ -78,9 +80,19 @@ int extension_release_validate( return 1; } - if (!streq(host_os_release_id, extension_release_id)) { - log_debug("Extension '%s' is for OS '%s', but deployed on top of '%s'.", - name, extension_release_id, host_os_release_id); + /* Match extension OS ID against host OS ID or ID_LIKE */ + if (host_os_release_id_like) { + id_like_l = strv_split(host_os_release_id_like, WHITESPACE); + if (!id_like_l) + return log_oom(); + } + + if (!streq(host_os_release_id, extension_release_id) && !strv_contains(id_like_l, extension_release_id)) { + log_debug("Extension '%s' is for OS '%s', but deployed on top of '%s'%s%s%s.", + name, extension_release_id, host_os_release_id, + host_os_release_id_like ? " (like '" : "", + strempty(host_os_release_id_like), + host_os_release_id_like ? "')" : ""); return 0; } diff --git a/src/shared/extension-util.h b/src/shared/extension-util.h index 8bc0359820..08bdd7f33e 100644 --- a/src/shared/extension-util.h +++ b/src/shared/extension-util.h @@ -9,6 +9,7 @@ int extension_release_validate( const char *name, const char *host_os_release_id, + const char *host_os_release_id_like, const char *host_os_release_version_id, const char *host_os_extension_release_level, const char *host_extension_scope, diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index 44a523ac63..a4aea64380 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -1686,7 +1686,9 @@ static int merge_subprocess( Hashmap *images, const char *workspace) { - _cleanup_free_ char *host_os_release_id = NULL, *host_os_release_version_id = NULL, *host_os_release_api_level = NULL, *filename = NULL; + _cleanup_free_ char *host_os_release_id = NULL, *host_os_release_id_like = NULL, + *host_os_release_version_id = NULL, *host_os_release_api_level = NULL, + *filename = NULL; _cleanup_strv_free_ char **extensions = NULL, **extensions_v = NULL, **paths = NULL; size_t n_extensions = 0; unsigned n_ignored = 0; @@ -1718,6 +1720,7 @@ static int merge_subprocess( r = parse_os_release( arg_root, "ID", &host_os_release_id, + "ID_LIKE", &host_os_release_id_like, "VERSION_ID", &host_os_release_version_id, image_class_info[image_class].level_env, &host_os_release_api_level); if (r < 0) @@ -1859,6 +1862,7 @@ static int merge_subprocess( r = extension_release_validate( img->name, host_os_release_id, + host_os_release_id_like, host_os_release_version_id, host_os_release_api_level, in_initrd() ? "initrd" : "system", diff --git a/test/units/TEST-50-DISSECT.sysext.sh b/test/units/TEST-50-DISSECT.sysext.sh index b0f0792690..6bb106bac2 100755 --- a/test/units/TEST-50-DISSECT.sysext.sh +++ b/test/units/TEST-50-DISSECT.sysext.sh @@ -85,6 +85,7 @@ prepare_root() { { echo "ID=testtest" + echo "ID_LIKE=\"foobar test_alike something-else\"" echo "VERSION=1.2.3" } >"$root/usr/lib/os-release" @@ -120,6 +121,38 @@ prepare_extension_image() { prepend_trap "rm -rf ${ext_dir@Q}" } +prepare_extension_image_with_matching_id() { + local root=${1:-} + local hierarchy=${2:?} + local ext_dir ext_release name + + name="test-extension-matching-id" + ext_dir="$root/var/lib/extensions/$name" + ext_release="$ext_dir/usr/lib/extension-release.d/extension-release.$name" + mkdir -p "${ext_release%/*}" + echo "ID=testtest" >"$ext_release" + mkdir -p "$ext_dir/$hierarchy" + touch "$ext_dir$hierarchy/preexisting-file-in-extension-image" + + prepend_trap "rm -rf ${ext_dir@Q}" +} + +prepare_extension_image_with_matching_id_like() { + local root=${1:-} + local hierarchy=${2:?} + local ext_dir ext_release name + + name="test-extension-matching-id-like" + ext_dir="$root/var/lib/extensions/$name" + ext_release="$ext_dir/usr/lib/extension-release.d/extension-release.$name" + mkdir -p "${ext_release%/*}" + echo "ID=test_alike" >"$ext_release" + mkdir -p "$ext_dir/$hierarchy" + touch "$ext_dir$hierarchy/preexisting-file-in-extension-image" + + prepend_trap "rm -rf ${ext_dir@Q}" +} + prepare_extension_mutable_dir() { local dir=${1:?} @@ -981,6 +1014,40 @@ for mutable_mode in no yes ephemeral; do done +( init_trap +: "Check if merging an extension with matching ID succeeds" +fake_root=${roots_dir:+"$roots_dir/matching-id"} +hierarchy=/opt + +prepare_root "$fake_root" "$hierarchy" +prepare_extension_image_with_matching_id "$fake_root" "$hierarchy" +prepare_read_only_hierarchy "$fake_root" "$hierarchy" + +run_systemd_sysext "$fake_root" merge +extension_verify_after_merge "$fake_root" "$hierarchy" -e -h + +run_systemd_sysext "$fake_root" unmerge +extension_verify_after_unmerge "$fake_root" "$hierarchy" -h +) + + +( init_trap +: "Check if merging an extension that matches host ID_LIKE succeeds" +fake_root=${roots_dir:+"$roots_dir/matching-id-like"} +hierarchy=/opt + +prepare_root "$fake_root" "$hierarchy" +prepare_extension_image_with_matching_id_like "$fake_root" "$hierarchy" +prepare_read_only_hierarchy "$fake_root" "$hierarchy" + +run_systemd_sysext "$fake_root" merge +extension_verify_after_merge "$fake_root" "$hierarchy" -e -h + +run_systemd_sysext "$fake_root" unmerge +extension_verify_after_unmerge "$fake_root" "$hierarchy" -h +) + + ( init_trap : "Check if merging fails in case of invalid mutable directory permissions"