From: Lennart Poettering Date: Thu, 23 Apr 2026 07:00:06 +0000 (+0200) Subject: storage: add systemd-storage-block@.service provider X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=4fd0df2a4b0aeb7aa317666f7e25626b0129c87a;p=thirdparty%2Fsystemd.git 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. --- diff --git a/man/rules/meson.build b/man/rules/meson.build index 4aae5615129..439c33d5abd 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 00000000000..ee6022af053 --- /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 4f1a791bc76..325b954a78b 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 00000000000..06af278a5a4 --- /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 00000000000..714e50ad9a1 --- /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 00000000000..4c21795c360 --- /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 00000000000..793946c03a6 --- /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 00000000000..f7a62aeec08 --- /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 622e1e69cf7..3cac3c876ae 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 00000000000..1d18b481a37 --- /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 00000000000..801551e2ff8 --- /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