From: Lennart Poettering Date: Wed, 5 Mar 2025 16:42:33 +0000 (+0100) Subject: udev: add dissect_image builtin X-Git-Tag: v258-rc1~923^2~18 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=a8b2302bc12364662ec0576f0ef28345e079679f;p=thirdparty%2Fsystemd.git udev: add dissect_image builtin Fixes: #33453 #27897 #18035 --- diff --git a/rules.d/90-image-dissect.rules b/rules.d/90-image-dissect.rules index e606d266b48..9bb097a7fe3 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 8c8592cc77f..76db2834db3 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 9de93039da6..dbdd13b5aee 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 00bc581a7dc..9b2615173a8 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 00000000000..9ba58cdadd0 --- /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 8df25e16036..692b244cb81 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 f38f8bd93ed..1467f36cd6d 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 d01d8d98753..d744f959ae9 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,