]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
portable,sysext: match extension OS ID also against host ID_LIKE
authorChristian Glombek <lorbus@fedoraproject.org>
Thu, 29 May 2025 14:53:19 +0000 (16:53 +0200)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Sat, 31 May 2025 01:02:11 +0000 (10:02 +0900)
man/os-release.xml
src/core/namespace.c
src/portable/portable.c
src/shared/dissect-image.c
src/shared/dissect-image.h
src/shared/extension-util.c
src/shared/extension-util.h
src/sysext/sysext.c
test/units/TEST-50-DISSECT.sysext.sh

index 17165071ece2677a91989cddd34d92b255bb6a9e..ca5ab0fce3fc6174651215e44fa639b74cb95649 100644 (file)
       follows the syntax and rules as described in the <ulink
       url="https://systemd.io/PORTABLE_SERVICES">Portable Services</ulink> 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 <varname>ID=</varname> options
-      match, and either <varname>SYSEXT_LEVEL=</varname> exists and matches too, or if it is not present,
+      matches the base OS. This is typically implemented by checking that the extension <varname>ID=</varname>
+      option either matches the host <varname>ID=</varname> option or is included the host <varname>ID_LIKE=</varname>
+      option, and either <varname>SYSEXT_LEVEL=</varname> exists and matches too, or if it is not present,
       <varname>VERSION_ID=</varname> exists and matches. This ensures ABI/API compatibility between the
       layers and prevents merging of an incompatible image in an overlay.</para>
 
index 8ced4ead2e8325e9b8748c6dec5edddf0656f039..8226f8949310c17b06c44452ede2ba3a6579d435 100644 (file)
@@ -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 */
index 75e32abcce272889b807e525af0d7a9d28ea36e0..1e190961743c46a9c490caff528c05f5048f89aa 100644 (file)
@@ -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);
index 646bbfc6337285c2698a5dac01351222adb819ef..34877ffa565c3b01af7f39ab740b1ed163d7a985 100644 (file)
@@ -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);
index 54fdd8da0dd6d356ae6d99775756eb1bb2811fad..97431bca673bdbe970bb2338edbd6e0f526a0afb 100644 (file)
@@ -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;
index afaea7e3ae94642f05f5f420882e227354f10cfb..f2361b3fd1d390484927744755f986456f00c554 100644 (file)
@@ -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;
         }
 
index 8bc0359820a997afcefa3630105a02f534f6bf2e..08bdd7f33e3226e0d688288539e2fd844cdeda6f 100644 (file)
@@ -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,
index 44a523ac63ae445c141e0a11ed18b7a996036e99..a4aea64380c6f6e4794784837f543590907f212a 100644 (file)
@@ -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",
index b0f07926907d425dcfc5eddf141e7e7ae59d47e3..6bb106bac26b1464bb0618c281f8f4bde51499fe 100755 (executable)
@@ -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"