diff --git a/man/rules/meson.build b/man/rules/meson.build
index 4aae561512..439c33d5ab 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -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',
diff --git a/man/systemd-storage-block@.service.xml b/man/systemd-storage-block@.service.xml
new file mode 100644
index 0000000000..ee6022af05
--- /dev/null
+++ b/man/systemd-storage-block@.service.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+ systemd-storage-block@.service
+ systemd
+
+
+
+ systemd-storage-block@.service
+ 8
+
+
+
+ systemd-storage-block@.service
+ systemd-storage-block.socket
+ systemd-storage-block
+ Storage provider exposing local block devices as storage volumes
+
+
+
+ systemd-storage-block@.service
+ systemd-storage-block.socket
+
+
+
+ Description
+
+ systemd-storage-block@.service is a system service that implements the
+ io.systemd.StorageProvider Varlink
+ 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.
+
+ The service is socket-activated via systemd-storage-block.socket, which
+ listens on the AF_UNIX socket /run/systemd/io.systemd.StorageProvider/block. The
+ socket directory /run/systemd/io.systemd.StorageProvider/ is the well-known location
+ where storage providers register, see
+ storagectl1 for an
+ enumeration tool.
+
+ See also
+ systemd-storage-fs@.service8
+ for a complementary implementation that exposes regular files and directories from a backing file
+ system.
+
+
+
+ Volumes
+
+ The volumes exposed via the provider are identified by an absolute path (which must begin with
+ /dev/), i.e. as a kernel block device node such as /dev/sda or
+ /dev/disk/by-id/…. Volume names that are not normalized or that do not begin with
+ /dev/ are not accepted.
+
+
+
+ Options
+
+ The following options are understood:
+
+
+
+
+
+
+
+
+ Files
+
+
+
+ /run/systemd/io.systemd.StorageProvider/block
+
+ AF_UNIX socket the service listens on. This is the canonical location
+ for the block storage provider, and is enumerated by
+ storagectl providers.
+
+
+
+
+
+
+
+ See Also
+
+ systemd1
+ storagectl1
+ systemd-storage-fs@.service8
+
+
+
+
diff --git a/meson.build b/meson.build
index 4f1a791bc7..325b954a78 100644
--- a/meson.build
+++ b/meson.build
@@ -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')
diff --git a/src/storage/io.systemd.storage.policy b/src/storage/io.systemd.storage.policy
new file mode 100644
index 0000000000..06af278a5a
--- /dev/null
+++ b/src/storage/io.systemd.storage.policy
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ The systemd Project
+ https://systemd.io
+
+
+ Allow access to block storage volumes
+ Authentication is required for an application to gain access to block storage volume '$(name)'.
+
+ auth_admin
+ auth_admin
+ auth_admin_keep
+
+
+
diff --git a/src/storage/meson.build b/src/storage/meson.build
new file mode 100644
index 0000000000..714e50ad9a
--- /dev/null
+++ b/src/storage/meson.build
@@ -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)
diff --git a/src/storage/storage-block.c b/src/storage/storage-block.c
new file mode 100644
index 0000000000..4c21795c36
--- /dev/null
+++ b/src/storage/storage-block.c
@@ -0,0 +1,439 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#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);
diff --git a/src/storage/storage-util.c b/src/storage/storage-util.c
new file mode 100644
index 0000000000..793946c03a
--- /dev/null
+++ b/src/storage/storage-util.c
@@ -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);
diff --git a/src/storage/storage-util.h b/src/storage/storage-util.h
new file mode 100644
index 0000000000..f7a62aeec0
--- /dev/null
+++ b/src/storage/storage-util.h
@@ -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);
+}
diff --git a/units/meson.build b/units/meson.build
index 622e1e69cf..3cac3c876a 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -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'],
diff --git a/units/systemd-storage-block.socket b/units/systemd-storage-block.socket
new file mode 100644
index 0000000000..1d18b481a3
--- /dev/null
+++ b/units/systemd-storage-block.socket
@@ -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
diff --git a/units/systemd-storage-block@.service.in b/units/systemd-storage-block@.service.in
new file mode 100644
index 0000000000..801551e2ff
--- /dev/null
+++ b/units/systemd-storage-block@.service.in
@@ -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