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