diff options
Diffstat (limited to 'src/shared')
-rw-r--r-- | src/shared/bus-unit-util.c | 5 | ||||
-rw-r--r-- | src/shared/discover-image.c | 34 | ||||
-rw-r--r-- | src/shared/discover-image.h | 3 | ||||
-rw-r--r-- | src/shared/dissect-image.c | 428 | ||||
-rw-r--r-- | src/shared/dissect-image.h | 22 | ||||
-rw-r--r-- | src/shared/image-policy.c | 689 | ||||
-rw-r--r-- | src/shared/image-policy.h | 97 | ||||
-rw-r--r-- | src/shared/meson.build | 1 | ||||
-rw-r--r-- | src/shared/mount-util.c | 10 | ||||
-rw-r--r-- | src/shared/mount-util.h | 2 |
10 files changed, 1176 insertions, 115 deletions
diff --git a/src/shared/bus-unit-util.c b/src/shared/bus-unit-util.c index 3307691f28..1c991ae54f 100644 --- a/src/shared/bus-unit-util.c +++ b/src/shared/bus-unit-util.c @@ -959,7 +959,10 @@ static int bus_append_execute_property(sd_bus_message *m, const char *field, con "ProcSubset", "NetworkNamespacePath", "IPCNamespacePath", - "LogNamespace")) + "LogNamespace", + "RootImagePolicy", + "MountImagePolicy", + "ExtensionImagePolicy")) return bus_append_string(m, field, eq); if (STR_IN_SET(field, "IgnoreSIGPIPE", diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index d0b3245a27..ac6a8033dd 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -28,6 +28,7 @@ #include "hashmap.h" #include "hostname-setup.h" #include "id128-util.h" +#include "initrd-util.h" #include "lock-util.h" #include "log.h" #include "loop-util.h" @@ -73,6 +74,19 @@ static const char* const image_search_path[_IMAGE_CLASS_MAX] = { "/usr/lib/confexts\0", }; +/* Inside the initrd, use a slightly different set of search path (i.e. include .extra/sysext in extension + * search dir) */ +static const char* const image_search_path_initrd[_IMAGE_CLASS_MAX] = { + /* (entries that aren't listed here will get the same search path as for the non initrd-case) */ + + [IMAGE_EXTENSION] = "/etc/extensions\0" /* only place symlinks here */ + "/run/extensions\0" /* and here too */ + "/var/lib/extensions\0" /* the main place for images */ + "/usr/local/lib/extensions\0" + "/usr/lib/extensions\0" + "/.extra/sysext\0" /* put sysext picked up by systemd-stub last, since not trusted */ +}; + static Image *image_free(Image *i) { assert(i); @@ -446,6 +460,14 @@ static int image_make( return -EMEDIUMTYPE; } +static const char *pick_image_search_path(ImageClass class) { + if (class < 0 || class >= _IMAGE_CLASS_MAX) + return NULL; + + /* Use the initrd search path if there is one, otherwise use the common one */ + return in_initrd() && image_search_path_initrd[class] ? image_search_path_initrd[class] : image_search_path[class]; +} + int image_find(ImageClass class, const char *name, const char *root, @@ -461,7 +483,7 @@ int image_find(ImageClass class, if (!image_name_is_valid(name)) return -ENOENT; - NULSTR_FOREACH(path, image_search_path[class]) { + NULSTR_FOREACH(path, pick_image_search_path(class)) { _cleanup_free_ char *resolved = NULL; _cleanup_closedir_ DIR *d = NULL; struct stat st; @@ -560,7 +582,7 @@ int image_discover( assert(class < _IMAGE_CLASS_MAX); assert(h); - NULSTR_FOREACH(path, image_search_path[class]) { + NULSTR_FOREACH(path, pick_image_search_path(class)) { _cleanup_free_ char *resolved = NULL; _cleanup_closedir_ DIR *d = NULL; @@ -1138,7 +1160,7 @@ int image_set_limit(Image *i, uint64_t referenced_max) { return btrfs_subvol_set_subtree_quota_limit(i->path, 0, referenced_max); } -int image_read_metadata(Image *i) { +int image_read_metadata(Image *i, const ImagePolicy *image_policy) { _cleanup_(release_lock_file) LockFile global_lock = LOCK_FILE_INIT, local_lock = LOCK_FILE_INIT; int r; @@ -1219,7 +1241,9 @@ int image_read_metadata(Image *i) { r = dissect_loop_device( d, - NULL, NULL, + /* verity= */ NULL, + /* mount_options= */ NULL, + image_policy, DISSECT_IMAGE_GENERIC_ROOT | DISSECT_IMAGE_REQUIRE_ROOT | DISSECT_IMAGE_RELAX_VAR_CHECK | @@ -1287,7 +1311,7 @@ bool image_in_search_path( assert(image); - NULSTR_FOREACH(path, image_search_path[class]) { + NULSTR_FOREACH(path, pick_image_search_path(class)) { const char *p, *q; size_t k; diff --git a/src/shared/discover-image.h b/src/shared/discover-image.h index 342b161577..edfb1412a4 100644 --- a/src/shared/discover-image.h +++ b/src/shared/discover-image.h @@ -7,6 +7,7 @@ #include "sd-id128.h" #include "hashmap.h" +#include "image-policy.h" #include "lock-util.h" #include "macro.h" #include "os-util.h" @@ -75,7 +76,7 @@ int image_name_lock(const char *name, int operation, LockFile *ret); int image_set_limit(Image *i, uint64_t referenced_max); -int image_read_metadata(Image *i); +int image_read_metadata(Image *i, const ImagePolicy *image_policy); bool image_in_search_path(ImageClass class, const char *root, const char *image); diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index a68610b80e..46c42cfc95 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -301,7 +301,99 @@ not_found: } #if HAVE_BLKID -static int dissected_image_probe_filesystems(DissectedImage *m, int fd) { +static int image_policy_may_use( + const ImagePolicy *policy, + PartitionDesignator designator) { + + PartitionPolicyFlags f; + + /* For each partition we find in the partition table do a first check if it may exist at all given + * the policy, or if it shall be ignored. */ + + f = image_policy_get_exhaustively(policy, designator); + if (f < 0) + return f; + + if ((f & _PARTITION_POLICY_USE_MASK) == PARTITION_POLICY_ABSENT) + /* only flag set in policy is "absent"? then this partition may not exist at all */ + return log_debug_errno( + SYNTHETIC_ERRNO(ERFKILL), + "Partition of designator '%s' exists, but not allowed by policy, refusing.", + partition_designator_to_string(designator)); + if ((f & _PARTITION_POLICY_USE_MASK & ~PARTITION_POLICY_ABSENT) == PARTITION_POLICY_UNUSED) { + /* only "unused" or "unused" + "absent" are set? then don't use it */ + log_debug("Partition of designator '%s' exists, and policy dictates to ignore it, doing so.", + partition_designator_to_string(designator)); + return false; /* ignore! */ + } + + return true; /* use! */ +} + +static int image_policy_check_protection( + const ImagePolicy *policy, + PartitionDesignator designator, + PartitionPolicyFlags found_flags) { + + PartitionPolicyFlags policy_flags; + + /* Checks if the flags in the policy for the designated partition overlap the flags of what we found */ + + if (found_flags < 0) + return found_flags; + + policy_flags = image_policy_get_exhaustively(policy, designator); + if (policy_flags < 0) + return policy_flags; + + if ((found_flags & policy_flags) == 0) { + _cleanup_free_ char *found_flags_string = NULL, *policy_flags_string = NULL; + + (void) partition_policy_flags_to_string(found_flags, /* simplify= */ true, &found_flags_string); + (void) partition_policy_flags_to_string(policy_flags, /* simplify= */ true, &policy_flags_string); + + return log_debug_errno(SYNTHETIC_ERRNO(ERFKILL), "Partition %s discovered with policy '%s' but '%s' was required, refusing.", + partition_designator_to_string(designator), + strnull(found_flags_string), strnull(policy_flags_string)); + } + + return 0; +} + +static int image_policy_check_partition_flags( + const ImagePolicy *policy, + PartitionDesignator designator, + uint64_t gpt_flags) { + + PartitionPolicyFlags policy_flags; + bool b; + + /* Checks if the partition flags in the policy match reality */ + + policy_flags = image_policy_get_exhaustively(policy, designator); + if (policy_flags < 0) + return policy_flags; + + b = FLAGS_SET(gpt_flags, SD_GPT_FLAG_READ_ONLY); + if ((policy_flags & _PARTITION_POLICY_READ_ONLY_MASK) == (b ? PARTITION_POLICY_READ_ONLY_OFF : PARTITION_POLICY_READ_ONLY_ON)) + return log_debug_errno(SYNTHETIC_ERRNO(ERFKILL), "Partition %s has 'read-only' flag incorrectly set (must be %s, is %s), refusing.", + partition_designator_to_string(designator), + one_zero(!b), one_zero(b)); + + b = FLAGS_SET(gpt_flags, SD_GPT_FLAG_GROWFS); + if ((policy_flags & _PARTITION_POLICY_GROWFS_MASK) == (b ? PARTITION_POLICY_GROWFS_OFF : PARTITION_POLICY_GROWFS_ON)) + return log_debug_errno(SYNTHETIC_ERRNO(ERFKILL), "Partition %s has 'growfs' flag incorrectly set (must be %s, is %s), refusing.", + partition_designator_to_string(designator), + one_zero(!b), one_zero(b)); + + return 0; +} + +static int dissected_image_probe_filesystems( + DissectedImage *m, + int fd, + const ImagePolicy *policy) { + int r; assert(m); @@ -310,6 +402,7 @@ static int dissected_image_probe_filesystems(DissectedImage *m, int fd) { for (PartitionDesignator i = 0; i < _PARTITION_DESIGNATOR_MAX; i++) { DissectedPartition *p = m->partitions + i; + PartitionPolicyFlags found_flags; if (!p->found) continue; @@ -325,14 +418,34 @@ static int dissected_image_probe_filesystems(DissectedImage *m, int fd) { return r; } - if (streq_ptr(p->fstype, "crypto_LUKS")) + if (streq_ptr(p->fstype, "crypto_LUKS")) { m->encrypted = true; + found_flags = PARTITION_POLICY_ENCRYPTED; /* found this one, and its definitely encrypted */ + } else + /* found it, but it's definitely not encrypted, hence mask the encrypted flag, but + * set all other ways that indicate "present". */ + found_flags = PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED; if (p->fstype && fstype_is_ro(p->fstype)) p->rw = false; if (!p->rw) p->growfs = false; + + /* We might have learnt more about the file system now (i.e. whether it is encrypted or not), + * hence we need to validate this against policy again, to see if the policy still matches + * with this new information. Note that image_policy_check_protection() will check for + * overlap between what's allowed in the policy and what we pass as 'found_policy' here. In + * the unencrypted case we thus might pass an overly unspecific mask here (i.e. unprotected + * OR verity OR signed), but that's fine since the earlier policy check already checked more + * specific which of those three cases where OK. Keep in mind that this function here only + * looks at specific partitions (and thus can only deduce encryption or not) but not the + * overall partition table (and thus cannot deduce verity or not). The earlier dissection + * checks already did the relevant checks that look at the whole partition table, and + * enforced policy there as needed. */ + r = image_policy_check_protection(policy, i, found_flags); + if (r < 0) + return r; } return 0; @@ -363,9 +476,7 @@ static void check_partition_flags( log_debug("Unexpected partition flag %llu set on %s!", bit, node); } } -#endif -#if HAVE_BLKID static int dissected_image_new(const char *path, DissectedImage **ret) { _cleanup_(dissected_image_unrefp) DissectedImage *m = NULL; _cleanup_free_ char *name = NULL; @@ -543,6 +654,7 @@ static int dissect_image( const char *devname, const VeritySettings *verity, const MountOptions *mount_options, + const ImagePolicy *policy, DissectImageFlags flags) { sd_id128_t root_uuid = SD_ID128_NULL, root_verity_uuid = SD_ID128_NULL; @@ -572,7 +684,11 @@ static int dissect_image( * Returns -ENOPKG if no suitable partition table or file system could be found. * Returns -EADDRNOTAVAIL if a root hash was specified but no matching root/verity partitions found. * Returns -ENXIO if we couldn't find any partition suitable as root or /usr partition - * Returns -ENOTUNIQ if we only found multiple generic partitions and thus don't know what to do with that */ + * Returns -ENOTUNIQ if we only found multiple generic partitions and thus don't know what to do with that + * Returns -ERFKILL if image doesn't match image policy + * Returns -EBADR if verity data was provided externally for an image that has a GPT partition table (i.e. is not just a naked fs) + * Returns -EPROTONOSUPPORT if DISSECT_IMAGE_ADD_PARTITION_DEVICES is set but the block device does not have partition logic enabled + * Returns -ENOMSG if we didn't find a single usable partition (and DISSECT_IMAGE_REFUSE_EMPTY is set) */ uint64_t diskseq = m->loop ? m->loop->diskseq : 0; @@ -650,6 +766,34 @@ static int dissect_image( const char *fstype = NULL, *options = NULL, *suuid = NULL; _cleanup_close_ int mount_node_fd = -EBADF; sd_id128_t uuid = SD_ID128_NULL; + PartitionPolicyFlags found_flags; + bool encrypted; + + /* OK, we have found a file system, that's our root partition then. */ + + r = image_policy_may_use(policy, PARTITION_ROOT); + if (r < 0) + return r; + if (r == 0) /* policy says ignore this, so we ignore it */ + return -ENOPKG; + + (void) blkid_probe_lookup_value(b, "TYPE", &fstype, NULL); + (void) blkid_probe_lookup_value(b, "UUID", &suuid, NULL); + + encrypted = streq_ptr(fstype, "crypto_LUKS"); + + if (verity_settings_data_covers(verity, PARTITION_ROOT)) + found_flags = verity->root_hash_sig ? PARTITION_POLICY_SIGNED : PARTITION_POLICY_VERITY; + else + found_flags = encrypted ? PARTITION_POLICY_ENCRYPTED : PARTITION_POLICY_UNPROTECTED; + + r = image_policy_check_protection(policy, PARTITION_ROOT, found_flags); + if (r < 0) + return r; + + r = image_policy_check_partition_flags(policy, PARTITION_ROOT, 0); /* we have no gpt partition flags, hence check against all bits off */ + if (r < 0) + return r; if (FLAGS_SET(flags, DISSECT_IMAGE_PIN_PARTITION_DEVICES)) { mount_node_fd = open_partition(devname, /* is_partition = */ false, m->loop); @@ -657,10 +801,6 @@ static int dissect_image( return mount_node_fd; } - /* OK, we have found a file system, that's our root partition then. */ - (void) blkid_probe_lookup_value(b, "TYPE", &fstype, NULL); - (void) blkid_probe_lookup_value(b, "UUID", &suuid, NULL); - if (fstype) { t = strdup(fstype); if (!t) @@ -681,7 +821,7 @@ static int dissect_image( return r; m->single_file_system = true; - m->encrypted = streq_ptr(fstype, "crypto_LUKS"); + m->encrypted = encrypted; m->has_verity = verity && verity->data_path; m->verity_ready = verity_settings_data_covers(verity, PARTITION_ROOT); @@ -1049,6 +1189,18 @@ static int dissect_image( _cleanup_close_ int mount_node_fd = -EBADF; const char *options = NULL; + r = image_policy_may_use(policy, type.designator); + if (r < 0) + return r; + if (r == 0) { + /* Policy says: ignore; Remember this fact, so that we later can distinguish between "found but ignored" and "not found at all" */ + + if (!m->partitions[type.designator].found) + m->partitions[type.designator].ignored = true; + + continue; + } + if (m->partitions[type.designator].found) { /* For most partition types the first one we see wins. Except for the * rootfs and /usr, where we do a version compare of the label, and @@ -1139,6 +1291,16 @@ static int dissect_image( sd_id128_t id = SD_ID128_NULL; const char *options = NULL; + r = image_policy_may_use(policy, PARTITION_XBOOTLDR); + if (r < 0) + return r; + if (r == 0) { /* policy says: ignore */ + if (!m->partitions[PARTITION_XBOOTLDR].found) + m->partitions[PARTITION_XBOOTLDR].ignored = true; + + continue; + } + /* First one wins */ if (m->partitions[PARTITION_XBOOTLDR].found) continue; @@ -1223,41 +1385,49 @@ static int dissect_image( /* If we didn't find a generic node, then we can't fix this up either */ if (generic_node) { - _cleanup_close_ int mount_node_fd = -EBADF; - _cleanup_free_ char *o = NULL, *n = NULL; - const char *options; - - if (FLAGS_SET(flags, DISSECT_IMAGE_PIN_PARTITION_DEVICES)) { - mount_node_fd = open_partition(generic_node, /* is_partition = */ true, m->loop); - if (mount_node_fd < 0) - return mount_node_fd; - } - - r = make_partition_devname(devname, diskseq, generic_nr, flags, &n); + r = image_policy_may_use(policy, PARTITION_ROOT); if (r < 0) return r; + if (r == 0) + /* Policy says: ignore; remember that we did */ + m->partitions[PARTITION_ROOT].ignored = true; + else { + _cleanup_close_ int mount_node_fd = -EBADF; + _cleanup_free_ char *o = NULL, *n = NULL; + const char *options; - options = mount_options_from_designator(mount_options, PARTITION_ROOT); - if (options) { - o = strdup(options); - if (!o) - return -ENOMEM; - } + if (FLAGS_SET(flags, DISSECT_IMAGE_PIN_PARTITION_DEVICES)) { + mount_node_fd = open_partition(generic_node, /* is_partition = */ true, m->loop); + if (mount_node_fd < 0) + return mount_node_fd; + } - assert(generic_nr >= 0); - m->partitions[PARTITION_ROOT] = (DissectedPartition) { - .found = true, - .rw = generic_rw, - .growfs = generic_growfs, - .partno = generic_nr, - .architecture = _ARCHITECTURE_INVALID, - .node = TAKE_PTR(n), - .uuid = generic_uuid, - .mount_options = TAKE_PTR(o), - .mount_node_fd = TAKE_FD(mount_node_fd), - .offset = UINT64_MAX, - .size = UINT64_MAX, - }; + r = make_partition_devname(devname, diskseq, generic_nr, flags, &n); + if (r < 0) + return r; + + options = mount_options_from_designator(mount_options, PARTITION_ROOT); + if (options) { + o = strdup(options); + if (!o) + return -ENOMEM; + } + + assert(generic_nr >= 0); + m->partitions[PARTITION_ROOT] = (DissectedPartition) { + .found = true, + .rw = generic_rw, + .growfs = generic_growfs, + .partno = generic_nr, + .architecture = _ARCHITECTURE_INVALID, + .node = TAKE_PTR(n), + .uuid = generic_uuid, + .mount_options = TAKE_PTR(o), + .mount_node_fd = TAKE_FD(mount_node_fd), + .offset = UINT64_MAX, + .size = UINT64_MAX, + }; + } } } @@ -1319,7 +1489,42 @@ static int dissect_image( } } - r = dissected_image_probe_filesystems(m, fd); + bool any = false; + + /* After we discovered all partitions let's see if the verity requirements match the policy. (Note: + * we don't check encryption requirements here, because we haven't probed the file system yet, hence + * don't know if this is encrypted or not) */ + for (PartitionDesignator di = 0; di < _PARTITION_DESIGNATOR_MAX; di++) { + PartitionDesignator vi, si; + PartitionPolicyFlags found_flags; + + any = any || m->partitions[di].found; + + vi = partition_verity_of(di); + si = partition_verity_sig_of(di); + + /* Determine the verity protection level for this partition. */ + found_flags = m->partitions[di].found ? + (vi >= 0 && m->partitions[vi].found ? + (si >= 0 && m->partitions[si].found ? PARTITION_POLICY_SIGNED : PARTITION_POLICY_VERITY) : + PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED) : + (m->partitions[di].ignored ? PARTITION_POLICY_UNUSED : PARTITION_POLICY_ABSENT); + + r = image_policy_check_protection(policy, di, found_flags); + if (r < 0) + return r; + + if (m->partitions[di].found) { + r = image_policy_check_partition_flags(policy, di, m->partitions[di].gpt_flags); + if (r < 0) + return r; + } + } + + if (!any && !FLAGS_SET(flags, DISSECT_IMAGE_ALLOW_EMPTY)) + return -ENOMSG; + + r = dissected_image_probe_filesystems(m, fd, policy); if (r < 0) return r; @@ -1331,6 +1536,7 @@ int dissect_image_file( const char *path, const VeritySettings *verity, const MountOptions *mount_options, + const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret) { @@ -1340,7 +1546,6 @@ int dissect_image_file( int r; assert(path); - assert(ret); fd = open(path, O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); if (fd < 0) @@ -1358,17 +1563,81 @@ int dissect_image_file( if (r < 0) return r; - r = dissect_image(m, fd, path, verity, mount_options, flags); + r = dissect_image(m, fd, path, verity, mount_options, image_policy, flags); if (r < 0) return r; - *ret = TAKE_PTR(m); + if (ret) + *ret = TAKE_PTR(m); return 0; #else return -EOPNOTSUPP; #endif } +static int dissect_log_error(int r, const char *name, const VeritySettings *verity) { + assert(name); + + switch (r) { + + case 0 ... INT_MAX: /* success! */ + return r; + + case -EOPNOTSUPP: + return log_error_errno(r, "Dissecting images is not supported, compiled without blkid support."); + + case -ENOPKG: + return log_error_errno(r, "%s: Couldn't identify a suitable partition table or file system.", name); + + case -ENOMEDIUM: + return log_error_errno(r, "%s: The image does not pass os-release/extension-release validation.", name); + + case -EADDRNOTAVAIL: + return log_error_errno(r, "%s: No root partition for specified root hash found.", name); + + case -ENOTUNIQ: + return log_error_errno(r, "%s: Multiple suitable root partitions found in image.", name); + + case -ENXIO: + return log_error_errno(r, "%s: No suitable root partition found in image.", name); + + case -EPROTONOSUPPORT: + return log_error_errno(r, "Device '%s' is a loopback block device with partition scanning turned off, please turn it on.", name); + + case -ENOTBLK: + return log_error_errno(r, "%s: Image is not a block device.", name); + + case -EBADR: + return log_error_errno(r, + "Combining partitioned images (such as '%s') with external Verity data (such as '%s') not supported. " + "(Consider setting $SYSTEMD_DISSECT_VERITY_SIDECAR=0 to disable automatic discovery of external Verity data.)", + name, strna(verity ? verity->data_path : NULL)); + + case -ERFKILL: + return log_error_errno(r, "%s: image does not match image policy.", name); + + case -ENOMSG: + return log_error_errno(r, "%s: no suitable partitions found.", name); + + default: + return log_error_errno(r, "Failed to dissect image '%s': %m", name); + } +} + +int dissect_image_file_and_warn( + const char *path, + const VeritySettings *verity, + const MountOptions *mount_options, + const ImagePolicy *image_policy, + DissectImageFlags flags, + DissectedImage **ret) { + + return dissect_log_error( + dissect_image_file(path, verity, mount_options, image_policy, flags, ret), + path, + verity); +} + DissectedImage* dissected_image_unref(DissectedImage *m) { if (!m) return NULL; @@ -3250,6 +3519,7 @@ int dissect_loop_device( LoopDevice *loop, const VeritySettings *verity, const MountOptions *mount_options, + const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret) { @@ -3258,7 +3528,6 @@ int dissect_loop_device( int r; assert(loop); - assert(ret); r = dissected_image_new(loop->backing_file ?: loop->node, &m); if (r < 0) @@ -3267,11 +3536,13 @@ int dissect_loop_device( m->loop = loop_device_ref(loop); m->sector_size = m->loop->sector_size; - r = dissect_image(m, loop->fd, loop->node, verity, mount_options, flags); + r = dissect_image(m, loop->fd, loop->node, verity, mount_options, image_policy, flags); if (r < 0) return r; - *ret = TAKE_PTR(m); + if (ret) + *ret = TAKE_PTR(m); + return 0; #else return -EOPNOTSUPP; @@ -3282,56 +3553,17 @@ int dissect_loop_device_and_warn( LoopDevice *loop, const VeritySettings *verity, const MountOptions *mount_options, + const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret) { - const char *name; - int r; - assert(loop); - assert(loop->fd >= 0); - - name = ASSERT_PTR(loop->backing_file ?: loop->node); - - r = dissect_loop_device(loop, verity, mount_options, flags, ret); - switch (r) { - case -EOPNOTSUPP: - return log_error_errno(r, "Dissecting images is not supported, compiled without blkid support."); - - case -ENOPKG: - return log_error_errno(r, "%s: Couldn't identify a suitable partition table or file system.", name); - - case -ENOMEDIUM: - return log_error_errno(r, "%s: The image does not pass validation.", name); + return dissect_log_error( + dissect_loop_device(loop, verity, mount_options, image_policy, flags, ret), + loop->backing_file ?: loop->node, + verity); - case -EADDRNOTAVAIL: - return log_error_errno(r, "%s: No root partition for specified root hash found.", name); - - case -ENOTUNIQ: - return log_error_errno(r, "%s: Multiple suitable root partitions found in image.", name); - - case -ENXIO: - return log_error_errno(r, "%s: No suitable root partition found in image.", name); - - case -EPROTONOSUPPORT: - return log_error_errno(r, "Device '%s' is loopback block device with partition scanning turned off, please turn it on.", name); - - case -ENOTBLK: - return log_error_errno(r, "%s: Image is not a block device.", name); - - case -EBADR: - return log_error_errno(r, - "Combining partitioned images (such as '%s') with external Verity data (such as '%s') not supported. " - "(Consider setting $SYSTEMD_DISSECT_VERITY_SIDECAR=0 to disable automatic discovery of external Verity data.)", - name, strna(verity ? verity->data_path : NULL)); - - default: - if (r < 0) - return log_error_errno(r, "Failed to dissect image '%s': %m", name); - - return r; - } } bool dissected_image_verity_candidate(const DissectedImage *image, PartitionDesignator partition_designator) { @@ -3407,6 +3639,7 @@ const char* mount_options_from_designator(const MountOptions *options, Partition int mount_image_privately_interactively( const char *image, + const ImagePolicy *image_policy, DissectImageFlags flags, char **ret_directory, int *ret_dir_fd, @@ -3449,7 +3682,13 @@ int mount_image_privately_interactively( if (r < 0) return log_error_errno(r, "Failed to set up loopback device for %s: %m", image); - r = dissect_loop_device_and_warn(d, &verity, NULL, flags, &dissected_image); + r = dissect_loop_device_and_warn( + d, + &verity, + /* mount_options= */ NULL, + image_policy, + flags, + &dissected_image); if (r < 0) return r; @@ -3513,6 +3752,7 @@ int verity_dissect_and_mount( const char *src, const char *dest, const MountOptions *options, + const ImagePolicy *image_policy, const char *required_host_os_release_id, const char *required_host_os_release_version_id, const char *required_host_os_release_sysext_level, @@ -3556,6 +3796,7 @@ int verity_dissect_and_mount( loop_device, &verity, options, + image_policy, dissect_image_flags, &dissected_image); /* No partition table? Might be a single-filesystem image, try again */ @@ -3564,6 +3805,7 @@ int verity_dissect_and_mount( loop_device, &verity, options, + image_policy, dissect_image_flags | DISSECT_IMAGE_NO_PARTITION_TABLE, &dissected_image); if (r < 0) diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 2e741e8267..a55ad63d2d 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -19,6 +19,7 @@ typedef struct VeritySettings VeritySettings; struct DissectedPartition { bool found:1; + bool ignored:1; bool rw:1; bool growfs:1; int partno; /* -1 if there was no partition and the images contains a file system directly */ @@ -79,6 +80,7 @@ typedef enum DissectImageFlags { DISSECT_IMAGE_PIN_PARTITION_DEVICES = 1 << 21, /* Open dissected partitions and decrypted partitions and pin them by fd */ DISSECT_IMAGE_RELAX_SYSEXT_CHECK = 1 << 22, /* Don't insist that the extension-release file name matches the image name */ DISSECT_IMAGE_DISKSEQ_DEVNODE = 1 << 23, /* Prefer /dev/disk/by-diskseq/… device nodes */ + DISSECT_IMAGE_ALLOW_EMPTY = 1 << 24, /* Allow that no usable partitions is present */ } DissectImageFlags; struct DissectedImage { @@ -133,6 +135,9 @@ struct VeritySettings { .designator = _PARTITION_DESIGNATOR_INVALID \ } +/* We include image-policy.h down here, since ImagePolicy wants a complete definition of PartitionDesignator first. */ +#include "image-policy.h" + MountOptions* mount_options_free_all(MountOptions *options); DEFINE_TRIVIAL_CLEANUP_FUNC(MountOptions*, mount_options_free_all); const char* mount_options_from_designator(const MountOptions *options, PartitionDesignator designator); @@ -141,14 +146,11 @@ int probe_filesystem_full(int fd, const char *path, uint64_t offset, uint64_t si static inline int probe_filesystem(const char *path, char **ret_fstype) { return probe_filesystem_full(-1, path, 0, UINT64_MAX, ret_fstype); } -int dissect_image_file( - const char *path, - const VeritySettings *verity, - const MountOptions *mount_options, - DissectImageFlags flags, - DissectedImage **ret); -int dissect_loop_device(LoopDevice *loop, const VeritySettings *verity, const MountOptions *mount_options, DissectImageFlags flags, DissectedImage **ret); -int dissect_loop_device_and_warn(LoopDevice *loop, const VeritySettings *verity, const MountOptions *mount_options, DissectImageFlags flags, DissectedImage **ret); + +int dissect_image_file(const char *path, const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret); +int dissect_image_file_and_warn(const char *path, const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret); +int dissect_loop_device(LoopDevice *loop, const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret); +int dissect_loop_device_and_warn(LoopDevice *loop, const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, DissectImageFlags flags, DissectedImage **ret); DissectedImage* dissected_image_unref(DissectedImage *m); DEFINE_TRIVIAL_CLEANUP_FUNC(DissectedImage*, dissected_image_unref); @@ -185,9 +187,9 @@ bool dissected_image_verity_candidate(const DissectedImage *image, PartitionDesi bool dissected_image_verity_ready(const DissectedImage *image, PartitionDesignator d); bool dissected_image_verity_sig_ready(const DissectedImage *image, PartitionDesignator d); -int mount_image_privately_interactively(const char *path, DissectImageFlags flags, char **ret_directory, int *ret_dir_fd, LoopDevice **ret_loop_device); +int mount_image_privately_interactively(const char *path, const ImagePolicy *image_policy, DissectImageFlags flags, char **ret_directory, int *ret_dir_fd, LoopDevice **ret_loop_device); -int verity_dissect_and_mount(int src_fd, const char *src, const char *dest, const MountOptions *options, const char *required_host_os_release_id, const char *required_host_os_release_version_id, const char *required_host_os_release_sysext_level, const char *required_sysext_scope); +int verity_dissect_and_mount(int src_fd, const char *src, const char *dest, const MountOptions *options, const ImagePolicy *image_policy, const char *required_host_os_release_id, const char *required_host_os_release_version_id, const char *required_host_os_release_sysext_level, const char *required_sysext_scope); int dissect_fstype_ok(const char *fstype); diff --git a/src/shared/image-policy.c b/src/shared/image-policy.c new file mode 100644 index 0000000000..5baeac4c5d --- /dev/null +++ b/src/shared/image-policy.c @@ -0,0 +1,689 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "extract-word.h" +#include "image-policy.h" +#include "logarithm.h" +#include "sort-util.h" +#include "string-util.h" +#include "strv.h" + +/* Rationale for the chosen syntax: + * + * → one line, so that it can be reasonably added to a shell command line, for example via `systemd-dissect + * --image-policy=…` or to the kernel command line via `systemd.image_policy=`. + * + * → no use of "," or ";" as separators, so that it can be included in mount/fstab-style option strings and + * doesn't require escaping. Instead, separators are ":", "=", "+" which should be fine both in shell + * command lines and in mount/fstab style option strings. + */ + +static int partition_policy_compare(const PartitionPolicy *a, const PartitionPolicy *b) { + return CMP(ASSERT_PTR(a)->designator, ASSERT_PTR(b)->designator); +} + +static PartitionPolicy* image_policy_bsearch(const ImagePolicy *policy, PartitionDesignator designator) { + if (!policy) + return NULL; + + return typesafe_bsearch( + &(PartitionPolicy) { .designator = designator }, + ASSERT_PTR(policy)->policies, + ASSERT_PTR(policy)->n_policies, + partition_policy_compare); +} + +static PartitionPolicyFlags partition_policy_normalized_flags(const PartitionPolicy *policy) { + PartitionPolicyFlags flags = ASSERT_PTR(policy)->flags; + + /* This normalizes the per-partition policy flags. This means if the user left some things + * unspecified, we'll fill in the appropriate "dontcare" policy instead. We'll also mask out bits + * that do not make any sense for specific partition types. */ + + /* If no protection flag is set, then this means all are set */ + if ((flags & _PARTITION_POLICY_USE_MASK) == 0) + flags |= PARTITION_POLICY_OPEN; + + /* If this is a verity or verity signature designator, then mask off all protection bits, this after + * all needs no protection, because it *is* the protection */ + if (partition_verity_to_data(policy->designator) >= 0 || + partition_verity_sig_to_data(policy->designator) >= 0) + flags &= ~(PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED); + + /* if this designator has no verity concept, then mask off verity protection flags */ + if (partition_verity_of(policy->designator) < 0) + flags &= ~(PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED); + + if ((flags & _PARTITION_POLICY_USE_MASK) == PARTITION_POLICY_ABSENT) + /* If the partition must be absent, then the gpt flags don't matter */ + flags &= ~(_PARTITION_POLICY_READ_ONLY_MASK|_PARTITION_POLICY_GROWFS_MASK); + else { + /* If the gpt flags bits are not specified, set both options for each */ + if ((flags & _PARTITION_POLICY_READ_ONLY_MASK) == 0) + flags |= PARTITION_POLICY_READ_ONLY_ON|PARTITION_POLICY_READ_ONLY_OFF; + if ((flags & _PARTITION_POLICY_GROWFS_MASK) == 0) + flags |= PARTITION_POLICY_GROWFS_ON|PARTITION_POLICY_GROWFS_OFF; + } + + return flags; +} + +PartitionPolicyFlags image_policy_get(const ImagePolicy *policy, PartitionDesignator designator) { + PartitionDesignator data_designator = _PARTITION_DESIGNATOR_INVALID; + PartitionPolicy *pp; + + /* No policy means: everything may be used in any mode */ + if (!policy) + return partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = PARTITION_POLICY_OPEN, + .designator = designator, + }); + + pp = image_policy_bsearch(policy, designator); + if (pp) + return partition_policy_normalized_flags(pp); + + /* Hmm, so this didn't work, then let's see if we can derive some policy from the underlying data + * partition in case of verity/signature partitions */ + + data_designator = partition_verity_to_data(designator); + if (data_designator >= 0) { + PartitionPolicyFlags data_flags; + + /* So we are asked for the policy for a verity partition, and there's no explicit policy for + * that case. Let's synthesize a policy from the protection setting for the underlying data + * partition. */ + + data_flags = image_policy_get(policy, data_designator); + if (data_flags < 0) + return data_flags; + + /* We need verity if verity or verity with sig is requested */ + if (!(data_flags & (PARTITION_POLICY_SIGNED|PARTITION_POLICY_VERITY))) + return _PARTITION_POLICY_FLAGS_INVALID; + + /* If the data partition may be unused or absent, then the verity partition may too. Also, inherit the partition flags policy */ + return partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = PARTITION_POLICY_UNPROTECTED | (data_flags & (PARTITION_POLICY_UNUSED|PARTITION_POLICY_ABSENT)) | + (data_flags & _PARTITION_POLICY_PFLAGS_MASK), + .designator = designator, + }); + } + + data_designator = partition_verity_sig_to_data(designator); + if (data_designator >= 0) { + PartitionPolicyFlags data_flags; + + /* Similar case as for verity partitions, but slightly more strict rules */ + + data_flags = image_policy_get(policy, data_designator); + if (data_flags < 0) + return data_flags; + + if (!(data_flags & PARTITION_POLICY_SIGNED)) + return _PARTITION_POLICY_FLAGS_INVALID; + + return partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = PARTITION_POLICY_UNPROTECTED | (data_flags & (PARTITION_POLICY_UNUSED|PARTITION_POLICY_ABSENT)) | + (data_flags & _PARTITION_POLICY_PFLAGS_MASK), + .designator = designator, + }); + } + + return _PARTITION_POLICY_FLAGS_INVALID; /* got nothing */ +} + +PartitionPolicyFlags image_policy_get_exhaustively(const ImagePolicy *policy, PartitionDesignator designator) { + PartitionPolicyFlags flags; + + /* This is just like image_policy_get() but whenever there is no policy for a specific designator, we + * return the default policy. */ + + flags = image_policy_get(policy, designator); + if (flags < 0) + return partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = image_policy_default(policy), + .designator = designator, + }); + + return flags; +} + +static PartitionPolicyFlags policy_flag_from_string_one(const char *s) { + assert(s); + + /* This is a bitmask (i.e. not dense), hence we don't use the "string-table.h" stuff here. */ + + if (streq(s, "verity")) + return PARTITION_POLICY_VERITY; + if (streq(s, "signed")) + return PARTITION_POLICY_SIGNED; + if (streq(s, "encrypted")) + return PARTITION_POLICY_ENCRYPTED; + if (streq(s, "unprotected")) + return PARTITION_POLICY_UNPROTECTED; + if (streq(s, "unused")) + return PARTITION_POLICY_UNUSED; + if (streq(s, "absent")) + return PARTITION_POLICY_ABSENT; + if (streq(s, "open")) /* shortcut alias */ + return PARTITION_POLICY_OPEN; + if (streq(s, "ignore")) /* ditto */ + return PARTITION_POLICY_IGNORE; + if (streq(s, "read-only-on")) + return PARTITION_POLICY_READ_ONLY_ON; + if (streq(s, "read-only-off")) + return PARTITION_POLICY_READ_ONLY_OFF; + if (streq(s, "growfs-on")) + return PARTITION_POLICY_GROWFS_ON; + if (streq(s, "growfs-off")) + return PARTITION_POLICY_GROWFS_OFF; + + return _PARTITION_POLICY_FLAGS_INVALID; +} + +PartitionPolicyFlags partition_policy_flags_from_string(const char *s) { + PartitionPolicyFlags flags = 0; + int r; + + assert(s); + + if (empty_or_dash(s)) + return 0; + + for (;;) { + _cleanup_free_ char *f = NULL; + PartitionPolicyFlags ff; + + r = extract_first_word(&s, &f, "+", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + if (r == 0) + break; + + ff = policy_flag_from_string_one(strstrip(f)); + if (ff < 0) + return -EBADRQC; /* recognizable error */ + + flags |= ff; + } + + return flags; +} + +static ImagePolicy* image_policy_new(size_t n_policies) { + ImagePolicy *p; + + if (n_policies > (SIZE_MAX - offsetof(ImagePolicy, policies)) / sizeof(PartitionPolicy)) /* overflow check */ + return NULL; + + p = malloc(offsetof(ImagePolicy, policies) + sizeof(PartitionPolicy) * n_policies); + if (!p) + return NULL; + + *p = (ImagePolicy) { + .default_flags = PARTITION_POLICY_IGNORE, + }; + return p; +} + +int image_policy_from_string(const char *s, ImagePolicy **ret) { + _cleanup_free_ ImagePolicy *p = NULL; + uint64_t dmask = 0; + ImagePolicy *t; + PartitionPolicyFlags symbolic_policy; + int r; + + assert(s); + assert_cc(sizeof(dmask) * 8 >= _PARTITION_DESIGNATOR_MAX); + + /* Recognizable errors: + * + * ENOTUNIQ → Two or more rules for the same partition + * EBADSLT → Unknown partition designator + * EBADRQC → Unknown policy flags + */ + + /* First, let's handle "symbolic" policies, i.e. "-", "*", "~" */ + if (empty_or_dash(s)) + /* ignore policy: everything may exist, but nothing used */ + symbolic_policy = PARTITION_POLICY_IGNORE; + else if (streq(s, "*")) + /* allow policy: everything is allowed */ + symbolic_policy = PARTITION_POLICY_OPEN; + else if (streq(s, "~")) + /* deny policy: nothing may exist */ + symbolic_policy = PARTITION_POLICY_ABSENT; + else + symbolic_policy = _PARTITION_POLICY_FLAGS_INVALID; + + if (symbolic_policy >= 0) { + if (!ret) + return 0; + + p = image_policy_new(0); + if (!p) + return -ENOMEM; + + p->default_flags = symbolic_policy; + *ret = TAKE_PTR(p); + return 0; + } + + /* Allocate the policy at maximum size, i.e. for all designators. We might overshoot a bit, but the + * items are cheap, and we can return unused space to libc once we know we don't need it */ + p = image_policy_new(_PARTITION_DESIGNATOR_MAX); + if (!p) + return -ENOMEM; + + const char *q = s; + bool default_specified = false; + for (;;) { + _cleanup_free_ char *e = NULL, *d = NULL; + PartitionDesignator designator; + PartitionPolicyFlags flags; + char *f, *ds, *fs; + + r = extract_first_word(&q, &e, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + if (r == 0) + break; + + f = e; + r = extract_first_word((const char**) &f, &d, "=", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Expected designator name followed by '='; got instead: %s", e); + if (!f) /* no separator? */ + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Missing '=' in policy expression: %s", e); + + ds = strstrip(d); + if (isempty(ds)) { + /* Not partition name? then it's the default policy */ + if (default_specified) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Default partition policy flags specified more than once."); + + designator = _PARTITION_DESIGNATOR_INVALID; + default_specified = true; + } else { + designator = partition_designator_from_string(ds); + if (designator < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EBADSLT), "Unknown partition designator: %s", ds); /* recognizable error */ + if (dmask & (UINT64_C(1) << designator)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTUNIQ), "Partition designator specified more than once: %s", ds); + dmask |= UINT64_C(1) << designator; + } + + fs = strstrip(f); + flags = partition_policy_flags_from_string(fs); + if (flags == -EBADRQC) + return log_debug_errno(flags, "Unknown partition policy flag: %s", fs); + if (flags < 0) + return log_debug_errno(flags, "Failed to parse partition policy flags '%s': %m", fs); + + if (designator < 0) + p->default_flags = flags; + else { + p->policies[p->n_policies++] = (PartitionPolicy) { + .designator = designator, + .flags = flags, + }; + } + }; + + assert(p->n_policies <= _PARTITION_DESIGNATOR_MAX); + + /* Return unused space to libc */ + t = realloc(p, offsetof(ImagePolicy, policies) + sizeof(PartitionPolicy) * p->n_policies); + if (t) + p = t; + + typesafe_qsort(p->policies, p->n_policies, partition_policy_compare); + + if (ret) + *ret = TAKE_PTR(p); + + return 0; +} + +int partition_policy_flags_to_string(PartitionPolicyFlags flags, bool simplify, char **ret) { + _cleanup_free_ char *buf = NULL; + const char *l[CONST_LOG2U(_PARTITION_POLICY_MASK) + 1]; /* one string per known flag at most */ + size_t m = 0; + + assert(ret); + + if (flags < 0) + return -EINVAL; + + /* If 'simplify' is false we'll output the precise value of every single flag. + * + * If 'simplify' is true we'll try to make the output shorter, by doing the following: + * + * → we'll spell the long form "verity+signed+encrypted+unprotected+unused+absent" via its + * equivalent shortcut form "open" (which we happily parse btw, see above) + * + * → we'll spell the long form "unused+absent" via its shortcut "ignore" (which we are also happy + * to parse) + * + * → if the read-only/growfs policy flags are both set, we suppress them. this thus removes the + * distinction between "user explicitly declared don't care" and "we implied don't care because + * user didn't say anything". + * + * net result: the resulting string is shorter, but the effective policy declared that way will have + * the same results as the long form. */ + + if (simplify && (flags & _PARTITION_POLICY_USE_MASK) == PARTITION_POLICY_OPEN) + l[m++] = "open"; + else if (simplify && (flags & _PARTITION_POLICY_USE_MASK) == PARTITION_POLICY_IGNORE) + l[m++] = "ignore"; + else { + if (flags & PARTITION_POLICY_VERITY) + l[m++] = "verity"; + if (flags & PARTITION_POLICY_SIGNED) + l[m++] = "signed"; + if (flags & PARTITION_POLICY_ENCRYPTED) + l[m++] = "encrypted"; + if (flags & PARTITION_POLICY_UNPROTECTED) + l[m++] = "unprotected"; + if (flags & PARTITION_POLICY_UNUSED) + l[m++] = "unused"; + if (flags & PARTITION_POLICY_ABSENT) + l[m++] = "absent"; + } + + if (!simplify || (!(flags & PARTITION_POLICY_READ_ONLY_ON) != !(flags & PARTITION_POLICY_READ_ONLY_OFF))) { + if (flags & PARTITION_POLICY_READ_ONLY_ON) + l[m++] = "read-only-on"; + if (flags & PARTITION_POLICY_READ_ONLY_OFF) + l[m++] = "read-only-off"; + } + + if (!simplify || (!(flags & PARTITION_POLICY_GROWFS_ON) != !(flags & PARTITION_POLICY_GROWFS_OFF))) { + if (flags & PARTITION_POLICY_GROWFS_OFF) + l[m++] = "growfs-off"; + if (flags & PARTITION_POLICY_GROWFS_ON) + l[m++] = "growfs-on"; + } + + if (m == 0) + buf = strdup("-"); + else { + assert(m+1 < ELEMENTSOF(l)); + l[m] = NULL; + + buf = strv_join((char**) l, "+"); + } + if (!buf) + return -ENOMEM; + + *ret = TAKE_PTR(buf); + return 0; +} + +static int image_policy_flags_all_match(const ImagePolicy *policy, PartitionPolicyFlags expected) { + + if (expected < 0) + return -EINVAL; + + if (image_policy_default(policy) != expected) + return false; + + for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) { + PartitionPolicyFlags f, w; + + f = image_policy_get_exhaustively(policy, d); + if (f < 0) + return f; + + w = partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = expected, + .designator = d, + }); + if (w < 0) + return w; + if (f != w) + return false; + } + + return true; +} + +bool image_policy_equiv_ignore(const ImagePolicy *policy) { + /* Checks if this is the ignore policy (or equivalent to it), i.e. everything is ignored, aka '-', aka '' */ + return image_policy_flags_all_match(policy, PARTITION_POLICY_IGNORE); +} + +bool image_policy_equiv_allow(const ImagePolicy *policy) { + /* Checks if this is the allow policy (or equivalent to it), i.e. everything is allowed, aka '*' */ + return image_policy_flags_all_match(policy, PARTITION_POLICY_OPEN); +} + +bool image_policy_equiv_deny(const ImagePolicy *policy) { + /* Checks if this is the deny policy (or equivalent to it), i.e. everything must be absent, aka '~' */ + return image_policy_flags_all_match(policy, PARTITION_POLICY_ABSENT); +} + +int image_policy_to_string(const ImagePolicy *policy, bool simplify, char **ret) { + _cleanup_free_ char *s = NULL; + int r; + + assert(ret); + + if (simplify) { + const char *fixed; + + if (image_policy_equiv_allow(policy)) + fixed = "*"; + else if (image_policy_equiv_ignore(policy)) + fixed = "-"; + else if (image_policy_equiv_deny(policy)) + fixed = "~"; + else + fixed = NULL; + + if (fixed) { + s = strdup(fixed); + if (!s) + return -ENOMEM; + + *ret = TAKE_PTR(s); + return 0; + } + } + + for (size_t i = 0; i < image_policy_n_entries(policy); i++) { + const PartitionPolicy *p = policy->policies + i; + _cleanup_free_ char *f = NULL; + const char *t; + + assert(i == 0 || p->designator > policy->policies[i-1].designator); /* Validate perfect ordering */ + + assert_se(t = partition_designator_to_string(p->designator)); + + if (simplify) { + /* Skip policy entries that match the default anyway */ + PartitionPolicyFlags df; + + df = partition_policy_normalized_flags( + &(const PartitionPolicy) { + .flags = image_policy_default(policy), + .designator = p->designator, + }); + if (df < 0) + return df; + + if (df == p->flags) + continue; + } + + r = partition_policy_flags_to_string(p->flags, simplify, &f); + if (r < 0) + return r; + + if (!strextend(&s, isempty(s) ? "" : ":", t, "=", f)) + return -ENOMEM; + } + + if (!simplify || image_policy_default(policy) != PARTITION_POLICY_IGNORE) { + _cleanup_free_ char *df = NULL; + + r = partition_policy_flags_to_string(image_policy_default(policy), simplify, &df); + if (r < 0) + return r; + + if (!strextend(&s, isempty(s) ? "" : ":", "=", df)) + return -ENOMEM; + } + + if (isempty(s)) { /* no rule and default policy? then let's return "-" */ + s = strdup("-"); + if (!s) + return -ENOMEM; + } + + *ret = TAKE_PTR(s); + return 0; +} + +bool image_policy_equal(const ImagePolicy *a, const ImagePolicy *b) { + if (a == b) + return true; + if (image_policy_n_entries(a) != image_policy_n_entries(b)) + return false; + if (image_policy_default(a) != image_policy_default(b)) + return false; + for (size_t i = 0; i < image_policy_n_entries(a); i++) { + if (a->policies[i].designator != b->policies[i].designator) + return false; + if (a->policies[i].flags != b->policies[i].flags) + return false; + } + + return true; +} + +int image_policy_equivalent(const ImagePolicy *a, const ImagePolicy *b) { + + /* The image_policy_equal() function checks if the policy is defined the exact same way. This + * function here instead looks at the outcome of the two policies instead. Where does this come to + * different results you ask? We imply some logic regarding Verity/Encryption: when no rule is + * defined for a verity partition we can synthesize it from the protection level of the data + * partition it protects. Or: any per-partition rule that is identical to the default rule is + * redundant, and will be recognized as such by image_policy_equivalent() but not by + * image_policy_equal()- */ + + if (image_policy_default(a) != image_policy_default(b)) + return false; + + for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) { + PartitionPolicyFlags f, w; + + f = image_policy_get_exhaustively(a, d); + if (f < 0) + return f; + + w = image_policy_get_exhaustively(b, d); + if (w < 0) + return w; + + if (f != w) + return false; + } + + return true; +} + +const ImagePolicy image_policy_allow = { + /* Allow policy */ + .n_policies = 0, + .default_flags = PARTITION_POLICY_OPEN, +}; + +const ImagePolicy image_policy_deny = { + /* Allow policy */ + .n_policies = 0, + .default_flags = PARTITION_POLICY_ABSENT, +}; + +const ImagePolicy image_policy_ignore = { + /* Allow policy */ + .n_policies = 0, + .default_flags = PARTITION_POLICY_IGNORE, +}; + +const ImagePolicy image_policy_sysext = { + /* For system extensions, honour root file system, and /usr/ and ignore everything else. After all, + * we are only interested in /usr/ + /opt/ trees anyway, and that's really the only place they can + * be. */ + .n_policies = 2, + .policies = { + { PARTITION_ROOT, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_USR, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + }, + .default_flags = PARTITION_POLICY_IGNORE, +}; + +const ImagePolicy image_policy_sysext_strict = { + /* For system extensions, requiring signing */ + .n_policies = 2, + .policies = { + { PARTITION_ROOT, PARTITION_POLICY_SIGNED|PARTITION_POLICY_ABSENT }, + { PARTITION_USR, PARTITION_POLICY_SIGNED|PARTITION_POLICY_ABSENT }, + }, + .default_flags = PARTITION_POLICY_IGNORE, +}; + +const ImagePolicy image_policy_container = { + /* For systemd-nspawn containers we use all partitions, with the exception of swap */ + .n_policies = 8, + .policies = { + { PARTITION_ROOT, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_USR, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_HOME, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_SRV, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_ESP, PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_XBOOTLDR, PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_TMP, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_VAR, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + }, + .default_flags = PARTITION_POLICY_IGNORE, +}; + +const ImagePolicy image_policy_host = { + /* For the host policy we basically use everything */ + .n_policies = 9, + .policies = { + { PARTITION_ROOT, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_USR, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_HOME, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_SRV, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_ESP, PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_XBOOTLDR, PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_SWAP, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_TMP, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_VAR, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + }, + .default_flags = PARTITION_POLICY_IGNORE, +}; + +const ImagePolicy image_policy_service = { + /* For RootImage= in services we skip ESP/XBOOTLDR and swap */ + .n_policies = 6, + .policies = { + { PARTITION_ROOT, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_USR, PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_HOME, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_SRV, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_TMP, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + { PARTITION_VAR, PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_ABSENT }, + }, + .default_flags = PARTITION_POLICY_IGNORE, +}; diff --git a/src/shared/image-policy.h b/src/shared/image-policy.h new file mode 100644 index 0000000000..a5e37642af --- /dev/null +++ b/src/shared/image-policy.h @@ -0,0 +1,97 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct ImagePolicy ImagePolicy; + +#include "dissect-image.h" +#include "errno-list.h" + +typedef enum PartitionPolicyFlags { + /* Not all policy flags really make sense on all partition types, see comments. But even if they + * don't make sense we'll parse them anyway, because maybe one day we'll add them for more partition + * types, too. Moreover, we allow configuring a "default" policy for all partition types for which no + * explicit policy is specified. It's useful if we can use policy flags in there and apply this + * default policy gracefully even to partition types where they don't really make too much sense + * on. Example: a default policy of "verity+encrypted" certainly makes sense, but for /home/ + * partitions this gracefully degrades to "encrypted" (as we do not have a concept of verity for + * /home/), and so on. */ + PARTITION_POLICY_VERITY = 1 << 0, /* must exist, activate with verity (only applies to root/usr partitions) */ + PARTITION_POLICY_SIGNED = 1 << 1, /* must exist, activate with signed verity (only applies to root/usr partitions) */ + PARTITION_POLICY_ENCRYPTED = 1 << 2, /* must exist, activate with LUKS encryption (applies to any data partition, but not to verity/signature partitions */ + PARTITION_POLICY_UNPROTECTED = 1 << 3, /* must exist, activate without encryption/verity */ + PARTITION_POLICY_UNUSED = 1 << 4, /* must exist, don't use */ + PARTITION_POLICY_ABSENT = 1 << 5, /* must not exist */ + PARTITION_POLICY_OPEN = PARTITION_POLICY_VERITY|PARTITION_POLICY_SIGNED|PARTITION_POLICY_ENCRYPTED| + PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_UNUSED|PARTITION_POLICY_ABSENT, + PARTITION_POLICY_IGNORE = PARTITION_POLICY_UNUSED|PARTITION_POLICY_ABSENT, + _PARTITION_POLICY_USE_MASK = PARTITION_POLICY_OPEN, + + PARTITION_POLICY_READ_ONLY_OFF = 1 << 6, /* State of GPT partition flag "read-only" must be on */ + PARTITION_POLICY_READ_ONLY_ON = 1 << 7, + _PARTITION_POLICY_READ_ONLY_MASK = PARTITION_POLICY_READ_ONLY_OFF|PARTITION_POLICY_READ_ONLY_ON, + PARTITION_POLICY_GROWFS_OFF = 1 << 8, /* State of GPT partition flag "growfs" must be on */ + PARTITION_POLICY_GROWFS_ON = 1 << 9, + _PARTITION_POLICY_GROWFS_MASK = PARTITION_POLICY_GROWFS_OFF|PARTITION_POLICY_GROWFS_ON, + _PARTITION_POLICY_PFLAGS_MASK = _PARTITION_POLICY_READ_ONLY_MASK|_PARTITION_POLICY_GROWFS_MASK, + + _PARTITION_POLICY_MASK = _PARTITION_POLICY_USE_MASK|_PARTITION_POLICY_READ_ONLY_MASK|_PARTITION_POLICY_GROWFS_MASK, + + _PARTITION_POLICY_FLAGS_INVALID = -EINVAL, + _PARTITION_POLICY_FLAGS_ERRNO_MAX = -ERRNO_MAX, /* Ensure the whole errno range fits into this enum */ +} PartitionPolicyFlags; + +assert_cc((_PARTITION_POLICY_USE_MASK | _PARTITION_POLICY_PFLAGS_MASK) >= 0); /* ensure flags don't collide with errno range */ + +typedef struct PartitionPolicy { + PartitionDesignator designator; + PartitionPolicyFlags flags; +} PartitionPolicy; + +struct ImagePolicy { + PartitionPolicyFlags default_flags; /* for any designator not listed in the list below */ + size_t n_policies; + PartitionPolicy policies[]; /* sorted by designator, hence suitable for binary search */ +}; + +/* Default policies for various usecases */ +extern const ImagePolicy image_policy_allow; +extern const ImagePolicy image_policy_deny; +extern const ImagePolicy image_policy_ignore; +extern const ImagePolicy image_policy_sysext; /* No verity required */ +extern const ImagePolicy image_policy_sysext_strict; /* Signed verity required */ +extern const ImagePolicy image_policy_container; +extern const ImagePolicy image_policy_service; +extern const ImagePolicy image_policy_host; + +PartitionPolicyFlags image_policy_get(const ImagePolicy *policy, PartitionDesignator designator); +PartitionPolicyFlags image_policy_get_exhaustively(const ImagePolicy *policy, PartitionDesignator designator); + +/* We want that the NULL image policy means "everything" allowed, hence use these simple accessors to make + * NULL policies work reasonably */ +static inline PartitionPolicyFlags image_policy_default(const ImagePolicy *policy) { + return policy ? policy->default_flags : PARTITION_POLICY_OPEN; +} + +static inline size_t image_policy_n_entries(const ImagePolicy *policy) { + return policy ? policy->n_policies : 0; +} + +PartitionPolicyFlags partition_policy_flags_from_string(const char *s); +int partition_policy_flags_to_string(PartitionPolicyFlags flags, bool simplify, char **ret); + +int image_policy_from_string(const char *s, ImagePolicy **ret); +int image_policy_to_string(const ImagePolicy *policy, bool simplify, char **ret); + +/* Recognizes three special policies by equivalence */ +bool image_policy_equiv_ignore(const ImagePolicy *policy); +bool image_policy_equiv_allow(const ImagePolicy *policy); +bool image_policy_equiv_deny(const ImagePolicy *policy); + +bool image_policy_equal(const ImagePolicy *a, const ImagePolicy *b); /* checks if defined the same way, i.e. has literally the same ruleset */ +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. */ + +static inline ImagePolicy* image_policy_free(ImagePolicy *p) { + return mfree(p); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(ImagePolicy*, image_policy_free); diff --git a/src/shared/meson.build b/src/shared/meson.build index 0f2e2d1a67..df82778f9d 100644 --- a/src/shared/meson.build +++ b/src/shared/meson.build @@ -81,6 +81,7 @@ shared_sources = files( 'id128-print.c', 'idn-util.c', 'ima-util.c', + 'image-policy.c', 'import-util.c', 'in-addr-prefix-util.c', 'install-file.c', diff --git a/src/shared/mount-util.c b/src/shared/mount-util.c index 1eac51b81e..edf01fe092 100644 --- a/src/shared/mount-util.c +++ b/src/shared/mount-util.c @@ -805,6 +805,7 @@ static int mount_in_namespace( bool read_only, bool make_file_or_directory, const MountOptions *options, + const ImagePolicy *image_policy, bool is_image) { _cleanup_close_pair_ int errno_pipe_fd[2] = PIPE_EBADF; @@ -892,7 +893,7 @@ static int mount_in_namespace( mount_tmp_created = true; if (is_image) - r = verity_dissect_and_mount(chased_src_fd, chased_src_path, mount_tmp, options, NULL, NULL, NULL, NULL); + r = verity_dissect_and_mount(chased_src_fd, chased_src_path, mount_tmp, options, image_policy, NULL, NULL, NULL, NULL); else r = mount_follow_verbose(LOG_DEBUG, FORMAT_PROC_FD_PATH(chased_src_fd), mount_tmp, NULL, MS_BIND, NULL); if (r < 0) @@ -1042,7 +1043,7 @@ int bind_mount_in_namespace( bool read_only, bool make_file_or_directory) { - return mount_in_namespace(target, propagate_path, incoming_path, src, dest, read_only, make_file_or_directory, NULL, false); + return mount_in_namespace(target, propagate_path, incoming_path, src, dest, read_only, make_file_or_directory, /* options= */ NULL, /* image_policy= */ NULL, /* is_image= */ false); } int mount_image_in_namespace( @@ -1053,9 +1054,10 @@ int mount_image_in_namespace( const char *dest, bool read_only, bool make_file_or_directory, - const MountOptions *options) { + const MountOptions *options, + const ImagePolicy *image_policy) { - return mount_in_namespace(target, propagate_path, incoming_path, src, dest, read_only, make_file_or_directory, options, true); + return mount_in_namespace(target, propagate_path, incoming_path, src, dest, read_only, make_file_or_directory, options, image_policy, /* is_image=*/ true); } int make_mount_point(const char *path) { diff --git a/src/shared/mount-util.h b/src/shared/mount-util.h index 84ea4b6392..f52687828a 100644 --- a/src/shared/mount-util.h +++ b/src/shared/mount-util.h @@ -81,7 +81,7 @@ static inline char* umount_and_rmdir_and_free(char *p) { DEFINE_TRIVIAL_CLEANUP_FUNC(char*, umount_and_rmdir_and_free); int bind_mount_in_namespace(pid_t target, const char *propagate_path, const char *incoming_path, const char *src, const char *dest, bool read_only, bool make_file_or_directory); -int mount_image_in_namespace(pid_t target, const char *propagate_path, const char *incoming_path, const char *src, const char *dest, bool read_only, bool make_file_or_directory, const MountOptions *options); +int mount_image_in_namespace(pid_t target, const char *propagate_path, const char *incoming_path, const char *src, const char *dest, bool read_only, bool make_file_or_directory, const MountOptions *options, const ImagePolicy *image_policy); int make_mount_point(const char *path); |