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).
This commit is contained in:
parent
9fbe26cfa8
commit
0bdd5ccc81
|
@ -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'],
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
<?xml version='1.0'?> <!--*-nxml-*-->
|
||||
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
|
||||
"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
|
||||
<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
|
||||
|
||||
<refentry id="systemd-validatefs_.service" conditional='HAVE_BLKID'
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude">
|
||||
|
||||
<refentryinfo>
|
||||
<title>systemd-validatefs@.service</title>
|
||||
<productname>systemd</productname>
|
||||
</refentryinfo>
|
||||
|
||||
<refmeta>
|
||||
<refentrytitle>systemd-validatefs@.service</refentrytitle>
|
||||
<manvolnum>8</manvolnum>
|
||||
</refmeta>
|
||||
|
||||
<refnamediv>
|
||||
<refname>systemd-validatefs@.service</refname>
|
||||
<refpurpose>Validate File System Mount Constraint Data</refpurpose>
|
||||
</refnamediv>
|
||||
|
||||
<refsynopsisdiv>
|
||||
<para><filename>systemd-validatefs@.service</filename></para>
|
||||
<para><filename>/usr/lib/systemd/systemd-validatefs</filename> <optional><replaceable>DEVICE</replaceable></optional></para>
|
||||
</refsynopsisdiv>
|
||||
|
||||
<refsect1>
|
||||
<title>Description</title>
|
||||
|
||||
<para><filename>systemd-validatefs@.service</filename> 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.</para>
|
||||
|
||||
<para>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
|
||||
<citerefentry><refentrytitle>systemd-gpt-auto-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry>
|
||||
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.</para>
|
||||
|
||||
<para>Specifically, the mount constraints are encoded in the following extended attributes on the root
|
||||
inode of the file systems:</para>
|
||||
|
||||
<orderedlist>
|
||||
<listitem><para><varname>user.validatefs.mount_point</varname>: 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.</para></listitem>
|
||||
|
||||
<listitem><para><varname>user.validatefs.gpt_label</varname>: 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.</para></listitem>
|
||||
|
||||
<listitem><para><varname>user.validatefs.gpt_type_uuid</varname>: 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.</para></listitem>
|
||||
</orderedlist>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>Options</title>
|
||||
|
||||
<para>The <filename>/usr/lib/systemd/system-validatefs</filename> executable may also be invoked from the
|
||||
command line, where it expects a path to a mount and the following options:</para>
|
||||
|
||||
<variablelist>
|
||||
<varlistentry>
|
||||
<term><option>--root=</option></term>
|
||||
|
||||
<listitem><para>Takes an absolute path or the special string <literal>auto</literal>. The specified
|
||||
path if removed as prefix from the specified mount point argument before the validation. If set to
|
||||
<literal>auto</literal> defaults to unspecified on the host and <filename>/sysroot/</filename> when
|
||||
run in initrd context, in order to validate the mount constraint data relative to the future file
|
||||
system root.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
|
||||
</varlistentry>
|
||||
|
||||
<xi:include href="standard-options.xml" xpointer="help" />
|
||||
<xi:include href="standard-options.xml" xpointer="version" />
|
||||
|
||||
</variablelist>
|
||||
</refsect1>
|
||||
|
||||
<refsect1>
|
||||
<title>See Also</title>
|
||||
<para><simplelist type="inline">
|
||||
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>systemd-gpt-auto-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
|
||||
<member><citerefentry><refentrytitle>systemd-fstab-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
|
||||
</simplelist></para>
|
||||
</refsect1>
|
||||
|
||||
</refentry>
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
],
|
||||
},
|
||||
]
|
|
@ -0,0 +1,331 @@
|
|||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <getopt.h>
|
||||
|
||||
#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);
|
|
@ -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'],
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue