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 <