storage: add systemd-storage-block@.service provider

First implementation of io.systemd.StorageProvider, exposing all block
devices known to udev (disks, partitions, dm nodes, …) as volumes of
type "blk". Names are picked from stable /dev/mapper and /dev/disk/by-*
symlinks; content-derived identifiers (by-uuid, by-label, …) are
intentionally avoided for security. Volume creation is not supported by
this backend.

Socket-activated via /run/systemd/io.systemd.StorageProvider/block.
Also adds shared storage-util.[ch] (VolumeType / CreateMode helpers)
that subsequent providers reuse.
This commit is contained in:
Lennart Poettering
2026-04-29 12:25:47 +02:00
parent 087fa20166
commit 4fd0df2a4b
11 changed files with 697 additions and 0 deletions
+4
View File
@@ -1186,6 +1186,10 @@ manpages = [
['systemd-ssh-issue', '1', [], ''],
['systemd-ssh-proxy', '1', [], ''],
['systemd-stdio-bridge', '1', [], ''],
['systemd-storage-block@.service',
'8',
['systemd-storage-block', 'systemd-storage-block.socket'],
''],
['systemd-storagetm.service', '8', ['systemd-storagetm'], 'ENABLE_STORAGETM'],
['systemd-stub',
'7',
+97
View File
@@ -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-storage-block_.service"
xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>systemd-storage-block@.service</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-storage-block@.service</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-storage-block@.service</refname>
<refname>systemd-storage-block.socket</refname>
<refname>systemd-storage-block</refname>
<refpurpose>Storage provider exposing local block devices as storage volumes</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-storage-block@.service</filename></para>
<para><filename>systemd-storage-block.socket</filename></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><filename>systemd-storage-block@.service</filename> is a system service that implements the
<constant>io.systemd.StorageProvider</constant> <ulink url="https://varlink.org/">Varlink</ulink>
interface, exposing the system's block devices (such as disks, partitions, and device-mapper
nodes) as storage volumes that may be acquired by other programs as file descriptors.</para>
<para>The service is socket-activated via <filename>systemd-storage-block.socket</filename>, which
listens on the AF_UNIX socket <filename>/run/systemd/io.systemd.StorageProvider/block</filename>. The
socket directory <filename>/run/systemd/io.systemd.StorageProvider/</filename> is the well-known location
where storage providers register, see
<citerefentry><refentrytitle>storagectl</refentrytitle><manvolnum>1</manvolnum></citerefentry> for an
enumeration tool.</para>
<para>See also
<citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
for a complementary implementation that exposes regular files and directories from a backing file
system.</para>
</refsect1>
<refsect1>
<title>Volumes</title>
<para>The volumes exposed via the provider are identified by an absolute path (which must begin with
<filename>/dev/</filename>), i.e. as a kernel block device node such as <filename>/dev/sda</filename> or
<filename>/dev/disk/by-id/…</filename>. Volume names that are not normalized or that do not begin with
<filename>/dev/</filename> are not accepted.</para>
</refsect1>
<refsect1>
<title>Options</title>
<para>The following options are understood:</para>
<variablelist>
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
</refsect1>
<refsect1>
<title>Files</title>
<variablelist>
<varlistentry>
<term><filename>/run/systemd/io.systemd.StorageProvider/block</filename></term>
<listitem><para>AF_UNIX socket the service listens on. This is the canonical location
for the <literal>block</literal> storage provider, and is enumerated by
<command>storagectl providers</command>.</para>
<xi:include href="version-info.xml" xpointer="v261"/></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>See Also</title>
<para><simplelist type="inline">
<member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>storagectl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
<member><citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
</simplelist></para>
</refsect1>
</refentry>
+1
View File
@@ -2139,6 +2139,7 @@ subdir('src/socket-activate')
subdir('src/socket-proxy')
subdir('src/ssh-generator')
subdir('src/stdio-bridge')
subdir('src/storage')
subdir('src/storagetm')
subdir('src/sulogin-shell')
subdir('src/sysctl')
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<!--
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.
-->
<policyconfig>
<vendor>The systemd Project</vendor>
<vendor_url>https://systemd.io</vendor_url>
<action id="io.systemd.storage.block.acquire">
<description gettext-domain="systemd">Allow access to block storage volumes</description>
<message gettext-domain="systemd">Authentication is required for an application to gain access to block storage volume '$(name)'.</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>
+11
View File
@@ -0,0 +1,11 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
executables += [
libexec_template + {
'name' : 'systemd-storage-block',
'sources' : files('storage-block.c', 'storage-util.c'),
},
]
install_data('io.systemd.storage.policy',
install_dir : polkitpolicydir)
+439
View File
@@ -0,0 +1,439 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <fnmatch.h>
#include "sd-device.h"
#include "sd-json.h"
#include "sd-varlink.h"
#include "blockdev-list.h"
#include "build.h"
#include "bus-polkit.h"
#include "device-private.h"
#include "device-util.h"
#include "errno-util.h"
#include "fd-util.h"
#include "format-table.h"
#include "hashmap.h"
#include "help-util.h"
#include "json-util.h"
#include "log.h"
#include "main-func.h"
#include "options.h"
#include "path-util.h"
#include "storage-util.h"
#include "strv.h"
#include "varlink-io.systemd.StorageProvider.h"
#include "varlink-util.h"
static int block_device_pick_name(
const BlockDevice *d,
const char **ret_name,
char ***ret_aliases) {
int r;
assert(d);
assert(d->node);
assert(ret_name);
assert(ret_aliases);
static const char *const prefixes[] = {
/* The list of preferred prefixes, in order of preference. Note: for security reasons we only
* use identifiers that do not depend on the *contents* of the device, i.e. we restrict
* ourselves to IDs whose fields are either chosen by whoever created the kernel device or are
* hardware properties, but not names generated from superblock metainformation or similar. */
"/dev/mapper",
"/dev/disk/by-loop-ref",
"/dev/disk/by-id",
"/dev/disk/by-path",
};
const char* found[ELEMENTSOF(prefixes)] = {};
_cleanup_strv_free_ char **aliases = NULL;
size_t best = SIZE_MAX;
STRV_FOREACH(sl, d->symlinks) {
bool matched = false;
for (size_t i = 0; i < ELEMENTSOF(prefixes); i++) {
if (!path_startswith(*sl, prefixes[i]))
continue;
if (found[i]) {
/* Two symlinks with the same prefix? Then keep the lower one. */
if (path_compare(*sl, found[i]) > 0)
continue;
r = strv_extend(&aliases, found[i]);
if (r < 0)
return r;
}
found[i] = *sl;
if (i < best)
best = i;
matched = true;
}
if (!matched) {
r = strv_extend(&aliases, *sl);
if (r < 0)
return r;
}
}
if (best == SIZE_MAX) /* No preferred prefix found, use the kernel device name */
*ret_name = d->node;
else {
/* We found a preferred prefix, add the kernel device name to the aliases then. */
r = strv_extend(&aliases, d->node);
if (r < 0)
return r;
/* If there are any less preferred prefixes also add them to the aliases array */
for (size_t i = best + 1; i < ELEMENTSOF(prefixes); i++) {
if (!found[i])
continue;
r = strv_extend(&aliases, found[i]);
if (r < 0)
return r;
}
*ret_name = found[best];
}
strv_sort(aliases);
*ret_aliases = TAKE_PTR(aliases);
return 0;
}
static bool block_device_match(const BlockDevice *d, const char *match) {
assert(d);
assert(d->node);
if (!match)
return true;
if (fnmatch(match, d->node, FNM_NOESCAPE) == 0)
return true;
STRV_FOREACH(sl, d->symlinks)
if (fnmatch(match, *sl, FNM_NOESCAPE) == 0)
return true;
return false;
}
static int vl_method_list_volumes(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
int r;
assert(link);
assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE));
struct {
const char *match_name;
} p = {};
static const sd_json_dispatch_field dispatch_table[] = {
{ "matchName", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, match_name), 0 },
{}
};
r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
if (r != 0)
return r;
BlockDevice *l = NULL;
size_t n = 0;
CLEANUP_ARRAY(l, n, block_device_array_free);
r = blockdev_list(
BLOCKDEV_LIST_SHOW_SYMLINKS|
BLOCKDEV_LIST_IGNORE_ROOT|
BLOCKDEV_LIST_IGNORE_EMPTY|
BLOCKDEV_LIST_METADATA,
&l,
&n);
if (r < 0)
return r;
r = sd_varlink_set_sentinel(link, "io.systemd.StorageProvider.NoSuchVolume");
if (r < 0)
return r;
FOREACH_ARRAY(d, l, n) {
const char *name = NULL;
_cleanup_strv_free_ char **aliases = NULL;
if (!block_device_match(d, p.match_name))
continue;
r = block_device_pick_name(d, &name, &aliases);
if (r < 0)
return r;
r = sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR_STRING("name", name),
JSON_BUILD_PAIR_STRV_NON_EMPTY("aliases", aliases),
SD_JSON_BUILD_PAIR_STRING("type", "blk"),
SD_JSON_BUILD_PAIR_CONDITION(d->read_only >= 0, "readOnly", SD_JSON_BUILD_BOOLEAN(d->read_only)),
JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("sizeBytes", d->size, UINT64_MAX));
if (r < 0)
return r;
}
return 0;
}
static int vl_method_list_templates(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
/* This storage provider does not support templates */
assert(link);
assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE));
return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchTemplate", NULL);
}
static int device_open_disk_auto_rw(sd_device *d, int *read_only) {
assert(d);
assert(read_only);
int fd = sd_device_open(d, *read_only > 0 ? O_RDONLY : O_RDWR);
if (fd < 0) {
if (!ERRNO_IS_NEG_FS_WRITE_REFUSED(fd) || *read_only >= 0)
return log_device_debug_errno(d, fd, "Failed to open device in %s mode: %m", *read_only > 0 ? "read-only" : "read-write");
/* Try again in read-only mode */
fd = sd_device_open(d, O_RDONLY);
if (fd < 0)
return log_device_debug_errno(d, fd, "Failed to open device in read-only mode, too: %m");
*read_only = true;
} else
*read_only = *read_only > 0;
return fd;
}
static int vl_method_acquire(
sd_varlink *link,
sd_json_variant *parameters,
sd_varlink_method_flags_t flags,
void *userdata) {
Hashmap **polkit_registry = ASSERT_PTR(userdata);
int r;
assert(link);
struct {
const char *name;
CreateMode create_mode;
const char *template;
int read_only;
VolumeType request_as;
uint64_t create_size;
} p = {
.create_mode = CREATE_ANY,
.read_only = -1,
.request_as = _VOLUME_TYPE_INVALID,
.create_size = UINT64_MAX, /* never actually used here, just validated; we don't allow creation of block devices here */
};
static const sd_json_dispatch_field dispatch_table[] = {
{ "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), SD_JSON_MANDATORY },
{ "createMode", SD_JSON_VARIANT_STRING, json_dispatch_create_mode, voffsetof(p, create_mode), 0 },
{ "template", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, template), 0 },
{ "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(p, read_only), 0 },
{ "requestAs", SD_JSON_VARIANT_STRING, json_dispatch_volume_type, voffsetof(p, request_as), 0 },
{ "createSizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, create_size), 0 },
VARLINK_DISPATCH_POLKIT_FIELD,
{}
};
r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
if (r != 0)
return r;
if (!storage_volume_name_is_valid(p.name))
return sd_varlink_error_invalid_parameter_name(link, "name");
if (!path_startswith(p.name, "/dev") || !path_is_normalized(p.name))
return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchVolume", NULL);
if (!IN_SET(p.create_mode, CREATE_ANY, CREATE_OPEN))
return sd_varlink_error(link, "io.systemd.StorageProvider.CreateNotSupported", NULL);
/* off_t is signed, hence refuse overly long requests */
if (p.create_size != UINT64_MAX && p.create_size > INT64_MAX)
return sd_varlink_error_invalid_parameter_name(link, "createSizeBytes");
if (!isempty(p.template)) {
if (!storage_template_name_is_valid(p.template))
return sd_varlink_error_invalid_parameter_name(link, "template");
return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchTemplate", NULL);
}
if (p.request_as >= 0 && p.request_as != VOLUME_BLK)
return sd_varlink_error(link, "io.systemd.StorageProvider.TypeNotSupported", NULL);
const char *details[] = {
"name", p.name,
NULL
};
r = varlink_verify_polkit_async(
link,
/* bus= */ NULL,
"io.systemd.storage.block.acquire",
details,
polkit_registry);
if (r <= 0)
return r;
_cleanup_(sd_device_unrefp) sd_device *d = NULL;
r = sd_device_new_from_devname(&d, p.name);
if (ERRNO_IS_NEG_DEVICE_ABSENT(r))
return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchVolume", NULL);
if (r < 0)
return r;
if (!device_in_subsystem(d, "block"))
return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchVolume", NULL);
/* The error returns are sometimes a bit inconclusive (i.e. read-only media might appear as
* inaccessible due to a permission issue), hence let's do an explicit check first, to give good
* answers */
if (p.read_only <= 0) {
r = device_get_sysattr_bool(d, "ro");
if (r < 0)
log_device_debug_errno(d, r, "Failed to acquire read-only flag of device '%s', ignoring: %m", p.name);
else if (r > 0) {
if (p.read_only == 0)
return sd_varlink_error(link, "io.systemd.StorageProvider.ReadOnlyVolume", NULL);
p.read_only = true;
}
}
_cleanup_close_ int fd = device_open_disk_auto_rw(d, &p.read_only);
if (ERRNO_IS_NEG_FS_WRITE_REFUSED(fd))
return sd_varlink_error(link, "io.systemd.StorageProvider.ReadOnlyVolume", NULL);
if (fd < 0)
return fd;
assert(p.read_only >= 0); /* flag is now definitely initialized to either true or false, not negative anymore */
int idx = sd_varlink_push_fd(link, fd);
if (idx < 0)
return idx;
TAKE_FD(fd);
return sd_varlink_replybo(
link,
SD_JSON_BUILD_PAIR_INTEGER("fileDescriptorIndex", idx),
SD_JSON_BUILD_PAIR_STRING("type", "blk"),
SD_JSON_BUILD_PAIR_BOOLEAN("readOnly", p.read_only));
}
static int vl_server(void) {
int r;
_cleanup_(hashmap_freep) Hashmap *polkit_registry = NULL;
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL;
r = varlink_server_new(
&varlink_server,
SD_VARLINK_SERVER_HANDLE_SIGINT|
SD_VARLINK_SERVER_HANDLE_SIGTERM|
SD_VARLINK_SERVER_ALLOW_FD_PASSING_OUTPUT|
SD_VARLINK_SERVER_INHERIT_USERDATA,
&polkit_registry);
if (r < 0)
return log_error_errno(r, "Failed to allocate Varlink server: %m");
r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_StorageProvider);
if (r < 0)
return log_error_errno(r, "Failed to add Varlink interface: %m");
r = sd_varlink_server_bind_method_many(
varlink_server,
"io.systemd.StorageProvider.Acquire", vl_method_acquire,
"io.systemd.StorageProvider.ListVolumes", vl_method_list_volumes,
"io.systemd.StorageProvider.ListTemplates", vl_method_list_templates);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink methods: %m");
r = sd_varlink_server_loop_auto(varlink_server);
if (r < 0)
return log_error_errno(r, "Failed to run Varlink event loop: %m");
return 0;
}
static int help(void) {
int r;
help_cmdline("[OPTIONS...]");
help_abstract("Simple block device backed storage provider");
_cleanup_(table_unrefp) Table *options = NULL;
r = option_parser_get_help_table(&options);
if (r < 0)
return r;
help_section("Options:");
r = table_print_or_warn(options);
if (r < 0)
return r;
help_man_page_reference("systemd-storage-block", "8");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
assert(argc >= 0);
assert(argv);
OptionParser opts = { argc, argv };
FOREACH_OPTION(c, &opts, /* on_error= */ return c)
switch (c) {
OPTION_COMMON_HELP:
return help();
OPTION_COMMON_VERSION:
return version();
}
if (option_parser_get_n_args(&opts) > 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
return 1;
}
static int run(int argc, char* argv[]) {
int r;
log_setup();
r = parse_argv(argc, argv);
if (r <= 0)
return r;
return vl_server();
}
DEFINE_MAIN_FUNCTION(run);
+23
View File
@@ -0,0 +1,23 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "json-util.h"
#include "string-table.h"
#include "storage-util.h"
static const char *volume_type_table[_VOLUME_TYPE_MAX] = {
[VOLUME_BLK] = "blk",
[VOLUME_REG] = "reg",
[VOLUME_DIR] = "dir",
};
static const char *create_mode_table[_CREATE_MODE_MAX] = {
[CREATE_ANY] = "any",
[CREATE_NEW] = "new",
[CREATE_OPEN] = "open",
};
DEFINE_STRING_TABLE_LOOKUP(volume_type, VolumeType);
DEFINE_STRING_TABLE_LOOKUP(create_mode, CreateMode);
JSON_DISPATCH_ENUM_DEFINE(json_dispatch_volume_type, VolumeType, volume_type_from_string);
JSON_DISPATCH_ENUM_DEFINE(json_dispatch_create_mode, CreateMode, create_mode_from_string);
+43
View File
@@ -0,0 +1,43 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "sd-json.h"
#include "string-table-fundamental.h"
#include "string-util.h"
/* This closely follows the kernel's inode type naming, i.e. is supposed to be a subset of what
* inode_type_from_string() parses. */
typedef enum VolumeType {
VOLUME_BLK,
VOLUME_REG,
VOLUME_DIR,
_VOLUME_TYPE_MAX,
_VOLUME_TYPE_INVALID = -EINVAL,
} VolumeType;
typedef enum CreateMode {
CREATE_ANY,
CREATE_NEW,
CREATE_OPEN,
_CREATE_MODE_MAX,
_CREATE_MODE_INVALID = -EINVAL,
} CreateMode;
DECLARE_STRING_TABLE_LOOKUP(volume_type, VolumeType);
DECLARE_STRING_TABLE_LOOKUP(create_mode, CreateMode);
int json_dispatch_volume_type(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_create_mode(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
static inline bool storage_volume_name_is_valid(const char *n) {
return string_is_safe(n, /* flags= */ 0);
}
static inline bool storage_template_name_is_valid(const char *n) {
return string_is_safe(n, /* flags= */ 0);
}
static inline bool storage_provider_name_is_valid(const char *n) {
return string_is_safe(n, STRING_FILENAME);
}
+7
View File
@@ -804,6 +804,13 @@ units = [
'conditions' : ['ENABLE_SYSUSERS'],
'symlinks' : ['sysinit.target.wants/'],
},
{
'file' : 'systemd-storage-block.socket',
'symlinks' : ['sockets.target.wants/']
},
{
'file' : 'systemd-storage-block@.service.in',
},
{
'file' : 'systemd-storagetm.service.in',
'conditions' : ['ENABLE_STORAGETM'],
+24
View File
@@ -0,0 +1,24 @@
# 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=Simple Block Device Backed Storage Provider
Documentation=man:systemd-storage-block@..service(8)
DefaultDependencies=no
Before=sockets.target
[Socket]
ListenStream=/run/systemd/io.systemd.StorageProvider/block
FileDescriptorName=varlink
SocketMode=0666
Accept=yes
MaxConnectionsPerSource=16
[Install]
WantedBy=sockets.target
+18
View File
@@ -0,0 +1,18 @@
# 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=Simple Block Device Backed Storage Provider
Documentation=man:systemd-storage-block@.service(8)
DefaultDependencies=no
Conflicts=shutdown.target initrd-switch-root.target
Before=shutdown.target initrd-switch-root.target
[Service]
ExecStart=-{{LIBEXECDIR}}/systemd-storage-block