From 5d3243bd468cee0dc9ec9e46c5c57f6cc7b1dba2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 13 Mar 2025 22:03:04 +0100 Subject: [PATCH 01/21] udev-builtin-blkid: look for ESP/XBOOTLDR only in initrd; afterwards just look at / So far the gpt-auto symlinks would point to: 1. the disk the ESP/XBOOTLDR is located on 2. or to a loopback device whose filename field is set to "rootdisk" or "rootdisk.raw" This makes sense in the initrd. But once we transition to the host this is quite confusing, since the symlinks might point to a different place than what we actually ended up transitioning too: the actual backing device of the root file system might be different from what gpt-auto found. Let's clean this up: let's avoid any ambiguities here: let's extend the rules above with one more rule: 3. if we left the initrd, we'll make gpt-auto point to the selected root file system, if it otherwise would have been a candidate. Or in other words, the ID_PART_GPT_AUTO_ROOT_DISK=1 udev property now always makes sense: in the initrd it points to the future root disk, and on the host to the actual root disk. Fixes: #34319 --- src/udev/udev-builtin-blkid.c | 186 +++++++++++++++++++++++----------- 1 file changed, 125 insertions(+), 61 deletions(-) diff --git a/src/udev/udev-builtin-blkid.c b/src/udev/udev-builtin-blkid.c index aa56215825..363f07c24a 100644 --- a/src/udev/udev-builtin-blkid.c +++ b/src/udev/udev-builtin-blkid.c @@ -22,12 +22,14 @@ #include "alloc-util.h" #include "blkid-util.h" +#include "blockdev-util.h" #include "device-util.h" #include "devnum-util.h" #include "efi-loader.h" #include "errno-util.h" #include "fd-util.h" #include "gpt.h" +#include "initrd-util.h" #include "parse-util.h" #include "string-util.h" #include "strv.h" @@ -129,101 +131,163 @@ static int find_gpt_root(UdevEvent *event, blkid_probe pr, const char *loop_back #if defined(SD_GPT_ROOT_NATIVE) && ENABLE_EFI sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); - _cleanup_free_ char *root_label = NULL; - bool found_esp_or_xbootldr = false, need_esp_or_xbootldr; - sd_id128_t root_id = SD_ID128_NULL, esp_or_xbootldr = SD_ID128_NULL; int r; assert(event); assert(pr); - /* Iterate through the partitions on this disk, and see if the UEFI ESP or XBOOTLDR partition we - * booted from is on it. If so, find the newest root partition, and add a property indicating its - * partition UUID. We also do this if we are dealing with a loopback block device whose "backing - * filename" field is set to the string "root". In the latter case we do not search for ESP or - * XBOOTLDR. */ + /* In the initrd: Iterate through the partitions on this disk, and see if the UEFI ESP or XBOOTLDR + * partition we booted from is on it. If so, find the newest root partition, and add a property + * indicating its partition UUID. We also do this if we are dealing with a loopback block device + * whose "backing filename" field is set to the string "root". In the latter case we do not search + * for ESP or XBOOTLDR. + * + * After the initrd→host transition: look at the current block device mounted at / and set the same + * properties to its whole block device. */ if (!device_is_devtype(dev, "disk")) { log_device_debug(dev, "Skipping GPT root logic on partition block device."); return 0; } - r = efi_loader_get_device_part_uuid(&esp_or_xbootldr); - if (r < 0) { - if (r != -ENOENT && !ERRNO_IS_NEG_NOT_SUPPORTED(r)) - return log_debug_errno(r, "Unable to determine loader partition UUID: %m"); + sd_id128_t esp_or_xbootldr = SD_ID128_NULL; + bool need_esp_or_xbootldr; + dev_t root_devno = 0; + if (in_initrd()) { + /* In the initrd look at the boot loader provided data (and loopback backing fname) to find + * our *future* root */ - log_device_debug(dev, "No loader partition UUID EFI variable set, not using partition data to search for default root block device."); + r = efi_loader_get_device_part_uuid(&esp_or_xbootldr); + if (r < 0) { + if (r != -ENOENT && !ERRNO_IS_NEG_NOT_SUPPORTED(r)) + return log_debug_errno(r, "Unable to determine loader partition UUID: %m"); - /* NB: if an ESP/xbootldr field is set, we always use that. We do this in order to guarantee - * systematic behaviour. */ - if (!STRPTR_IN_SET(loop_backing_fname, "rootdisk", "rootdisk.raw")) { - log_device_debug(dev, "Device is not a loopback block device with reference string 'root', not considering block device as default root block device."); + log_device_debug(dev, "No loader partition UUID EFI variable set, not using partition data to search for default root block device."); + + /* NB: if an ESP/xbootldr field is set, we always use that. We do this in order to guarantee + * systematic behaviour. */ + if (!STRPTR_IN_SET(loop_backing_fname, "rootdisk", "rootdisk.raw")) { + log_device_debug(dev, "Device is not a loopback block device with reference string 'root', not considering block device as default root block device."); + return 0; + } + + /* OK, we have now sufficiently identified this device as the right root "whole" device, + * hence no need to bother with searching for ESP/XBOOTLDR */ + need_esp_or_xbootldr = false; + } else + /* We now know the the ESP/xbootldr UUID, but we cannot be sure yet it's on this block + * device, hence look for it among partitions now */ + need_esp_or_xbootldr = true; + } else { + /* On the main system look at the *current* root instead */ + + r = blockdev_get_root(LOG_DEBUG, &root_devno); + if (r < 0) { + log_device_debug_errno(dev, r, "Unable to determine current root block device, skipping gpt-auto probing: %m"); + return 0; + } + if (r == 0) { + log_device_debug(dev, "Root block device not backed by a (single) whole block device, skipping gpt-auto probing."); return 0; } - /* OK, we have now sufficiently identified this device as the right root "whole" device, - * hence no need to bother with searching for ESP/XBOOTLDR */ + dev_t whole_devno; + r = block_get_whole_disk(root_devno, &whole_devno); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to find whole block device for root block device: %m"); + + dev_t this_devno; + r = sd_device_get_devnum(dev, &this_devno); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device major/minor of device: %m"); + + if (whole_devno != this_devno) { + log_device_debug(dev, "This device is not the current root block device."); + return 0; + } + + /* We don't need to check ESP/XBOOTLDR UUID, we *know* what our root disk is */ need_esp_or_xbootldr = false; - } else - /* We now know the the ESP/xbootldr UUID, but we cannot be sure yet it's on this block - * device, hence look for it among partitions now */ - need_esp_or_xbootldr = true; + } errno = 0; blkid_partlist pl = blkid_probe_get_partitions(pr); if (!pl) return log_device_debug_errno(dev, errno_or_else(ENOMEM), "Failed to probe partitions: %m"); - int nvals = blkid_partlist_numof_partitions(pl); - for (int i = 0; i < nvals; i++) { - blkid_partition pp; - const char *label; - sd_id128_t type, id; + sd_id128_t root_id = SD_ID128_NULL; + bool found_esp_or_xbootldr = false; - pp = blkid_partlist_get_partition(pl, i); - if (!pp) - continue; + if (root_devno != 0) { + /* If we already know the root partition, let's verify its type ID and then directly query + * its ID */ - r = blkid_partition_get_uuid_id128(pp, &id); - if (r < 0) { - log_device_debug_errno(dev, r, "Failed to get partition UUID, ignoring: %m"); - continue; + blkid_partition root_partition = blkid_partlist_devno_to_partition(pl, root_devno); + if (root_partition) { + sd_id128_t type; + r = blkid_partition_get_type_id128(root_partition, &type); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to get root partition type UUID, ignoring: %m"); + else if (sd_id128_equal(type, SD_GPT_ROOT_NATIVE)) { + r = blkid_partition_get_uuid_id128(root_partition, &root_id); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to get partition UUID, ignoring: %m"); + } } + } else { + /* We do not know the root partition, let's search for it. */ - r = blkid_partition_get_type_id128(pp, &type); - if (r < 0) { - log_device_debug_errno(dev, r, "Failed to get partition type UUID, ignoring: %m"); - continue; - } + _cleanup_free_ char *root_label = NULL; + int nvals = blkid_partlist_numof_partitions(pl); + for (int i = 0; i < nvals; i++) { + blkid_partition pp; + const char *label; + sd_id128_t type, id; - label = blkid_partition_get_name(pp); /* returns NULL if empty */ - - if (need_esp_or_xbootldr && sd_id128_in_set(type, SD_GPT_ESP, SD_GPT_XBOOTLDR)) { - - /* We found an ESP or XBOOTLDR, let's see if it matches the ESP/XBOOTLDR we booted from. */ - if (sd_id128_equal(id, esp_or_xbootldr)) - found_esp_or_xbootldr = true; - - } else if (sd_id128_equal(type, SD_GPT_ROOT_NATIVE)) { - unsigned long long flags; - - flags = blkid_partition_get_flags(pp); - if (flags & SD_GPT_FLAG_NO_AUTO) + pp = blkid_partlist_get_partition(pl, i); + if (!pp) continue; - /* systemd-sysupdate expects empty partitions to be marked with an "_empty" label, hence ignore them here. */ - if (streq_ptr(label, "_empty")) + r = blkid_partition_get_uuid_id128(pp, &id); + if (r < 0) { + log_device_debug_errno(dev, r, "Failed to get partition UUID, ignoring: %m"); continue; + } - /* We found a suitable root partition, let's remember the first one, or the one with - * the newest version, as determined by comparing the partition labels. */ + r = blkid_partition_get_type_id128(pp, &type); + if (r < 0) { + log_device_debug_errno(dev, r, "Failed to get partition type UUID, ignoring: %m"); + continue; + } - if (sd_id128_is_null(root_id) || strverscmp_improved(label, root_label) > 0) { - root_id = id; + label = blkid_partition_get_name(pp); /* returns NULL if empty */ - if (free_and_strdup(&root_label, label) < 0) - return log_oom_debug(); + if (need_esp_or_xbootldr && sd_id128_in_set(type, SD_GPT_ESP, SD_GPT_XBOOTLDR)) { + + /* We found an ESP or XBOOTLDR, let's see if it matches the ESP/XBOOTLDR we booted from. */ + if (sd_id128_equal(id, esp_or_xbootldr)) + found_esp_or_xbootldr = true; + + } else if (sd_id128_equal(type, SD_GPT_ROOT_NATIVE)) { + unsigned long long flags; + + flags = blkid_partition_get_flags(pp); + if (flags & SD_GPT_FLAG_NO_AUTO) + continue; + + /* systemd-sysupdate expects empty partitions to be marked with an "_empty" label, hence ignore them here. */ + if (streq_ptr(label, "_empty")) + continue; + + /* We found a suitable root partition, let's remember the first one, or the one with + * the newest version, as determined by comparing the partition labels. */ + + if (sd_id128_is_null(root_id) || strverscmp_improved(label, root_label) > 0) { + root_id = id; + + if (free_and_strdup(&root_label, label) < 0) + return log_oom_debug(); + } } } } From e71f60476b9a82c308bcd2b0f9d3df3d0dc8d291 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Mar 2025 16:13:48 +0100 Subject: [PATCH 02/21] udev-builtin-blkid: skip GPT dissection unless we actually have partition support --- src/udev/udev-builtin-blkid.c | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/udev/udev-builtin-blkid.c b/src/udev/udev-builtin-blkid.c index 363f07c24a..1448fa3f04 100644 --- a/src/udev/udev-builtin-blkid.c +++ b/src/udev/udev-builtin-blkid.c @@ -145,8 +145,24 @@ static int find_gpt_root(UdevEvent *event, blkid_probe pr, const char *loop_back * After the initrd→host transition: look at the current block device mounted at / and set the same * properties to its whole block device. */ - if (!device_is_devtype(dev, "disk")) { - log_device_debug(dev, "Skipping GPT root logic on partition block device."); + const char *devnode; + r = sd_device_get_devname(dev, &devnode); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device node: %m"); + + r = block_device_is_whole_disk(dev); + if (r < 0) + return log_device_debug_errno(dev, r, "Unable to determine if device '%s' is a whole-block device: %m", devnode); + if (r == 0) { + log_device_debug(dev, "Invoked on device '%s' which is not a whole-disk block device, ignoring.", devnode); + return 0; + } + + r = blockdev_partscan_enabled(dev); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to determine if block device '%s' supports partitions: %m", devnode); + if (r == 0) { + log_device_debug(dev, "Invoked on block device '%s' that lacks partition scanning, ignoring.", devnode); return 0; } From a8b2302bc12364662ec0576f0ef28345e079679f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 5 Mar 2025 17:42:33 +0100 Subject: [PATCH 03/21] udev: add dissect_image builtin Fixes: #33453 #27897 #18035 --- rules.d/90-image-dissect.rules | 23 ++ src/shared/dissect-image.c | 41 +++ src/shared/dissect-image.h | 4 +- src/udev/meson.build | 1 + src/udev/udev-builtin-dissect_image.c | 379 ++++++++++++++++++++++++++ src/udev/udev-builtin.c | 1 + src/udev/udev-builtin.h | 1 + src/udev/udev-def.h | 2 + 8 files changed, 451 insertions(+), 1 deletion(-) create mode 100644 src/udev/udev-builtin-dissect_image.c diff --git a/rules.d/90-image-dissect.rules b/rules.d/90-image-dissect.rules index e606d266b4..9bb097a7fe 100644 --- a/rules.d/90-image-dissect.rules +++ b/rules.d/90-image-dissect.rules @@ -30,4 +30,27 @@ LABEL="gpt_auto_root_end" ENV{DM_UUID}=="CRYPT-*", ENV{DM_NAME}=="root", IMPORT{builtin}="factory_reset status", SYMLINK+="gpt-auto-root" ENV{DM_UUID}=="CRYPT-*", ENV{DM_NAME}=="root", ENV{ID_FACTORY_RESET}=="on|complete", SYMLINK+="gpt-auto-root-ignore-factory-reset" +# If this is the whole disk that we booted from, then dissect it +ENV{DEVTYPE}=="disk", ENV{ID_PART_GPT_AUTO_ROOT_DISK}=="1", IMPORT{builtin}="dissect_image probe" +ENV{DEVTYPE}=="disk", ENV{ID_PART_GPT_AUTO_ROOT_DISK}=="1", ENV{ID_FACTORY_RESET}=="", IMPORT{builtin}="factory_reset status" + +# If this is a partition, and we found something on the parent, then copy the +# right properties from the parent, and rename them +ENV{DEVTYPE}=="partition", ENV{ID_DISSECT_IMAGE}!="", IMPORT{builtin}="dissect_image copy" + +# Create symlinks based on the designator for the partitions themselves. If we detect LUKS or Verity, suffix them with "-luks" or "-vdata" +ENV{DEVTYPE}!="partition", GOTO="dissect_partition_symlinks_end" + ENV{ID_DISSECT_PART_DESIGNATOR}=="", GOTO="dissect_partition_symlinks_end" + ENV{ID_FS_TYPE}!="crypto_LUKS", ENV{ID_DISSECT_PART_HAS_VERITY}!="1", ENV{ID_FACTORY_RESET}!="on", SYMLINK+="disk/by-designator/$env{ID_DISSECT_PART_DESIGNATOR}" + ENV{ID_FS_TYPE}!="crypto_LUKS", ENV{ID_DISSECT_PART_HAS_VERITY}!="1", ENV{ID_FACTORY_RESET}=="on|complete", SYMLINK+="disk/by-designator/$env{ID_DISSECT_PART_DESIGNATOR}-ignore-factory-reset" + ENV{ID_FS_TYPE}=="crypto_LUKS", ENV{ID_FACTORY_RESET}!="on", SYMLINK+="disk/by-designator/$env{ID_DISSECT_PART_DESIGNATOR}-luks" + ENV{ID_FS_TYPE}=="crypto_LUKS", ENV{ID_FACTORY_RESET}=="on|complete", SYMLINK+="disk/by-designator/$env{ID_DISSECT_PART_DESIGNATOR}-luks-ignore-factory-reset" + ENV{ID_FS_TYPE}!="crypto_LUKS", ENV{ID_DISSECT_PART_HAS_VERITY}=="1", ENV{ID_FACTORY_RESET}!="on", SYMLINK+="disk/by-designator/$env{ID_DISSECT_PART_DESIGNATOR}-verity-data" + ENV{ID_FS_TYPE}!="crypto_LUKS", ENV{ID_DISSECT_PART_HAS_VERITY}=="1", ENV{ID_FACTORY_RESET}=="on|complete", SYMLINK+="disk/by-designator/$env{ID_DISSECT_PART_DESIGNATOR}-verity-data-ignore-factory-reset" +LABEL="dissect_partition_symlinks_end" + +# For LUKS or Verity partitions we rely on the selected volume name +ENV{DM_UUID}=="CRYPT-*", ENV{DM_NAME}=="root|usr|home|srv|swap|tmp|var", IMPORT{builtin}="factory_reset status", SYMLINK+="disk/by-designator/$env{DM_NAME}" +ENV{DM_UUID}=="CRYPT-*", ENV{DM_NAME}=="root|usr|home|srv|swap|tmp|var", ENV{ID_FACTORY_RESET}=="on|complete", SYMLINK+="disk/by-designator/$env{DM_NAME}-ignore-factory-reset" + LABEL="image_dissect_end" diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 8c8592cc77..76db2834db 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -3384,6 +3384,47 @@ int verity_settings_load( return 1; } +int verity_settings_copy(VeritySettings *dest, const VeritySettings *source) { + assert(dest); + + if (!source) { + *dest = VERITY_SETTINGS_DEFAULT; + return 0; + } + + _cleanup_free_ void *rh = NULL; + if (source->root_hash_size > 0) { + rh = memdup(source->root_hash, source->root_hash_size); + if (!rh) + return log_oom_debug(); + } + + _cleanup_free_ void *sig = NULL; + if (source->root_hash_sig_size > 0) { + sig = memdup(source->root_hash_sig, source->root_hash_sig_size); + if (!sig) + return log_oom_debug(); + } + + _cleanup_free_ char *p = NULL; + if (source->data_path) { + p = strdup(source->data_path); + if (!p) + return log_oom_debug(); + } + + *dest = (VeritySettings) { + .root_hash = TAKE_PTR(rh), + .root_hash_size = source->root_hash_size, + .root_hash_sig = TAKE_PTR(sig), + .root_hash_sig_size = source->root_hash_sig_size, + .data_path = TAKE_PTR(p), + .designator = source->designator, + }; + + return 1; +} + int dissected_image_load_verity_sig_partition( DissectedImage *m, int fd, diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 9de93039da..dbdd13b5ae 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -144,7 +144,7 @@ struct VeritySettings { PartitionDesignator designator; }; -#define VERITY_SETTINGS_DEFAULT { \ +#define VERITY_SETTINGS_DEFAULT (VeritySettings) { \ .designator = _PARTITION_DESIGNATOR_INVALID \ } @@ -226,6 +226,8 @@ static inline bool verity_settings_data_covers(const VeritySettings *verity, Par verity->data_path; } +int verity_settings_copy(VeritySettings *dest, const VeritySettings *source); + int dissected_image_load_verity_sig_partition(DissectedImage *m, int fd, VeritySettings *verity); bool dissected_image_verity_candidate(const DissectedImage *image, PartitionDesignator d); diff --git a/src/udev/meson.build b/src/udev/meson.build index 00bc581a7d..9b2615173a 100644 --- a/src/udev/meson.build +++ b/src/udev/meson.build @@ -22,6 +22,7 @@ libudevd_core_sources = files( 'net/link-config.c', 'udev-builtin.c', 'udev-builtin-btrfs.c', + 'udev-builtin-dissect_image.c', 'udev-builtin-factory_reset.c', 'udev-builtin-hwdb.c', 'udev-builtin-input_id.c', diff --git a/src/udev/udev-builtin-dissect_image.c b/src/udev/udev-builtin-dissect_image.c new file mode 100644 index 0000000000..9ba58cdadd --- /dev/null +++ b/src/udev/udev-builtin-dissect_image.c @@ -0,0 +1,379 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "blockdev-util.h" +#include "device-util.h" +#include "dissect-image.h" +#include "fd-util.h" +#include "hexdecoct.h" +#include "image-policy.h" +#include "initrd-util.h" +#include "loop-util.h" +#include "proc-cmdline.h" +#include "udev-builtin.h" + +static ImagePolicy *arg_image_policy = NULL; +static VeritySettings arg_verity_settings = VERITY_SETTINGS_DEFAULT; + +static int acquire_image_policy(ImagePolicy **ret) { + int r; + + assert(ret); + + _cleanup_free_ char *value = NULL; + r = proc_cmdline_get_key("systemd.image_policy", /* flags= */ 0, &value); + if (r < 0) + return log_error_errno(r, "Failed to read systemd.image_policy= kernel command line switch: %m"); + if (r == 0) { + *ret = NULL; + return 0; + } + + r = image_policy_from_string(value, ret); + if (r < 0) + return log_error_errno(r, "Failed to parse image policy '%s': %m", value); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *s = NULL; + + (void) image_policy_to_string(*ret, /* simplify= */ true, &s); + log_debug("Loaded image policy: %s", strna(s)); + } + + return 1; +} + +static int acquire_verity_settings(VeritySettings *ret) { + _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT; + int r; + + assert(ret); + + _cleanup_free_ char *h = NULL; + r = proc_cmdline_get_key("roothash", /* flags= */ 0, &h); + if (r < 0) + return log_error_errno(r, "Failed to read roothash= kernel command line switch: %m"); + if (r > 0) + verity.designator = PARTITION_ROOT; + + _cleanup_free_ char *uh = NULL; + r = proc_cmdline_get_key("usrhash", /* flags= */ 0, &uh); + if (r < 0) + return log_error_errno(r, "Failed to read usrhash= kernel command line switch: %m"); + if (r > 0) { + if (h) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Both roothash= and usrhash= specified, refusing."); + + h = TAKE_PTR(uh); + verity.designator = PARTITION_USR; + } + + if (h) { + r = unhexmem(h, &verity.root_hash, &verity.root_hash_size); + if (r < 0) + return log_error_errno(r, "Failed to parse root hash from kernel command line switch: %m"); + } + + *ret = TAKE_GENERIC(verity, VeritySettings, VERITY_SETTINGS_DEFAULT); + return 0; +} + +static int verb_probe(UdevEvent *event, sd_device *dev) { + int r; + + assert(event); + assert(dev); + + /* This is invoked on 'main' block devices to probe the partition table. We will generate some + * properties with general image information, and then a bunch of properties for each partition, with + * the partition index in the variable name. These fields will be copied into partition block devices + * when the dissect_image builtin is later called with the "copy" verb, i.e. in verb_copy() below. */ + + const char *devnode; + r = sd_device_get_devname(dev, &devnode); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device node: %m"); + if (block_device_is_whole_disk(dev) <= 0) { + log_device_debug(dev, "Must be invoked on whole block device (was invoked in '%s), ignoring.", devnode); + return 0; + } + + r = blockdev_partscan_enabled(dev); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to determine if block device '%s' supports partitions: %m", devnode); + if (r == 0) { + log_device_debug(dev, "Invoked on block device '%s' that lacks partition scanning, ignoring.", devnode); + return 0; + } + + _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL; + r = loop_device_open(dev, O_RDONLY, LOCK_SH, &loop); + if (ERRNO_IS_NEG_DEVICE_ABSENT(r)) { + log_device_debug(dev, "Device absent while opening block device '%s', ignoring.", devnode); + return 0; + } + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to open block device '%s: %m", devnode); + + const ImagePolicy *image_policy = arg_image_policy ?: &image_policy_host; + _cleanup_(dissected_image_unrefp) DissectedImage *image = NULL; + r = dissect_loop_device( + loop, + &arg_verity_settings, + /* mount_options= */ NULL, + image_policy, + DISSECT_IMAGE_READ_ONLY| + DISSECT_IMAGE_GPT_ONLY| + DISSECT_IMAGE_USR_NO_ROOT| + DISSECT_IMAGE_ALLOW_EMPTY, + &image); + if (r == -ERFKILL && !in_initrd()) { + /* If we transitioned into the main system and we couldn't dissect the image with the full + * policy, let's see if it works if we set the policies for /usr/ and the root fs out of the + * policy. After all, we already made our choices, there's no point in insisting on the + * policy here. */ + + static const PartitionDesignator ignore_designators[] = { + PARTITION_ROOT, + PARTITION_ROOT_VERITY, + PARTITION_ROOT_VERITY_SIG, + PARTITION_USR, + PARTITION_USR_VERITY, + PARTITION_USR_VERITY_SIG, + }; + + _cleanup_(image_policy_freep) ImagePolicy *image_policy_mangled = NULL; + r = image_policy_ignore_designators( + image_policy, + ignore_designators, + ELEMENTSOF(ignore_designators), + &image_policy_mangled); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to remove root/usr partitions from image policy: %m"); + + if (image_policy_equal(image_policy, image_policy_mangled)) + r = -ERFKILL; /* restore original error, if this didn't change anything */ + else { + if (DEBUG_LOGGING) { + _cleanup_free_ char *a = NULL, *b = NULL; + + (void) image_policy_to_string(image_policy, /* simplify= */ false, &a); + (void) image_policy_to_string(image_policy_mangled, /* simplify= */ false, &b); + + log_device_debug_errno(dev, ERFKILL, "Couldn't dissect block device with regular policy '%s', retrying with policy where root/usr are set to ignore '%s'.", a, b); + } + + r = dissect_loop_device( + loop, + &arg_verity_settings, + /* mount_options= */ NULL, + image_policy_mangled, + DISSECT_IMAGE_READ_ONLY| + DISSECT_IMAGE_GPT_ONLY| + DISSECT_IMAGE_USR_NO_ROOT| + DISSECT_IMAGE_ALLOW_EMPTY, + &image); + } + } + if (IN_SET(r, -ENOPKG, -ENOMSG, -ENXIO, -ENOTUNIQ)) { + log_device_debug_errno(dev, r, "Device does not carry a GPT disk label with suitable partitions, ignoring."); + return 0; + } + if (r == -ERFKILL) { + log_device_debug_errno(dev, r, "Device carries GPT disk label that doesn't match our image policy, ignoring."); + return 0; + } + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to dissect disk image: %m"); + + /* Let's try to load verity data from the image now, so that we can attach it to the device via udev + * properties */ + _cleanup_(verity_settings_done) VeritySettings verity = VERITY_SETTINGS_DEFAULT; + r = verity_settings_copy(&verity, &arg_verity_settings); + if (r < 0) + return r; + + r = dissected_image_load_verity_sig_partition(image, loop->fd, &verity); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to load verity signature data from image: %m"); + + /* Marker that we determined this to be a suitable image */ + (void) udev_builtin_add_property(event, "ID_DISSECT_IMAGE", "1"); + + /* Output the primary architecture this image is intended for */ + Architecture a = dissected_image_architecture(image); + if (a >= 0) + (void) udev_builtin_add_property(event, "ID_DISSECT_IMAGE_ARCHITECTURE", architecture_to_string(a)); + + /* And now output the intended designator and architecture (if it applies) for all partitions we + * found and think belong to this system */ + FOREACH_ELEMENT(p, image->partitions) { + PartitionDesignator d = p - image->partitions; + if (!p->found) + continue; + + assert(p->partno > 0); + + /* Indicate designator for partition */ + _cleanup_free_ char *df = NULL; + if (asprintf(&df, "ID_DISSECT_PART%i_DESIGNATOR", p->partno) < 0) + return log_oom_debug(); + (void) udev_builtin_add_property(event, df, partition_designator_to_string(d)); + + if (p->architecture >= 0) { + _cleanup_free_ char *f = NULL; + if (asprintf(&f, "ID_DISSECT_PART%i_ARCHITECTURE", p->partno) < 0) + return log_oom_debug(); + (void) udev_builtin_add_property(event, f, architecture_to_string(p->architecture)); + } + + /* Indicate whether this partition has verity protection */ + PartitionDesignator dv = partition_verity_of(d); + if (dv >= 0 && image->partitions[dv].found) { + _cleanup_free_ char *f = NULL; + if (asprintf(&f, "ID_DISSECT_PART%i_HAS_VERITY", p->partno) < 0) + return log_oom_debug(); + + (void) udev_builtin_add_property(event, f, "1"); + } + + dv = partition_verity_sig_of(d); + if (dv >= 0 && image->partitions[dv].found) { + _cleanup_free_ char *f = NULL; + if (asprintf(&f, "ID_DISSECT_PART%i_HAS_VERITY_SIG", p->partno) < 0) + return log_oom_debug(); + + (void) udev_builtin_add_property(event, f, "1"); + } + + if (d == verity.designator) { + if (verity.root_hash_size > 0) { + _cleanup_free_ char *f = NULL; + if (asprintf(&f, "ID_DISSECT_PART%i_ROOTHASH", p->partno) < 0) + return log_oom_debug(); + + _cleanup_free_ char *h = hexmem(verity.root_hash, verity.root_hash_size); + if (!h) + return log_oom_debug(); + + (void) udev_builtin_add_property(event, f, h); + } + + if (verity.root_hash_sig_size > 0) { + _cleanup_free_ char *f = NULL; + if (asprintf(&f, "ID_DISSECT_PART%i_ROOTHASH_SIG", p->partno) < 0) + return log_oom_debug(); + + _cleanup_free_ char *h = NULL; + if (base64mem(verity.root_hash_sig, verity.root_hash_sig_size, &h) < 0) + return log_oom_debug(); + + (void) udev_builtin_add_property(event, f, h); + } + } + } + + return 0; +} + +static int verb_copy(UdevEvent *event, sd_device *dev) { + int r; + + assert(event); + assert(dev); + + /* This is called for the partition block devices, and will copy the per-partition properties we + * probed on the main block device into the partition device */ + + const char *devnode; + r = sd_device_get_devname(dev, &devnode); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get device node: %m"); + + if (!device_in_subsystem(dev, "block")) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Invoked on non-block device '%s', refusing: %m", devnode); + if (!device_is_devtype(dev, "partition")) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Invoked on non-partition block device '%s', refusing: %m", devnode); + + sd_device *parent; + r = sd_device_get_parent(dev, &parent); + if (r < 0) + return log_error_errno(r, "Failed to get parent of device '%s': %m", devnode); + + if (!device_in_subsystem(parent, "block")) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Parent of block device '%s' is not a block device, refusing: %m", devnode); + if (!device_is_devtype(parent, "disk")) + return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Parent of block device '%s' is not a whole block device, refusing: %m", devnode); + + const char *partn; + r = sd_device_get_property_value(dev, "PARTN", &partn); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get partition number of partition block device '%s': %m", devnode); + + FOREACH_STRING(f, "_DESIGNATOR", "_ARCHITECTURE", "_HAS_VERITY", "_HAS_VERITY_SIG", "_ROOTHASH", "_ROOTHASH_SIG") { + /* The property on the parent device contains the partition number */ + _cleanup_free_ char *p = strjoin("ID_DISSECT_PART", partn, f); + if (!p) + return log_oom_debug(); + + const char *v; + r = sd_device_get_property_value(parent, p, &v); + if (r == -ENOENT) + continue; + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to get '%s' property of parent of '%s': %m", p, devnode); + + /* When we copy this property to the partition we drop the partition number, so that we have + * a constant field name */ + _cleanup_free_ char *c = strjoin("ID_DISSECT_PART", f); + if (!c) + return log_oom_debug(); + + (void) udev_builtin_add_property(event, c, v); + } + + return 0; +} + +static int builtin_dissect_image(UdevEvent *event, int argc, char *argv[]) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + + if (argc != 2) + return log_device_warning_errno( + dev, SYNTHETIC_ERRNO(EINVAL), "%s: expected single argument.", argv[0]); + + if (streq(argv[1], "probe")) + return verb_probe(event, dev); + if (streq(argv[1], "copy")) + return verb_copy(event, dev); + + return log_device_warning_errno( + dev, SYNTHETIC_ERRNO(EINVAL), "%s: unknown verb '%s'", argv[0], argv[1]); +} + +static int builtin_dissect_image_init(void) { + int r; + + r = acquire_image_policy(&arg_image_policy); + if (r < 0) + return r; + + r = acquire_verity_settings(&arg_verity_settings); + if (r < 0) + return r; + + return 0; +} + +static void builtin_dissect_image_exit(void) { + arg_image_policy = image_policy_free(arg_image_policy); + verity_settings_done(&arg_verity_settings); +} + +const UdevBuiltin udev_builtin_dissect_image = { + .name = "dissect_image", + .cmd = builtin_dissect_image, + .init = builtin_dissect_image_init, + .exit = builtin_dissect_image_exit, + .help = "Dissect Disk Images", + .run_once = true, +}; diff --git a/src/udev/udev-builtin.c b/src/udev/udev-builtin.c index 8df25e1603..692b244cb8 100644 --- a/src/udev/udev-builtin.c +++ b/src/udev/udev-builtin.c @@ -15,6 +15,7 @@ static const UdevBuiltin *const builtins[_UDEV_BUILTIN_MAX] = { [UDEV_BUILTIN_BLKID] = &udev_builtin_blkid, #endif [UDEV_BUILTIN_BTRFS] = &udev_builtin_btrfs, + [UDEV_BUILTIN_DISSECT_IMAGE] = &udev_builtin_dissect_image, [UDEV_BUILTIN_FACTORY_RESET] = &udev_builtin_factory_reset, [UDEV_BUILTIN_HWDB] = &udev_builtin_hwdb, [UDEV_BUILTIN_INPUT_ID] = &udev_builtin_input_id, diff --git a/src/udev/udev-builtin.h b/src/udev/udev-builtin.h index f38f8bd93e..1467f36cd6 100644 --- a/src/udev/udev-builtin.h +++ b/src/udev/udev-builtin.h @@ -37,6 +37,7 @@ typedef struct UdevBuiltin { extern const UdevBuiltin udev_builtin_blkid; #endif extern const UdevBuiltin udev_builtin_btrfs; +extern const UdevBuiltin udev_builtin_dissect_image; extern const UdevBuiltin udev_builtin_factory_reset; extern const UdevBuiltin udev_builtin_hwdb; extern const UdevBuiltin udev_builtin_input_id; diff --git a/src/udev/udev-def.h b/src/udev/udev-def.h index d01d8d9875..d744f959ae 100644 --- a/src/udev/udev-def.h +++ b/src/udev/udev-def.h @@ -40,6 +40,7 @@ typedef enum UdevBuiltinCommand { UDEV_BUILTIN_BLKID, #endif UDEV_BUILTIN_BTRFS, + UDEV_BUILTIN_DISSECT_IMAGE, UDEV_BUILTIN_FACTORY_RESET, UDEV_BUILTIN_HWDB, UDEV_BUILTIN_INPUT_ID, @@ -64,6 +65,7 @@ typedef enum UdevReloadFlags { UDEV_RELOAD_BUILTIN_BLKID = 1u << UDEV_BUILTIN_BLKID, #endif UDEV_RELOAD_BUILTIN_BTRFS = 1u << UDEV_BUILTIN_BTRFS, + UDEV_RELOAD_BUILTIN_DISSECT_IMAGE = 1u << UDEV_BUILTIN_DISSECT_IMAGE, UDEV_RELOAD_BUILTIN_FACTORY_RESET = 1u << UDEV_BUILTIN_FACTORY_RESET, UDEV_RELOAD_BUILTIN_HWDB = 1u << UDEV_BUILTIN_HWDB, UDEV_RELOAD_BUILTIN_INPUT_ID = 1u << UDEV_BUILTIN_INPUT_ID, From 923eabf085a03a213af3a2c0eeb2086bcf5959a5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Mar 2025 11:46:38 +0100 Subject: [PATCH 04/21] dissect-image: rework how we determine that we are ready to do verity/verity-sig Previously, if during early enumeration of the partition table we figured out we have a verity + verity-sig partition, we determined we are "ready" to do verity/verity-sig, as reported by the "verity_ready" and "verity_ready_sig" fields of DissectedImage. Let's rework this: only determine we are ready in case the VeritySettings structure is fully populated. Mark verity-sig/verity only as ready once we actually *load* the verity sig metadata from the special partition. This is conceptually more correct, as we consider things "ready" only if we actually have all data for it loaded. It's also preparation for a later commit that guesses the verity root hash based on what we discover. --- src/shared/dissect-image.c | 81 +++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 76db2834db..27509515f0 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -1487,12 +1487,6 @@ static int dissect_image( if (verity->designator >= 0 && !m->partitions[verity->designator].found) return -EADDRNOTAVAIL; - bool have_verity_sig_partition; - if (verity->designator >= 0) - have_verity_sig_partition = m->partitions[verity->designator == PARTITION_USR ? PARTITION_USR_VERITY_SIG : PARTITION_ROOT_VERITY_SIG].found; - else - have_verity_sig_partition = m->partitions[PARTITION_USR_VERITY_SIG].found || m->partitions[PARTITION_ROOT_VERITY_SIG].found; - if (verity->root_hash) { /* If we have an explicit root hash and found the partitions for it, then we are ready to use * Verity, set things up for it */ @@ -1503,8 +1497,6 @@ static int dissect_image( /* If we found a verity setup, then the root partition is necessarily read-only. */ m->partitions[PARTITION_ROOT].rw = false; - m->verity_ready = true; - } else { assert(verity->designator == PARTITION_USR); @@ -1512,23 +1504,12 @@ static int dissect_image( return -EADDRNOTAVAIL; m->partitions[PARTITION_USR].rw = false; - m->verity_ready = true; } - if (m->verity_ready) - m->verity_sig_ready = verity->root_hash_sig || have_verity_sig_partition; + m->verity_ready = true; - } else if (have_verity_sig_partition) { - - /* If we found an embedded signature partition, we are ready, too. */ - - m->verity_ready = m->verity_sig_ready = true; - if (verity->designator >= 0) - m->partitions[verity->designator == PARTITION_USR ? PARTITION_USR : PARTITION_ROOT].rw = false; - else if (m->partitions[PARTITION_USR_VERITY_SIG].found) - m->partitions[PARTITION_USR].rw = false; - else if (m->partitions[PARTITION_ROOT_VERITY_SIG].found) - m->partitions[PARTITION_ROOT].rw = false; + if (verity->root_hash_sig) + m->verity_sig_ready = true; } } @@ -3430,15 +3411,6 @@ int dissected_image_load_verity_sig_partition( int fd, VeritySettings *verity) { - _cleanup_free_ void *root_hash = NULL, *root_hash_sig = NULL; - _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; - size_t root_hash_size, root_hash_sig_size; - _cleanup_free_ char *buf = NULL; - PartitionDesignator d; - DissectedPartition *p; - sd_json_variant *rh, *sig; - ssize_t n; - char *e; int r; assert(m); @@ -3454,10 +3426,28 @@ int dissected_image_load_verity_sig_partition( if (r == 0) return 0; - d = partition_verity_sig_of(verity->designator < 0 ? PARTITION_ROOT : verity->designator); - assert(d >= 0); + PartitionDesignator dd = verity->designator; + if (dd < 0) { + if (m->partitions[PARTITION_ROOT_VERITY].found) + dd = PARTITION_ROOT; + else if (m->partitions[PARTITION_USR_VERITY].found) + dd = PARTITION_USR; + else + return 0; + } - p = m->partitions + d; + if (!m->partitions[dd].found) + return 0; + + PartitionDesignator dv = partition_verity_of(dd); + assert(dv >= 0); + if (!m->partitions[dv].found) + return 0; + + PartitionDesignator ds = partition_verity_sig_of(dd); + assert(ds >= 0); + + DissectedPartition *p = m->partitions + ds; if (!p->found) return 0; if (p->offset == UINT64_MAX || p->size == UINT64_MAX) @@ -3466,17 +3456,17 @@ int dissected_image_load_verity_sig_partition( if (p->size > 4*1024*1024) /* Signature data cannot possible be larger than 4M, refuse that */ return log_debug_errno(SYNTHETIC_ERRNO(EFBIG), "Verity signature partition is larger than 4M, refusing."); - buf = new(char, p->size+1); + _cleanup_free_ char *buf = new(char, p->size+1); if (!buf) return -ENOMEM; - n = pread(fd, buf, p->size, p->offset); + ssize_t n = pread(fd, buf, p->size, p->offset); if (n < 0) return -ENOMEM; if ((uint64_t) n != p->size) return -EIO; - e = memchr(buf, 0, p->size); + const char *e = memchr(buf, 0, p->size); if (e) { /* If we found a NUL byte then the rest of the data must be NUL too */ if (!memeqzero(e, p->size - (e - buf))) @@ -3484,14 +3474,17 @@ int dissected_image_load_verity_sig_partition( } else buf[p->size] = 0; - r = sd_json_parse(buf, 0, &v, NULL, NULL); + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + r = sd_json_parse(buf, 0, &v, /* reterr_line= */ NULL, /* reterr_column= */ NULL); if (r < 0) return log_debug_errno(r, "Failed to parse signature JSON data: %m"); - rh = sd_json_variant_by_key(v, "rootHash"); + sd_json_variant *rh = sd_json_variant_by_key(v, "rootHash"); if (!rh) return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Signature JSON object lacks 'rootHash' field."); + _cleanup_free_ void *root_hash = NULL; + size_t root_hash_size; r = sd_json_variant_unhex(rh, &root_hash, &root_hash_size); if (r < 0) return log_debug_errno(r, "Failed to parse root hash field: %m"); @@ -3507,10 +3500,12 @@ int dissected_image_load_verity_sig_partition( return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Root hash in signature JSON data (%s) doesn't match configured hash (%s).", strna(a), strna(b)); } - sig = sd_json_variant_by_key(v, "signature"); + sd_json_variant *sig = sd_json_variant_by_key(v, "signature"); if (!sig) return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Signature JSON object lacks 'signature' field."); + _cleanup_free_ void *root_hash_sig = NULL; + size_t root_hash_sig_size; r = sd_json_variant_unbase64(sig, &root_hash_sig, &root_hash_sig_size); if (r < 0) return log_debug_errno(r, "Failed to parse signature field: %m"); @@ -3521,6 +3516,12 @@ int dissected_image_load_verity_sig_partition( free_and_replace(verity->root_hash_sig, root_hash_sig); verity->root_hash_sig_size = root_hash_sig_size; + verity->designator = dd; + + m->verity_ready = true; + m->verity_sig_ready = true; + m->partitions[dd].rw = false; + return 1; } From e34c89897af9b0c7af49d0137bf04fa31b172c4a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Mar 2025 11:57:34 +0100 Subject: [PATCH 05/21] dissect-image: guess verity root hash from the resources we found When dissecting an image, let's make use of the Verity data even if we got told no root hash explicitly: we can simply determine it by concatenating the data partition uuid with the verity partition uuid. Of course, on first thought this doesn't really add much: if the root hash is not pinned from somewhere, this does not guarantee trust in the image. However, this is very useful for attestation: if we have the root hash we can measure it before mounting things, even if we don't actually authenticate it. Hence, at best this helps us with attestation, at worst it doesn't improve security but certainly doesn't hurt it. --- docs/ENVIRONMENT.md | 8 +++ src/core/namespace.c | 6 +++ src/dissect/dissect.c | 12 ++++- src/mountfsd/mountwork.c | 6 +++ src/nspawn/nspawn.c | 14 +++++- src/shared/dissect-image.c | 71 +++++++++++++++++++++++++++ src/shared/dissect-image.h | 1 + src/sysext/sysext.c | 6 +++ src/udev/udev-builtin-dissect_image.c | 4 ++ 9 files changed, 124 insertions(+), 4 deletions(-) diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index c340e01d01..55f6d22b49 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -529,6 +529,14 @@ disk images with `--image=` or similar: images. Defaults to true, i.e. userspace signature validation is allowed. If false, authentication can be done only via the kernel's internal keyring. +* `$SYSTEMD_DISSECT_VERITY_GUESS` – takes a boolean. Controls whether to guess + the Verity root hash from the partition UUIDs of a suitable pair of data + partition and matching Verity partition: the UUIDs two are simply joined and + used as root hash, in accordance with the recommendations in [Discoverable + Partitions + Specification](https://uapi-group.org/specifications/specs/discoverable_partitions_specification). Defaults + to true. + `systemd-cryptsetup`: * `$SYSTEMD_CRYPTSETUP_USE_TOKEN_MODULE` – takes a boolean, which controls diff --git a/src/core/namespace.c b/src/core/namespace.c index 7e131b1425..fac3c05f61 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -2364,6 +2364,12 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { if (r < 0) return r; + r = dissected_image_guess_verity_roothash( + dissected_image, + p->verity); + if (r < 0) + return r; + r = dissected_image_decrypt( dissected_image, NULL, diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index fc03df5cc6..06d6d3935f 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -2177,8 +2177,10 @@ static int run(int argc, char *argv[]) { if (arg_image) { r = verity_settings_load( - &arg_verity_settings, - arg_image, NULL, NULL); + &arg_verity_settings, + arg_image, + /* root_hash_path= */ NULL, + /* root_hash_sig_path= */ NULL); if (r < 0) return log_error_errno(r, "Failed to read verity artifacts for %s: %m", arg_image); @@ -2244,6 +2246,12 @@ static int run(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to load verity signature partition: %m"); + r = dissected_image_guess_verity_roothash( + m, + &arg_verity_settings); + if (r < 0) + return log_error_errno(r, "Failed to guess verity root hash: %m"); + if (arg_action != ACTION_DISSECT) { r = dissected_image_decrypt_interactively( m, NULL, diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c index 41c1d65391..adbb91d8a0 100644 --- a/src/mountfsd/mountwork.c +++ b/src/mountfsd/mountwork.c @@ -461,6 +461,12 @@ static int vl_method_mount_image( if (r < 0) return r; + r = dissected_image_guess_verity_roothash( + di, + &verity); + if (r < 0) + return r; + r = dissected_image_decrypt( di, p.password, diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 3004bfc1b7..32796375c5 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -6391,10 +6391,20 @@ static int run(int argc, char *argv[]) { dissected_image, loop->fd, &arg_verity_settings); - if (r < 0) + if (r < 0) { + log_error_errno(r, "Failed to load Verity signature partition: %m"); goto finish; + } - if (dissected_image->has_verity && !arg_verity_settings.root_hash && !dissected_image->has_verity_sig) + r = dissected_image_guess_verity_roothash( + dissected_image, + &arg_verity_settings); + if (r < 0) { + log_error_errno(r, "Failed to guess Verity root hash: %m"); + goto finish; + } + + if (dissected_image->has_verity && !arg_verity_settings.root_hash) log_notice("Note: image %s contains verity information, but no root hash specified and no embedded " "root hash signature found! Proceeding without integrity checking.", arg_image); diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 27509515f0..c8e6f8f121 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -3525,6 +3525,69 @@ int dissected_image_load_verity_sig_partition( return 1; } +int dissected_image_guess_verity_roothash( + DissectedImage *m, + VeritySettings *verity) { + + int r; + + assert(m); + assert(verity); + + /* Guesses the Verity root hash from the partitions we found, taking into account that as per + * https://uapi-group.org/specifications/specs/discoverable_partitions_specification/ the UUIDS of + * the data and verity partitions are respectively the first and second halves of the dm-verity + * roothash. + * + * Note of course that relying on this guesswork is mostly useful for later attestation, not so much + * for a-priori security. */ + + if (verity->root_hash) /* Already loaded? */ + return 0; + + r = secure_getenv_bool("SYSTEMD_DISSECT_VERITY_GUESS"); + if (r < 0 && r != -ENXIO) + log_debug_errno(r, "Failed to parse $SYSTEMD_DISSECT_VERITY_GUESS, ignoring: %m"); + if (r == 0) + return 0; + + PartitionDesignator dd = verity->designator; + if (dd < 0) { + if (m->partitions[PARTITION_ROOT_VERITY].found) + dd = PARTITION_ROOT; + else if (m->partitions[PARTITION_USR_VERITY].found) + dd = PARTITION_USR; + else + return 0; + } + + DissectedPartition *d = m->partitions + dd; + if (!d->found) + return 0; + + PartitionDesignator dv = partition_verity_of(dd); + assert(dv >= 0); + + DissectedPartition *p = m->partitions + dv; + if (!p->found) + return 0; + + _cleanup_free_ uint8_t *rh = malloc(sizeof(sd_id128_t) * 2); + if (!rh) + return log_oom_debug(); + + memcpy(mempcpy(rh, &d->uuid, sizeof(sd_id128_t)), &p->uuid, sizeof(sd_id128_t)); + verity->root_hash = TAKE_PTR(rh); + verity->root_hash_size = sizeof(sd_id128_t) * 2; + + verity->designator = dd; + + m->verity_ready = true; + m->partitions[dd].rw = false; + + return 0; +} + int dissected_image_acquire_metadata( DissectedImage *m, int userns_fd, @@ -4038,6 +4101,10 @@ int mount_image_privately_interactively( if (r < 0) return r; + r = dissected_image_guess_verity_roothash(dissected_image, &verity); + if (r < 0) + return r; + r = dissected_image_decrypt_interactively(dissected_image, NULL, &verity, flags); if (r < 0) return r; @@ -4181,6 +4248,10 @@ int verity_dissect_and_mount( if (r < 0) return r; + r = dissected_image_guess_verity_roothash(dissected_image, verity); + if (r < 0) + return r; + r = dissected_image_decrypt( dissected_image, NULL, diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index dbdd13b5ae..8725ff9921 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -229,6 +229,7 @@ static inline bool verity_settings_data_covers(const VeritySettings *verity, Par int verity_settings_copy(VeritySettings *dest, const VeritySettings *source); int dissected_image_load_verity_sig_partition(DissectedImage *m, int fd, VeritySettings *verity); +int dissected_image_guess_verity_roothash(DissectedImage *m, VeritySettings *verity); bool dissected_image_verity_candidate(const DissectedImage *image, PartitionDesignator d); bool dissected_image_verity_ready(const DissectedImage *image, PartitionDesignator d); diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index 420262cd45..d2bb992d14 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -1814,6 +1814,12 @@ static int merge_subprocess( if (r < 0) return r; + r = dissected_image_guess_verity_roothash( + m, + &verity_settings); + if (r < 0) + return r; + r = dissected_image_decrypt_interactively( m, NULL, &verity_settings, diff --git a/src/udev/udev-builtin-dissect_image.c b/src/udev/udev-builtin-dissect_image.c index 9ba58cdadd..3a598952ac 100644 --- a/src/udev/udev-builtin-dissect_image.c +++ b/src/udev/udev-builtin-dissect_image.c @@ -196,6 +196,10 @@ static int verb_probe(UdevEvent *event, sd_device *dev) { if (r < 0) return log_device_debug_errno(dev, r, "Failed to load verity signature data from image: %m"); + r = dissected_image_guess_verity_roothash(image, &verity); + if (r < 0) + return log_device_debug_errno(dev, r, "Failed to guess root hash from image: %m"); + /* Marker that we determined this to be a suitable image */ (void) udev_builtin_add_property(event, "ID_DISSECT_IMAGE", "1"); From 1e6f03a45d992030b45edcb7260c8abde96f9f88 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Mar 2025 12:01:17 +0100 Subject: [PATCH 06/21] dissect-image: relax image policy logic a bit Previously if we found a verity signature partition in an image, and the image policy required "verity" (but did not allow "signature") we'd refuse the image. This is of course unnecessarily strict: if "verity" is allowed, we can make use of the verity data, and ignore the signature data. hence, relax the rules here: when we pick up a partition and want to test it against the policy, always consider all "weaker" uses too, maybe they are allowed if the "stronger" users isn't. --- src/shared/dissect-image.c | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index c8e6f8f121..660a05379f 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -1519,20 +1519,29 @@ static int dissect_image( * 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); + PartitionPolicyFlags found_flags; + if (m->partitions[di].found) { + found_flags = PARTITION_POLICY_ENCRYPTED|PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_UNUSED; + + PartitionDesignator vi = partition_verity_of(di); + if (vi >= 0 && m->partitions[vi].found) { + found_flags |= PARTITION_POLICY_VERITY; + + PartitionDesignator si = partition_verity_sig_of(di); + if (si >= 0 && m->partitions[si].found) + found_flags |= PARTITION_POLICY_SIGNED; + } + } else + found_flags = m->partitions[di].ignored ? PARTITION_POLICY_UNUSED : PARTITION_POLICY_ABSENT; + + if (DEBUG_LOGGING) { + _cleanup_free_ char *s = NULL; + (void) partition_policy_flags_to_string(found_flags, /* simplify= */ false, &s); + log_debug("Found for designator %s: %s", partition_designator_to_string(di), strna(s)); + } r = image_policy_check_protection(policy, di, found_flags); if (r < 0) From 198f7badaa884714439fbf96074acff404dc5bf2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Mar 2025 15:44:50 +0100 Subject: [PATCH 07/21] gpt-auto-generator: optionally, set up root fs via dissection logic This introduces root=dissect, which is a lot like root=gpt-auto, but uses the image dissection logic to find the root partition. It's also based on the GPT efi var telling us about the root disk, but instead of just picking some suitable root disk manually it uses the dissection logic. This is a big step forward, since it means we can actually make use of the image policy logic for the root fs too. --- src/gpt-auto-generator/gpt-auto-generator.c | 29 +++++++++++++++------ src/shared/generator.c | 17 ++++++++++-- src/shared/generator.h | 8 +++--- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index 167d15318f..28242850ac 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -656,19 +656,27 @@ static int add_partition_root_flags(DissectedPartition *p) { static int add_root_cryptsetup(void) { #if HAVE_LIBCRYPTSETUP - /* If a device /dev/gpt-auto-root-luks appears, then make it pull in systemd-cryptsetup-root.service, which - * sets it up, and causes /dev/gpt-auto-root to appear which is all we are looking for. */ + assert(arg_auto_root != GPT_AUTO_ROOT_OFF); - const char *bdev = "/dev/gpt-auto-root-luks"; + /* If a device /dev/gpt-auto-root-luks or /dev/disk/by-designator/root-luks appears, then make it + * pull in systemd-cryptsetup@root.service, which sets it up, and causes /dev/gpt-auto-root or + * /dev/disk/by-designator/root to appear which is all we are looking for. */ - if (arg_auto_root == GPT_AUTO_ROOT_FORCE) { + const char *bdev = + IN_SET(arg_auto_root, GPT_AUTO_ROOT_DISSECT, GPT_AUTO_ROOT_DISSECT_FORCE) ? + "/dev/disk/by-designator/root-luks" : "/dev/gpt-auto-root-luks"; + + if (IN_SET(arg_auto_root, GPT_AUTO_ROOT_FORCE, GPT_AUTO_ROOT_DISSECT_FORCE)) { /* Similar logic as in add_root_mount(), see below */ FactoryResetMode f = factory_reset_mode(); if (f < 0) log_warning_errno(f, "Failed to determine whether we are in factory reset mode, assuming not: %m"); if (IN_SET(f, FACTORY_RESET_ON, FACTORY_RESET_COMPLETE)) - bdev = "/dev/gpt-auto-root-luks-ignore-factory-reset"; + bdev = + IN_SET(arg_auto_root, GPT_AUTO_ROOT_DISSECT, GPT_AUTO_ROOT_DISSECT_FORCE) ? + "/dev/disk/by-designator/root-luks-ignore-factory-reset" : + "/dev/gpt-auto-root-luks-ignore-factory-reset"; } return add_cryptsetup("root", bdev, arg_root_options, MOUNT_RW|MOUNT_MEASURE, /* require= */ false, NULL); @@ -715,14 +723,19 @@ static int add_root_mount(void) { * factory reset the latter is the link to use, otherwise the former (so that we don't accidentally * mount a root partition too early that is about to be wiped and replaced by another one). */ - const char *bdev = "/dev/gpt-auto-root"; - if (arg_auto_root == GPT_AUTO_ROOT_FORCE) { + const char *bdev = + IN_SET(arg_auto_root, GPT_AUTO_ROOT_DISSECT, GPT_AUTO_ROOT_DISSECT_FORCE) ? + "/dev/disk/by-designator/root" : "/dev/gpt-auto-root"; + if (IN_SET(arg_auto_root, GPT_AUTO_ROOT_FORCE, GPT_AUTO_ROOT_DISSECT_FORCE)) { FactoryResetMode f = factory_reset_mode(); if (f < 0) log_warning_errno(f, "Failed to determine whether we are in factory reset mode, assuming not: %m"); if (IN_SET(f, FACTORY_RESET_ON, FACTORY_RESET_COMPLETE)) - bdev = "/dev/gpt-auto-root-ignore-factory-reset"; + bdev = + IN_SET(arg_auto_root, GPT_AUTO_ROOT_DISSECT, GPT_AUTO_ROOT_DISSECT_FORCE) ? + "/dev/disk/by-designator/root-ignore-factory-reset" : + "/dev/gpt-auto-root-ignore-factory-reset"; } if (in_initrd()) { diff --git a/src/shared/generator.c b/src/shared/generator.c index c3e3eeabb2..0fa7dca867 100644 --- a/src/shared/generator.c +++ b/src/shared/generator.c @@ -1089,7 +1089,10 @@ bool generator_soft_rebooted(void) { GptAutoRoot parse_gpt_auto_root(const char *value) { assert(value); - /* Parses the 'gpt-auto'/'gpt-auto-root' parameters to root= */ + /* Parses the 'gpt-auto'/'gpt-auto-root'/'dissect'/'dissect-force' parameters to root= + * + * note that we are not using a regular string table here, because the mode names don't fully match + * the parameter names. And root= being something else is not an error. */ if (streq(value, "gpt-auto")) { log_debug("Enabling root partition auto-detection (respecting factory reset mode), root= is explicitly set to 'gpt-auto'."); @@ -1101,6 +1104,16 @@ GptAutoRoot parse_gpt_auto_root(const char *value) { return GPT_AUTO_ROOT_FORCE; } - log_debug("Disabling root partition auto-detection, root= is neither unset, nor set to 'gpt-auto' or 'gpt-auto-force'."); + if (streq(value, "dissect")) { + log_debug("Enabling root partition auto-detection via full image dissection (respecting factory reset mode), root= is explicitly set to 'dissect'."); + return GPT_AUTO_ROOT_DISSECT; + } + + if (streq(value, "dissect-force")) { + log_debug("Enabling root partition auto-detection via full image dissection (ignoring factory reset mode), root= is explicitly set to 'dissect-force'."); + return GPT_AUTO_ROOT_DISSECT_FORCE; + } + + log_debug("Disabling root partition auto-detection, root= is neither unset, nor set to 'gpt-auto', 'gpt-auto-force', 'dissect' or 'dissect-force'."); return GPT_AUTO_ROOT_OFF; } diff --git a/src/shared/generator.h b/src/shared/generator.h index fd342a280c..4ded62d011 100644 --- a/src/shared/generator.h +++ b/src/shared/generator.h @@ -122,9 +122,11 @@ bool generator_soft_rebooted(void); exit_failure_if_negative) typedef enum GptAutoRoot { - GPT_AUTO_ROOT_OFF = 0, /* root= set to something else */ - GPT_AUTO_ROOT_ON, /* root= set explicitly to "gpt-auto" */ - GPT_AUTO_ROOT_FORCE, /* root= set explicitly to "gpt-auto-force" → ignores factory reset mode */ + GPT_AUTO_ROOT_OFF = 0, /* root= set to something else */ + GPT_AUTO_ROOT_ON, /* root= set explicitly to "gpt-auto" */ + GPT_AUTO_ROOT_FORCE, /* root= set explicitly to "gpt-auto-force" → ignores factory reset mode */ + GPT_AUTO_ROOT_DISSECT, /* root= set to "dissect" */ + GPT_AUTO_ROOT_DISSECT_FORCE, /* root= set to "dissect-force" → ignores factory reset mode */ _GPT_AUTO_ROOT_MAX, _GPT_AUTO_ROOT_INVALID = -EINVAL, } GptAutoRoot; From c05a132fef0e9843b499b40fca3af49fe2cc9a4b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Mar 2025 15:57:03 +0100 Subject: [PATCH 08/21] generator: add root=off to explicitly turn search for rootfs off This is useful when booting into storage target mode, where we do not want to enter a root fs, we just want to stay forever in the initrd, and hence there's value in not even generating and jobs to find the rootfs --- src/fstab-generator/fstab-generator.c | 5 +++++ src/shared/generator.c | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/fstab-generator/fstab-generator.c b/src/fstab-generator/fstab-generator.c index 51df843f12..ab009e8b21 100644 --- a/src/fstab-generator/fstab-generator.c +++ b/src/fstab-generator/fstab-generator.c @@ -1132,6 +1132,11 @@ static bool validate_root_or_usr_mount_source(const char *what, const char *swit return false; } + if (streq(what, "off")) { + log_debug("Skipping %s directory handling, as this was explicitly turned off.", switch_name); + return false; + } + if (parse_gpt_auto_root(what) > 0) { /* This is handled by gpt-auto-generator */ log_debug("Skipping %s directory handling, as gpt-auto was requested.", switch_name); diff --git a/src/shared/generator.c b/src/shared/generator.c index 0fa7dca867..d1c643b923 100644 --- a/src/shared/generator.c +++ b/src/shared/generator.c @@ -1114,6 +1114,10 @@ GptAutoRoot parse_gpt_auto_root(const char *value) { return GPT_AUTO_ROOT_DISSECT_FORCE; } - log_debug("Disabling root partition auto-detection, root= is neither unset, nor set to 'gpt-auto', 'gpt-auto-force', 'dissect' or 'dissect-force'."); + if (streq(value, "off")) + log_debug("Disabling root partition auto-detection, root= handling is explicitly turned off."); + else + log_debug("Disabling root partition auto-detection, root= is neither unset, nor set to 'gpt-auto', 'gpt-auto-force', 'dissect' or 'dissect-force'."); + return GPT_AUTO_ROOT_OFF; } From 7852e301e0eb839adf4bf45aa41e39c0dfc03403 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Mar 2025 17:01:49 +0100 Subject: [PATCH 09/21] gpt-auto-generator: add support for mount.usr=dissect So far, we did not support auto-discovery the /usr/ partition at boot. Change that: on explicit request (i.e. mount.usr=dissect) automatically discover the /usr/ partition based on our usual dissection logic. --- src/fstab-generator/fstab-generator.c | 2 +- src/gpt-auto-generator/gpt-auto-generator.c | 125 +++++++++++++++++++- src/shared/generator.c | 21 ++-- src/shared/generator.h | 2 +- 4 files changed, 136 insertions(+), 14 deletions(-) diff --git a/src/fstab-generator/fstab-generator.c b/src/fstab-generator/fstab-generator.c index ab009e8b21..5adf48f20c 100644 --- a/src/fstab-generator/fstab-generator.c +++ b/src/fstab-generator/fstab-generator.c @@ -1137,7 +1137,7 @@ static bool validate_root_or_usr_mount_source(const char *what, const char *swit return false; } - if (parse_gpt_auto_root(what) > 0) { + if (parse_gpt_auto_root(switch_name, what) > 0) { /* This is handled by gpt-auto-generator */ log_debug("Skipping %s directory handling, as gpt-auto was requested.", switch_name); return false; diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index 28242850ac..c7acbcb2b0 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -48,15 +48,20 @@ typedef enum MountPointFlags { static const char *arg_dest = NULL; static bool arg_enabled = true; static GptAutoRoot arg_auto_root = _GPT_AUTO_ROOT_INVALID; +static GptAutoRoot arg_auto_usr = _GPT_AUTO_ROOT_INVALID; static bool arg_swap_enabled = true; static char *arg_root_fstype = NULL; static char *arg_root_options = NULL; static int arg_root_rw = -1; +static char *arg_usr_fstype = NULL; +static char *arg_usr_options = NULL; static ImagePolicy *arg_image_policy = NULL; STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_root_fstype, freep); STATIC_DESTRUCTOR_REGISTER(arg_root_options, freep); +STATIC_DESTRUCTOR_REGISTER(arg_usr_fstype, freep); +STATIC_DESTRUCTOR_REGISTER(arg_usr_options, freep); #define LOADER_PARTITION_IDLE_USEC (120 * USEC_PER_SEC) @@ -679,7 +684,13 @@ static int add_root_cryptsetup(void) { "/dev/gpt-auto-root-luks-ignore-factory-reset"; } - return add_cryptsetup("root", bdev, arg_root_options, MOUNT_RW|MOUNT_MEASURE, /* require= */ false, NULL); + return add_cryptsetup( + "root", + bdev, + arg_root_options, + MOUNT_RW|MOUNT_MEASURE, + /* require= */ false, + /* ret_device= */ NULL); #else return 0; #endif @@ -782,6 +793,84 @@ static int add_root_mount(void) { #endif } + +static int add_usr_mount(void) { +#if ENABLE_EFI + int r; + + /* /usr/ discovery must be enabled explicitly. */ + if (arg_auto_usr == GPT_AUTO_ROOT_OFF || + arg_auto_usr < 0) + return 0; + + /* We do not support the other gpt-auto modes for /usr/, but the parser should already have checked that. */ + assert(arg_auto_usr == GPT_AUTO_ROOT_DISSECT); + + if (arg_root_fstype && !arg_usr_fstype) { + arg_usr_fstype = strdup(arg_root_fstype); + if (!arg_usr_fstype) + return log_oom(); + } + + if (arg_root_options && !arg_usr_options) { + arg_usr_options = strdup(arg_root_options); + if (!arg_usr_options) + return log_oom(); + } + + if (in_initrd()) { + r = add_cryptsetup( + "usr", + "/dev/disk/by-designator/usr-luks", + arg_usr_options, + MOUNT_RW|MOUNT_MEASURE, + /* require= */ false, + /* ret_device= */ NULL); + if (r < 0) + return r; + } + + _cleanup_free_ char *options = NULL; + r = partition_pick_mount_options( + PARTITION_USR, + arg_usr_fstype, + /* rw= */ false, + /* discard= */ true, + &options, + /* ret_ms_flags= */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to pick /usr/ mount options: %m"); + + if (arg_usr_options) + if (!strextend_with_separator(&options, ",", arg_usr_options)) + return log_oom(); + + r = add_mount("usr", + "/dev/disk/by-designator/usr", + in_initrd() ? "/sysusr/usr" : "/usr", + arg_usr_fstype, + (in_initrd() ? MOUNT_VALIDATEFS : 0), + options, + "/usr/ Partition", + in_initrd() ? SPECIAL_INITRD_USR_FS_TARGET : SPECIAL_LOCAL_FS_TARGET); + if (r < 0) + return r; + + log_debug("Synthesizing entry what=/sysusr/usr where=/sysroot/usr opts=bind"); + + return add_mount("usr-bind", + "/sysusr/usr", + "/sysroot/usr", + /* fstype= */ NULL, + /* flags= */ 0, + "bind", + "/usr/ Partition (Final)", + in_initrd() ? SPECIAL_INITRD_FS_TARGET : SPECIAL_LOCAL_FS_TARGET); +#else + return 0; +#endif +} + static int process_loader_partitions(DissectedPartition *esp, DissectedPartition *xbootldr) { sd_id128_t loader_uuid; int r; @@ -973,7 +1062,7 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat /* Disable root disk logic if there's a root= value specified (unless it happens to be * "gpt-auto" or "gpt-auto-force") */ - arg_auto_root = parse_gpt_auto_root(value); + arg_auto_root = parse_gpt_auto_root("root=", value); assert(arg_auto_root >= 0); } else if (streq(key, "roothash")) { @@ -1001,6 +1090,37 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat if (!strextend_with_separator(&arg_root_options, ",", value)) return log_oom(); + } else if (streq(key, "mount.usr")) { + + if (proc_cmdline_value_missing(key, value)) + return 0; + + /* Disable root disk logic if there's a root= value specified (unless it happens to be + * "gpt-auto" or "gpt-auto-force") */ + + arg_auto_usr = parse_gpt_auto_root("mount.usr=", value); + assert(arg_auto_usr >= 0); + + if (IN_SET(arg_auto_usr, GPT_AUTO_ROOT_ON, GPT_AUTO_ROOT_FORCE, GPT_AUTO_ROOT_DISSECT_FORCE)) { + log_warning("'gpt-auto', 'gpt-auto-force' and 'dissect-force' are not supported for mount.usr=. Automatically resorting to mount.usr=dissect mode instead."); + arg_auto_usr = GPT_AUTO_ROOT_DISSECT; + } + + } else if (streq(key, "mount.usrfstype")) { + + if (proc_cmdline_value_missing(key, value)) + return 0; + + return free_and_strdup_warn(&arg_usr_fstype, empty_to_null(value)); + + } else if (streq(key, "mount.usrflags")) { + + if (proc_cmdline_value_missing(key, value)) + return 0; + + if (!strextend_with_separator(&arg_usr_options, ",", value)) + return log_oom(); + } else if (streq(key, "rw") && !value) arg_root_rw = true; else if (streq(key, "ro") && !value) @@ -1045,6 +1165,7 @@ static int run(const char *dest, const char *dest_early, const char *dest_late) r = 0; RET_GATHER(r, add_root_mount()); + RET_GATHER(r, add_usr_mount()); RET_GATHER(r, add_mounts()); return r; diff --git a/src/shared/generator.c b/src/shared/generator.c index d1c643b923..90568e3ec9 100644 --- a/src/shared/generator.c +++ b/src/shared/generator.c @@ -216,8 +216,8 @@ static int write_fsck_sysroot_service( /* Writes out special versions of systemd-fsck-root.service and systemd-fsck-usr.service for use in * the initrd. The regular statically shipped versions of these unit files use / and /usr for as - * paths, which doesn't match what we need for the initrd (where the dirs are /sysroot + - * /sysusr/usr), hence we overwrite those versions here. */ + * paths, which doesn't match what we need for the initrd (where the dirs are /sysroot/ + + * /sysusr/usr/), hence we overwrite those versions here. */ escaped = specifier_escape(what); if (!escaped) @@ -281,7 +281,7 @@ int generator_write_fsck_deps( assert(where); /* Let's do an early exit if we are invoked for the root and /usr/ trees in the initrd, to avoid - * generating confusing log messages */ + * generating confusing log messages. */ if (in_initrd() && PATH_IN_SET(where, "/", "/usr")) { log_debug("Skipping fsck for %s in initrd.", where); return 0; @@ -1086,7 +1086,8 @@ bool generator_soft_rebooted(void) { return (cached = (u > 0)); } -GptAutoRoot parse_gpt_auto_root(const char *value) { +GptAutoRoot parse_gpt_auto_root(const char *switch_name, const char *value) { + assert(switch_name); assert(value); /* Parses the 'gpt-auto'/'gpt-auto-root'/'dissect'/'dissect-force' parameters to root= @@ -1095,29 +1096,29 @@ GptAutoRoot parse_gpt_auto_root(const char *value) { * the parameter names. And root= being something else is not an error. */ if (streq(value, "gpt-auto")) { - log_debug("Enabling root partition auto-detection (respecting factory reset mode), root= is explicitly set to 'gpt-auto'."); + log_debug("Enabling partition auto-detection (respecting factory reset mode), %s is explicitly set to 'gpt-auto'.", switch_name); return GPT_AUTO_ROOT_ON; } if (streq(value, "gpt-auto-force")) { - log_debug("Enabling root partition auto-detection (ignoring factory reset mode), root= is explicitly set to 'gpt-auto-force'."); + log_debug("Enabling partition auto-detection (ignoring factory reset mode), %s is explicitly set to 'gpt-auto-force'.", switch_name); return GPT_AUTO_ROOT_FORCE; } if (streq(value, "dissect")) { - log_debug("Enabling root partition auto-detection via full image dissection (respecting factory reset mode), root= is explicitly set to 'dissect'."); + log_debug("Enabling partition auto-detection via full image dissection (respecting factory reset mode), %s is explicitly set to 'dissect'.", switch_name); return GPT_AUTO_ROOT_DISSECT; } if (streq(value, "dissect-force")) { - log_debug("Enabling root partition auto-detection via full image dissection (ignoring factory reset mode), root= is explicitly set to 'dissect-force'."); + log_debug("Enabling partition auto-detection via full image dissection (ignoring factory reset mode), %s is explicitly set to 'dissect-force'.", switch_name); return GPT_AUTO_ROOT_DISSECT_FORCE; } if (streq(value, "off")) - log_debug("Disabling root partition auto-detection, root= handling is explicitly turned off."); + log_debug("Disabling partition auto-detection, %s handling is explicitly turned off.", switch_name); else - log_debug("Disabling root partition auto-detection, root= is neither unset, nor set to 'gpt-auto', 'gpt-auto-force', 'dissect' or 'dissect-force'."); + log_debug("Disabling partition auto-detection, %s is neither unset, nor set to 'gpt-auto', 'gpt-auto-force', 'dissect' or 'dissect-force'.", switch_name); return GPT_AUTO_ROOT_OFF; } diff --git a/src/shared/generator.h b/src/shared/generator.h index 4ded62d011..68ea9b376f 100644 --- a/src/shared/generator.h +++ b/src/shared/generator.h @@ -131,4 +131,4 @@ typedef enum GptAutoRoot { _GPT_AUTO_ROOT_INVALID = -EINVAL, } GptAutoRoot; -GptAutoRoot parse_gpt_auto_root(const char *value); +GptAutoRoot parse_gpt_auto_root(const char *switch_name, const char *value); From 0a64b7ba6e4e77b0e70d3022078032d1e63599a5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 17 Mar 2025 21:38:12 +0100 Subject: [PATCH 10/21] gpt-auto-generator: add support for setting up verity partitions automatically So far the gpt-auto-generator would only cover unprotected and encrypted partitions. Teach it handling of verity too. --- src/gpt-auto-generator/gpt-auto-generator.c | 126 +++++++++++++++++++- src/shared/generator.c | 10 +- 2 files changed, 131 insertions(+), 5 deletions(-) diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index c7acbcb2b0..b9b5b60da8 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -24,6 +24,7 @@ #include "fstab-util.h" #include "generator.h" #include "gpt.h" +#include "hexdecoct.h" #include "image-policy.h" #include "initrd-util.h" #include "mountpoint-util.h" @@ -184,6 +185,106 @@ static int add_cryptsetup( #endif } +#if ENABLE_EFI +static int add_veritysetup( + const char *id, + const char *data_what, + const char *hash_what, + const char *mount_opts) { + +#if HAVE_LIBCRYPTSETUP + int r; + + assert(id); + assert(data_what); + assert(hash_what); + + _cleanup_free_ char *dd = NULL; + r = unit_name_from_path(data_what, ".device", &dd); + if (r < 0) + return log_error_errno(r, "Failed to generate data device unit name: %m"); + + _cleanup_free_ char *dh = NULL; + r = unit_name_from_path(hash_what, ".device", &dh); + if (r < 0) + return log_error_errno(r, "Failed to generate hash device unit name: %m"); + + _cleanup_free_ char *e = unit_name_escape(id); + if (!e) + return log_oom(); + + _cleanup_free_ char *n = NULL; + r = unit_name_build("systemd-veritysetup", e, ".service", &n); + if (r < 0) + return log_error_errno(r, "Failed to generate unit name: %m"); + + _cleanup_fclose_ FILE *f = NULL; + r = generator_open_unit_file(arg_dest, /* source= */ NULL, n, &f); + if (r < 0) + return r; + + r = generator_write_veritysetup_unit_section(f, /* source= */ NULL); + if (r < 0) + return r; + + fprintf(f, + "Before=veritysetup.target\n" + "BindsTo=%1$s %2$s\n" + "After=%1$s %2$s\n", + dd, dh); + + r = generator_write_veritysetup_service_section( + f, + id, + data_what, + hash_what, + /* roothash= */ NULL, /* NULL means: derive root hash from udev property ID_DISSECT_PART_ROOTHASH */ + "root-hash-signature=auto"); /* auto means: derive signature from udev property ID_DISSECT_PART_ROOTHASH_SIG */ + if (r < 0) + return r; + + r = fflush_and_check(f); + if (r < 0) + return log_error_errno(r, "Failed to write file %s: %m", n); + + r = generator_write_device_timeout(arg_dest, data_what, mount_opts, /* filtered= */ NULL); + if (r < 0) + return r; + + r = generator_write_device_timeout(arg_dest, hash_what, mount_opts, /* filtered= */ NULL); + if (r < 0) + return r; + + r = generator_add_symlink(arg_dest, dd, "wants", n); + if (r < 0) + return r; + + r = generator_add_symlink(arg_dest, dh, "wants", n); + if (r < 0) + return r; + + _cleanup_free_ char *dmname = NULL; + dmname = strjoin("dev-mapper-", e, ".device"); + if (!dmname) + return log_oom(); + + r = write_drop_in_format( + arg_dest, + dmname, 50, "job-timeout", + "# Automatically generated by systemd-gpt-auto-generator\n\n" + "[Unit]\n" + "JobTimeoutSec=infinity"); /* the binary handles timeouts anyway */ + if (r < 0) + return log_error_errno(r, "Failed to write device timeout drop-in: %m"); + + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Partition is Verity protected, but systemd-gpt-auto-generator was compiled without libcryptsetup support."); +#endif +} +#endif + static int add_mount( const char *id, const char *what, @@ -757,6 +858,18 @@ static int add_root_mount(void) { r = add_root_cryptsetup(); if (r < 0) return r; + + /* If a device /dev/disk/by-designator/root-verity or + * /dev/disk/by-designator/root-verity-data appears, then make it pull in + * systemd-cryptsetup@root.service, which sets it up, and causes /dev/disk/by-designator/root + * to appear. */ + r = add_veritysetup( + "root", + "/dev/disk/by-designator/root-verity-data", + "/dev/disk/by-designator/root-verity", + arg_root_options); + if (r < 0) + return r; } /* Note that we do not need to enable systemd-remount-fs.service here. If /etc/fstab exists, @@ -793,7 +906,6 @@ static int add_root_mount(void) { #endif } - static int add_usr_mount(void) { #if ENABLE_EFI int r; @@ -828,6 +940,18 @@ static int add_usr_mount(void) { /* ret_device= */ NULL); if (r < 0) return r; + + /* If a device /dev/disk/by-designator/usr-verity or + * /dev/disk/by-designator/usr-verity-data appears, then make it pull in + * systemd-cryptsetup@usr.service, which sets it up, and causes /dev/disk/by-designator/usr + * to appear. */ + r = add_veritysetup( + "usr", + "/dev/disk/by-designator/usr-verity-data", + "/dev/disk/by-designator/usr-verity", + arg_usr_options); + if (r < 0) + return r; } _cleanup_free_ char *options = NULL; diff --git a/src/shared/generator.c b/src/shared/generator.c index 90568e3ec9..5e3cf2a6cb 100644 --- a/src/shared/generator.c +++ b/src/shared/generator.c @@ -1026,9 +1026,11 @@ int generator_write_veritysetup_service_section( if (!hash_what_escaped) return log_oom(); - roothash_escaped = specifier_escape(roothash); - if (!roothash_escaped) - return log_oom(); + if (roothash) { + roothash_escaped = specifier_escape(roothash); + if (!roothash_escaped) + return log_oom(); + } if (options) { options_escaped = specifier_escape(options); @@ -1043,7 +1045,7 @@ int generator_write_veritysetup_service_section( "RemainAfterExit=yes\n" "ExecStart=" SYSTEMD_VERITYSETUP_PATH " attach '%s' '%s' '%s' '%s' '%s'\n" "ExecStop=" SYSTEMD_VERITYSETUP_PATH " detach '%s'\n", - name_escaped, data_what_escaped, hash_what_escaped, roothash_escaped, strempty(options_escaped), + name_escaped, data_what_escaped, hash_what_escaped, empty_to_dash(roothash_escaped), strempty(options_escaped), name_escaped); return 0; From fb008a7333e702c2ff3f728134a4822fad4a38e7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 18 Mar 2025 12:51:04 +0100 Subject: [PATCH 11/21] veritysetup: optionally pick up roothash/roothashsig from udev device db entry --- man/veritytab.xml | 15 ++-- src/veritysetup/veritysetup.c | 140 ++++++++++++++++++++++------------ 2 files changed, 102 insertions(+), 53 deletions(-) diff --git a/man/veritytab.xml b/man/veritytab.xml index 74b757a961..9b131e350c 100644 --- a/man/veritytab.xml +++ b/man/veritytab.xml @@ -53,7 +53,9 @@ This is based on crypttab(5). The third field contains a path to the underlying block hash device, or a specification of a block device via UUID= followed by the UUID. - The fourth field is the roothash in hexadecimal. + The fourth field is the roothash in hexadecimal. If this field is + specified as dash, it is attempted to read the root hash from the udev property + ID_DISSECT_PART_ROOTHASH= (encoded in hexadecimal) of the data device. The fifth field, if present, is a comma-delimited list of options. The following options are recognized: @@ -213,11 +215,14 @@ This is based on crypttab(5). - + - A base64 string encoding the root hash signature prefixed by - base64: or an absolute path to a root hash signature file used to verify the root - hash (in kernel). This feature requires Linux kernel version 5.4 or more recent. + A Base64 string encoding the root hash signature prefixed by + base64:, or an absolute path to a root hash signature file used to verify the root + hash (in kernel). If the special string auto is specified, the root hash signature + is attempted to be read from the udev property ID_DISSECT_PART_ROOTHASH_SIG= (in + Base64 format) of the data device. This feature requires Linux kernel version 5.4 or more + recent. diff --git a/src/veritysetup/veritysetup.c b/src/veritysetup/veritysetup.c index 97f233c906..6fd5527424 100644 --- a/src/veritysetup/veritysetup.c +++ b/src/veritysetup/veritysetup.c @@ -4,6 +4,8 @@ #include #include +#include "sd-device.h" + #include "alloc-util.h" #include "cryptsetup-util.h" #include "fileio.h" @@ -19,7 +21,7 @@ #include "terminal-util.h" #include "verbs.h" -static char *arg_hash = NULL; +static char *arg_hash = NULL; /* the hash algorithm */ static bool arg_superblock = true; static int arg_format = 1; static uint64_t arg_data_block_size = 4096; @@ -33,7 +35,9 @@ static uint32_t arg_activate_flags = CRYPT_ACTIVATE_READONLY; static char *arg_fec_what = NULL; static uint64_t arg_fec_offset = 0; static uint64_t arg_fec_roots = 2; -static char *arg_root_hash_signature = NULL; +static void *arg_root_hash_signature = NULL; +static size_t arg_root_hash_signature_size = 0; +static bool arg_root_hash_signature_auto = false; STATIC_DESTRUCTOR_REGISTER(arg_hash, freep); STATIC_DESTRUCTOR_REGISTER(arg_salt, freep); @@ -60,26 +64,52 @@ static int help(void) { return 0; } -static int save_roothashsig_option(const char *option, bool strict) { +static int parse_roothashsig_option(const char *option, bool strict) { + _cleanup_free_ void *rhs = NULL; + size_t rhss = 0; + bool set_auto = false; int r; - if (path_is_absolute(option) || startswith(option, "base64:")) { - if (!HAVE_CRYPT_ACTIVATE_BY_SIGNED_KEY) - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), - "Activation of verity device with signature requested, but cryptsetup does not support crypt_activate_by_signed_key()."); + assert(option); - r = free_and_strdup_warn(&arg_root_hash_signature, option); + const char *value = startswith(option, "base64:"); + if (value) { + r = unbase64mem(value, &rhs, &rhss); if (r < 0) - return r; + return log_error_errno(r, "Failed to parse root hash signature '%s': %m", option); - return true; - } + } else if (path_is_absolute(option)) { + r = read_full_file_full( + AT_FDCWD, + option, + /* offset= */ UINT64_MAX, + /* size= */ SIZE_MAX, + READ_FULL_FILE_CONNECT_SOCKET, + /* bind_name= */ NULL, + (char**) &rhs, + &rhss); + if (r < 0) + return log_error_errno(r, "Failed to read root hash signature: %m"); - if (!strict) + } else if (streq(option, "auto")) + /* auto → Derive signature from udev property ID_DISSECT_PART_ROOTHASH_SIG */ + set_auto = true; + else if (strict) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "root-hash-signature= expects either full path to signature file or " + "base64 string encoding signature prefixed by base64:."); + else return false; - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "root-hash-signature= expects either full path to signature file or " - "base64 string encoding signature prefixed by base64:."); + + if (!HAVE_CRYPT_ACTIVATE_BY_SIGNED_KEY) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Activation of verity device with signature requested, but cryptsetup does not support crypt_activate_by_signed_key()."); + + free_and_replace(arg_root_hash_signature, rhs); + arg_root_hash_signature_size = rhss; + arg_root_hash_signature_auto = set_auto; + + return true; } static int parse_block_size(const char *t, uint64_t *size) { @@ -105,7 +135,7 @@ static int parse_options(const char *options) { int r; /* backward compatibility with the obsolete ROOTHASHSIG positional argument */ - r = save_roothashsig_option(options, /* strict= */ false); + r = parse_roothashsig_option(options, /* strict= */ false); if (r < 0) return r; if (r > 0) { @@ -264,7 +294,7 @@ static int parse_options(const char *options) { arg_fec_roots = u; } else if ((val = startswith(word, "root-hash-signature="))) { - r = save_roothashsig_option(val, /* strict= */ true); + r = parse_roothashsig_option(val, /* strict= */ true); if (r < 0) return r; @@ -277,10 +307,10 @@ static int parse_options(const char *options) { static int verb_attach(int argc, char *argv[], void *userdata) { _cleanup_(crypt_freep) struct crypt_device *cd = NULL; - _cleanup_free_ void *m = NULL; + _cleanup_free_ void *rh = NULL; struct crypt_params_verity p = {}; crypt_status_info status; - size_t l; + size_t rh_size = 0; int r; assert(argc >= 5); @@ -294,10 +324,48 @@ static int verb_attach(int argc, char *argv[], void *userdata) { if (!filename_is_valid(volume)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Volume name '%s' is not valid.", volume); - r = unhexmem(root_hash, &m, &l); + if (options) { + r = parse_options(options); + if (r < 0) + return log_error_errno(r, "Failed to parse options: %m"); + } + + if (empty_or_dash(root_hash) || streq_ptr(root_hash, "auto")) + root_hash = NULL; + + _cleanup_(sd_device_unrefp) sd_device *datadev = NULL; + if (!root_hash || arg_root_hash_signature_auto) { + r = sd_device_new_from_path(&datadev, data_device); + if (r < 0) + return log_error_errno(r, "Failed to acquire udev object for data device '%s': %m", data_device); + } + + if (!root_hash) { + /* If no literal root hash is specified try to determine it automatically from the + * ID_DISSECT_PART_ROOTHASH udev property. */ + r = sd_device_get_property_value(ASSERT_PTR(datadev), "ID_DISSECT_PART_ROOTHASH", &root_hash); + if (r < 0) + return log_error_errno(r, "No root hash specified, and device doesn't carry ID_DISSECT_PART_ROOTHASH property, cannot determine root hash."); + } + + r = unhexmem(root_hash, &rh, &rh_size); if (r < 0) return log_error_errno(r, "Failed to parse root hash: %m"); + if (arg_root_hash_signature_auto) { + assert(!arg_root_hash_signature); + assert(arg_root_hash_signature_size == 0); + + const char *t; + r = sd_device_get_property_value(ASSERT_PTR(datadev), "ID_DISSECT_PART_ROOTHASH_SIG", &t); + if (r < 0) + return log_error_errno(r, "Automatic root hash signature pick up requested, and device doesn't carry ID_DISSECT_PART_ROOTHASH_SIG property, cannot determine root hash signature."); + + r = unbase64mem(t, &arg_root_hash_signature, &arg_root_hash_signature_size); + if (r < 0) + return log_error_errno(r, "Failed to decode root hash signature data from udev data device: %m"); + } + r = crypt_init(&cd, verity_device); if (r < 0) return log_error_errno(r, "Failed to open verity device %s: %m", verity_device); @@ -310,12 +378,6 @@ static int verb_attach(int argc, char *argv[], void *userdata) { return 0; } - if (options) { - r = parse_options(options); - if (r < 0) - return log_error_errno(r, "Failed to parse options: %m"); - } - if (arg_superblock) { p = (struct crypt_params_verity) { .fec_device = arg_fec_what, @@ -353,32 +415,14 @@ static int verb_attach(int argc, char *argv[], void *userdata) { if (r < 0) return log_error_errno(r, "Failed to configure data device: %m"); - if (arg_root_hash_signature) { + if (arg_root_hash_signature_size > 0) #if HAVE_CRYPT_ACTIVATE_BY_SIGNED_KEY - _cleanup_free_ char *hash_sig = NULL; - size_t hash_sig_size; - char *value; - - if ((value = startswith(arg_root_hash_signature, "base64:"))) { - r = unbase64mem(value, (void*) &hash_sig, &hash_sig_size); - if (r < 0) - return log_error_errno(r, "Failed to parse root hash signature '%s': %m", arg_root_hash_signature); - } else { - r = read_full_file_full( - AT_FDCWD, arg_root_hash_signature, UINT64_MAX, SIZE_MAX, - READ_FULL_FILE_CONNECT_SOCKET, - NULL, - &hash_sig, &hash_sig_size); - if (r < 0) - return log_error_errno(r, "Failed to read root hash signature: %m"); - } - - r = crypt_activate_by_signed_key(cd, volume, m, l, hash_sig, hash_sig_size, arg_activate_flags); + r = crypt_activate_by_signed_key(cd, volume, rh, rh_size, arg_root_hash_signature, arg_root_hash_signature_size, arg_activate_flags); #else assert_not_reached(); #endif - } else - r = crypt_activate_by_volume_key(cd, volume, m, l, arg_activate_flags); + else + r = crypt_activate_by_volume_key(cd, volume, rh, rh_size, arg_activate_flags); if (r < 0) return log_error_errno(r, "Failed to set up verity device '%s': %m", volume); From f1395724f608f8d192615235daaca0cec7ad1c93 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 18 Mar 2025 16:46:27 +0100 Subject: [PATCH 12/21] dissect-image: add a concept for "filtering" partitions we dissect DDIs may contain multiple versions of the same OS, or even multiple OSes. Hence it makes sense to not just pick the "newest", whatever that might be, but only partitions associated with specific images, or in a specific version. Let's a concept for such filtering: a per-designator glob expression that can be applied to the partition label string, and can be used for such filtering. Usecase: when picking UKI belonging to OS image X in version Y, make sure we only pick a /usr/ partition belonging to X in version Y, and a root and home partition belonging to X in any version. This only adds the basic infrastructure, but doesn't actually expose it anywhere. --- src/core/namespace.c | 2 + src/dissect/dissect.c | 6 +- src/gpt-auto-generator/gpt-auto-generator.c | 1 + src/mountfsd/mountwork.c | 1 + src/nspawn/nspawn.c | 3 +- src/portable/portable.c | 1 + src/shared/discover-image.c | 1 + src/shared/dissect-image.c | 114 +++++++++++++++++++- src/shared/dissect-image.h | 21 +++- src/shared/mount-util.c | 2 + src/sysext/sysext.c | 1 + src/test/meson.build | 1 + src/test/test-image-filter.c | 39 +++++++ src/test/test-loop-block.c | 45 +++++++- src/udev/udev-builtin-dissect_image.c | 2 + src/vmspawn/vmspawn.c | 1 + 16 files changed, 224 insertions(+), 17 deletions(-) create mode 100644 src/test/test-image-filter.c diff --git a/src/core/namespace.c b/src/core/namespace.c index fac3c05f61..2912f819e9 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1507,6 +1507,7 @@ static int mount_image( mount_entry_path(m), m->image_options_const, image_policy, + /* image_filter= */ NULL, host_os_release_id, host_os_release_version_id, host_os_release_sysext_level, @@ -2352,6 +2353,7 @@ int setup_namespace(const NamespaceParameters *p, char **reterr_path) { p->verity, p->root_image_options, p->root_image_policy, + /* image_filter= */ NULL, dissect_image_flags, &dissected_image); if (r < 0) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index 06d6d3935f..fda688b21e 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -2106,10 +2106,11 @@ static int action_validate(void) { r = dissect_image_file_and_warn( arg_image, &arg_verity_settings, - NULL, + /* mount_options= */ NULL, arg_image_policy, + /* image_filter= */ NULL, arg_flags, - NULL); + /* ret= */ NULL); if (r < 0) return r; @@ -2231,6 +2232,7 @@ static int run(int argc, char *argv[]) { &arg_verity_settings, /* mount_options= */ NULL, arg_image_policy, + /* image_filter= */ NULL, arg_flags, &m); if (r < 0) diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index b9b5b60da8..f1a4f9cca5 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -1103,6 +1103,7 @@ static int enumerate_partitions(dev_t devnum) { /* verity= */ NULL, /* mount_options= */ NULL, image_policy, + /* image_filter= */ NULL, DISSECT_IMAGE_GPT_ONLY| DISSECT_IMAGE_USR_NO_ROOT| DISSECT_IMAGE_DISKSEQ_DEVNODE| diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c index adbb91d8a0..a693419eec 100644 --- a/src/mountfsd/mountwork.c +++ b/src/mountfsd/mountwork.c @@ -414,6 +414,7 @@ static int vl_method_mount_image( &verity, /* mount_options= */ NULL, use_policy, + /* image_filter= */ NULL, dissect_flags, &di); if (r == -ENOPKG) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 32796375c5..17b0c85185 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -6370,8 +6370,9 @@ static int run(int argc, char *argv[]) { r = dissect_loop_device_and_warn( loop, &arg_verity_settings, - /* mount_options=*/ NULL, + /* mount_options= */ NULL, arg_image_policy ?: &image_policy_container, + /* image_filter= */ NULL, dissect_image_flags, &dissected_image); if (r == -ENOPKG) { diff --git a/src/portable/portable.c b/src/portable/portable.c index 84fc4fa706..a70484eb66 100644 --- a/src/portable/portable.c +++ b/src/portable/portable.c @@ -425,6 +425,7 @@ static int portable_extract_by_path( /* verity= */ NULL, /* mount_options= */ NULL, image_policy, + /* image_filter= */ NULL, flags, &m); if (r == -ENOPKG) diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index f9a1b2ffaa..721ad2d0f2 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -1726,6 +1726,7 @@ int image_read_metadata(Image *i, const ImagePolicy *image_policy) { /* verity= */ NULL, /* mount_options= */ NULL, image_policy, + /* image_filter= */ NULL, flags, &m); if (r < 0) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 660a05379f..ca9b8d985b 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -667,6 +667,18 @@ static int compare_arch(Architecture a, Architecture b) { return 0; } +static bool image_filter_test(const ImageFilter *filter, PartitionDesignator d, const char *name) { + assert(d < _PARTITION_DESIGNATOR_MAX); + + if (d < 0) /* For unspecified designators we have no filter expression */ + return true; + + if (!filter || !filter->pattern[d]) + return true; + + return fnmatch(filter->pattern[d], strempty(name), FNM_NOESCAPE) == 0; +} + static int dissect_image( DissectedImage *m, int fd, @@ -674,6 +686,7 @@ static int dissect_image( const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *policy, + const ImageFilter *filter, DissectImageFlags flags) { sd_id128_t root_uuid = SD_ID128_NULL, root_verity_uuid = SD_ID128_NULL; @@ -792,6 +805,9 @@ static int dissect_image( /* OK, we have found a file system, that's our root partition then. */ + if (!image_filter_test(filter, PARTITION_ROOT, /* label= */ NULL)) /* do a filter check with an empty partition label */ + return -ECOMM; + r = image_policy_may_use(policy, PARTITION_ROOT); if (r < 0) return r; @@ -1006,6 +1022,9 @@ static int dissect_image( if (streq_ptr(label, "_empty")) continue; + if (!image_filter_test(filter, type.designator, label)) + continue; + log_debug("Dissecting %s partition with label %s and UUID %s", strna(partition_designator_to_string(type.designator)), strna(label), SD_ID128_TO_UUID_STRING(id)); @@ -1162,6 +1181,9 @@ static int dissect_image( /* We don't have a designator for SD_GPT_LINUX_GENERIC so check the UUID instead. */ } else if (sd_id128_equal(type.uuid, SD_GPT_LINUX_GENERIC)) { + if (!image_filter_test(filter, PARTITION_ROOT, label)) + continue; + check_partition_flags(node, pflags, SD_GPT_FLAG_NO_AUTO | SD_GPT_FLAG_READ_ONLY | SD_GPT_FLAG_GROWFS); @@ -1307,6 +1329,9 @@ static int dissect_image( if (pflags != 0x80) /* Bootable flag */ continue; + if (!image_filter_test(filter, PARTITION_ROOT, /* label= */ NULL)) + continue; + if (generic_node) multiple_generic = true; else { @@ -1324,6 +1349,9 @@ static int dissect_image( sd_id128_t id = SD_ID128_NULL; const char *options = NULL; + if (!image_filter_test(filter, PARTITION_XBOOTLDR, /* label= */ NULL)) + continue; + r = image_policy_may_use(policy, PARTITION_XBOOTLDR); if (r < 0) return r; @@ -1570,6 +1598,7 @@ int dissect_image_file( const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, + const ImageFilter *image_filter, DissectImageFlags flags, DissectedImage **ret) { @@ -1602,7 +1631,7 @@ int dissect_image_file( if (r < 0) return r; - r = dissect_image(m, fd, path, verity, mount_options, image_policy, flags); + r = dissect_image(m, fd, path, verity, mount_options, image_policy, image_filter, flags); if (r < 0) return r; @@ -1672,12 +1701,13 @@ int dissect_image_file_and_warn( const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, + const ImageFilter *image_filter, DissectImageFlags flags, DissectedImage **ret) { return dissect_log_error( LOG_ERR, - dissect_image_file(path, verity, mount_options, image_policy, flags, ret), + dissect_image_file(path, verity, mount_options, image_policy, image_filter, flags, ret), path, verity); } @@ -3134,6 +3164,68 @@ int dissected_image_relinquish(DissectedImage *m) { return 0; } +void image_filter_done(ImageFilter *f) { + assert(f); + + FOREACH_ELEMENT(p, f->pattern) + *p = mfree(*p); +} + +ImageFilter *image_filter_free(ImageFilter *f) { + if (!f) + return NULL; + + image_filter_done(f); + return mfree(f); +} + +int image_filter_parse(const char *s, ImageFilter **ret) { + _cleanup_(image_filter_freep) ImageFilter *f = NULL; + int r; + + if (isempty(s)) { + if (ret) + *ret = NULL; + return 0; + } + + for (;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&s, &word, ":", EXTRACT_UNQUOTE|EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return log_debug_errno(r, "Failed to extract word: %m"); + if (r == 0) + break; + + _cleanup_free_ char *designator = NULL, *pattern = NULL; + const char *x = word; + r = extract_many_words(&x, "=", EXTRACT_UNQUOTE|EXTRACT_DONT_COALESCE_SEPARATORS, &designator, &pattern); + if (r < 0) + return log_debug_errno(r, "Failed to extract designator: %m"); + if (r != 2 || !isempty(x)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Unable to split: %m"); + + PartitionDesignator d = partition_designator_from_string(designator); + if (d < 0) + return log_debug_errno(d, "Failed to parse partition designator: %s", designator); + + if (!f) { + f = new0(ImageFilter, 1); + if (!f) + return log_oom_debug(); + } else if (f->pattern[d]) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Duplicate pattern for '%s', refusing.", partition_designator_to_string(d)); + + f->pattern[d] = TAKE_PTR(pattern); + } + + if (ret) + *ret = TAKE_PTR(f); + + return 0; +} + static char *build_auxiliary_path(const char *image, const char *suffix) { const char *e; char *n; @@ -3940,6 +4032,7 @@ int dissect_loop_device( const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, + const ImageFilter *image_filter, DissectImageFlags flags, DissectedImage **ret) { @@ -3957,7 +4050,15 @@ int dissect_loop_device( m->image_size = m->loop->device_size; m->sector_size = m->loop->sector_size; - r = dissect_image(m, loop->fd, loop->node, verity, mount_options, image_policy, flags); + r = dissect_image( + m, + loop->fd, + loop->node, + verity, + mount_options, + image_policy, + image_filter, + flags); if (r < 0) return r; @@ -3975,6 +4076,7 @@ int dissect_loop_device_and_warn( const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, + const ImageFilter *image_filter, DissectImageFlags flags, DissectedImage **ret) { @@ -3982,7 +4084,7 @@ int dissect_loop_device_and_warn( return dissect_log_error( LOG_ERR, - dissect_loop_device(loop, verity, mount_options, image_policy, flags, ret), + dissect_loop_device(loop, verity, mount_options, image_policy, image_filter, flags, ret), loop->backing_file ?: loop->node, verity); } @@ -4101,6 +4203,7 @@ int mount_image_privately_interactively( &verity, /* mount_options= */ NULL, image_policy, + /* image_filter= */ NULL, flags, &dissected_image); if (r < 0) @@ -4182,6 +4285,7 @@ int verity_dissect_and_mount( const char *dest, const MountOptions *options, const ImagePolicy *image_policy, + const ImageFilter *image_filter, const char *required_host_os_release_id, const char *required_host_os_release_version_id, const char *required_host_os_release_sysext_level, @@ -4239,6 +4343,7 @@ int verity_dissect_and_mount( verity, options, image_policy, + image_filter, dissect_image_flags, &dissected_image); /* No partition table? Might be a single-filesystem image, try again */ @@ -4248,6 +4353,7 @@ int verity_dissect_and_mount( verity, options, image_policy, + image_filter, 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 8725ff9921..3d919a65e0 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -19,6 +19,7 @@ typedef struct DissectedPartition DissectedPartition; typedef struct DecryptedImage DecryptedImage; typedef struct MountOptions MountOptions; typedef struct VeritySettings VeritySettings; +typedef struct ImageFilter ImageFilter; struct DissectedPartition { bool found:1; @@ -148,6 +149,11 @@ struct VeritySettings { .designator = _PARTITION_DESIGNATOR_INVALID \ } +struct ImageFilter { + /* A per designator glob matching against the partition label */ + char *pattern[_PARTITION_DESIGNATOR_MAX]; +}; + /* We include image-policy.h down here, since ImagePolicy wants a complete definition of PartitionDesignator first. */ #include "image-policy.h" @@ -161,10 +167,10 @@ static inline int probe_filesystem(const char *path, char **ret_fstype) { } int dissect_log_error(int log_level, int r, const char *name, const VeritySettings *verity); -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); +int dissect_image_file(const char *path, const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, const ImageFilter *filter, DissectImageFlags flags, DissectedImage **ret); +int dissect_image_file_and_warn(const char *path, const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, const ImageFilter *filter, DissectImageFlags flags, DissectedImage **ret); +int dissect_loop_device(LoopDevice *loop, const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, const ImageFilter *filter, DissectImageFlags flags, DissectedImage **ret); +int dissect_loop_device_and_warn(LoopDevice *loop, const VeritySettings *verity, const MountOptions *mount_options, const ImagePolicy *image_policy, const ImageFilter *filter, DissectImageFlags flags, DissectedImage **ret); void dissected_image_close(DissectedImage *m); DissectedImage* dissected_image_unref(DissectedImage *m); @@ -201,6 +207,11 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(DecryptedImage*, decrypted_image_unref); int dissected_image_relinquish(DissectedImage *m); +void image_filter_done(ImageFilter *f); +ImageFilter *image_filter_free(ImageFilter *f); +DEFINE_TRIVIAL_CLEANUP_FUNC(ImageFilter*, image_filter_free); +int image_filter_parse(const char *s, ImageFilter **ret); + int verity_settings_load(VeritySettings *verity, const char *image, const char *root_hash_path, const char *root_hash_sig_path); static inline bool verity_settings_set(const VeritySettings *settings) { @@ -237,7 +248,7 @@ bool dissected_image_verity_sig_ready(const DissectedImage *image, PartitionDesi 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 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_host_os_release_confext_level, const char *required_sysext_scope, VeritySettings *verity, DissectedImage **ret_image); +int verity_dissect_and_mount(int src_fd, const char *src, const char *dest, const MountOptions *options, const ImagePolicy *image_policy, const ImageFilter *image_filter, 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_host_os_release_confext_level, const char *required_sysext_scope, VeritySettings *verity, DissectedImage **ret_image); int dissect_fstype_ok(const char *fstype); diff --git a/src/shared/mount-util.c b/src/shared/mount-util.c index d7471bc78d..c92e779a87 100644 --- a/src/shared/mount-util.c +++ b/src/shared/mount-util.c @@ -969,6 +969,7 @@ static int mount_in_namespace_legacy( mount_tmp, options, image_policy, + /* image_filter= */ NULL, /* required_host_os_release_id= */ NULL, /* required_host_os_release_version_id= */ NULL, /* required_host_os_release_sysext_level= */ NULL, @@ -1193,6 +1194,7 @@ static int mount_in_namespace( /* dest= */ NULL, options, image_policy, + /* image_filter= */ NULL, /* required_host_os_release_id= */ NULL, /* required_host_os_release_version_id= */ NULL, /* required_host_os_release_sysext_level= */ NULL, diff --git a/src/sysext/sysext.c b/src/sysext/sysext.c index d2bb992d14..201787b251 100644 --- a/src/sysext/sysext.c +++ b/src/sysext/sysext.c @@ -1802,6 +1802,7 @@ static int merge_subprocess( &verity_settings, /* mount_options= */ NULL, pick_image_policy(img), + /* image_filter= */ NULL, flags, &m); if (r < 0) diff --git a/src/test/meson.build b/src/test/meson.build index 4ef296a41a..424e4f5563 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -105,6 +105,7 @@ simple_tests += files( 'test-hostname-setup.c', 'test-hostname-util.c', 'test-id128.c', + 'test-image-filter.c', 'test-image-policy.c', 'test-import-util.c', 'test-in-addr-prefix-util.c', diff --git a/src/test/test-image-filter.c b/src/test/test-image-filter.c new file mode 100644 index 0000000000..d5d157727d --- /dev/null +++ b/src/test/test-image-filter.c @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "dissect-image.h" +#include "tests.h" + +TEST(image_filter) { + _cleanup_(image_filter_freep) ImageFilter *f = NULL; + + ASSERT_OK(image_filter_parse(NULL, &f)); + ASSERT_NULL(f); + ASSERT_OK(image_filter_parse("", &f)); + ASSERT_NULL(f); + + ASSERT_OK(image_filter_parse("root=*", &f)); + ASSERT_NOT_NULL(f); + ASSERT_STREQ(f->pattern[PARTITION_ROOT], "*"); + f = image_filter_free(f); + + ASSERT_OK(image_filter_parse("usr=foox?:root=kn*arz", &f)); + ASSERT_NOT_NULL(f); + ASSERT_STREQ(f->pattern[PARTITION_ROOT], "kn*arz"); + ASSERT_STREQ(f->pattern[PARTITION_USR], "foox?"); + f = image_filter_free(f); + + ASSERT_OK(image_filter_parse("usr=foox?:root=kn*arz:home=wumpi", &f)); + ASSERT_NOT_NULL(f); + ASSERT_STREQ(f->pattern[PARTITION_ROOT], "kn*arz"); + ASSERT_STREQ(f->pattern[PARTITION_USR], "foox?"); + ASSERT_STREQ(f->pattern[PARTITION_HOME], "wumpi"); + f = image_filter_free(f); + + ASSERT_ERROR(image_filter_parse("usr=foox?:root=kn*arz:home=wumpi:schlumpf=smurf", &f), EINVAL); + ASSERT_ERROR(image_filter_parse(":", &f), EINVAL); + ASSERT_ERROR(image_filter_parse("::", &f), EINVAL); + ASSERT_ERROR(image_filter_parse("-", &f), EINVAL); + ASSERT_ERROR(image_filter_parse("root=knuff:root=knuff", &f), EINVAL); +} + +DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/test/test-loop-block.c b/src/test/test-loop-block.c index c18582795b..c2addc5924 100644 --- a/src/test/test-loop-block.c +++ b/src/test/test-loop-block.c @@ -81,7 +81,14 @@ static void* thread_func(void *ptr) { log_notice("Acquired loop device %s, will mount on %s", loop->node, mounted); - r = dissect_loop_device(loop, NULL, NULL, NULL, DISSECT_IMAGE_READ_ONLY|DISSECT_IMAGE_ADD_PARTITION_DEVICES|DISSECT_IMAGE_PIN_PARTITION_DEVICES, &dissected); + r = dissect_loop_device( + loop, + /* verity= */ NULL, + /* mount_options= */ NULL, + /* image_policy= */ NULL, + /* image_filter= */ NULL, + DISSECT_IMAGE_READ_ONLY|DISSECT_IMAGE_ADD_PARTITION_DEVICES|DISSECT_IMAGE_PIN_PARTITION_DEVICES, + &dissected); if (r < 0) log_error_errno(r, "Failed dissect loopback device %s: %m", loop->node); assert_se(r >= 0); @@ -218,7 +225,14 @@ static int run(int argc, char *argv[]) { sfdisk = NULL; #if HAVE_BLKID - assert_se(dissect_image_file(p, NULL, NULL, NULL, 0, &dissected) >= 0); + assert_se(dissect_image_file( + p, + /* verity= */ NULL, + /* mount_options= */ NULL, + /* image_policy= */ NULL, + /* image_filter= */ NULL, + /* flags= */ 0, + &dissected) >= 0); verify_dissected_image(dissected); dissected = dissected_image_unref(dissected); #endif @@ -232,7 +246,14 @@ static int run(int argc, char *argv[]) { assert_se(loop_device_make(fd, O_RDWR, 0, UINT64_MAX, 0, LO_FLAGS_PARTSCAN, LOCK_EX, &loop) >= 0); #if HAVE_BLKID - assert_se(dissect_loop_device(loop, NULL, NULL, NULL, DISSECT_IMAGE_ADD_PARTITION_DEVICES|DISSECT_IMAGE_PIN_PARTITION_DEVICES, &dissected) >= 0); + assert_se(dissect_loop_device( + loop, + /* verity= */ NULL, + /* mount_options= */ NULL, + /* image_policy= */ NULL, + /* image_filter= */ NULL, + DISSECT_IMAGE_ADD_PARTITION_DEVICES|DISSECT_IMAGE_PIN_PARTITION_DEVICES, + &dissected) >= 0); verify_dissected_image(dissected); FOREACH_STRING(fs, "vfat", "ext4") { @@ -268,12 +289,26 @@ static int run(int argc, char *argv[]) { /* Try to read once, without pinning or adding partitions, i.e. by only accessing the whole block * device. */ - assert_se(dissect_loop_device(loop, NULL, NULL, NULL, 0, &dissected) >= 0); + assert_se(dissect_loop_device( + loop, + /* verity= */ NULL, + /* mount_options= */ NULL, + /* image_policy= */ NULL, + /* image_filter= */ NULL, + /* flags= */ 0, + &dissected) >= 0); verify_dissected_image_harder(dissected); dissected = dissected_image_unref(dissected); /* Now go via the loopback device after all, but this time add/pin, because now we want to mount it. */ - assert_se(dissect_loop_device(loop, NULL, NULL, NULL, DISSECT_IMAGE_ADD_PARTITION_DEVICES|DISSECT_IMAGE_PIN_PARTITION_DEVICES, &dissected) >= 0); + assert_se(dissect_loop_device( + loop, + /* verity= */ NULL, + /* mount_options= */ NULL, + /* image_policy= */ NULL, + /* image_filter= */ NULL, + DISSECT_IMAGE_ADD_PARTITION_DEVICES|DISSECT_IMAGE_PIN_PARTITION_DEVICES, + &dissected) >= 0); verify_dissected_image_harder(dissected); assert_se(mkdtemp_malloc(NULL, &mounted) >= 0); diff --git a/src/udev/udev-builtin-dissect_image.c b/src/udev/udev-builtin-dissect_image.c index 3a598952ac..f4acd41409 100644 --- a/src/udev/udev-builtin-dissect_image.c +++ b/src/udev/udev-builtin-dissect_image.c @@ -121,6 +121,7 @@ static int verb_probe(UdevEvent *event, sd_device *dev) { &arg_verity_settings, /* mount_options= */ NULL, image_policy, + /* image_filter= */ NULL, DISSECT_IMAGE_READ_ONLY| DISSECT_IMAGE_GPT_ONLY| DISSECT_IMAGE_USR_NO_ROOT| @@ -167,6 +168,7 @@ static int verb_probe(UdevEvent *event, sd_device *dev) { &arg_verity_settings, /* mount_options= */ NULL, image_policy_mangled, + /* image_filter= */ NULL, DISSECT_IMAGE_READ_ONLY| DISSECT_IMAGE_GPT_ONLY| DISSECT_IMAGE_USR_NO_ROOT| diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 4629cf0e79..cbf7badbfe 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -1194,6 +1194,7 @@ static int discover_root(char **ret) { /* verity= */ NULL, /* mount_options= */ NULL, /* image_policy= */ NULL, + /* image_filter= */ NULL, /* flags= */ 0, &image); if (r < 0) From 743667e0693520bcb9500894f9ea38d53023ab02 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Mar 2025 09:09:16 +0100 Subject: [PATCH 13/21] dissect-tool: expose image filtering via new --image-filter= parameter --- src/dissect/dissect.c | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c index fda688b21e..55232914ed 100644 --- a/src/dissect/dissect.c +++ b/src/dissect/dissect.c @@ -102,6 +102,7 @@ static RuntimeScope arg_runtime_scope = _RUNTIME_SCOPE_INVALID; static bool arg_all = false; static uid_t arg_uid_base = UID_INVALID; static bool arg_quiet = false; +static ImageFilter *arg_image_filter = NULL; STATIC_DESTRUCTOR_REGISTER(arg_image, freep); STATIC_DESTRUCTOR_REGISTER(arg_root, freep); @@ -110,6 +111,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_verity_settings, verity_settings_done); STATIC_DESTRUCTOR_REGISTER(arg_argv, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_loop_ref, freep); STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); +STATIC_DESTRUCTOR_REGISTER(arg_image_filter, image_filter_freep); static int help(void) { _cleanup_free_ char *link = NULL; @@ -155,6 +157,8 @@ static int help(void) { " not embedded in IMAGE\n" " --image-policy=POLICY\n" " Specify image dissection policy\n" + " --image-filter=FILTER\n" + " Specify image dissection filter\n" " --json=pretty|short|off\n" " Generate JSON output\n" " --loop-ref=NAME Set reference string for loopback device\n" @@ -295,6 +299,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_SYSTEM, ARG_USER, ARG_ALL, + ARG_IMAGE_FILTER, }; static const struct option options[] = { @@ -336,6 +341,7 @@ static int parse_argv(int argc, char *argv[]) { { "user", no_argument, NULL, ARG_USER }, { "all", no_argument, NULL, ARG_ALL }, { "quiet", no_argument, NULL, 'q' }, + { "image-filter", required_argument, NULL, ARG_IMAGE_FILTER }, {} }; @@ -610,6 +616,17 @@ static int parse_argv(int argc, char *argv[]) { arg_quiet = true; break; + case ARG_IMAGE_FILTER: { + _cleanup_(image_filter_freep) ImageFilter *f = NULL; + r = image_filter_parse(optarg, &f); + if (r < 0) + return log_error_errno(r, "Failed to parse image filter expression: %s", optarg); + + image_filter_free(arg_image_filter); + arg_image_filter = TAKE_PTR(f); + break; + } + case '?': return -EINVAL; @@ -2108,7 +2125,7 @@ static int action_validate(void) { &arg_verity_settings, /* mount_options= */ NULL, arg_image_policy, - /* image_filter= */ NULL, + arg_image_filter, arg_flags, /* ret= */ NULL); if (r < 0) @@ -2232,7 +2249,7 @@ static int run(int argc, char *argv[]) { &arg_verity_settings, /* mount_options= */ NULL, arg_image_policy, - /* image_filter= */ NULL, + arg_image_filter, arg_flags, &m); if (r < 0) From f02868d296c5ad28bc707510ea09aba56fd04764 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Mar 2025 10:36:46 +0100 Subject: [PATCH 14/21] builtin-dissect_image: add support for image filters --- src/udev/udev-builtin-dissect_image.c | 31 +++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/udev/udev-builtin-dissect_image.c b/src/udev/udev-builtin-dissect_image.c index f4acd41409..73cf9b4242 100644 --- a/src/udev/udev-builtin-dissect_image.c +++ b/src/udev/udev-builtin-dissect_image.c @@ -13,6 +13,7 @@ static ImagePolicy *arg_image_policy = NULL; static VeritySettings arg_verity_settings = VERITY_SETTINGS_DEFAULT; +static ImageFilter *arg_image_filter = NULL; static int acquire_image_policy(ImagePolicy **ret) { int r; @@ -77,6 +78,27 @@ static int acquire_verity_settings(VeritySettings *ret) { return 0; } +static int acquire_image_filter(ImageFilter **ret) { + int r; + + assert(ret); + + _cleanup_free_ char *value = NULL; + r = proc_cmdline_get_key("systemd.image_filter", /* flags= */ 0, &value); + if (r < 0) + return log_error_errno(r, "Failed to read systemd.image_filter= kernel command line switch: %m"); + if (r == 0) { + *ret = NULL; + return 0; + } + + r = image_filter_parse(value, ret); + if (r < 0) + return log_error_errno(r, "Failed to parse image filter '%s': %m", value); + + return 1; +} + static int verb_probe(UdevEvent *event, sd_device *dev) { int r; @@ -121,7 +143,7 @@ static int verb_probe(UdevEvent *event, sd_device *dev) { &arg_verity_settings, /* mount_options= */ NULL, image_policy, - /* image_filter= */ NULL, + arg_image_filter, DISSECT_IMAGE_READ_ONLY| DISSECT_IMAGE_GPT_ONLY| DISSECT_IMAGE_USR_NO_ROOT| @@ -168,7 +190,7 @@ static int verb_probe(UdevEvent *event, sd_device *dev) { &arg_verity_settings, /* mount_options= */ NULL, image_policy_mangled, - /* image_filter= */ NULL, + arg_image_filter, DISSECT_IMAGE_READ_ONLY| DISSECT_IMAGE_GPT_ONLY| DISSECT_IMAGE_USR_NO_ROOT| @@ -367,12 +389,17 @@ static int builtin_dissect_image_init(void) { if (r < 0) return r; + r = acquire_image_filter(&arg_image_filter); + if (r < 0) + return r; + return 0; } static void builtin_dissect_image_exit(void) { arg_image_policy = image_policy_free(arg_image_policy); verity_settings_done(&arg_verity_settings); + arg_image_filter = image_filter_free(arg_image_filter); } const UdevBuiltin udev_builtin_dissect_image = { From 74cfd0921c01c464b79548467e909e577bfc25ac Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Mar 2025 17:23:52 +0100 Subject: [PATCH 15/21] gpt-auto-generator: parse systemd.image_filter= from kernel cmdline --- src/gpt-auto-generator/gpt-auto-generator.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index f1a4f9cca5..fbd036343c 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -57,6 +57,7 @@ static int arg_root_rw = -1; static char *arg_usr_fstype = NULL; static char *arg_usr_options = NULL; static ImagePolicy *arg_image_policy = NULL; +static ImageFilter *arg_image_filter = NULL; STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); STATIC_DESTRUCTOR_REGISTER(arg_root_fstype, freep); @@ -1103,7 +1104,7 @@ static int enumerate_partitions(dev_t devnum) { /* verity= */ NULL, /* mount_options= */ NULL, image_policy, - /* image_filter= */ NULL, + arg_image_filter, DISSECT_IMAGE_GPT_ONLY| DISSECT_IMAGE_USR_NO_ROOT| DISSECT_IMAGE_DISKSEQ_DEVNODE| @@ -1252,8 +1253,17 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat arg_root_rw = false; else if (proc_cmdline_key_streq(key, "systemd.image_policy")) return parse_image_policy_argument(value, &arg_image_policy); + else if (proc_cmdline_key_streq(key, "systemd.image_filter")) { + _cleanup_(image_filter_freep) ImageFilter *f = NULL; - else if (streq(key, "systemd.swap")) { + r = image_filter_parse(value, &f); + if (r < 0) + return log_error_errno(r, "Failed to parse image filter: %s", value); + + image_filter_free(arg_image_filter); + arg_image_filter = TAKE_PTR(f); + + } else if (streq(key, "systemd.swap")) { r = value ? parse_boolean(value) : 1; if (r < 0) From a05b34433002e0db38e8f5b19594dab7165ace0e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Mar 2025 17:15:41 +0100 Subject: [PATCH 16/21] man: document image filters --- man/kernel-command-line.xml | 12 +++++ man/rules/meson.build | 1 + man/standard-options.xml | 8 +++ man/systemd-dissect.xml | 1 + man/systemd-gpt-auto-generator.xml | 11 ++++ man/systemd.image-filter.xml | 80 ++++++++++++++++++++++++++++++ man/systemd.image-policy.xml | 1 + 7 files changed, 114 insertions(+) create mode 100644 man/systemd.image-filter.xml diff --git a/man/kernel-command-line.xml b/man/kernel-command-line.xml index 3a4619edee..17740fb4e4 100644 --- a/man/kernel-command-line.xml +++ b/man/kernel-command-line.xml @@ -481,6 +481,18 @@ + + systemd.image_filter= + + When GPT-based partition auto-discovery is used, configures the image dissection + filter string to apply, as per + systemd.image-filter7. For + details see + systemd-gpt-auto-generator8. + + + + systemd.default_timeout_start_sec= diff --git a/man/rules/meson.build b/man/rules/meson.build index c33d9fee0e..0b930d3beb 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1188,6 +1188,7 @@ manpages = [ ['systemd.environment-generator', '7', [], 'ENABLE_ENVIRONMENT_D'], ['systemd.exec', '5', [], ''], ['systemd.generator', '7', [], ''], + ['systemd.image-filter', '7', [], ''], ['systemd.image-policy', '7', [], ''], ['systemd.journal-fields', '7', [], ''], ['systemd.kill', '5', [], ''], diff --git a/man/standard-options.xml b/man/standard-options.xml index 1e958594ad..d07ea4a09a 100644 --- a/man/standard-options.xml +++ b/man/standard-options.xml @@ -114,6 +114,14 @@ in the image are used. + + Takes an + image filter string as argument, as per + systemd.image-filter7. The + filter is taken into consideration when operating on the disk image specified via + , see above. If not specified no filtering is applied. + + diff --git a/man/systemd-dissect.xml b/man/systemd-dissect.xml index 4b51fb887c..3094a1e6d9 100644 --- a/man/systemd-dissect.xml +++ b/man/systemd-dissect.xml @@ -582,6 +582,7 @@ + diff --git a/man/systemd-gpt-auto-generator.xml b/man/systemd-gpt-auto-generator.xml index af450f9fbe..11b08f0b8d 100644 --- a/man/systemd-gpt-auto-generator.xml +++ b/man/systemd-gpt-auto-generator.xml @@ -320,6 +320,17 @@ + + systemd.image_filter= + + Takes an image dissection filter string as argument (as per + systemd.image-filter7), + and allows enforcing a set of globbing patterns on the partition matching of the automatically + discovered GPT partition table entries. + + + + root= rootfstype= diff --git a/man/systemd.image-filter.xml b/man/systemd.image-filter.xml new file mode 100644 index 0000000000..f5a8a811da --- /dev/null +++ b/man/systemd.image-filter.xml @@ -0,0 +1,80 @@ + + + + + + + + systemd.image-filter + systemd + + + + systemd.image-filter + 7 + + + + systemd.image-filter + Disk Image Dissection Filter + + + + Description + + In systemd, whenever a disk image (DDI) implementing the Discoverable + Partitions Specification is activated, a filter may be specified controlling which partitions to + consider for mounting. Such a disk image dissection filter is a string that contains per-partition-type + patterns, separated by colons (:). The individual rules consist of a partition + identifier, an equal sign (=), and a shell globbing pattern applied to the GPT label + string of the partition. See glob7 for + details on shell globbing. + + The partition identifiers currently defined are: , , + , , , , + , , , + , , , + . These identifiers match the relevant partition types in the Discoverable Partitions + Specification, but are agnostic to CPU architectures. + + + + Use + + Various systemd components that support operating with disk images support a + command line option to specify the image filter to use. If no filter is + specified all partitions in partition table are considered and no per-label filtering is applied (except + that partitions with the _empty label are always ignored). + + For the host root file system image itself + systemd-gpt-auto-generator8 + is responsible for processing the GPT partition table and making use of the included discoverable + partitions. It accepts an image filter via the kernel command line option + . + + + + Examples + + The following image filter string dictates that for the root file system partition only partitions + shall be considered whose label begins with ParticleOS-. For the + /usr/ partition the precise label ParticleOS_47110815 is + required. + + root=ParticleOS-*:usr=ParticleOS_47110815 + + + + See Also + + systemd1 + systemd-dissect1 + systemd-gpt-auto-generator8 + systemd.image-policy7 + + + + diff --git a/man/systemd.image-policy.xml b/man/systemd.image-policy.xml index 36a8395bf0..d351ffeea5 100644 --- a/man/systemd.image-policy.xml +++ b/man/systemd.image-policy.xml @@ -185,6 +185,7 @@ systemd-gpt-auto-generator8 systemd-sysext8 systemd-analyze1 + systemd.filter7 From bcd904d471ef93da748e3ca65c18692926a7f2b7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 18 Mar 2025 22:24:00 +0100 Subject: [PATCH 17/21] dissect-image: move extension release arguments of verity_dissect_and_mount() into a structure --- src/core/namespace.c | 37 ++++++++++++++++--------------------- src/shared/dissect-image.c | 30 ++++++++++++++++++------------ src/shared/dissect-image.h | 13 ++++++++++++- src/shared/mount-util.c | 12 ++---------- 4 files changed, 48 insertions(+), 44 deletions(-) diff --git a/src/core/namespace.c b/src/core/namespace.c index 2912f819e9..01d3c977d4 100644 --- a/src/core/namespace.c +++ b/src/core/namespace.c @@ -1476,9 +1476,8 @@ static int mount_image( const char *root_directory, const ImagePolicy *image_policy) { - _cleanup_free_ char *host_os_release_id = NULL, *host_os_release_version_id = NULL, - *host_os_release_sysext_level = NULL, *host_os_release_confext_level = NULL, - *extension_name = NULL; + _cleanup_(extension_release_data_done) ExtensionReleaseData rdata = {}; + _cleanup_free_ char *extension_name = NULL; int r; assert(m); @@ -1490,14 +1489,14 @@ static int mount_image( if (m->mode == MOUNT_EXTENSION_IMAGE) { r = parse_os_release( empty_to_root(root_directory), - "ID", &host_os_release_id, - "VERSION_ID", &host_os_release_version_id, - image_class_info[IMAGE_SYSEXT].level_env, &host_os_release_sysext_level, - image_class_info[IMAGE_CONFEXT].level_env, &host_os_release_confext_level, + "ID", &rdata.os_release_id, + "VERSION_ID", &rdata.os_release_version_id, + image_class_info[IMAGE_SYSEXT].level_env, &rdata.os_release_sysext_level, + image_class_info[IMAGE_CONFEXT].level_env, &rdata.os_release_confext_level, NULL); if (r < 0) return log_debug_errno(r, "Failed to acquire 'os-release' data of OS tree '%s': %m", empty_to_root(root_directory)); - if (isempty(host_os_release_id)) + if (isempty(rdata.os_release_id)) return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "'ID' field not found or empty in 'os-release' data of OS tree '%s'.", empty_to_root(root_directory)); } @@ -1508,26 +1507,22 @@ static int mount_image( m->image_options_const, image_policy, /* image_filter= */ NULL, - host_os_release_id, - host_os_release_version_id, - host_os_release_sysext_level, - host_os_release_confext_level, - /* required_sysext_scope= */ NULL, + &rdata, &m->verity, /* ret_image= */ NULL); if (r == -ENOENT && m->ignore) return 0; - if (r == -ESTALE && host_os_release_id) + if (r == -ESTALE && rdata.os_release_id) return log_error_errno(r, // FIXME: this should not be logged ad LOG_ERR, as it will result in duplicate logging. "Failed to mount image %s, extension-release metadata does not match the lower layer's: ID=%s%s%s%s%s%s%s", mount_entry_source(m), - host_os_release_id, - host_os_release_version_id ? " VERSION_ID=" : "", - strempty(host_os_release_version_id), - host_os_release_sysext_level ? image_class_info[IMAGE_SYSEXT].level_env_print : "", - strempty(host_os_release_sysext_level), - host_os_release_confext_level ? image_class_info[IMAGE_CONFEXT].level_env_print : "", - strempty(host_os_release_confext_level)); + rdata.os_release_id, + rdata.os_release_version_id ? " VERSION_ID=" : "", + strempty(rdata.os_release_version_id), + rdata.os_release_sysext_level ? image_class_info[IMAGE_SYSEXT].level_env_print : "", + strempty(rdata.os_release_sysext_level), + rdata.os_release_confext_level ? image_class_info[IMAGE_CONFEXT].level_env_print : "", + strempty(rdata.os_release_confext_level)); if (r < 0) return log_debug_errno(r, "Failed to mount image %s on %s: %m", mount_entry_source(m), mount_entry_path(m)); diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index ca9b8d985b..db7cbd44b3 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -4286,11 +4286,7 @@ int verity_dissect_and_mount( const MountOptions *options, const ImagePolicy *image_policy, const ImageFilter *image_filter, - 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_host_os_release_confext_level, - const char *required_sysext_scope, + const ExtensionReleaseData *extension_release_data, VeritySettings *verity, DissectedImage **ret_image) { @@ -4304,7 +4300,7 @@ int verity_dissect_and_mount( assert(src); /* Verifying release metadata requires mounted image for now, so ensure the check is skipped when * opening an image without mounting it immediately (i.e.: 'dest' is NULL). */ - assert(!required_host_os_release_id || dest); + assert(!extension_release_data || dest); relax_extension_release_check = mount_options_relax_extension_release_checks(options); @@ -4403,11 +4399,11 @@ int verity_dissect_and_mount( * First, check the distro ID. If that matches, then check the new SYSEXT_LEVEL value if * available, or else fallback to VERSION_ID. If neither is present (eg: rolling release), * then a simple match on the ID will be performed. */ - if (required_host_os_release_id) { + if (extension_release_data && extension_release_data->os_release_id) { _cleanup_strv_free_ char **extension_release = NULL; ImageClass class = IMAGE_SYSEXT; - assert(!isempty(required_host_os_release_id)); + assert(!isempty(extension_release_data->os_release_id)); r = load_extension_release_pairs(dest, IMAGE_SYSEXT, dissected_image->image_name, relax_extension_release_check, &extension_release); if (r == -ENOENT) { @@ -4420,10 +4416,10 @@ int verity_dissect_and_mount( r = extension_release_validate( dissected_image->image_name, - required_host_os_release_id, - required_host_os_release_version_id, - class == IMAGE_SYSEXT ? required_host_os_release_sysext_level : required_host_os_release_confext_level, - required_sysext_scope, + extension_release_data->os_release_id, + extension_release_data->os_release_version_id, + class == IMAGE_SYSEXT ? extension_release_data->os_release_sysext_level : extension_release_data->os_release_confext_level, + extension_release_data->os_release_extension_scope, extension_release, class); if (r == 0) @@ -4442,6 +4438,16 @@ int verity_dissect_and_mount( return 0; } +void extension_release_data_done(ExtensionReleaseData *data) { + assert(data); + + data->os_release_id = mfree(data->os_release_id); + data->os_release_version_id = mfree(data->os_release_version_id); + data->os_release_sysext_level = mfree(data->os_release_sysext_level); + data->os_release_confext_level = mfree(data->os_release_confext_level); + data->os_release_extension_scope = mfree(data->os_release_extension_scope); +} + int get_common_dissect_directory(char **ret) { _cleanup_free_ char *t = NULL; int r; diff --git a/src/shared/dissect-image.h b/src/shared/dissect-image.h index 3d919a65e0..3313d14921 100644 --- a/src/shared/dissect-image.h +++ b/src/shared/dissect-image.h @@ -20,6 +20,7 @@ typedef struct DecryptedImage DecryptedImage; typedef struct MountOptions MountOptions; typedef struct VeritySettings VeritySettings; typedef struct ImageFilter ImageFilter; +typedef struct ExtensionReleaseData ExtensionReleaseData; struct DissectedPartition { bool found:1; @@ -154,6 +155,14 @@ struct ImageFilter { char *pattern[_PARTITION_DESIGNATOR_MAX]; }; +struct ExtensionReleaseData { + char *os_release_id; + char *os_release_version_id; + char *os_release_sysext_level; + char *os_release_confext_level; + char *os_release_extension_scope; +}; + /* We include image-policy.h down here, since ImagePolicy wants a complete definition of PartitionDesignator first. */ #include "image-policy.h" @@ -248,7 +257,7 @@ bool dissected_image_verity_sig_ready(const DissectedImage *image, PartitionDesi 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 ImagePolicy *image_policy, const ImageFilter *image_filter, 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_host_os_release_confext_level, const char *required_sysext_scope, VeritySettings *verity, DissectedImage **ret_image); +int verity_dissect_and_mount(int src_fd, const char *src, const char *dest, const MountOptions *options, const ImagePolicy *image_policy, const ImageFilter *image_filter, const ExtensionReleaseData *required_release_data, VeritySettings *verity, DissectedImage **ret_image); int dissect_fstype_ok(const char *fstype); @@ -257,6 +266,8 @@ int probe_sector_size_prefer_ioctl(int fd, uint32_t *ret); int partition_pick_mount_options(PartitionDesignator d, const char *fstype, bool rw, bool discard, char **ret_options, unsigned long *ret_ms_flags); +void extension_release_data_done(ExtensionReleaseData *data); + static inline const char* dissected_partition_fstype(const DissectedPartition *m) { assert(m); diff --git a/src/shared/mount-util.c b/src/shared/mount-util.c index c92e779a87..b836cebdde 100644 --- a/src/shared/mount-util.c +++ b/src/shared/mount-util.c @@ -970,11 +970,7 @@ static int mount_in_namespace_legacy( options, image_policy, /* image_filter= */ NULL, - /* required_host_os_release_id= */ NULL, - /* required_host_os_release_version_id= */ NULL, - /* required_host_os_release_sysext_level= */ NULL, - /* required_host_os_release_confext_level= */ NULL, - /* required_sysext_scope= */ NULL, + /* extension_release_data= */ NULL, /* verity= */ NULL, /* ret_image= */ NULL); else @@ -1195,11 +1191,7 @@ static int mount_in_namespace( options, image_policy, /* image_filter= */ NULL, - /* required_host_os_release_id= */ NULL, - /* required_host_os_release_version_id= */ NULL, - /* required_host_os_release_sysext_level= */ NULL, - /* required_host_os_release_confext_level= */ NULL, - /* required_sysext_scope= */ NULL, + /* extension_release_data= */ NULL, /* verity= */ NULL, &img); if (r < 0) From 96386bb5a75c512fa44a218676770b9655abd4d6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Mar 2025 14:35:50 +0100 Subject: [PATCH 18/21] veritysetup: when we fail to unlock a disk with the root hash signature logic, retry without Currently, there's no nice way to get a key into the dm-verity kernel keyring unless recompiling the kernel, or enabling SB or buying into shim. Neither sounds particularly attractive. hence provide a reasonable fallback: if unlocking with signed roothash doesn#t work, just try without. maybe the kernel policy allows this, maybe not. It's worth a try. --- src/veritysetup/veritysetup.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/veritysetup/veritysetup.c b/src/veritysetup/veritysetup.c index 6fd5527424..5f92cc9011 100644 --- a/src/veritysetup/veritysetup.c +++ b/src/veritysetup/veritysetup.c @@ -415,13 +415,22 @@ static int verb_attach(int argc, char *argv[], void *userdata) { if (r < 0) return log_error_errno(r, "Failed to configure data device: %m"); - if (arg_root_hash_signature_size > 0) + if (arg_root_hash_signature_size > 0) { #if HAVE_CRYPT_ACTIVATE_BY_SIGNED_KEY r = crypt_activate_by_signed_key(cd, volume, rh, rh_size, arg_root_hash_signature, arg_root_hash_signature_size, arg_activate_flags); + if (r < 0) { + log_info_errno(r, "Unable to activate verity device '%s' with root hash signature (%m), retrying without.", volume); + + r = crypt_activate_by_volume_key(cd, volume, rh, rh_size, arg_activate_flags); + if (r < 0) + return log_error_errno(r, "Failed to activate verity device '%s' both with and without root hash signature: %m", volume); + + log_info("Activation of verity device '%s' succeeded without root hash signature.", volume); + } #else assert_not_reached(); #endif - else + } else r = crypt_activate_by_volume_key(cd, volume, rh, rh_size, arg_activate_flags); if (r < 0) return log_error_errno(r, "Failed to set up verity device '%s': %m", volume); From e5d2701bc211a4c7aead52658835604699b35fe6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Mar 2025 10:34:42 +0100 Subject: [PATCH 19/21] gpt-auto-generator: write fsck override unit to "middle" generator dir We generally let gpt-auto-generator write to the "late" generator dir, so that the explicit fstab-generator can write to the "middle" one. And I think we should leave it that way. But we also want to override the generic systemd-fsck-root.service service potentially, and if we'd do that in the "late" generator dir, it would be pointless, since that's ordered *after* the static version hence would never be taken into consdiration. Hence clean this up: keep writing to the late dir for everything, except for the fsck stuff. --- src/gpt-auto-generator/gpt-auto-generator.c | 60 +++++++++++---------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index fbd036343c..b10dd44c9d 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -47,6 +47,7 @@ typedef enum MountPointFlags { } MountPointFlags; static const char *arg_dest = NULL; +static const char *arg_dest_late = NULL; static bool arg_enabled = true; static GptAutoRoot arg_auto_root = _GPT_AUTO_ROOT_INVALID; static GptAutoRoot arg_auto_usr = _GPT_AUTO_ROOT_INVALID; @@ -95,7 +96,7 @@ static int add_cryptsetup( if (r < 0) return log_error_errno(r, "Failed to generate unit name: %m"); - r = generator_open_unit_file(arg_dest, /* source = */ NULL, n, &f); + r = generator_open_unit_file(arg_dest_late, /* source = */ NULL, n, &f); if (r < 0) return r; @@ -142,27 +143,27 @@ static int add_cryptsetup( if (r < 0) return log_error_errno(r, "Failed to write file %s: %m", n); - r = generator_write_device_timeout(arg_dest, what, mount_opts, /* filtered = */ NULL); + r = generator_write_device_timeout(arg_dest_late, what, mount_opts, /* filtered = */ NULL); if (r < 0) return r; - r = generator_add_symlink(arg_dest, d, "wants", n); + r = generator_add_symlink(arg_dest_late, d, "wants", n); if (r < 0) return r; const char *dmname = strjoina("dev-mapper-", e, ".device"); if (require) { - r = generator_add_symlink(arg_dest, "cryptsetup.target", "requires", n); + r = generator_add_symlink(arg_dest_late, "cryptsetup.target", "requires", n); if (r < 0) return r; - r = generator_add_symlink(arg_dest, dmname, "requires", n); + r = generator_add_symlink(arg_dest_late, dmname, "requires", n); if (r < 0) return r; } - r = write_drop_in_format(arg_dest, dmname, 50, "job-timeout", + r = write_drop_in_format(arg_dest_late, dmname, 50, "job-timeout", "# Automatically generated by systemd-gpt-auto-generator\n\n" "[Unit]\n" "JobTimeoutSec=infinity"); /* the binary handles timeouts anyway */ @@ -220,7 +221,7 @@ static int add_veritysetup( return log_error_errno(r, "Failed to generate unit name: %m"); _cleanup_fclose_ FILE *f = NULL; - r = generator_open_unit_file(arg_dest, /* source= */ NULL, n, &f); + r = generator_open_unit_file(arg_dest_late, /* source= */ NULL, n, &f); if (r < 0) return r; @@ -248,19 +249,19 @@ static int add_veritysetup( if (r < 0) return log_error_errno(r, "Failed to write file %s: %m", n); - r = generator_write_device_timeout(arg_dest, data_what, mount_opts, /* filtered= */ NULL); + r = generator_write_device_timeout(arg_dest_late, data_what, mount_opts, /* filtered= */ NULL); if (r < 0) return r; - r = generator_write_device_timeout(arg_dest, hash_what, mount_opts, /* filtered= */ NULL); + r = generator_write_device_timeout(arg_dest_late, hash_what, mount_opts, /* filtered= */ NULL); if (r < 0) return r; - r = generator_add_symlink(arg_dest, dd, "wants", n); + r = generator_add_symlink(arg_dest_late, dd, "wants", n); if (r < 0) return r; - r = generator_add_symlink(arg_dest, dh, "wants", n); + r = generator_add_symlink(arg_dest_late, dh, "wants", n); if (r < 0) return r; @@ -270,7 +271,7 @@ static int add_veritysetup( return log_oom(); r = write_drop_in_format( - arg_dest, + arg_dest_late, dmname, 50, "job-timeout", "# Automatically generated by systemd-gpt-auto-generator\n\n" "[Unit]\n" @@ -331,7 +332,7 @@ static int add_mount( fstype, where); } - r = generator_write_device_timeout(arg_dest, what, options, &opts_filtered); + r = generator_write_device_timeout(arg_dest_late, what, options, &opts_filtered); if (r < 0) return r; @@ -339,7 +340,7 @@ static int add_mount( if (r < 0) return log_error_errno(r, "Failed to generate unit name: %m"); - r = generator_open_unit_file(arg_dest, /* source = */ NULL, unit, &f); + r = generator_open_unit_file(arg_dest_late, /* source = */ NULL, unit, &f); if (r < 0) return r; @@ -352,6 +353,10 @@ static int add_mount( if (post) fprintf(f, "Before=%s\n", post); + /* NB: here we do not write to arg_dest_late, but to arg_dest! We typically leave the normal + * generator drop-in dir for explicit configuration via systemd-fstab-generator or similar, and put + * out automatic configuration in the arg_dest_late directory. But this one is an exception, since we + * need to override the static version of the fsck root service file. */ r = generator_write_fsck_deps(f, arg_dest, what, where, fstype, opts_filtered); if (r < 0) return r; @@ -382,25 +387,25 @@ static int add_mount( return log_error_errno(r, "Failed to write unit %s: %m", unit); if (FLAGS_SET(flags, MOUNT_VALIDATEFS)) { - r = generator_hook_up_validatefs(arg_dest, where, post); + r = generator_hook_up_validatefs(arg_dest_late, where, post); if (r < 0) return r; } if (FLAGS_SET(flags, MOUNT_GROWFS)) { - r = generator_hook_up_growfs(arg_dest, where, post); + r = generator_hook_up_growfs(arg_dest_late, where, post); if (r < 0) return r; } if (FLAGS_SET(flags, MOUNT_MEASURE)) { - r = generator_hook_up_pcrfs(arg_dest, where, post); + r = generator_hook_up_pcrfs(arg_dest_late, where, post); if (r < 0) return r; } if (post) { - r = generator_add_symlink(arg_dest, post, "requires", unit); + r = generator_add_symlink(arg_dest_late, post, "requires", unit); if (r < 0) return r; } @@ -514,7 +519,7 @@ static int add_partition_swap(DissectedPartition *p) { if (r < 0) return log_error_errno(r, "Failed to generate unit name: %m"); - r = generator_open_unit_file(arg_dest, /* source = */ NULL, name, &f); + r = generator_open_unit_file(arg_dest_late, /* source = */ NULL, name, &f); if (r < 0) return r; @@ -537,7 +542,7 @@ static int add_partition_swap(DissectedPartition *p) { if (r < 0) return log_error_errno(r, "Failed to write unit %s: %m", name); - return generator_add_symlink(arg_dest, SPECIAL_SWAP_TARGET, "wants", name); + return generator_add_symlink(arg_dest_late, SPECIAL_SWAP_TARGET, "wants", name); } static int add_automount( @@ -573,7 +578,7 @@ static int add_automount( if (r < 0) return log_error_errno(r, "Failed to generate unit name: %m"); - r = generator_open_unit_file(arg_dest, /* source = */ NULL, unit, &f); + r = generator_open_unit_file(arg_dest_late, /* source = */ NULL, unit, &f); if (r < 0) return r; @@ -592,7 +597,7 @@ static int add_automount( if (r < 0) return log_error_errno(r, "Failed to write unit %s: %m", unit); - return generator_add_symlink(arg_dest, SPECIAL_LOCAL_FS_TARGET, "wants", unit); + return generator_add_symlink(arg_dest_late, SPECIAL_LOCAL_FS_TARGET, "wants", unit); } static int add_partition_xbootldr(DissectedPartition *p) { @@ -715,11 +720,11 @@ static int add_partition_root_rw(DissectedPartition *p) { return 0; } - r = generator_enable_remount_fs_service(arg_dest); + r = generator_enable_remount_fs_service(arg_dest_late); if (r < 0) return r; - path = strjoina(arg_dest, "/systemd-remount-fs.service.d/50-remount-rw.conf"); + path = strjoina(arg_dest_late, "/systemd-remount-fs.service.d/50-remount-rw.conf"); r = write_string_file(path, "# Automatically generated by systemd-gpt-auto-generator\n\n" @@ -744,7 +749,7 @@ static int add_partition_root_growfs(DissectedPartition *p) { return 0; } - return generator_hook_up_growfs(arg_dest, "/", SPECIAL_LOCAL_FS_TARGET); + return generator_hook_up_growfs(arg_dest_late, "/", SPECIAL_LOCAL_FS_TARGET); } static int add_partition_root_flags(DissectedPartition *p) { @@ -852,7 +857,7 @@ static int add_root_mount(void) { } if (in_initrd()) { - r = generator_write_initrd_root_device_deps(arg_dest, bdev); + r = generator_write_initrd_root_device_deps(arg_dest_late, bdev); if (r < 0) return 0; @@ -1282,7 +1287,8 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat static int run(const char *dest, const char *dest_early, const char *dest_late) { int r; - assert_se(arg_dest = dest_late); + assert_se(arg_dest = dest); + assert_se(arg_dest_late = dest_late); if (detect_container() > 0) { log_debug("In a container, exiting."); From c481605d0d9ef119eb392074aa58fd8af040ad25 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 20 Mar 2025 22:51:04 +0100 Subject: [PATCH 20/21] man: document new gpt-auto-generator features --- man/systemd-fstab-generator.xml | 33 +++++++++------ man/systemd-gpt-auto-generator.xml | 65 +++++++++++++++++++++++++----- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/man/systemd-fstab-generator.xml b/man/systemd-fstab-generator.xml index f04334b985..608a661a83 100644 --- a/man/systemd-fstab-generator.xml +++ b/man/systemd-fstab-generator.xml @@ -83,10 +83,14 @@ Configures the operating system's root filesystem to mount when running in the initrd. This accepts a device node path (usually /dev/disk/by-uuid/… or - /dev/disk/by-label/… or similar), or the special values gpt-auto, - fstab, and tmpfs. + /dev/disk/by-label/… or similar), or the special values + gpt-auto, gpt-auto-force, dissect, + dissect-force, fstab, fstab, and + off. - Use gpt-auto to explicitly request automatic root file system discovery via + Set to gpt-auto, gpt-auto-force, + dissect, dissect-force to explicitly request automatic root + file system discovery, implemented in systemd-gpt-auto-generator8. Use fstab to explicitly request automatic root file system discovery via @@ -103,6 +107,11 @@ filesystems (added in v258). Expects an absolute path name referencing an existing directory within the initrd's file hierarchy to boot into. + Set to off to turn off mounting of a root file system. + + Note that further root= values may be supported, implemented in additional + packages. + @@ -133,20 +142,18 @@ mount.usr= - Takes the /usr/ filesystem - to be mounted by the initrd. If - mount.usrfstype= or - mount.usrflags= is set, then - mount.usr= will default to the value set in + Takes the /usr/ filesystem to be mounted by the initrd. If + mount.usrfstype= or mount.usrflags= is set, then the mount + configured via mount.usr= will default to the the same value set in root=. - Otherwise, this parameter defaults to the - /usr/ entry found in - /etc/fstab on the root filesystem. + Set to dissect to explicitly request automatic /usr/ + file system discovery, implemented in + systemd-gpt-auto-generator8. - mount.usr= is honored by the initrd. - + Set to off to turn off mounting of a separate /usr/ file system. + mount.usr= is honored by the initrd. diff --git a/man/systemd-gpt-auto-generator.xml b/man/systemd-gpt-auto-generator.xml index 11b08f0b8d..c95221a448 100644 --- a/man/systemd-gpt-auto-generator.xml +++ b/man/systemd-gpt-auto-generator.xml @@ -70,11 +70,11 @@ systemd-import-generator8 make sure to set the blockdev option and set the local name string to rootdisk to achieve this effect. Note that discovery of the root file system on - loopback block devices like this is only done if root=gpt-auto is specified - explicitly on the kernel command line, unlike the discovery based on the boot loader reported ESP which - is also enabled if no root= parameter is specified at all. (The latter relies on - systemd-udevd.service's /dev/gpt-auto-root block device symlink - generation). + loopback block devices like this is only done if root=gpt-auto or + root=dissect is specified explicitly on the kernel command line, unlike the + discovery based on the boot loader reported ESP which is also enabled if no root= + parameter is specified at all. (The latter relies on systemd-udevd.service's + /dev/gpt-auto-root block device symlink generation). @@ -317,6 +317,12 @@ and allows enforcing a policy on dissection and use of the automatically discovered GPT partition table entries. + Note that the specified image policy is not taken into account for automatic root or + /usr/ file system discovery unless + root=dissect/mount.usr=dissect (or + root=dissect-force) are specified. (The policy will always be applied to the other + auto-discoverable partition types.) + @@ -328,6 +334,12 @@ and allows enforcing a set of globbing patterns on the partition matching of the automatically discovered GPT partition table entries. + Note that the specified image filter is not taken into account for automatic root or + /usr/ file system discovery unless + root=dissect/mount.usr=dissect (or + root=dissect-force) are specified. (The filter will always be applied to the other + auto-discoverable partition types.) + @@ -337,17 +349,32 @@ rootflags= When root= is used with the special value - gpt-auto, full automatic discovery of the root partition based on the GPT + gpt-auto, basic automatic discovery of the root partition based on the GPT partition type is enabled. Use of the root partition is delayed until factory reset mode is left, in case it is enabled during the current boot. See Factory Reset for more information on that. If gpt-auto-force is specified - automatic discovery of the root partition is enabled, ignoring any factory reset mode. Any other - value disables this logic. + automatic discovery of the root partition is enabled, ignoring any factory reset mode. + + If root= is set to the special value dissect full + automatic discovery of the root partition based on GPT partition information is enabled. This is a + superset of root=gpt-auto, as it automatically configures Verity partitions + (including signature-based setup) following the logic defined for that in the Discoverable + Partitions Specification. Moreover it takes the configured image policy and image filter into + account for all partition types, including the root file system. root=dissect will + wait for the factory reset phase to be completed if it is in effect before activating the root file + system. Use root=dissect-force to ignore the factory reset phase and activate the + root file system immediately. + + Any other value (i.e. besides gpt-auto, gpt-auto-force, + dissect, dissect-force) disables automatic root file system + discovery. If root= is not specified at all on the kernel command line automatic discovery of the root partition via the ESP reported by the boot loader is also enabled (taking - factory reset state into account), however in this case discovery based on the loopback block device - .lo_name field is not enabled. + factory reset state into account, i.e. equivalent to root=gpt-auto), however in + this case discovery based on the loopback block device .lo_name field is not + enabled. The rootfstype= and rootflags= options are used to select the file system type and options when the root file system is automatically discovered. @@ -355,6 +382,24 @@ + + mount.usr= + mount.usrfstype= + mount.usrflags= + + Similar to root=, rootfstype=, + rootflags= (see above), but applies to the /usr/ partition + instead. Note that the gpt-auto, gpt-auto-force, + dissect-force settings that root= understands are not + supported by mount.usr= (however dissect is). + + Also note that automatic partition discovery for /usr/ must be enabled + explicitly, unlike the discovery for the root file system, which is enabled if no + root= paramater is passed at all. + + + + rw ro From 50270ecee3a7d998bbec4ab2465c60238085ee7a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 14 Mar 2025 16:13:20 +0100 Subject: [PATCH 21/21] update TODO --- TODO | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/TODO b/TODO index 325a96333c..8ae09d69cd 100644 --- a/TODO +++ b/TODO @@ -126,6 +126,8 @@ Deprecations and removals: * In v260: remove support for deprecated FactoryReset EFI variable in systemd-repart, replaced by FactoryResetRequest. +* Consider removing root=gpt-auto, and push people to use root=dissect instead. + Features: * maybe replace nss-machines with logic in networkd that registers records with @@ -163,6 +165,8 @@ Features: the PCR then also reboot. * cryptsetup: add boolean for disabling use of any password/recovery key slots. + (i.e. that we can operate in a tpm-only mode, and thus protect us from rogue + root disks) * complete varlink introspection comments: - io.systemd.BootControl @@ -176,12 +180,6 @@ Features: - io.systemd.oom - io.systemd.sysext -* dissect: instead of searching for root and /usr partitions first, look for - verity signature partitions first instead, then match up what we find with - locally available keys, and then use first that works. - -* gpt-auto-root doesn't take image policy into account. - * maybe define a /etc/machine-info field for the ANSI color to associate with a hostname. Then use it for the shell prompt to highlight the hostname. If no color is explicitly set, hash a color automatically from the hostname as a @@ -1177,10 +1175,6 @@ Features: * consider adding a new partition type, just for /opt/ for usage in system extensions -* gpt-auto-discovery: also use the pkcs7 signature stuff, and pass signature to - kernel. So far we only did this for the various --image= switches, but not - for the root fs or /usr/. - * dissection policy should enforce that unlocking can only take place by certain means, i.e. only via pw, only via tpm2, or only via fido, or a combination thereof. @@ -1345,9 +1339,6 @@ Features: * chase(): take inspiration from path_extract_filename() and return O_DIRECTORY if input path contains trailing slash. -* chase(): refuse resolution if trailing slash is specified on input, - but final node is not a directory - * document in boot loader spec that symlinks in XBOOTLDR/ESP are not OK even if non-VFAT fs is used. @@ -1667,12 +1658,6 @@ Features: data in the image, make sure the image filename actually matches this, so that images cannot be misused. -* New udev block device symlink names: - /dev/disk/by-parttypelabel/-. Use case: if pt label is used - as partition image version string, this is a safe way to reference a specific - version of a specific partition type, in particular where related partitions - are processed (e.g. verity + rootfs both named "LennartOS_0.7"). - * sysupdate: - add fuzzing to the pattern parser - support casync as download mechanism @@ -1689,11 +1674,6 @@ Features: * systemd-sysext: optionally, run it in initrd already, before transitioning into host, to open up possibility for services shipped like that. -* introduce /dev/disk/root/* symlinks that allow referencing partitions on the - disk the rootfs is on in a reasonably secure way. (or maybe: add - /dev/gpt-auto-{home,srv,boot,…} similar in style to /dev/gpt-auto-root as we - already have it. - * whenever we receive fds via SCM_RIGHTS make sure none got dropped due to the reception limit the kernel silently enforces.