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