diff --git a/TODO b/TODO index 35afd73992..e4597084ea 100644 --- a/TODO +++ b/TODO @@ -143,10 +143,17 @@ Features: waits and then reboots. Then use OnFailure=bsod.target from various jobs that should result in system reboots, such as TPM tamper detection cases. +* honour validatefs xattrs in dissect-image.c too + * pcrextend: maybe add option to disable measurements entirely via kernel cmdline * tpm2-setup: reboot if we detect SRK changed +* validatefs: validate more things: check if image id + os id of initrd match + target mount, so that we refuse early any attempts to boot into different + images with the wrong kernels. check min/max kernel version too. all encoded + via xattrs in the target fs. + * pcrextend: when we fail to measure, reboot the system (at least optionally). important because certain measurements are supposed to "destroy" tpm object access. @@ -157,11 +164,6 @@ Features: * cryptsetup: add boolean for disabling use of any password/recovery key slots. -* dissect: when mounting a file system, look into certain xattrs on / in them, and - if that exists, check if gpt partition flags + type uuid + uuid match the - data encoded therein, so that attackers cannot make us misuse our file - systems - * complete varlink introspection comments: - io.systemd.BootControl - io.systemd.Hostname diff --git a/man/repart.d.xml b/man/repart.d.xml index 2f933efdae..27c78f63da 100644 --- a/man/repart.d.xml +++ b/man/repart.d.xml @@ -958,6 +958,30 @@ + + + AddValidateFS= + + Takes a boolean argument. If enabled will set the user.validatefs.gpt_label, + user.validatefs.gpt_type_uuid and user.validatefs.mount_point + extended attributes on the root inode of the formatted file system to the partition label, partition + type UUID and the intended mount point for the partition. Defaults to on if + Format= is used and the specified argument is neither swap nor + vfat. + + These extended attributes are read by + systemd-validatefs@.service8 + and may encode constraints on mounted file systems that must be fulfilled for the system to + successfully boot. This is particular important in + systemd-gpt-auto-generator8 + scenarios, which puts together the mount hierarchy from untrusted data from the GPT partition + table. As these extended attributes are stored inside the file system, they are typically + authenticated as part of the file system (assuming it is contained in protected volume; i.e. LUKS or + dm-verity), and hence may be used to securely validate the matching partition table fields. + + + + diff --git a/man/rules/meson.build b/man/rules/meson.build index ba7b6d9223..f98a55437c 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1164,6 +1164,7 @@ manpages = [ 'ENABLE_UTMP'], ['systemd-user-sessions.service', '8', ['systemd-user-sessions'], 'HAVE_PAM'], ['systemd-userdbd.service', '8', ['systemd-userdbd'], 'ENABLE_USERDB'], + ['systemd-validatefs@.service', '8', [], 'HAVE_BLKID'], ['systemd-vconsole-setup.service', '8', ['systemd-vconsole-setup'], diff --git a/man/systemd-gpt-auto-generator.xml b/man/systemd-gpt-auto-generator.xml index d3724e7e9d..af450f9fbe 100644 --- a/man/systemd-gpt-auto-generator.xml +++ b/man/systemd-gpt-auto-generator.xml @@ -281,6 +281,10 @@ automatically measured into PCR 15 on activation, via systemd-pcrfs@.service8. + Mount constraint metadata contained in the file systems is validated by pulling in + systemd-validatefs@.service8 + for generated mounts. + systemd-gpt-auto-generator implements systemd.generator7. @@ -375,6 +379,7 @@ systemd-fstab-generator8 systemd-cryptsetup@.service8 systemd-pcrfs@.service8 + systemd-validatefs@.service8 machine-id5 cryptsetup8 fstab5 diff --git a/man/systemd-validatefs@.service.xml b/man/systemd-validatefs@.service.xml new file mode 100644 index 0000000000..397bb0d669 --- /dev/null +++ b/man/systemd-validatefs@.service.xml @@ -0,0 +1,111 @@ + + + + + + + + systemd-validatefs@.service + systemd + + + + systemd-validatefs@.service + 8 + + + + systemd-validatefs@.service + Validate File System Mount Constraint Data + + + + systemd-validatefs@.service + /usr/lib/systemd/systemd-validatefs DEVICE + + + + Description + + systemd-validatefs@.service is a system service template that can be + instantiated for newly established mount points. It reads file system mount constraint data from the file + system, and ensures the mount runtime setup matches it. If it doesn't the service fails, which effects an + immediate reboot. + + This functionality is supposed to ensure that trusted file systems cannot be used in a different + context then what they were intended for. More specifically: in an + systemd-gpt-auto-generator8 + based environment the file systems to mount are largely auto-discovered based on (unprotected) GPT + partition table data. The mount constraint information can be used to validate the GPT partition data, + based on the (protected) file system contents. + + Specifically, the mount constraints are encoded in the following extended attributes on the root + inode of the file systems: + + + user.validatefs.mount_point: this extended attribute shall contain + one or more absolute, normalized paths, separated by NUL bytes. If set and the specified file system is + mounted to a location not matching any of the listed paths the validation check will + fail. + + user.validatefs.gpt_label: this extended attribute may contain a + free-form string. It is compared with the partition label string of the partition this file system is + located on, and if different the validation will fail. + + user.validatefs.gpt_type_uuid: this extended attribute may contain a + GPT partition type UUID formatted as string. It is compared with the partition type UUID of the + partition this file system is located on, and if different the validation will fail. + + + The systemd-validatefs@.service unit is automatically pulled into the initial + transaction by + systemd-gpt-auto-generator8 + for all file systems it discovers and generates mounts + for. systemd-fstab-generator8 + will do this for all mounts with the mount option in + /etc/fstab. + + The + systemd-repart8 tool + generates these extended attributes automatically for the file systems it puts together, which may be + controlled with the AddValidateFS= configuration option. + + + + Options + + The /usr/lib/systemd/system-validatefs executable may also be invoked from the + command line, where it expects a path to a mount and the following options: + + + + + + Takes an absolute path or the special string auto. The specified + path if removed as prefix from the specified mount point argument before the validation. If set to + auto defaults to unspecified on the host and /sysroot/ when + run in initrd context, in order to validate the mount constraint data relative to the future file + system root. + + + + + + + + + + + + See Also + + systemd1 + systemd-gpt-auto-generator8 + systemd-fstab-generator8 + systemd-repart8 + + + + diff --git a/man/systemd.mount.xml b/man/systemd.mount.xml index 26bc116f97..c3d06d696a 100644 --- a/man/systemd.mount.xml +++ b/man/systemd.mount.xml @@ -432,6 +432,22 @@ + + + + Validates mount constraint metadata of the mounted file system after mounting + it. This ensures the + systemd-validatefs@.service8 + service is pulled in by the mount unit. + + Note that this option can only be used in /etc/fstab, and will be ignored + when part of the Options= setting in a unit file. It is also implied for all + partitions discovered by + systemd-gpt-auto-generator8. + + + + diff --git a/meson.build b/meson.build index 38de520aae..43858480a6 100644 --- a/meson.build +++ b/meson.build @@ -2392,6 +2392,7 @@ subdir('src/update-done') subdir('src/update-utmp') subdir('src/user-sessions') subdir('src/userdb') +subdir('src/validatefs') subdir('src/varlinkctl') subdir('src/vconsole') subdir('src/veritysetup') diff --git a/src/basic/special.h b/src/basic/special.h index 166737a9ca..a5cdfebae5 100644 --- a/src/basic/special.h +++ b/src/basic/special.h @@ -95,6 +95,7 @@ #define SPECIAL_GROWFS_ROOT_SERVICE "systemd-growfs-root.service" #define SPECIAL_PCRFS_SERVICE "systemd-pcrfs@.service" #define SPECIAL_PCRFS_ROOT_SERVICE "systemd-pcrfs-root.service" +#define SPECIAL_VALIDATEFS_SERVICE "systemd-validatefs@.service" #define SPECIAL_HIBERNATE_RESUME_SERVICE "systemd-hibernate-resume.service" /* Services systemd relies on */ diff --git a/src/basic/xattr-util.c b/src/basic/xattr-util.c index 4e69693e06..f958e480ff 100644 --- a/src/basic/xattr-util.c +++ b/src/basic/xattr-util.c @@ -13,11 +13,13 @@ #include "fd-util.h" #include "macro.h" #include "missing_syscall.h" +#include "nulstr-util.h" #include "parse-util.h" #include "sparse-endian.h" #include "stat-util.h" #include "stdio-util.h" #include "string-util.h" +#include "strv.h" #include "time-util.h" #include "xattr-util.h" @@ -189,6 +191,24 @@ int getxattr_at_bool(int fd, const char *path, const char *name, int at_flags) { return parse_boolean(v); } +int getxattr_at_strv(int fd, const char *path, const char *name, int at_flags, char ***ret_strv) { + int r; + + _cleanup_free_ char *nulstr = NULL; + size_t nulstr_size = 0; + + r = getxattr_at_malloc(fd, path, name, at_flags, &nulstr, &nulstr_size); + if (r < 0) + return r; + + _cleanup_strv_free_ char **l = strv_parse_nulstr(nulstr, nulstr_size); + if (!l) + return -ENOMEM; + + *ret_strv = TAKE_PTR(l); + return 0; +} + static int listxattr_pinned_internal( int fd, const char *path, @@ -323,6 +343,18 @@ int xsetxattr_full( return 0; } +int xsetxattr_strv(int fd, const char *path, int at_flags, const char *name, char * const* l) { + int r; + + _cleanup_free_ char *nulstr = NULL; + size_t size = 0; + r = strv_make_nulstr(l, &nulstr, &size); + if (r < 0) + return r; + + return xsetxattr_full(fd, path, at_flags, name, nulstr, size, /* xattr_flags= */ 0); +} + int xremovexattr(int fd, const char *path, int at_flags, const char *name) { int r; diff --git a/src/basic/xattr-util.h b/src/basic/xattr-util.h index 8882ebf774..02293e5007 100644 --- a/src/basic/xattr-util.h +++ b/src/basic/xattr-util.h @@ -20,6 +20,7 @@ static inline int fgetxattr_malloc(int fd, const char *name, char **ret, size_t } int getxattr_at_bool(int fd, const char *path, const char *name, int at_flags); +int getxattr_at_strv(int fd, const char *path, const char *name, int at_flags, char ***ret_strv); int listxattr_at_malloc(int fd, const char *path, int at_flags, char **ret); static inline int listxattr_malloc(const char *path, char **ret) { @@ -49,6 +50,8 @@ static inline int xsetxattr( return xsetxattr_full(fd, path, at_flags, name, value, SIZE_MAX, 0); } +int xsetxattr_strv(int fd, const char *path, int at_flags, const char *name, char *const*l); + int xremovexattr(int fd, const char *path, int at_flags, const char *name); int fd_setcrtime(int fd, usec_t usec); diff --git a/src/fstab-generator/fstab-generator.c b/src/fstab-generator/fstab-generator.c index 7e84be499d..33c4c941f0 100644 --- a/src/fstab-generator/fstab-generator.c +++ b/src/fstab-generator/fstab-generator.c @@ -39,14 +39,15 @@ #include "volatile-util.h" typedef enum MountPointFlags { - MOUNT_NOAUTO = 1 << 0, - MOUNT_NOFAIL = 1 << 1, - MOUNT_AUTOMOUNT = 1 << 2, - MOUNT_MAKEFS = 1 << 3, - MOUNT_GROWFS = 1 << 4, - MOUNT_RW_ONLY = 1 << 5, - MOUNT_PCRFS = 1 << 6, - MOUNT_QUOTA = 1 << 7, + MOUNT_NOAUTO = 1 << 0, + MOUNT_NOFAIL = 1 << 1, + MOUNT_AUTOMOUNT = 1 << 2, + MOUNT_MAKEFS = 1 << 3, + MOUNT_GROWFS = 1 << 4, + MOUNT_RW_ONLY = 1 << 5, + MOUNT_PCRFS = 1 << 6, + MOUNT_QUOTA = 1 << 7, + MOUNT_VALIDATEFS = 1 << 8, } MountPointFlags; typedef struct Mount { @@ -249,9 +250,10 @@ static int add_swap( return true; } - log_debug("Found swap entry what=%s makefs=%s growfs=%s pcrfs=%s noauto=%s nofail=%s", + log_debug("Found swap entry what=%s makefs=%s growfs=%s pcrfs=%s validatefs=%s noauto=%s nofail=%s", what, - yes_no(flags & MOUNT_MAKEFS), yes_no(flags & MOUNT_GROWFS), yes_no(flags & MOUNT_PCRFS), + yes_no(flags & MOUNT_MAKEFS), yes_no(flags & MOUNT_GROWFS), + yes_no(flags & MOUNT_PCRFS), yes_no(flags & MOUNT_VALIDATEFS), yes_no(flags & MOUNT_NOAUTO), yes_no(flags & MOUNT_NOFAIL)); r = unit_name_from_path(what, ".swap", &name); @@ -304,6 +306,8 @@ static int add_swap( log_warning("%s: growing swap devices is currently unsupported.", what); if (flags & MOUNT_PCRFS) log_warning("%s: measuring swap devices is currently unsupported.", what); + if (flags & MOUNT_VALIDATEFS) + log_warning("%s: validating swap devices is currently unsupported.", what); if (!(flags & MOUNT_NOAUTO)) { r = generator_add_symlink(arg_dest, SPECIAL_SWAP_TARGET, @@ -688,6 +692,12 @@ static int add_mount( } } + if (flags & MOUNT_VALIDATEFS) { + r = generator_hook_up_validatefs(dest, where, target_unit); + if (r < 0) + return r; + } + if (flags & MOUNT_QUOTA) { r = generator_hook_up_quotacheck(dest, what, where, target_unit, fstype); if (r < 0) { @@ -842,6 +852,8 @@ static MountPointFlags fstab_options_to_flags(const char *options, bool is_swap) flags |= MOUNT_GROWFS; if (fstab_test_option(options, "x-systemd.pcrfs\0")) flags |= MOUNT_PCRFS; + if (fstab_test_option(options, "x-systemd.validatefs\0")) + flags |= MOUNT_VALIDATEFS; if (fstab_test_option(options, "usrquota\0" "grpquota\0" "quota\0" "usrjquota\0" "grpjquota\0" "prjquota\0")) flags |= MOUNT_QUOTA; if (fstab_test_yes_no_option(options, "noauto\0" "auto\0")) @@ -970,9 +982,10 @@ static int parse_fstab_one( free_and_replace(what, p); } - log_debug("Found entry what=%s where=%s type=%s makefs=%s growfs=%s pcrfs=%s noauto=%s nofail=%s", + log_debug("Found entry what=%s where=%s type=%s makefs=%s growfs=%s pcrfs=%s validatefs=%s noauto=%s nofail=%s", what, where, strna(fstype), - yes_no(flags & MOUNT_MAKEFS), yes_no(flags & MOUNT_GROWFS), yes_no(flags & MOUNT_PCRFS), + yes_no(flags & MOUNT_MAKEFS), yes_no(flags & MOUNT_GROWFS), + yes_no(flags & MOUNT_PCRFS), yes_no(flags & MOUNT_VALIDATEFS), yes_no(flags & MOUNT_NOAUTO), yes_no(flags & MOUNT_NOFAIL)); bool is_sysroot = in_initrd() && path_equal(where, "/sysroot"); diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index 0addb5a82c..54f674040b 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -38,6 +38,13 @@ #include "unit-name.h" #include "virt.h" +typedef enum MountPointFlags { + MOUNT_RW = 1 << 0, + MOUNT_GROWFS = 1 << 1, + MOUNT_MEASURE = 1 << 2, + MOUNT_VALIDATEFS = 1 << 3, +} MountPointFlags; + static const char *arg_dest = NULL; static bool arg_enabled = true; static GptAutoRoot arg_auto_root = _GPT_AUTO_ROOT_INVALID; @@ -57,9 +64,8 @@ static int add_cryptsetup( const char *id, const char *what, const char *mount_opts, - bool rw, + MountPointFlags flags, bool require, - bool measure, char **ret_device) { #if HAVE_LIBCRYPTSETUP @@ -97,7 +103,7 @@ static int add_cryptsetup( "After=%s\n", d, d); - if (!rw) { + if (!FLAGS_SET(flags, MOUNT_RW)) { options = strdup("read-only"); if (!options) return log_oom(); @@ -109,7 +115,7 @@ static int add_cryptsetup( if (!strextend_with_separator(&options, ",", "tpm2-device=auto")) return log_oom(); - if (measure) { + if (FLAGS_SET(flags, MOUNT_MEASURE)) { /* We only measure the root volume key into PCR 15 if we are booted with sd-stub (i.e. in a * UKI), and sd-stub measured the UKI. We do this in order not to step into people's own PCR * assignment, under the assumption that people who are fine to use sd-stub with its PCR @@ -178,9 +184,7 @@ static int add_mount( const char *what, const char *where, const char *fstype, - bool rw, - bool growfs, - bool measure, + MountPointFlags flags, const char *options, const char *description, const char *post) { @@ -203,7 +207,7 @@ static int add_mount( if (streq_ptr(fstype, "crypto_LUKS")) { /* Mount options passed are determined by partition_pick_mount_options(), whose result * is known to not contain timeout options. */ - r = add_cryptsetup(id, what, /* mount_opts = */ NULL, rw, /* require= */ true, measure, &crypto_what); + r = add_cryptsetup(id, what, /* mount_opts = */ NULL, flags, /* require= */ true, &crypto_what); if (r < 0) return r; @@ -270,13 +274,19 @@ static int add_mount( if (r < 0) return log_error_errno(r, "Failed to write unit %s: %m", unit); - if (growfs) { + if (FLAGS_SET(flags, MOUNT_VALIDATEFS)) { + r = generator_hook_up_validatefs(arg_dest, where, post); + if (r < 0) + return r; + } + + if (FLAGS_SET(flags, MOUNT_GROWFS)) { r = generator_hook_up_growfs(arg_dest, where, post); if (r < 0) return r; } - if (measure) { + if (FLAGS_SET(flags, MOUNT_MEASURE)) { r = generator_hook_up_pcrfs(arg_dest, where, post); if (r < 0) return r; @@ -353,9 +363,10 @@ static int add_partition_mount( p->node, where, p->fstype, - p->rw, - p->growfs, - /* measure= */ STR_IN_SET(id, "root", "var"), /* by default measure rootfs and /var, since they contain the "identity" of the system */ + (p->rw ? MOUNT_RW : 0) | + MOUNT_VALIDATEFS | + (p->growfs ? MOUNT_GROWFS : 0) | + (STR_IN_SET(id, "root", "var") ? MOUNT_MEASURE : 0), /* by default measure rootfs and /var, since they contain the "identity" of the system */ options, description, SPECIAL_LOCAL_FS_TARGET); @@ -383,7 +394,7 @@ static int add_partition_swap(DissectedPartition *p) { } if (streq_ptr(p->fstype, "crypto_LUKS")) { - r = add_cryptsetup("swap", p->node, /* mount_opts = */ NULL, /* rw= */ true, /* require= */ true, /* measure= */ false, &crypto_what); + r = add_cryptsetup("swap", p->node, /* mount_opts = */ NULL, MOUNT_RW, /* require= */ true, &crypto_what); if (r < 0) return r; what = crypto_what; @@ -427,8 +438,7 @@ static int add_automount( const char *what, const char *where, const char *fstype, - bool rw, - bool growfs, + MountPointFlags flags, const char *options, const char *description, usec_t timeout) { @@ -445,12 +455,10 @@ static int add_automount( what, where, fstype, - rw, - growfs, - /* measure= */ false, + flags, options, description, - NULL); + /* post= */ NULL); if (r < 0) return r; @@ -508,8 +516,7 @@ static int add_partition_xbootldr(DissectedPartition *p) { p->node, "/boot", p->fstype, - /* rw= */ true, - /* growfs= */ false, + MOUNT_RW, options, "Boot Loader Partition", LOADER_PARTITION_IDLE_USEC); @@ -571,8 +578,7 @@ static int add_partition_esp(DissectedPartition *p, bool has_xbootldr) { p->node, esp_path, p->fstype, - /* rw= */ true, - /* growfs= */ false, + MOUNT_RW, options, "EFI System Partition Automount", LOADER_PARTITION_IDLE_USEC); @@ -665,7 +671,7 @@ static int add_root_cryptsetup(void) { bdev = "/dev/gpt-auto-root-luks-ignore-factory-reset"; } - return add_cryptsetup("root", bdev, arg_root_options, /* rw= */ true, /* require= */ false, /* measure= */ true, NULL); + return add_cryptsetup("root", bdev, arg_root_options, MOUNT_RW|MOUNT_MEASURE, /* require= */ false, NULL); #else return 0; #endif @@ -752,9 +758,9 @@ static int add_root_mount(void) { bdev, in_initrd() ? "/sysroot" : "/", arg_root_fstype, - /* rw= */ arg_root_rw > 0, - /* growfs= */ false, - /* measure= */ true, + (arg_root_rw > 0 ? MOUNT_RW : 0) | + (in_initrd() ? MOUNT_VALIDATEFS : 0) | + MOUNT_MEASURE, options, "Root Partition", in_initrd() ? SPECIAL_INITRD_ROOT_FS_TARGET : SPECIAL_LOCAL_FS_TARGET); diff --git a/src/repart/repart.c b/src/repart/repart.c index 0526c4c803..a1236943ca 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -80,6 +80,7 @@ #include "tpm2-util.h" #include "user-util.h" #include "utf8.h" +#include "xattr-util.h" /* If not configured otherwise use a minimal partition size of 10M */ #define DEFAULT_MIN_SIZE (10ULL*1024ULL*1024ULL) @@ -396,6 +397,8 @@ typedef struct Partition { char *compression; char *compression_level; + int add_validatefs; + uint64_t gpt_flags; int no_auto; int read_only; @@ -578,6 +581,7 @@ static Partition *partition_new(void) { .growfs = -1, .verity_data_block_size = UINT64_MAX, .verity_hash_block_size = UINT64_MAX, + .add_validatefs = -1, }; return p; @@ -689,6 +693,7 @@ static void partition_foreignize(Partition *p) { p->read_only = -1; p->growfs = -1; p->verity = VERITY_OFF; + p->add_validatefs = false; partition_mountpoint_free_many(p->mountpoints, p->n_mountpoints); p->mountpoints = NULL; @@ -2337,10 +2342,23 @@ static int partition_finalize_fstype(Partition *p, const char *path) { return free_and_strdup_warn(&p->format, v); } +static bool partition_add_validatefs(const Partition *p) { + assert(p); + + if (p->add_validatefs >= 0) + return p->add_validatefs; + + return p->format && !STR_IN_SET(p->format, "swap", "vfat"); +} + static bool partition_needs_populate(const Partition *p) { assert(p); assert(!p->supplement_for || !p->suppressing); /* Avoid infinite recursion */ - return !strv_isempty(p->copy_files) || !strv_isempty(p->make_directories) || !strv_isempty(p->make_symlinks) || + + return !strv_isempty(p->copy_files) || + !strv_isempty(p->make_directories) || + !strv_isempty(p->make_symlinks) || + partition_add_validatefs(p) || (p->suppressing && partition_needs_populate(p->suppressing)); } @@ -2383,6 +2401,7 @@ static int partition_read_definition(Partition *p, const char *path, const char { "Partition", "Compression", config_parse_string, CONFIG_PARSE_STRING_SAFE_AND_ASCII, &p->compression }, { "Partition", "CompressionLevel", config_parse_string, CONFIG_PARSE_STRING_SAFE_AND_ASCII, &p->compression_level }, { "Partition", "SupplementFor", config_parse_string, 0, &p->supplement_for_name }, + { "Partition", "AddValidateFS", config_parse_tristate, 0, &p->add_validatefs }, {} }; _cleanup_free_ char *filename = NULL; @@ -5866,6 +5885,53 @@ static int set_default_subvolume(Partition *p, const char *root) { return 0; } +static int do_make_validatefs_xattrs(const Partition *p, const char *root) { + int r; + + assert(p); + assert(root); + + if (!partition_add_validatefs(p)) + return 0; + + _cleanup_close_ int fd = open(root, O_DIRECTORY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(errno, "Failed to open root inode '%s': %m", root); + + if (p->new_label) { + r = xsetxattr(fd, /* path= */ NULL, AT_EMPTY_PATH, "user.validatefs.gpt_label", p->new_label); + if (r < 0) + return log_error_errno(r, "Failed to set 'user.validatefs.gpt_label' extended attribute: %m"); + } + + r = xsetxattr(fd, /* path= */ NULL, AT_EMPTY_PATH, "user.validatefs.gpt_type_uuid", SD_ID128_TO_UUID_STRING(p->type.uuid)); + if (r < 0) + return log_error_errno(r, "Failed to set 'user.validatefs.gpt_type_uuid' extended attribute: %m"); + + /* Prefer the data from MountPoint= if specified, otherwise use data we derive from the partition type */ + _cleanup_strv_free_ char **l = NULL; + if (p->n_mountpoints > 0) { + FOREACH_ARRAY(m, p->mountpoints, p->n_mountpoints) + if (strv_extend(&l, m->where) < 0) + return log_oom(); + } else { + const char *m = gpt_partition_type_mountpoint_nulstr(p->type); + if (m) { + l = strv_split_nulstr(m); + if (!l) + return log_oom(); + } + } + + if (!strv_isempty(l)) { + r = xsetxattr_strv(fd, /* path= */ NULL, AT_EMPTY_PATH, "user.validatefs.mount_point", l); + if (r < 0) + return log_error_errno(r, "Failed to set 'user.validatefs.mount_point' extended attribute: %m"); + } + + return 0; +} + static int partition_populate_directory(Context *context, Partition *p, char **ret) { _cleanup_(rm_rf_physical_and_freep) char *root = NULL; const char *vt; @@ -5899,6 +5965,10 @@ static int partition_populate_directory(Context *context, Partition *p, char **r if (r < 0) return r; + r = do_make_validatefs_xattrs(p, root); + if (r < 0) + return r; + log_info("Ready to populate %s filesystem.", p->format); *ret = TAKE_PTR(root); @@ -5942,6 +6012,9 @@ static int partition_populate_filesystem(Context *context, Partition *p, const c if (do_make_symlinks(p, fs) < 0) _exit(EXIT_FAILURE); + if (do_make_validatefs_xattrs(p, fs) < 0) + _exit(EXIT_FAILURE); + if (make_subvolumes_read_only(p, fs) < 0) _exit(EXIT_FAILURE); @@ -6080,7 +6153,7 @@ static int context_mkfs(Context *context) { log_info("Formatting future partition %" PRIu64 ".", p->partno); /* If we're not writing to a loop device or if we're populating a read-only filesystem, we - * have to populate using the filesystem's mkfs's --root (or equivalent) option. To do that, + * have to populate using the filesystem's mkfs's --root= (or equivalent) option. To do that, * we need to set up the final directory tree beforehand. */ if (partition_needs_populate(p) && (!t->loop || fstype_is_ro(p->format))) { diff --git a/src/shared/generator.c b/src/shared/generator.c index 0bec2a2a27..70fcd6e935 100644 --- a/src/shared/generator.c +++ b/src/shared/generator.c @@ -764,6 +764,43 @@ int generator_hook_up_pcrfs( return generator_add_symlink_full(dir, where_unit, "wants", pcrfs_unit_path, instance); } +int generator_hook_up_validatefs( + const char *dir, + const char *where, + const char *target) { + + _cleanup_free_ char *where_unit = NULL, *instance = NULL; + const char *validatefs_unit, *validatefs_unit_path; + int r; + + assert(dir); + assert(where); + + /* never hook this in for the actual root fs, because it's too late then, we already are running from + * the root fs, it makes no sense to validate it anymore */ + if (empty_or_root(where)) + return 0; + + r = unit_name_from_path(where, ".mount", &where_unit); + if (r < 0) + return log_error_errno(r, "Failed to make unit name from path '%s': %m", where); + + validatefs_unit = SPECIAL_VALIDATEFS_SERVICE; + validatefs_unit_path = SYSTEM_DATA_UNIT_DIR "/" SPECIAL_VALIDATEFS_SERVICE; + + r = unit_name_path_escape(where, &instance); + if (r < 0) + return log_error_errno(r, "Failed to escape path '%s': %m", where); + + if (target) { + r = generator_add_ordering(dir, target, "After", validatefs_unit, instance); + if (r < 0) + return r; + } + + return generator_add_symlink_full(dir, where_unit, "wants", validatefs_unit_path, instance); +} + int generator_hook_up_quotacheck( const char *dir, const char *what, diff --git a/src/shared/generator.h b/src/shared/generator.h index 4738fe90af..47cccd9c59 100644 --- a/src/shared/generator.h +++ b/src/shared/generator.h @@ -67,6 +67,10 @@ int generator_hook_up_pcrfs( const char *dir, const char *where, const char *target); +int generator_hook_up_validatefs( + const char *dir, + const char *where, + const char *target); int generator_hook_up_quotacheck( const char *dir, const char *what, diff --git a/src/validatefs/meson.build b/src/validatefs/meson.build new file mode 100644 index 0000000000..4b80056389 --- /dev/null +++ b/src/validatefs/meson.build @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +executables += [ + libexec_template + { + 'name' : 'systemd-validatefs', + 'conditions' : [ + 'HAVE_BLKID', + ], + 'sources' : files('validatefs.c'), + 'dependencies' : [ + libblkid, + ], + }, +] diff --git a/src/validatefs/validatefs.c b/src/validatefs/validatefs.c new file mode 100644 index 0000000000..fb516744d3 --- /dev/null +++ b/src/validatefs/validatefs.c @@ -0,0 +1,331 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "blkid-util.h" +#include "blockdev-util.h" +#include "build.h" +#include "chase.h" +#include "device-util.h" +#include "fd-util.h" +#include "initrd-util.h" +#include "main-func.h" +#include "mountpoint-util.h" +#include "parse-argument.h" +#include "path-util.h" +#include "pretty-print.h" +#include "string-util.h" +#include "utf8.h" +#include "xattr-util.h" + +static char *arg_target = NULL; +static char *arg_root = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_target, freep); +STATIC_DESTRUCTOR_REGISTER(arg_root, freep); + +static int help(void) { + int r; + + _cleanup_free_ char *link = NULL; + r = terminal_urlify_man("systemd-validatefs@.service", "8", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] /path/to/mountpoint\n" + "\n%3$sCheck file system validation constraints.%4$s\n" + " -h --help Show this help and exit\n" + " --version Print version string and exit\n" + " --root=PATH|auto Operate relative to the specified path\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_highlight(), + ansi_normal()); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + ARG_ROOT, + }; + + int c, r; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version" , no_argument, NULL, ARG_VERSION }, + { "root", required_argument, NULL, ARG_ROOT }, + {} + }; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + switch (c) { + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case ARG_ROOT: + if (streq(optarg, "auto")) { + arg_root = mfree(arg_root); + + if (in_initrd()) { + arg_root = strdup("/sysroot"); + if (!arg_root) + return log_oom(); + } + + break; + } + + if (!path_is_absolute(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--root= argument must be 'auto' or absolute path, got: %s", optarg); + + r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_root); + if (r < 0) + return r; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (optind + 1 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "%s excepts exactly one argument (the mount point).", + program_invocation_short_name); + + arg_target = strdup(argv[optind]); + if (!arg_target) + return log_oom(); + + if (arg_root && !path_startswith(arg_target, arg_root)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified path '%s' does not start with specified root '%s', refusing.", arg_target, arg_root); + + return 1; +} + +typedef struct ValidateFields { + sd_id128_t gpt_type_uuid; + char *gpt_label; + char **mount_point; +} ValidateFields; + +static void validate_fields_done(ValidateFields *f) { + assert(f); + + free(f->gpt_label); + strv_free(f->mount_point); +} + +static int validate_fields_read(int fd, ValidateFields *ret) { + _cleanup_(validate_fields_done) ValidateFields f = {}; + int r; + + assert(fd >= 0); + assert(ret); + + _cleanup_free_ char *t = NULL; + r = getxattr_at_malloc(fd, /* path= */ NULL, "user.validatefs.gpt_type_uuid", AT_EMPTY_PATH, &t, /* ret_size= */ NULL); + if (r < 0) { + if (r != -ENODATA && !ERRNO_IS_NOT_SUPPORTED(r)) + return log_error_errno(r, "Failed to read 'user.validatefs.gpt_type_uuid' xattr: %m"); + } else { + r = sd_id128_from_string(t, &f.gpt_type_uuid); + if (r < 0) + return log_error_errno(r, "Failed to parse 'user.validatefs.gpt_type_uuid' xattr: %s", t); + } + + r = getxattr_at_malloc(fd, /* path= */ NULL, "user.validatefs.gpt_label", AT_EMPTY_PATH, &f.gpt_label, /* ret_size= */ NULL); + if (r < 0) { + if (r != -ENODATA && !ERRNO_IS_NOT_SUPPORTED(r)) + return log_error_errno(r, "Failed to read 'user.validatefs.gpt_label' xattr: %m"); + } else if (!utf8_is_valid(f.gpt_label) || string_has_cc(f.gpt_label, /* ok= */ NULL)) + return log_error_errno( + SYNTHETIC_ERRNO(EINVAL), + "Extended attribute 'user.validatefs.gpt_label' contains invalid characters, refusing."); + + _cleanup_strv_free_ char **l = NULL; + r = getxattr_at_strv(fd, /* path= */ NULL, "user.validatefs.mount_point", AT_EMPTY_PATH, &l); + if (r < 0) { + if (r != -ENODATA && !ERRNO_IS_NOT_SUPPORTED(r)) + return log_error_errno(r, "Failed to read 'user.validatefs.mount_point' xattr: %m"); + } else { + STRV_FOREACH(i, l) + if (!utf8_is_valid(*i) || + string_has_cc(*i, /* ok= */ NULL) || + !path_is_absolute(*i) || + !path_is_normalized(*i)) + return log_error_errno( + SYNTHETIC_ERRNO(EINVAL), + "Path listed in extended attribute 'user.validatefs.mount_point' is not a valid, normalized, absolute path or contains invalid characters, refusing: %s", *i); + + f.mount_point = TAKE_PTR(l); + } + + r = !sd_id128_is_null(f.gpt_type_uuid) || f.gpt_label || !strv_isempty(f.mount_point); + *ret = TAKE_STRUCT(f); + return r; +} + +static int validate_fields_check(int fd, const char *path, const ValidateFields *f) { + int r; + + assert(fd >= 0); + assert(path); + assert(f); + + if (!strv_isempty(f->mount_point)) { + bool good = false; + + STRV_FOREACH(i, f->mount_point) { + _cleanup_free_ char *jj = NULL; + const char *j; + + if (arg_root) { + jj = path_join(arg_root, *i); + if (!jj) + return log_oom(); + + j = jj; + } else + j = *i; + + if (path_equal(path, j)) { + good = true; + break; + } + } + + if (!good) { + _cleanup_free_ char *joined = strv_join(f->mount_point, ", "); + + return log_error_errno( + SYNTHETIC_ERRNO(EPERM), + "File system is supposed to be mounted on one of %s only, but is mounted on %s, refusing.", + strna(joined), path); + } + } + + if (f->gpt_label || !sd_id128_is_null(f->gpt_type_uuid)) { + _cleanup_(sd_device_unrefp) sd_device *d = NULL; + + r = block_device_new_from_fd(fd, BLOCK_DEVICE_LOOKUP_ORIGINATING|BLOCK_DEVICE_LOOKUP_BACKING, &d); + if (r < 0) + return log_error_errno(r, "Failed to find block device backing '%s': %m", path); + + _cleanup_close_ int block_fd = sd_device_open(d, O_RDONLY|O_CLOEXEC|O_NONBLOCK); + if (block_fd < 0) + return log_error_errno(block_fd, "Failed to open block device backing '%s': %m", path); + + _cleanup_(blkid_free_probep) blkid_probe b = blkid_new_probe(); + if (!b) + return log_oom(); + + errno = 0; + r = blkid_probe_set_device(b, block_fd, 0, 0); + if (r != 0) + return log_error_errno(errno_or_else(ENOMEM), "Failed to set up block device prober for '%s': %m", path); + + (void) blkid_probe_enable_superblocks(b, 1); + (void) blkid_probe_set_superblocks_flags(b, BLKID_SUBLKS_TYPE|BLKID_SUBLKS_LABEL); + (void) blkid_probe_enable_partitions(b, 1); + (void) blkid_probe_set_partitions_flags(b, BLKID_PARTS_ENTRY_DETAILS); + + errno = 0; + r = blkid_do_safeprobe(b); + if (r == _BLKID_SAFEPROBE_ERROR) + return log_error_errno(errno_or_else(EIO), "Failed to probe block device of '%s': %m", path); + if (r == _BLKID_SAFEPROBE_AMBIGUOUS) + return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "Found multiple file system labels on block device '%s'.", path); + if (r == _BLKID_SAFEPROBE_NOT_FOUND) + return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "Found no file system label on block device '%s'.", path); + + assert(r == _BLKID_SAFEPROBE_FOUND); + + const char *v = NULL; + (void) blkid_probe_lookup_value(b, "PART_ENTRY_SCHEME", &v, /* ret_len= */ NULL); + if (!streq_ptr(v, "gpt")) + return log_error_errno(SYNTHETIC_ERRNO(EPERM), "File system is supposed to be on a GPT partition table, but is not, refusing."); + + if (f->gpt_label) { + v = NULL; + (void) blkid_probe_lookup_value(b, "PART_ENTRY_NAME", &v, /* ret_len= */ NULL); + + if (!streq(f->gpt_label, strempty(v))) + return log_error_errno( + SYNTHETIC_ERRNO(EPERM), + "File system is supposed to be placed in a partition with label '%s' only, but is placed in one labelled '%s', refusing.", + f->gpt_label, strempty(v)); + } + + if (!sd_id128_is_null(f->gpt_type_uuid)) { + v = NULL; + (void) blkid_probe_lookup_value(b, "PART_ENTRY_TYPE", &v, /* ret_len= */ NULL); + + sd_id128_t id = SD_ID128_NULL; + if (!v || sd_id128_from_string(v, &id) < 0) + return log_error_errno( + SYNTHETIC_ERRNO(EPERM), + "File system is supposed to be placed in a partition of type UUID '%s' only, but has no type, refusing.", + SD_ID128_TO_UUID_STRING(f->gpt_type_uuid)); + + if (!sd_id128_equal(f->gpt_type_uuid, id)) + return log_error_errno( + SYNTHETIC_ERRNO(EPERM), + "File system is supposed to be placed in a partition of type UUID '%s' only, but has type '%s', refusing.", + SD_ID128_TO_UUID_STRING(f->gpt_type_uuid), SD_ID128_TO_UUID_STRING(id)); + } + } + + log_info("File system '%s' passed validation constraints, proceeding.", path); + return 0; +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + _cleanup_free_ char *resolved = NULL; + _cleanup_close_ int target_fd = chase_and_open(arg_target, arg_root, CHASE_MUST_BE_DIRECTORY, O_DIRECTORY|O_CLOEXEC, &resolved); + if (target_fd < 0) + return log_error_errno(target_fd, "Failed to open directory '%s': %m", arg_target); + + r = is_mount_point_at(target_fd, /* filename= */ NULL, /* flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to determine whether '%s' is a mount point: %m", resolved); + if (!r) + return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "Directory '%s' is not a mount point.", resolved); + + _cleanup_(validate_fields_done) ValidateFields f = {}; + r = validate_fields_read(target_fd, &f); + if (r < 0) + return r; + if (r == 0) { + log_info("File system '%s' has no validation constraints set, not validating.", resolved); + return EXIT_SUCCESS; + } + + r = validate_fields_check(target_fd, resolved, &f); + if (r < 0) + return r; + + return EXIT_SUCCESS; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/test/units/TEST-29-PORTABLE.directory.sh b/test/units/TEST-29-PORTABLE.directory.sh index 5458111ee2..2cbd7da009 100755 --- a/test/units/TEST-29-PORTABLE.directory.sh +++ b/test/units/TEST-29-PORTABLE.directory.sh @@ -17,8 +17,8 @@ if [[ -v ASAN_OPTIONS || -v UBSAN_OPTIONS ]]; then ARGS+=(--profile=trusted) fi -unsquashfs -dest /tmp/minimal_0 /usr/share/minimal_0.raw -unsquashfs -dest /tmp/minimal_1 /usr/share/minimal_1.raw +unsquashfs -no-xattrs -d /tmp/minimal_0 /usr/share/minimal_0.raw +unsquashfs -no-xattrs -d /tmp/minimal_1 /usr/share/minimal_1.raw portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/minimal_0 minimal-app0 diff --git a/test/units/TEST-81-GENERATORS.fstab-generator.sh b/test/units/TEST-81-GENERATORS.fstab-generator.sh index 0c1b27eb1d..95eba8a9f6 100755 --- a/test/units/TEST-81-GENERATORS.fstab-generator.sh +++ b/test/units/TEST-81-GENERATORS.fstab-generator.sh @@ -46,6 +46,7 @@ FSTAB_GENERAL=( "/dev/test22 /initrd/mount ext2 defaults,x-systemd.rw-only,x-initrd.mount 0 1" "/dev/test23 /initrd/mount/nofail ext3 defaults,nofail,x-initrd.mount 0 1" "/dev/test24 /initrd/mount/deps ext4 x-initrd.mount,x-systemd.before=early.service,x-systemd.after=late.service 0 1" + "/dev/test25 /x-systemd.validatefs xfs x-systemd.validatefs 0 0" # Incomplete, but valid entries "/dev/incomplete1 /incomplete1" @@ -282,6 +283,9 @@ check_fstab_mount_units() { elif [[ "$opt" == x-systemd.growfs ]]; then service="$(systemd-escape --template=systemd-growfs@.service --path "$where")" link_endswith "$out_dir/${unit}.wants/$service" "/lib/systemd/system/systemd-growfs@.service" + elif [[ "$opt" == x-systemd.validatefs ]]; then + service="$(systemd-escape --template=systemd-validatefs@.service --path "$where")" + link_endswith "$out_dir/${unit}.wants/$service" "/lib/systemd/system/systemd-validatefs@.service" elif [[ "$opt" == bg ]] && [[ "$fstype" =~ ^(nfs|nfs4)$ ]]; then # We "convert" nfs bg mounts to fg, so we can do the job-control # ourselves diff --git a/test/units/TEST-87-AUX-UTILS-VM.validatefs.sh b/test/units/TEST-87-AUX-UTILS-VM.validatefs.sh new file mode 100755 index 0000000000..5b142e178a --- /dev/null +++ b/test/units/TEST-87-AUX-UTILS-VM.validatefs.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +if ! command -v systemd-repart >/dev/null; then + echo "no systemd-repart" + return +fi + +if ! test -x /usr/lib/systemd/systemd-validatefs ; then + echo "no systemd-validatefs" + return +fi + +export SYSTEMD_LOG_LEVEL=debug +export PAGER=cat + +at_exit() { + set +e + rm -rf /tmp/validatefs-test/ + rm -f /var/tmp/validatefs-test.raw + systemd-dissect --umount --rmdir /tmp/validatefs-test.mount + umount /tmp/validatefs-test.fake + rmdir /tmp/validatefs-test.fake +} + +trap at_exit EXIT + +mkdir /tmp/validatefs-test +cat > /tmp/validatefs-test/validatefs-root.conf < /tmp/validatefs-test/validatefs-usr.conf < /tmp/validatefs-test/validatefs-home.conf < /tmp/validatefs-test/validatefs-esp.conf < /tmp/validatefs-test/validatefs-generic.conf <