diff --git a/man/portablectl.xml b/man/portablectl.xml
index 5678171a17..531bc2dbfe 100644
--- a/man/portablectl.xml
+++ b/man/portablectl.xml
@@ -141,6 +141,11 @@
immediately started (blocking operation unless is passed) and/or enabled after
attaching the image.
+ 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
+ systemd.image-policy7.
+
diff --git a/src/portable/portable.c b/src/portable/portable.c
index b7cb9af1ff..715344f84d 100644
--- a/src/portable/portable.c
+++ b/src/portable/portable.c
@@ -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);
}
diff --git a/src/shared/image-policy.c b/src/shared/image-policy.c
index b5f9e699d6..59d1161d04 100644
--- a/src/shared/image-policy.c
+++ b/src/shared/image-policy.c
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
+#include "dissect-image.h"
#include "extract-word.h"
#include "image-policy.h"
#include "log.h"
@@ -176,6 +177,28 @@ PartitionPolicyFlags image_policy_get_exhaustively(const ImagePolicy *policy, Pa
return flags;
}
+static PartitionPolicyFlags policy_flag_from_fstype(const char *s) {
+ if (!s)
+ return _PARTITION_POLICY_FLAGS_INVALID;
+
+ if (streq(s, "btrfs"))
+ return PARTITION_POLICY_BTRFS;
+ if (streq(s, "erofs"))
+ return PARTITION_POLICY_EROFS;
+ if (streq(s, "ext4"))
+ return PARTITION_POLICY_EXT4;
+ if (streq(s, "f2fs"))
+ return PARTITION_POLICY_F2FS;
+ if (streq(s, "squashfs"))
+ return PARTITION_POLICY_SQUASHFS;
+ if (streq(s, "vfat"))
+ return PARTITION_POLICY_VFAT;
+ if (streq(s, "xfs"))
+ return PARTITION_POLICY_XFS;
+
+ return _PARTITION_POLICY_FLAGS_INVALID;
+}
+
static PartitionPolicyFlags policy_flag_from_string_one(const char *s) {
assert(s);
@@ -207,22 +230,8 @@ static PartitionPolicyFlags policy_flag_from_string_one(const char *s) {
return PARTITION_POLICY_GROWFS_ON;
if (streq(s, "growfs-off"))
return PARTITION_POLICY_GROWFS_OFF;
- if (streq(s, "btrfs"))
- return PARTITION_POLICY_BTRFS;
- if (streq(s, "erofs"))
- return PARTITION_POLICY_EROFS;
- if (streq(s, "ext4"))
- return PARTITION_POLICY_EXT4;
- if (streq(s, "f2fs"))
- return PARTITION_POLICY_F2FS;
- if (streq(s, "squashfs"))
- return PARTITION_POLICY_SQUASHFS;
- if (streq(s, "vfat"))
- return PARTITION_POLICY_VFAT;
- if (streq(s, "xfs"))
- return PARTITION_POLICY_XFS;
- return _PARTITION_POLICY_FLAGS_INVALID;
+ return policy_flag_from_fstype(s);
}
PartitionPolicyFlags partition_policy_flags_from_string(const char *s, bool graceful) {
@@ -275,6 +284,60 @@ static ImagePolicy* image_policy_new(size_t n_policies) {
return p;
}
+ImagePolicy* image_policy_new_from_dissected(const DissectedImage *image, const VeritySettings *verity) {
+ assert(image);
+
+ ImagePolicy *image_policy = image_policy_new(_PARTITION_DESIGNATOR_MAX);
+ if (!image_policy)
+ return NULL;
+
+ /* Default to 'absent', only what we find is allowed to be used */
+ image_policy->default_flags = PARTITION_POLICY_ABSENT;
+
+ for (PartitionDesignator pd = 0; pd < _PARTITION_DESIGNATOR_MAX; pd++) {
+ PartitionPolicyFlags f = 0;
+
+ if (!image->partitions[pd].found)
+ f |= PARTITION_POLICY_ABSENT;
+ else {
+ if (streq_ptr(image->partitions[pd].fstype, "crypto_LUKS"))
+ f |= PARTITION_POLICY_ENCRYPTED;
+ else {
+ PartitionPolicyFlags fstype_flag = policy_flag_from_fstype(image->partitions[pd].fstype);
+ if (fstype_flag >= 0)
+ f |= fstype_flag;
+ }
+
+ if (!verity || verity->designator < 0 || verity->designator == pd) {
+ if (image->verity_sig_ready || (verity && iovec_is_set(&verity->root_hash_sig)))
+ f |= PARTITION_POLICY_SIGNED;
+ else if (image->verity_ready || (verity && iovec_is_set(&verity->root_hash)))
+ f |= PARTITION_POLICY_VERITY;
+ }
+
+ if (!image->single_file_system) {
+ if (image->partitions[pd].growfs)
+ f |= PARTITION_POLICY_GROWFS_ON;
+ else
+ f |= PARTITION_POLICY_GROWFS_OFF;
+
+ if (image->partitions[pd].rw)
+ f |= PARTITION_POLICY_READ_ONLY_OFF;
+ else
+ f |= PARTITION_POLICY_READ_ONLY_ON;
+ }
+ }
+
+ image_policy->policies[pd] = (PartitionPolicy) {
+ .designator = pd,
+ .flags = f,
+ };
+ image_policy->n_policies++;
+ }
+
+ return image_policy;
+}
+
int image_policy_from_string(const char *s, bool graceful, ImagePolicy **ret) {
_cleanup_free_ ImagePolicy *p = NULL;
uint64_t dmask = 0;
@@ -785,21 +848,35 @@ static bool partition_policy_flags_has_unspecified(PartitionPolicyFlags flags) {
return false;
}
-int image_policy_intersect(const ImagePolicy *a, const ImagePolicy *b, ImagePolicy **ret) {
+static PartitionPolicyFlags policy_flags_or(PartitionPolicyFlags a, PartitionPolicyFlags b) {
+ return a | b;
+}
+
+static PartitionPolicyFlags policy_flags_and(PartitionPolicyFlags a, PartitionPolicyFlags b) {
+ return a & b;
+}
+
+static int policy_intersect_or_union(
+ const ImagePolicy *a,
+ const ImagePolicy *b,
+ PartitionPolicyFlags (*op)(PartitionPolicyFlags a, PartitionPolicyFlags b),
+ ImagePolicy **ret) {
+
_cleanup_(image_policy_freep) ImagePolicy *p = NULL;
- /* Calculates the intersection of the specified policies, i.e. only what is permitted in both. This
- * might fail with -ENAVAIL if the intersection is an "impossible policy". For example, if a root
- * partition my neither be used, nor be absent, nor be unused then this is considered
- * "impossible". */
+ assert(op);
+
+ /* Calculates the intersection or union of the specified policies, i.e. only what is permitted in
+ * both or either. This might fail with -ENAVAIL if the intersection is an "impossible policy". For
+ * example, if a root partition my neither be used, nor be absent, nor be unused then this is
+ * considered "impossible". */
p = image_policy_new(_PARTITION_DESIGNATOR_MAX);
if (!p)
return -ENOMEM;
- p->default_flags =
- partition_policy_flags_extend(image_policy_default(a)) &
- partition_policy_flags_extend(image_policy_default(b));
+ p->default_flags = op(partition_policy_flags_extend(image_policy_default(a)),
+ partition_policy_flags_extend(image_policy_default(b)));
if (partition_policy_flags_has_unspecified(p->default_flags)) /* Intersection empty? */
return -ENAVAIL;
@@ -824,10 +901,12 @@ int image_policy_intersect(const ImagePolicy *a, const ImagePolicy *b, ImagePoli
return y;
/* Mask it */
- z = x & y;
+ z = op(x, y);
- /* Check if the intersection is empty for this partition. If so, generate a clear error */
- if (partition_policy_flags_has_unspecified(z))
+ /* Check if the intersection is empty for this partition. If so, generate a clear error.
+ * If the partition has to be absent, then it won't have read-only/growfs flags, as
+ * image_policy_get_exhaustively() intentionally strips them, so skip the check. */
+ if (z != PARTITION_POLICY_ABSENT && partition_policy_flags_has_unspecified(z))
return -ENAVAIL;
df = partition_policy_normalized_flags(
@@ -857,6 +936,14 @@ int image_policy_intersect(const ImagePolicy *a, const ImagePolicy *b, ImagePoli
return 0;
}
+int image_policy_intersect(const ImagePolicy *a, const ImagePolicy *b, ImagePolicy **ret) {
+ return policy_intersect_or_union(a, b, policy_flags_and, ret);
+}
+
+int image_policy_union(const ImagePolicy *a, const ImagePolicy *b, ImagePolicy **ret) {
+ return policy_intersect_or_union(a, b, policy_flags_or, ret);
+}
+
ImagePolicy* image_policy_free(ImagePolicy *p) {
return mfree(p);
}
diff --git a/src/shared/image-policy.h b/src/shared/image-policy.h
index 218d2c5beb..fe2bf0d200 100644
--- a/src/shared/image-policy.h
+++ b/src/shared/image-policy.h
@@ -2,6 +2,7 @@
#pragma once
#include "conf-parser-forward.h"
+#include "dissect-image.h"
#include "shared-forward.h"
#include "gpt.h"
@@ -111,7 +112,9 @@ bool image_policy_equal(const ImagePolicy *a, const ImagePolicy *b); /* ch
int image_policy_equivalent(const ImagePolicy *a, const ImagePolicy *b); /* checks if the outcome is the same, i.e. for all partitions results in the same decisions. */
int image_policy_intersect(const ImagePolicy *a, const ImagePolicy *b, ImagePolicy **ret);
+int image_policy_union(const ImagePolicy *a, const ImagePolicy *b, ImagePolicy **ret);
+ImagePolicy* image_policy_new_from_dissected(const DissectedImage *image, const VeritySettings *verity);
ImagePolicy* image_policy_free(ImagePolicy *p);
DEFINE_TRIVIAL_CLEANUP_FUNC(ImagePolicy*, image_policy_free);
diff --git a/src/test/test-image-policy.c b/src/test/test-image-policy.c
index f285d31d9e..14a0da0c9c 100644
--- a/src/test/test-image-policy.c
+++ b/src/test/test-image-policy.c
@@ -142,14 +142,17 @@ TEST(extend) {
assert_se(partition_policy_flags_extend(PARTITION_POLICY_GROWFS_ON) == (PARTITION_POLICY_GROWFS_ON|_PARTITION_POLICY_USE_MASK|_PARTITION_POLICY_READ_ONLY_MASK));
}
-static void test_policy_intersect_one(const char *a, const char *b, const char *c) {
+static void test_policy_intersect_one(const char *a, const char *b, const char *c, bool intersect) {
_cleanup_(image_policy_freep) ImagePolicy *x = NULL, *y = NULL, *z = NULL, *t = NULL;
assert_se(image_policy_from_string(a, /* graceful= */ false, &x) >= 0);
assert_se(image_policy_from_string(b, /* graceful= */ false, &y) >= 0);
assert_se(image_policy_from_string(c, /* graceful= */ false, &z) >= 0);
- assert_se(image_policy_intersect(x, y, &t) >= 0);
+ if (intersect)
+ assert_se(image_policy_intersect(x, y, &t) >= 0);
+ else
+ assert_se(image_policy_union(x, y, &t) >= 0);
_cleanup_free_ char *s1 = NULL, *s2 = NULL, *s3 = NULL, *s4 = NULL;
assert_se(image_policy_to_string(x, false, &s1) >= 0);
@@ -157,21 +160,33 @@ static void test_policy_intersect_one(const char *a, const char *b, const char *
assert_se(image_policy_to_string(z, false, &s3) >= 0);
assert_se(image_policy_to_string(t, false, &s4) >= 0);
- log_info("%s ^ %s → %s vs. %s", s1, s2, s3, s4);
+ log_info("%s %s %s → %s vs. %s", s1, intersect ? "^" : "U", s2, s3, s4);
assert_se(image_policy_equivalent(z, t) > 0);
}
TEST(image_policy_intersect) {
- test_policy_intersect_one("", "", "");
- test_policy_intersect_one("-", "-", "-");
- test_policy_intersect_one("*", "*", "*");
- test_policy_intersect_one("~", "~", "~");
- test_policy_intersect_one("root=verity+signed", "root=signed+verity", "root=verity+signed");
- test_policy_intersect_one("root=verity+signed", "root=signed", "root=signed");
- test_policy_intersect_one("root=verity+signed", "root=verity", "root=verity");
- test_policy_intersect_one("root=open", "root=verity", "root=verity");
- test_policy_intersect_one("root=open", "=verity+ignore", "root=verity+ignore:=ignore");
+ test_policy_intersect_one("", "", "", /* intersect= */ true);
+ test_policy_intersect_one("-", "-", "-", /* intersect= */ true);
+ test_policy_intersect_one("*", "*", "*", /* intersect= */ true);
+ test_policy_intersect_one("~", "~", "~", /* intersect= */ true);
+ test_policy_intersect_one("root=verity+signed", "root=signed+verity", "root=verity+signed", /* intersect= */ true);
+ test_policy_intersect_one("root=verity+signed", "root=signed", "root=signed", /* intersect= */ true);
+ test_policy_intersect_one("root=verity+signed", "root=verity", "root=verity", /* intersect= */ true);
+ test_policy_intersect_one("root=open", "root=verity", "root=verity", /* intersect= */ true);
+ test_policy_intersect_one("root=open", "=verity+ignore", "root=verity+ignore:=ignore", /* intersect= */ true);
+}
+
+TEST(image_policy_union) {
+ test_policy_intersect_one("root=verity+signed", "root=signed+verity", "root=verity+signed", /* intersect= */ false);
+ test_policy_intersect_one("root=verity+signed", "root=signed", "root=verity+signed", /* intersect= */ false);
+ test_policy_intersect_one("root=verity+signed", "root=verity", "root=verity+signed", /* intersect= */ false);
+ test_policy_intersect_one("root=signed", "root=verity", "root=verity+signed", /* intersect= */ false);
+ test_policy_intersect_one("root=signed:=absent", "root=verity:=unused", "root=verity+signed", /* intersect= */ false);
+ test_policy_intersect_one("root=open", "root=verity", "root=open", /* intersect= */ false);
+ test_policy_intersect_one("root=open", "=verity+ignore", "root=open:=verity+ignore", /* intersect= */ false);
+ test_policy_intersect_one("root=open:usr=absent", "root=open:usr=absent", "root=open:usr=absent", /* intersect= */ false);
+
}
static void test_policy_ignore_designators_one(const char *a, const PartitionDesignator array[], size_t n, const char *b) {
@@ -265,4 +280,382 @@ TEST(partition_policy_determine_fstype) {
ASSERT_FALSE(encrypted);
}
+TEST(image_policy_new_from_dissected) {
+ _cleanup_(image_policy_freep) ImagePolicy *policy = NULL;
+ DissectedImage image;
+ VeritySettings verity;
+ uint8_t dummy_data[4];
+
+ /* Test 1: Empty image - all partitions should be absent */
+ image = (DissectedImage) {};
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(image_policy_default(policy), PARTITION_POLICY_ABSENT);
+ ASSERT_EQ(image_policy_n_entries(policy), (size_t) _PARTITION_DESIGNATOR_MAX);
+
+ /* All partitions should have PARTITION_POLICY_ABSENT */
+ for (PartitionDesignator pd = 0; pd < _PARTITION_DESIGNATOR_MAX; pd++)
+ ASSERT_EQ(policy->policies[pd].flags, (PartitionPolicyFlags) PARTITION_POLICY_ABSENT);
+
+ policy = image_policy_free(policy);
+
+ /* Test 2: Image with a single ext4 root partition */
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "ext4",
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_EXT4 | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+ ASSERT_EQ(policy->policies[PARTITION_USR].flags, (PartitionPolicyFlags) PARTITION_POLICY_ABSENT);
+
+ policy = image_policy_free(policy);
+
+ /* Test 3: Image with encrypted root partition (LUKS) */
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "crypto_LUKS",
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_ENCRYPTED | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 4: Image with verity ready (without signature) */
+ image = (DissectedImage) {
+ .verity_ready = true,
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "squashfs",
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_SQUASHFS | PARTITION_POLICY_VERITY | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 5: Image with verity signature ready */
+ image = (DissectedImage) {
+ .verity_sig_ready = true,
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "erofs",
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_EROFS | PARTITION_POLICY_SIGNED | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 6: Image with growfs enabled */
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "btrfs",
+ .growfs = true,
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_BTRFS | PARTITION_POLICY_GROWFS_ON | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 7: Multiple partitions with different filesystems */
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "ext4",
+ },
+ [PARTITION_USR] = {
+ .found = true,
+ .fstype = (char*) "xfs",
+ },
+ [PARTITION_HOME] = {
+ .found = true,
+ .fstype = (char*) "btrfs",
+ .growfs = true,
+ },
+ [PARTITION_ESP] = {
+ .found = true,
+ .fstype = (char*) "vfat",
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_EXT4 | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+ ASSERT_EQ(policy->policies[PARTITION_USR].flags, (PartitionPolicyFlags) (PARTITION_POLICY_XFS | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+ ASSERT_EQ(policy->policies[PARTITION_HOME].flags, (PartitionPolicyFlags) (PARTITION_POLICY_BTRFS | PARTITION_POLICY_GROWFS_ON | PARTITION_POLICY_READ_ONLY_ON));
+ ASSERT_EQ(policy->policies[PARTITION_ESP].flags, (PartitionPolicyFlags) (PARTITION_POLICY_VFAT | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+ ASSERT_EQ(policy->policies[PARTITION_SWAP].flags, (PartitionPolicyFlags) PARTITION_POLICY_ABSENT);
+
+ policy = image_policy_free(policy);
+
+ /* Test 8: VeritySettings with root_hash set (no signature) */
+ dummy_data[0] = 0xde; dummy_data[1] = 0xad; dummy_data[2] = 0xbe; dummy_data[3] = 0xef;
+ verity = (VeritySettings) {
+ .designator = _PARTITION_DESIGNATOR_INVALID,
+ .root_hash = IOVEC_MAKE(dummy_data, sizeof(dummy_data)),
+ };
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "squashfs",
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, &verity);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_SQUASHFS | PARTITION_POLICY_VERITY | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 9: VeritySettings with root_hash_sig set */
+ dummy_data[0] = 0x01; dummy_data[1] = 0x02; dummy_data[2] = 0x03; dummy_data[3] = 0x04;
+ verity = (VeritySettings) {
+ .designator = _PARTITION_DESIGNATOR_INVALID,
+ .root_hash_sig = IOVEC_MAKE(dummy_data, sizeof(dummy_data)),
+ };
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "erofs",
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, &verity);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_EROFS | PARTITION_POLICY_SIGNED | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 10: VeritySettings with designator targeting specific partition */
+ dummy_data[0] = 0xab; dummy_data[1] = 0xcd;
+ verity = (VeritySettings) {
+ .designator = PARTITION_USR,
+ .root_hash = IOVEC_MAKE(dummy_data, 2),
+ };
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "ext4",
+ },
+ [PARTITION_USR] = {
+ .found = true,
+ .fstype = (char*) "squashfs",
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, &verity);
+ ASSERT_NOT_NULL(policy);
+ /* Root should NOT have verity since verity targets USR */
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_EXT4 | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+ /* USR should have verity */
+ ASSERT_EQ(policy->policies[PARTITION_USR].flags, (PartitionPolicyFlags) (PARTITION_POLICY_SQUASHFS | PARTITION_POLICY_VERITY | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 11: Unknown filesystem type (should have no fstype flag) */
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "unknown_fs",
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ /* Should only have GROWFS_OFF and READ_ONLY_ON for unknown filesystem */
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 12: NULL filesystem type (should have no fstype flag) */
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = NULL,
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ /* Should only have GROWFS_OFF and READ_ONLY_ON for NULL fstype */
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 13: Combination of verity_ready from image and verity settings - image takes precedence for sig */
+ dummy_data[0] = 0x11; dummy_data[1] = 0x22;
+ verity = (VeritySettings) {
+ .designator = _PARTITION_DESIGNATOR_INVALID,
+ .root_hash = IOVEC_MAKE(dummy_data, 2),
+ };
+ image = (DissectedImage) {
+ .verity_sig_ready = true, /* This should take precedence */
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "squashfs",
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, &verity);
+ ASSERT_NOT_NULL(policy);
+ /* verity_sig_ready should result in SIGNED, not just VERITY */
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_SQUASHFS | PARTITION_POLICY_SIGNED | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 14: All known filesystem types */
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_ROOT] = { .found = true, .fstype = (char*) "ext4" },
+ [PARTITION_USR] = { .found = true, .fstype = (char*) "btrfs" },
+ [PARTITION_HOME] = { .found = true, .fstype = (char*) "xfs" },
+ [PARTITION_SRV] = { .found = true, .fstype = (char*) "f2fs" },
+ [PARTITION_VAR] = { .found = true, .fstype = (char*) "erofs" },
+ [PARTITION_TMP] = { .found = true, .fstype = (char*) "squashfs" },
+ [PARTITION_ESP] = { .found = true, .fstype = (char*) "vfat" },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_EXT4 | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+ ASSERT_EQ(policy->policies[PARTITION_USR].flags, (PartitionPolicyFlags) (PARTITION_POLICY_BTRFS | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+ ASSERT_EQ(policy->policies[PARTITION_HOME].flags, (PartitionPolicyFlags) (PARTITION_POLICY_XFS | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+ ASSERT_EQ(policy->policies[PARTITION_SRV].flags, (PartitionPolicyFlags) (PARTITION_POLICY_F2FS | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+ ASSERT_EQ(policy->policies[PARTITION_VAR].flags, (PartitionPolicyFlags) (PARTITION_POLICY_EROFS | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+ ASSERT_EQ(policy->policies[PARTITION_TMP].flags, (PartitionPolicyFlags) (PARTITION_POLICY_SQUASHFS | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+ ASSERT_EQ(policy->policies[PARTITION_ESP].flags, (PartitionPolicyFlags) (PARTITION_POLICY_VFAT | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 15: Encrypted partition with verity (LUKS takes precedence, no verity flag) */
+ image = (DissectedImage) {
+ .verity_ready = true,
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "crypto_LUKS",
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ /* crypto_LUKS check happens first, then verity is added */
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_ENCRYPTED | PARTITION_POLICY_VERITY | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 16: Multiple flags combined - encrypted + growfs */
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_HOME] = {
+ .found = true,
+ .fstype = (char*) "crypto_LUKS",
+ .growfs = true,
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(policy->policies[PARTITION_HOME].flags, (PartitionPolicyFlags) (PARTITION_POLICY_ENCRYPTED | PARTITION_POLICY_GROWFS_ON | PARTITION_POLICY_READ_ONLY_ON));
+
+ policy = image_policy_free(policy);
+
+ /* Test 17: single_file_system=true should NOT set growfs or read-only flags */
+ image = (DissectedImage) {
+ .single_file_system = true,
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "ext4",
+ .rw = true,
+ .growfs = true,
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ /* single_file_system=true means no growfs/read-only flags */
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) PARTITION_POLICY_EXT4);
+
+ policy = image_policy_free(policy);
+
+ /* Test 18: rw=true should set READ_ONLY_OFF instead of READ_ONLY_ON */
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "ext4",
+ .rw = true,
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_EXT4 | PARTITION_POLICY_GROWFS_OFF | PARTITION_POLICY_READ_ONLY_OFF));
+
+ policy = image_policy_free(policy);
+
+ /* Test 19: rw=true with growfs=true */
+ image = (DissectedImage) {
+ .partitions = {
+ [PARTITION_ROOT] = {
+ .found = true,
+ .fstype = (char*) "btrfs",
+ .rw = true,
+ .growfs = true,
+ },
+ },
+ };
+
+ policy = image_policy_new_from_dissected(&image, /* verity= */ NULL);
+ ASSERT_NOT_NULL(policy);
+ ASSERT_EQ(policy->policies[PARTITION_ROOT].flags, (PartitionPolicyFlags) (PARTITION_POLICY_BTRFS | PARTITION_POLICY_GROWFS_ON | PARTITION_POLICY_READ_ONLY_OFF));
+}
+
DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/test/units/TEST-29-PORTABLE.image.sh b/test/units/TEST-29-PORTABLE.image.sh
index d034e2c48c..8b930d40f2 100755
--- a/test/units/TEST-29-PORTABLE.image.sh
+++ b/test/units/TEST-29-PORTABLE.image.sh
@@ -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