]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
portable: pin attached image via image-policy 40152/head
authorLuca Boccassi <luca.boccassi@gmail.com>
Fri, 19 Dec 2025 17:02:03 +0000 (17:02 +0000)
committerLuca Boccassi <luca.boccassi@gmail.com>
Mon, 19 Jan 2026 14:54:12 +0000 (15:54 +0100)
When attaching images generate a policy in the portable drop-in
that matches the partition types and content found while dissecting,
so that it can no longer be changed later without a reattach.

man/portablectl.xml
src/portable/portable.c
test/units/TEST-29-PORTABLE.image.sh

index 5678171a170aa200951278148f8e7516934b7bdb..531bc2dbfe33b864e3f6a17a9a17f0a2d6fbd6dc 100644 (file)
         immediately started (blocking operation unless <option>--no-block</option> is passed) and/or enabled after
         attaching the image.</para>
 
+        <para>When images are attached, an image policy will be generated that pins the attached image by
+        the exact content that was found while attaching, so that it cannot be swapped downgrading security
+        (e.g.: removing dm-verity protection) without a full reinstallation. For more details on policies, see
+        <citerefentry><refentrytitle>systemd.image-policy</refentrytitle><manvolnum>7</manvolnum></citerefentry>.</para>
+
         <xi:include href="vpick.xml" xpointer="image"/>
         <xi:include href="vpick.xml" xpointer="directory"/>
         <xi:include href="version-info.xml" xpointer="v239"/>
index b7cb9af1ff7cffcb26b78059e2c5b247fd245b7f..715344f84d9501d1d14db7f0f14f5bd0efaeff97 100644 (file)
@@ -27,6 +27,7 @@
 #include "fileio.h"
 #include "fs-util.h"
 #include "glyph-util.h"
+#include "image-policy.h"
 #include "install.h"
 #include "iovec-util.h"
 #include "libmount-util.h"
@@ -364,10 +365,12 @@ static int portable_extract_by_path(
                 const ImagePolicy *image_policy,
                 PortableMetadata **ret_os_release,
                 Hashmap **ret_unit_files,
+                ImagePolicy **ret_pinned_image_policy,
                 sd_bus_error *error) {
 
         _cleanup_hashmap_free_ Hashmap *unit_files = NULL;
         _cleanup_(portable_metadata_unrefp) PortableMetadata* os_release = NULL;
+        _cleanup_(image_policy_freep) ImagePolicy *pinned_image_policy = NULL;
         _cleanup_(loop_device_unrefp) LoopDevice *d = NULL;
         int r;
 
@@ -397,6 +400,7 @@ static int portable_extract_by_path(
         } else if (r < 0)
                 return log_debug_errno(r, "Failed to set up loopback device for %s: %m", path);
         else {
+                _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT;
                 _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL;
                 _cleanup_(rmdir_and_freep) char *tmpdir = NULL;
                 _cleanup_close_pair_ int seq[2] = EBADF_PAIR;
@@ -449,6 +453,24 @@ static int portable_extract_by_path(
                 if (r < 0)
                         return r;
 
+                r = verity_settings_load(&verity, path, NULL, NULL);
+                if (r < 0)
+                        return log_debug_errno(r, "Failed to load root hash: %m");
+
+                r = dissected_image_load_verity_sig_partition(m, d->fd, &verity);
+                if (r < 0)
+                        return r;
+
+                r = dissected_image_guess_verity_roothash(m, &verity);
+                if (r < 0)
+                        return r;
+
+                if (ret_pinned_image_policy) {
+                        pinned_image_policy = image_policy_new_from_dissected(m, &verity);
+                        if (!pinned_image_policy)
+                                return -ENOMEM;
+                }
+
                 if (socketpair(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC, 0, seq) < 0)
                         return log_debug_errno(errno, "Failed to allocated SOCK_SEQPACKET socket: %m");
 
@@ -558,6 +580,9 @@ static int portable_extract_by_path(
         if (ret_os_release)
                 *ret_os_release = TAKE_PTR(os_release);
 
+        if (ret_pinned_image_policy)
+                *ret_pinned_image_policy = TAKE_PTR(pinned_image_policy);
+
         return 0;
 }
 
@@ -575,9 +600,12 @@ static int extract_image_and_extensions(
                 PortableMetadata **ret_os_release,
                 Hashmap **ret_unit_files,
                 char ***ret_valid_prefixes,
+                ImagePolicy **ret_pinned_root_image_policy,
+                ImagePolicy **ret_pinned_ext_image_policy,
                 sd_bus_error *error) {
 
         _cleanup_free_ char *id = NULL, *id_like = NULL, *version_id = NULL, *sysext_level = NULL, *confext_level = NULL;
+        _cleanup_(image_policy_freep) ImagePolicy *pinned_root_image_policy = NULL, *pinned_ext_image_policy = 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;
@@ -669,6 +697,7 @@ static int extract_image_and_extensions(
                         image_policy,
                         &os_release,
                         &unit_files,
+                        &pinned_root_image_policy,
                         error);
         if (r < 0)
                 return r;
@@ -700,6 +729,7 @@ static int extract_image_and_extensions(
 
         ORDERED_HASHMAP_FOREACH(ext, extension_images) {
                 _cleanup_(portable_metadata_unrefp) PortableMetadata *extension_release_meta = NULL;
+                _cleanup_(image_policy_freep) ImagePolicy *policy = NULL;
                 _cleanup_hashmap_free_ Hashmap *extra_unit_files = NULL;
                 _cleanup_strv_free_ char **extension_release = NULL;
                 const char *e;
@@ -713,6 +743,7 @@ static int extract_image_and_extensions(
                                 image_policy,
                                 &extension_release_meta,
                                 &extra_unit_files,
+                                &policy,
                                 error);
                 if (r < 0)
                         return r;
@@ -721,6 +752,19 @@ static int extract_image_and_extensions(
                 if (r < 0)
                         return r;
 
+                if (!pinned_ext_image_policy && policy)
+                        pinned_ext_image_policy = TAKE_PTR(policy);
+                else if (policy) {
+                        _cleanup_(image_policy_freep) ImagePolicy *intersected_policy = NULL;
+
+                        /* There is a single policy for all extension images, so we need a union */
+                        r = image_policy_union(pinned_ext_image_policy, policy, &intersected_policy);
+                        if (r < 0)
+                                return log_debug_errno(r, "Failed to merge extension image policies: %m");
+
+                        free_and_replace(pinned_ext_image_policy, intersected_policy);
+                }
+
                 if (!validate_extension && !ret_valid_prefixes && !ret_extension_releases)
                         continue;
 
@@ -768,6 +812,10 @@ static int extract_image_and_extensions(
                 *ret_unit_files = TAKE_PTR(unit_files);
         if (ret_valid_prefixes)
                 *ret_valid_prefixes = TAKE_PTR(valid_prefixes);
+        if (ret_pinned_root_image_policy)
+                *ret_pinned_root_image_policy = TAKE_PTR(pinned_root_image_policy);
+        if (ret_pinned_ext_image_policy)
+                *ret_pinned_ext_image_policy = TAKE_PTR(pinned_ext_image_policy);
 
         return 0;
 }
@@ -808,6 +856,8 @@ int portable_extract(
                         &os_release,
                         &unit_files,
                         ret_valid_prefixes ? &valid_prefixes : NULL,
+                        /* pinned_root_image_policy= */ NULL,
+                        /* pinned_ext_image_policy= */ NULL,
                         error);
         if (r < 0)
                 return r;
@@ -1110,6 +1160,8 @@ static int install_chroot_dropin(
                 ImageType type,
                 OrderedHashmap *extension_images,
                 OrderedHashmap *extension_releases,
+                const ImagePolicy *pinned_root_image_policy,
+                const ImagePolicy *pinned_ext_image_policy,
                 const PortableMetadata *m,
                 const PortableMetadata *os_release,
                 const char *dropin_dir,
@@ -1152,6 +1204,18 @@ static int install_chroot_dropin(
                                "LogExtraFields=PORTABLE=", base_name, "\n"))
                         return -ENOMEM;
 
+                if (pinned_root_image_policy) {
+                        _cleanup_free_ char *policy_str = NULL;
+
+                        r = image_policy_to_string(pinned_root_image_policy, /* simplify= */ true, &policy_str);
+                        if (r < 0)
+                                return log_debug_errno(r, "Failed to serialize pinned image policy: %m");
+
+                        if (!strextend(&text,
+                                       "RootImagePolicy=", policy_str, "\n"))
+                                return -ENOMEM;
+                }
+
                 /* If we have a single image then PORTABLE= will point to it, so we add
                  * PORTABLE_NAME_AND_VERSION= with the os-release fields and we are done. But if we have
                  * extensions, PORTABLE= will point to the image where the current unit was found in. So we
@@ -1201,6 +1265,18 @@ static int install_chroot_dropin(
                                                "LogExtraFields=PORTABLE_EXTENSION=", extension_base_name, "\n"))
                                         return -ENOMEM;
 
+                                if (pinned_ext_image_policy) {
+                                        _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
@@ -1317,6 +1393,8 @@ static int attach_unit_file(
                 ImageType type,
                 OrderedHashmap *extension_images,
                 OrderedHashmap *extension_releases,
+                const ImagePolicy *pinned_root_image_policy,
+                const ImagePolicy *pinned_ext_image_policy,
                 const PortableMetadata *m,
                 const PortableMetadata *os_release,
                 const char *profile,
@@ -1362,7 +1440,20 @@ static int attach_unit_file(
          * is reloaded while we are creating things here: as long as only the drop-ins exist the unit doesn't exist at
          * all for PID 1. */
 
-        r = install_chroot_dropin(image_path, type, extension_images, extension_releases, m, os_release, dropin_dir, flags, &chroot_dropin, changes, n_changes);
+        r = install_chroot_dropin(
+                        image_path,
+                        type,
+                        extension_images,
+                        extension_releases,
+                        pinned_root_image_policy,
+                        pinned_ext_image_policy,
+                        m,
+                        os_release,
+                        dropin_dir,
+                        flags,
+                        &chroot_dropin,
+                        changes,
+                        n_changes);
         if (r < 0)
                 return r;
 
@@ -1631,6 +1722,7 @@ int portable_attach(
                 size_t *n_changes,
                 sd_bus_error *error) {
 
+        _cleanup_(image_policy_freep) ImagePolicy *pinned_root_image_policy = NULL, *pinned_ext_image_policy = NULL;
         _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL;
         _cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
         _cleanup_hashmap_free_ Hashmap *unit_files = NULL;
@@ -1656,6 +1748,8 @@ int portable_attach(
                         &os_release,
                         &unit_files,
                         &valid_prefixes,
+                        &pinned_root_image_policy,
+                        &pinned_ext_image_policy,
                         error);
         if (r < 0)
                 return r;
@@ -1720,8 +1814,20 @@ int portable_attach(
                 }
 
         HASHMAP_FOREACH(item, unit_files) {
-                r = attach_unit_file(&paths, image->path, image->type, extension_images, extension_releases,
-                                     item, os_release, profile, flags, changes, n_changes);
+                r = attach_unit_file(
+                                &paths,
+                                image->path,
+                                image->type,
+                                extension_images,
+                                extension_releases,
+                                pinned_root_image_policy,
+                                pinned_ext_image_policy,
+                                item,
+                                os_release,
+                                profile,
+                                flags,
+                                changes,
+                                n_changes);
                 if (r < 0)
                         return sd_bus_error_set_errnof(error, r, "Failed to attach unit '%s': %m", item->name);
         }
index d034e2c48c5a4ddad8180a41815cbac162ab9e7b..8b930d40f2c433290822457f7c47f5bb8b490b1d 100755 (executable)
@@ -25,6 +25,10 @@ systemctl is-active minimal-app0.service
 systemctl is-active minimal-app0-foo.service
 systemctl is-active minimal-app0-bar.service && exit 1
 
+# Ensure pinning by policy works
+cat /run/systemd/system.attached/minimal-app0-foo.service.d/20-portable.conf
+grep -q -F 'root=signed+squashfs:' /run/systemd/system.attached/minimal-app0-foo.service.d/20-portable.conf
+
 portablectl "${ARGS[@]}" reattach --now --runtime /usr/share/minimal_1.raw minimal-app0
 
 portablectl is-attached minimal-app0
@@ -91,6 +95,9 @@ status="$(portablectl is-attached --extension app0 minimal_0)"
 grep -q -F "LogExtraFields=PORTABLE_ROOT=minimal_0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
 grep -q -F "LogExtraFields=PORTABLE_EXTENSION=app0.raw" /run/systemd/system.attached/app0.service.d/20-portable.conf
 grep -q -F "LogExtraFields=PORTABLE_EXTENSION_NAME_AND_VERSION=app" /run/systemd/system.attached/app0.service.d/20-portable.conf
+# Ensure pinning by policy works
+grep -q -F 'RootImagePolicy=root=signed+squashfs:' /run/systemd/system.attached/app0.service.d/20-portable.conf >/dev/null
+grep -q -F 'ExtensionImagePolicy=root=signed+squashfs:' /run/systemd/system.attached/app0.service.d/20-portable.conf >/dev/null
 
 portablectl "${ARGS[@]}" reattach --now --runtime --extension /tmp/app0.raw /usr/share/minimal_1.raw app0