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>
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,
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 : "",
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;
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);
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 */
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;
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,
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);
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,
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);
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;
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,
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));
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;
}
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,
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;
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)
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",
{
echo "ID=testtest"
+ echo "ID_LIKE=\"foobar test_alike something-else\""
echo "VERSION=1.2.3"
} >"$root/usr/lib/os-release"
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:?}
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"