From 5a04e542c283a6331016739147a8639725e586fe Mon Sep 17 00:00:00 2001 From: Rodrigo Campos Date: Fri, 27 Feb 2026 18:10:40 +0100 Subject: [PATCH 1/2] portable: Add ExtensionImage drop-in for any extension Before this patch, when running: portablectl attach --extension ext.raw ./base.raw No drop-in is added for the "ExtensionImages" if there aren't units from the extension loaded. But the extension can just overlay files, as in my case. So before this patch, I also need to manually add a drop-in with "ExtensionImages=" for it to really be loaded. Let's just always add the drop-in for extensions. This way, it works for extensions that just overlay files too. Please note this commit just removes the if (simpler to view the diff with git show -w). Also, the if checked for m->image_path being not NULL, but removing it shouldn't cause a NULL pointer dereference. Because m->image_path is not used inside the if (it was needed just for the if itself) and image_path is asserted at the beginning of the function to be non-NULL too. This was like this since the beginning of time in 907952bbc9 ("portabled: add --extension parameter for layered images support") --- src/portable/portable.c | 103 ++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 52 deletions(-) diff --git a/src/portable/portable.c b/src/portable/portable.c index 2f6db85ff9..df505bbe2d 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -1491,65 +1491,64 @@ static int install_chroot_dropin( if (r < 0) return r; - if (m->image_path && !path_equal(m->image_path, image_path)) - ORDERED_HASHMAP_FOREACH(ext, extension_images) { + ORDERED_HASHMAP_FOREACH(ext, extension_images) { - const char *extension_setting = extension_setting_from_image(ext->type); - if (!extension_setting) - return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Image type '%s' not supported for extensions: %m", image_type_to_string(ext->type)); + const char *extension_setting = extension_setting_from_image(ext->type); + if (!extension_setting) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Image type '%s' not supported for extensions: %m", image_type_to_string(ext->type)); - _cleanup_free_ char *extension_base_name = NULL; - r = path_extract_filename(ext->path, &extension_base_name); + _cleanup_free_ char *extension_base_name = NULL; + r = path_extract_filename(ext->path, &extension_base_name); + if (r < 0) + return log_debug_errno(r, "Failed to extract basename from '%s': %m", ext->path); + + if (!strextend(&text, + "\n", + extension_setting, + ext->path, + /* With --force tell PID1 to avoid enforcing that the image and + * extension-release. have to match. */ + !IN_SET(ext->type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) && + FLAGS_SET(flags, PORTABLE_FORCE_EXTENSION) ? + ":x-systemd.relax-extension-release-check\n" : + "\n", + /* In PORTABLE= we list the 'main' image name for this unit + * (the image where the unit was extracted from), but we are + * stacking multiple images, so list those too. */ + "LogExtraFields=PORTABLE_EXTENSION=", extension_base_name, "\n")) + return -ENOMEM; + + if (pinned_ext_image_policy && !IN_SET(ext->type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME)) { + _cleanup_free_ char *policy_str = NULL; + + r = image_policy_to_string(pinned_ext_image_policy, /* simplify= */ true, &policy_str); if (r < 0) - return log_debug_errno(r, "Failed to extract basename from '%s': %m", ext->path); + return log_debug_errno(r, "Failed to serialize pinned image policy: %m"); if (!strextend(&text, - "\n", - extension_setting, - ext->path, - /* With --force tell PID1 to avoid enforcing that the image and - * extension-release. have to match. */ - !IN_SET(ext->type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME) && - FLAGS_SET(flags, PORTABLE_FORCE_EXTENSION) ? - ":x-systemd.relax-extension-release-check\n" : - "\n", - /* In PORTABLE= we list the 'main' image name for this unit - * (the image where the unit was extracted from), but we are - * stacking multiple images, so list those too. */ - "LogExtraFields=PORTABLE_EXTENSION=", extension_base_name, "\n")) + "ExtensionImagePolicy=", policy_str, "\n")) return -ENOMEM; - - if (pinned_ext_image_policy && !IN_SET(ext->type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME)) { - _cleanup_free_ char *policy_str = NULL; - - r = image_policy_to_string(pinned_ext_image_policy, /* simplify= */ true, &policy_str); - if (r < 0) - return log_debug_errno(r, "Failed to serialize pinned image policy: %m"); - - if (!strextend(&text, - "ExtensionImagePolicy=", policy_str, "\n")) - return -ENOMEM; - } - - /* Look for image/version identifiers in the extension release files. We - * look for all possible IDs, but typically only 1 or 2 will be set, so - * the number of fields added shouldn't be too large. We prefix the DDI - * name to the value, so that we can add the same field multiple times and - * still be able to identify what applies to what. */ - r = append_release_log_fields(&text, - ordered_hashmap_get(extension_releases, ext->name), - IMAGE_SYSEXT, - "PORTABLE_EXTENSION_NAME_AND_VERSION"); - if (r < 0) - return r; - - r = append_release_log_fields(&text, - ordered_hashmap_get(extension_releases, ext->name), - IMAGE_CONFEXT, - "PORTABLE_EXTENSION_NAME_AND_VERSION"); - if (r < 0) - return r; } + + /* Look for image/version identifiers in the extension release files. We + * look for all possible IDs, but typically only 1 or 2 will be set, so + * the number of fields added shouldn't be too large. We prefix the DDI + * name to the value, so that we can add the same field multiple times and + * still be able to identify what applies to what. */ + r = append_release_log_fields(&text, + ordered_hashmap_get(extension_releases, ext->name), + IMAGE_SYSEXT, + "PORTABLE_EXTENSION_NAME_AND_VERSION"); + if (r < 0) + return r; + + r = append_release_log_fields(&text, + ordered_hashmap_get(extension_releases, ext->name), + IMAGE_CONFEXT, + "PORTABLE_EXTENSION_NAME_AND_VERSION"); + if (r < 0) + return r; + } } r = write_string_file(dropin, text, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_SYNC); From 9c56f3020d87c027f6b401877237cfef4b38ef06 Mon Sep 17 00:00:00 2001 From: Rodrigo Campos Date: Mon, 2 Mar 2026 16:02:06 +0100 Subject: [PATCH 2/2] test/portable: Ensure ExtensionImages is set for any dep The previous commit made portablectl attach add a drop-in for any extension image. Let's add a test for that too. --- test/units/TEST-29-PORTABLE.image.sh | 13 +++++++++++++ test/units/util.sh | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/test/units/TEST-29-PORTABLE.image.sh b/test/units/TEST-29-PORTABLE.image.sh index 36d187a752..3018e86ecd 100755 --- a/test/units/TEST-29-PORTABLE.image.sh +++ b/test/units/TEST-29-PORTABLE.image.sh @@ -212,6 +212,19 @@ portablectl inspect --force --cat --extension /tmp/app0.raw --extension /tmp/con portablectl detach --now --runtime --extension /tmp/app0.raw --extension /tmp/conf0.raw /usr/share/minimal_0.raw app0 +# Ensure that ExtensionImages= is added to the drop-in even when the extension has no unit files +# (all units come from the base image). +portablectl "${ARGS[@]}" attach --force --now --runtime --extension /tmp/app-data-only.raw /usr/share/minimal_0.raw minimal-app0 + +systemctl is-active minimal-app0.service +status="$(portablectl is-attached --extension app-data-only minimal_0)" +[[ "${status}" == "running-runtime" ]] + +grep -q -F "ExtensionImages=/tmp/app-data-only.raw" /run/systemd/system.attached/minimal-app0.service.d/20-portable.conf +grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app-data-only.raw" /run/systemd/system.attached/minimal-app0.service.d/20-portable.conf + +portablectl detach --now --runtime --extension /tmp/app-data-only.raw /usr/share/minimal_0.raw minimal-app0 + # Ensure that mixed mode copies the images and units (client-owned) but symlinks the profile (OS owned) portablectl "${ARGS[@]}" attach --copy=mixed --runtime --extension /tmp/app0.raw /usr/share/minimal_0.raw app0 test -f /run/portables/app0.raw diff --git a/test/units/util.sh b/test/units/util.sh index 6f03f5e339..d9e561e791 100755 --- a/test/units/util.sh +++ b/test/units/util.sh @@ -401,6 +401,18 @@ EOF echo MARKER=1 >"$initdir/usr/lib/systemd/system/other_file" mksquashfs "$initdir" /tmp/app1.raw -noappend + # Create a data-only extension image (no unit files) to test that + # ExtensionImages= is added to the drop-in even when the extension + # does not carry any units. + initdir="/var/tmp/app-data-only" + mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/opt" + ( + echo "ID=_any" + echo "ARCHITECTURE=_any" + ) >"$initdir/usr/lib/extension-release.d/extension-release.app-data-only" + echo "MARKER_DATA_ONLY=1" >"$initdir/opt/data-file" + mksquashfs "$initdir" /tmp/app-data-only.raw -noappend + initdir="/var/tmp/app-nodistro" mkdir -p "$initdir/usr/lib/extension-release.d" "$initdir/usr/lib/systemd/system" (