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