From ed024abac6e4acad5a77943721e7622bb1b10b71 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 31 Mar 2025 14:47:22 +0200 Subject: [PATCH 1/8] xattr-util: add trivial getxattr_at_strv() helper --- src/basic/xattr-util.c | 20 ++++++++++++++++++++ src/basic/xattr-util.h | 1 + 2 files changed, 21 insertions(+) diff --git a/src/basic/xattr-util.c b/src/basic/xattr-util.c index 4e69693e06..f5b270c3a9 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, diff --git a/src/basic/xattr-util.h b/src/basic/xattr-util.h index 8882ebf774..9132dc2e37 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) { From 9fbe26cfa8fadb58e048aa6a57ad0dacf5de03fc Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 31 Mar 2025 14:55:36 +0200 Subject: [PATCH 2/8] xattr-util: add trivial xsetxattr_strv() wrapper --- src/basic/xattr-util.c | 12 ++++++++++++ src/basic/xattr-util.h | 2 ++ 2 files changed, 14 insertions(+) diff --git a/src/basic/xattr-util.c b/src/basic/xattr-util.c index f5b270c3a9..f958e480ff 100644 --- a/src/basic/xattr-util.c +++ b/src/basic/xattr-util.c @@ -343,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 9132dc2e37..02293e5007 100644 --- a/src/basic/xattr-util.h +++ b/src/basic/xattr-util.h @@ -50,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); From 0bdd5ccc8145af8dae9779751d3e7a34c4fa6aa5 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Mar 2025 08:37:05 +0100 Subject: [PATCH 3/8] validatefs: add new tool that enforces mount constraints This new tool looks for a three xattr on the root inode of a file system that encode mount constraints of the file system. The tool is supposed to be hooke into the mount logic and is supposed to protect against misappropriating trusted file systems in unintended ways. Consider the following scenario: we boot up on first boot and create a tpm-locked pair of /var/ and /srv/ partitions via systemd-repart. An attacker then offline modifies the partition table, exchanging the metadata of the /var/ and /srv/ partition. So far we'd happily accept that, honour the modified metadata and boot up. This could be used to revert changes to /var/ or similar. And all that even though both partitions are encrypted and locked to TPM! With this new mechanism we can encode in the protected contents of the file systems the ways it can be used: the partition type uuid, the partition label and the intended mount point can be stored in xattrs, and we can check them automatically on mount, and take action on mismatch. (action would typically be immediate reboot). --- man/rules/meson.build | 1 + man/systemd-validatefs@.service.xml | 97 ++++++++ meson.build | 1 + src/validatefs/meson.build | 14 ++ src/validatefs/validatefs.c | 331 +++++++++++++++++++++++++++ units/meson.build | 4 + units/systemd-validatefs@.service.in | 23 ++ 7 files changed, 471 insertions(+) create mode 100644 man/systemd-validatefs@.service.xml create mode 100644 src/validatefs/meson.build create mode 100644 src/validatefs/validatefs.c create mode 100644 units/systemd-validatefs@.service.in 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-validatefs@.service.xml b/man/systemd-validatefs@.service.xml new file mode 100644 index 0000000000..6535ed463b --- /dev/null +++ b/man/systemd-validatefs@.service.xml @@ -0,0 +1,97 @@ + + + + + + + + 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. + + + + + 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 + + + + 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/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/units/meson.build b/units/meson.build index eabd1eea13..cacb1524d6 100644 --- a/units/meson.build +++ b/units/meson.build @@ -849,6 +849,10 @@ units = [ 'file' : 'systemd-nsresourced.socket', 'conditions' : ['ENABLE_NSRESOURCED'], }, + { + 'file' : 'systemd-validatefs@.service.in', + 'conditions' : ['HAVE_BLKID'], + }, { 'file' : 'systemd-vconsole-setup.service.in', 'conditions' : ['ENABLE_VCONSOLE'], diff --git a/units/systemd-validatefs@.service.in b/units/systemd-validatefs@.service.in new file mode 100644 index 0000000000..f94482f982 --- /dev/null +++ b/units/systemd-validatefs@.service.in @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Validate File System Mount Constraints of %f +Documentation=man:systemd-validatefs@.service(8) +DefaultDependencies=no +BindsTo=%i.mount +Conflicts=shutdown.target +After=%i.mount +Before=shutdown.target systemd-pcrfs@%i.service systemd-quotacheck@%i.service systemd-growfs@%i.service + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-validatefs --root=auto %f +FailureAction=reboot-immediate From f872373a26dcaa0818b49220abfe35611d12fa82 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Mar 2025 09:49:52 +0100 Subject: [PATCH 4/8] generators: hook in validatefs on gpt-auto and fstab generator mounts Let's turn on validatefs automatically for all auto-discovered partitions. Let's add an x-systemd.validatefs option to optionally turn this on for fstab listed file systems. --- man/systemd-gpt-auto-generator.xml | 5 ++ man/systemd-validatefs@.service.xml | 8 +++ man/systemd.mount.xml | 16 ++++++ src/basic/special.h | 1 + src/fstab-generator/fstab-generator.c | 37 ++++++++---- src/gpt-auto-generator/gpt-auto-generator.c | 62 +++++++++++---------- src/shared/generator.c | 37 ++++++++++++ src/shared/generator.h | 4 ++ 8 files changed, 130 insertions(+), 40 deletions(-) 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 index 6535ed463b..b7c8adf43f 100644 --- a/man/systemd-validatefs@.service.xml +++ b/man/systemd-validatefs@.service.xml @@ -58,6 +58,14 @@ 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. 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/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/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/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, From 85afe4760baf7e5c77fce414327933f6e6c2a2af Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 12 Mar 2025 09:50:20 +0100 Subject: [PATCH 5/8] repart: automatically generate validatefs xattrs Let's automatically generate validatefs xattrs by default, that encode the intended use of partitions. This defaults to on, since the structure of repart definition files tells us enough on use for this to be safe. There's an option however, to turn this off. --- man/repart.d.xml | 24 +++++++++ man/systemd-validatefs@.service.xml | 6 +++ src/repart/repart.c | 77 ++++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 2 deletions(-) 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/systemd-validatefs@.service.xml b/man/systemd-validatefs@.service.xml index b7c8adf43f..397bb0d669 100644 --- a/man/systemd-validatefs@.service.xml +++ b/man/systemd-validatefs@.service.xml @@ -66,6 +66,11 @@ 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. @@ -99,6 +104,7 @@ systemd1 systemd-gpt-auto-generator8 systemd-fstab-generator8 + systemd-repart8 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))) { From 84b41ef08213f7a210932bada5c80c1779ffc0c2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 19 Mar 2025 09:19:24 +0100 Subject: [PATCH 6/8] ci: add -no-xattrs to unsquashfs cmdline If the target dir is tmpfs and we run on old kernels we cannot extract xattrs and the extracting will fail if there are any. hence add -no-xattrs to the two remaining unsquashfs invocations that don't have it. (Also all other invocations across our test tree spell "-dest" instead of "-d", hence do so here too.) --- test/units/TEST-29-PORTABLE.directory.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 6e003d18bbf5e9f55c08b25d80b138b3a98d5f26 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 13 Mar 2025 16:58:59 +0100 Subject: [PATCH 7/8] ci: add test for new validatefs work --- .../TEST-81-GENERATORS.fstab-generator.sh | 4 + test/units/TEST-87-AUX-UTILS-VM.validatefs.sh | 116 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100755 test/units/TEST-87-AUX-UTILS-VM.validatefs.sh 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 < Date: Wed, 12 Mar 2025 13:34:35 +0100 Subject: [PATCH 8/8] update TODO --- TODO | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) 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