]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
storage: add 'storagectl' command-line tool
authorLennart Poettering <lennart@amutable.com>
Wed, 22 Apr 2026 21:44:04 +0000 (23:44 +0200)
committerLennart Poettering <lennart@amutable.com>
Wed, 29 Apr 2026 10:25:47 +0000 (12:25 +0200)
CLI for inspecting and using storage providers. Scans
/run/systemd/io.systemd.StorageProvider/ (or the user-mode equivalent)
for AF_UNIX sockets and talks to each one over Varlink. Verbs:
"volumes" lists volumes across all providers, "templates" lists
supported creation templates, "providers" lists the endpoints
themselves.

Also installed as a mount.storage helper, so
'mount -t storage PROVIDER:VOLUME /mnt' (or 'mount -t storage.<fstype>'
to put a fresh filesystem on a block volume) acquires the volume and
mounts it. Ships with bash/zsh completions and a man page.

man/rules/meson.build
man/storagectl.xml [new file with mode: 0644]
shell-completion/bash/meson.build
shell-completion/bash/storagectl [new file with mode: 0644]
shell-completion/zsh/_storagectl [new file with mode: 0644]
shell-completion/zsh/meson.build
src/storage/meson.build
src/storage/storagectl.c [new file with mode: 0644]

index 7f4fa07f7ba776e97429d13679c00a773dafd080..719838064c02f8dc5a5c8675dc489c68f8e94a0e 100644 (file)
@@ -972,6 +972,7 @@ manpages = [
  ['sd_watchdog_enabled', '3', [], ''],
  ['shutdown', '8', [], ''],
  ['smbios-type-11', '7', [], ''],
+ ['storagectl', '1', ['mount.storage'], ''],
  ['sysctl.d', '5', [], ''],
  ['sysext.conf',
   '5',
diff --git a/man/storagectl.xml b/man/storagectl.xml
new file mode 100644 (file)
index 0000000..5fddf3c
--- /dev/null
@@ -0,0 +1,281 @@
+<?xml version='1.0'?>
+<!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="storagectl"
+    xmlns:xi="http://www.w3.org/2001/XInclude">
+
+  <refentryinfo>
+    <title>storagectl</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>storagectl</refentrytitle>
+    <manvolnum>1</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>storagectl</refname>
+    <refname>mount.storage</refname>
+    <refpurpose>Enumerate and mount storage volumes provided by storage providers</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>storagectl</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <arg choice="req">COMMAND</arg>
+      <arg choice="opt" rep="repeat">NAME</arg>
+    </cmdsynopsis>
+
+    <cmdsynopsis>
+      <command>mount</command>
+      <arg choice="plain">-t</arg>
+      <arg choice="plain">storage</arg>
+      <arg choice="plain"><replaceable>PROVIDER</replaceable>:<replaceable>VOLUME</replaceable></arg>
+      <arg choice="plain"><replaceable>DIRECTORY</replaceable></arg>
+    </cmdsynopsis>
+
+    <cmdsynopsis>
+      <command>mount</command>
+      <arg choice="plain">-t</arg>
+      <arg choice="plain">storage.<replaceable>FSTYPE</replaceable></arg>
+      <arg choice="plain"><replaceable>PROVIDER</replaceable>:<replaceable>VOLUME</replaceable></arg>
+      <arg choice="plain"><replaceable>DIRECTORY</replaceable></arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+
+    <para><command>storagectl</command> may be used to inspect storage providers and the storage
+    volumes they expose. A storage provider is a service implementing the
+    <constant>io.systemd.StorageProvider</constant> <ulink url="https://varlink.org/">Varlink</ulink>
+    interface, registered as an AF_UNIX socket below the well-known socket directory
+    <filename>/run/systemd/io.systemd.StorageProvider/</filename> (in system mode) or
+    <varname>$XDG_RUNTIME_DIR</varname><filename>/systemd/io.systemd.StorageProvider/</filename> (in user mode). The two
+    storage providers shipped with systemd are
+    <citerefentry><refentrytitle>systemd-storage-block@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+    which exposes the system's block devices, and
+    <citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+    which exposes regular files and directories from a backing file system.</para>
+
+    <para>The tool also provides a <citerefentry
+    project='man-pages'><refentrytitle>mount</refentrytitle><manvolnum>8</manvolnum></citerefentry> helper
+    for the file system type <literal>storage</literal>, which permits mounting storage volumes to arbitrary
+    places. See "Use as a mount helper" below for details.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>Commands</title>
+
+    <para>The following commands are understood:</para>
+
+    <variablelist>
+
+      <varlistentry>
+        <term><command>volumes</command> <optional><replaceable>GLOB</replaceable></optional></term>
+
+        <listitem><para>List storage volumes provided by all storage providers running on the
+        system (or, with <option>--user</option>, in the user runtime). The optional
+        <replaceable>GLOB</replaceable> argument is a shell-style pattern (see
+        <citerefentry project='man-pages'><refentrytitle>fnmatch</refentrytitle><manvolnum>3</manvolnum></citerefentry>)
+        that filters the result by volume name. The output is a table containing the providing
+        service, the volume name, its type (<literal>blk</literal>, <literal>reg</literal> or
+        <literal>dir</literal>), whether it is read-only, and — if known — its size and the number
+        of bytes used.</para>
+
+        <para>This is the default command if none is specified.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><command>templates</command> <optional><replaceable>GLOB</replaceable></optional></term>
+
+        <listitem><para>List volume templates supported by the running storage providers. Templates
+        encapsulate a configuration to use when creating volumes on-the-fly, when they are acquired. Template
+        support is an optional feature for providers, and only applies to providers that allow creation
+        of volumes on-the-fly. See the respective provider documentation for details, for example
+        <citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>. The
+        optional <replaceable>GLOB</replaceable> argument filters by template name. Storage providers that do
+        not implement template-based volume creation (such as the block-device provider) do not contribute to
+        this output.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><command>providers</command></term>
+
+        <listitem><para>List the storage providers known to the system. This is determined by scanning the
+        well-known socket directory for <constant>AF_UNIX</constant> sockets that look like
+        <constant>io.systemd.StorageProvider</constant> endpoints. For each provider it is also reported
+        whether the socket can currently be connected to.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Options</title>
+
+    <para>The following options are understood:</para>
+
+    <variablelist>
+      <varlistentry>
+        <term><option>--system</option></term>
+
+        <listitem><para>Operate on system-wide storage providers. Sockets are looked for in
+        <filename>/run/systemd/io.systemd.StorageProvider/</filename>. This is the default.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--user</option></term>
+
+        <listitem><para>Operate on per-user storage providers. Sockets are looked for in
+        <filename>$XDG_RUNTIME_DIR/systemd/io.systemd.StorageProvider/</filename>.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <xi:include href="standard-options.xml" xpointer="json" />
+      <xi:include href="standard-options.xml" xpointer="no-pager" />
+      <xi:include href="standard-options.xml" xpointer="no-legend" />
+      <xi:include href="standard-options.xml" xpointer="no-ask-password" />
+      <xi:include href="standard-options.xml" xpointer="help" />
+      <xi:include href="standard-options.xml" xpointer="version" />
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Use as a mount helper</title>
+
+    <para>The tool provides the <command>/sbin/mount.storage</command> alias, implementing the
+    <citerefentry project='man-pages'><refentrytitle>mount</refentrytitle><manvolnum>8</manvolnum></citerefentry>
+    "external helper" interface, allowing storage volumes to be mounted with the regular
+    <command>mount</command> command. The volume to mount is encoded as the source of the mount,
+    in the form
+    <literal><replaceable>PROVIDER</replaceable>:<replaceable>VOLUME</replaceable></literal>, where
+    <replaceable>PROVIDER</replaceable> is the name of a storage provider (as listed by
+    <command>storagectl providers</command>) and <replaceable>VOLUME</replaceable> is the volume
+    name. Two file system type spellings are recognized:</para>
+
+    <variablelist>
+      <varlistentry>
+        <term><literal>storage</literal></term>
+
+        <listitem><para>Acquires a directory volume and bind-mounts its directory tree onto the
+        target.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><literal>storage.<replaceable>FSTYPE</replaceable></literal></term>
+
+        <listitem><para>Acquires a regular file or block device volume and mounts it as a file system of type
+        <replaceable>FSTYPE</replaceable> (for example <literal>storage.ext4</literal>,
+        <literal>storage.btrfs</literal>, …).</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+    </variablelist>
+
+    <para>The standard <option>-o</option> mount options are forwarded to
+    <command>mount</command>. In addition, the following <literal>storage.</literal>-prefixed
+    options are interpreted by <command>mount.storage</command> itself and stripped from the
+    forwarded list:</para>
+
+    <variablelist>
+      <varlistentry>
+        <term><option>storage.create=</option><replaceable>MODE</replaceable></term>
+
+        <listitem><para>Takes one of <literal>any</literal> (open if it exists, otherwise create — the
+        default), <literal>open</literal> (fail if the volume does not yet exist) or <literal>new</literal>
+        (fail if the volume already exists).</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>storage.template=</option><replaceable>NAME</replaceable></term>
+
+        <listitem><para>The template to use when creating a new volume, if it is missing and the provider
+        supports on-the-fly creation of volumes.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>storage.create-size=</option><replaceable>BYTES</replaceable></term>
+
+        <listitem><para>When creating a new volume on-the-fly, the size in bytes to allocate. Accepts the
+        usual <literal>K</literal>/<literal>M</literal>/<literal>G</literal>/<literal>T</literal> suffixes
+        (base 1024). Required when creating a regular file volume.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+    </variablelist>
+
+  </refsect1>
+
+  <refsect1>
+    <title>Examples</title>
+
+    <example>
+      <title>Enumerate available storage providers, volumes and templates</title>
+
+      <programlisting>$ storagectl providers
+$ storagectl volumes
+$ storagectl volumes '*foo*'
+$ storagectl templates</programlisting>
+    </example>
+
+    <example>
+      <title>Mount a directory volume from the file system provider</title>
+
+      <programlisting># mount -t storage fs:myvol /mnt/myvol</programlisting>
+
+      <para>If the volume <literal>myvol</literal> does not yet exist, it will be created using
+      the default <literal>subvolume</literal> template.</para>
+    </example>
+
+    <example>
+      <title>Create and mount an ext4 file system from a regular file.</title>
+
+      <programlisting># mount -t storage.ext4 fs:scratch /mnt/scratch -o loop</programlisting>
+    </example>
+
+    <example>
+      <title>Mount a block device volume read-only</title>
+
+      <programlisting># mount -t storage.ext4 -o ro block:/dev/disk/by-id/usb-foo /mnt/foo</programlisting>
+    </example>
+  </refsect1>
+
+  <refsect1>
+    <title>Exit status</title>
+
+    <para>On success, 0 is returned, a non-zero failure code otherwise.</para>
+  </refsect1>
+
+  <xi:include href="common-variables.xml" />
+
+  <refsect1>
+    <title>See Also</title>
+    <para><simplelist type="inline">
+      <member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd-storage-block@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd-storage-fs@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>varlinkctl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+      <member><citerefentry project='man-pages'><refentrytitle>mount</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+    </simplelist></para>
+  </refsect1>
+
+</refentry>
index 154910979ea5693341661d48f5e5750d1c8ae5be..b0e56608e8f37def5c8eff478ce9ba0897d29993 100644 (file)
@@ -36,6 +36,7 @@ foreach item : [
         ['portablectl',         'ENABLE_PORTABLED'],
         ['resolvectl',          'ENABLE_RESOLVE'],
         ['run0',                ''],
+        ['storagectl',          ''],
         ['systemd-analyze',     ''],
         ['systemd-cat',         ''],
         ['systemd-cgls',        ''],
diff --git a/shell-completion/bash/storagectl b/shell-completion/bash/storagectl
new file mode 100644 (file)
index 0000000..5aefc30
--- /dev/null
@@ -0,0 +1,74 @@
+# shellcheck shell=bash
+# storagectl(1) completion                            -*- shell-script -*-
+# 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.
+#
+# systemd is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with systemd; If not, see <https://www.gnu.org/licenses/>.
+
+__contains_word () {
+    local w word=$1; shift
+    for w in "$@"; do
+        [[ $w = "$word" ]] && return
+    done
+}
+
+_storagectl() {
+    local i verb comps
+    local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]}
+
+    local -A OPTS=(
+        [STANDALONE]='-h --help --version --no-pager --no-legend --no-ask-password
+                      --system --user'
+        [ARG]='--json'
+    )
+
+    if __contains_word "$prev" ${OPTS[ARG]}; then
+        case $prev in
+            --json)
+                comps=$( storagectl --json=help 2>/dev/null )
+                ;;
+        esac
+        COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
+        return 0
+    fi
+
+    if [[ "$cur" = -* ]]; then
+        COMPREPLY=( $(compgen -W '${OPTS[*]}' -- "$cur") )
+        return 0
+    fi
+
+    local -A VERBS=(
+        [STANDALONE]='volumes templates providers help'
+    )
+
+    for ((i=0; i < COMP_CWORD; i++)); do
+        if __contains_word "${COMP_WORDS[i]}" ${VERBS[*]} &&
+                ! __contains_word "${COMP_WORDS[i-1]}" ${OPTS[ARG]}; then
+            verb=${COMP_WORDS[i]}
+            break
+        fi
+    done
+
+    if [[ -z ${verb-} ]]; then
+        comps=${VERBS[*]}
+    elif __contains_word "$verb" ${VERBS[STANDALONE]}; then
+        comps=''
+    fi
+
+    COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
+    return 0
+}
+
+complete -F _storagectl storagectl
diff --git a/shell-completion/zsh/_storagectl b/shell-completion/zsh/_storagectl
new file mode 100644 (file)
index 0000000..b2fdf59
--- /dev/null
@@ -0,0 +1,35 @@
+#compdef storagectl
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+(( $+functions[_storagectl_commands] )) || _storagectl_commands()
+{
+    local -a _storagectl_cmds
+    _storagectl_cmds=(
+        "volumes:List storage volumes"
+        "templates:List storage volume templates"
+        "providers:List storage providers"
+        "help:Prints a short help text and exits"
+    )
+    if (( CURRENT == 1 )); then
+        _describe -t commands 'storagectl command' _storagectl_cmds
+    else
+        local curcontext="$curcontext"
+        cmd="${${_storagectl_cmds[(r)$words[1]:*]%%:*}}"
+        if (( $+functions[_storagectl_$cmd] )); then
+            _storagectl_$cmd
+        else
+            _message "no more options"
+        fi
+    fi
+}
+
+_arguments \
+    '(- *)'{-h,--help}'[Prints a short help text and exits.]' \
+    '(- *)--version[Prints a short version string and exits.]' \
+    '--no-pager[Do not pipe output into a pager]' \
+    '--no-legend[Do not show the headers and footers]' \
+    '--no-ask-password[Do not query the user for authentication]' \
+    '--json=[Show output as JSON]:mode:(pretty short off help)' \
+    '--system[Operate in system mode]' \
+    '--user[Operate in user mode]' \
+    '*::storagectl command:_storagectl_commands'
index b1bff151e41a3425075385a8921e1c61628474a3..6cc8a2d57f83e3b1a830f3e3bef729112ef739ab 100644 (file)
@@ -33,6 +33,7 @@ foreach item : [
         ['_sd_machines',              'ENABLE_MACHINED'],
         ['_sd_outputmodes',           ''],
         ['_sd_unit_files',            ''],
+        ['_storagectl',               ''],
         ['_systemd',                  ''],
         ['_systemd-analyze',          ''],
         ['_systemd-delta',            ''],
index 05c5e24ece4ace1b6ace89bc7a572bd3fe6bd8dd..21456141dec8c6052178d1d0876e0222a7fa92c8 100644 (file)
@@ -11,7 +11,17 @@ executables += [
                 'sources' : files('storage-fs.c'),
                 'objects' : ['systemd-storage-block'],
         },
+        executable_template + {
+                'name' : 'storagectl',
+                'public' : true,
+                'sources' : files('storagectl.c'),
+                'objects' : ['systemd-storage-block'],
+        },
 ]
 
+install_symlink('mount.storage',
+                pointing_to : sbin_to_bin + 'storagectl',
+                install_dir : sbindir)
+
 install_data('io.systemd.storage.policy',
              install_dir : polkitpolicydir)
diff --git a/src/storage/storagectl.c b/src/storage/storagectl.c
new file mode 100644 (file)
index 0000000..a21072e
--- /dev/null
@@ -0,0 +1,812 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-varlink.h"
+
+#include <getopt.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "ansi-color.h"
+#include "argv-util.h"
+#include "build.h"
+#include "bus-util.h"
+#include "errno-list.h"
+#include "escape.h"
+#include "extract-word.h"
+#include "fd-util.h"
+#include "format-table.h"
+#include "format-util.h"
+#include "help-util.h"
+#include "json-util.h"
+#include "main-func.h"
+#include "mount-util.h"
+#include "namespace-util.h"
+#include "options.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "path-lookup.h"
+#include "path-util.h"
+#include "polkit-agent.h"
+#include "recurse-dir.h"
+#include "runtime-scope.h"
+#include "socket-util.h"
+#include "stat-util.h"
+#include "stdio-util.h"
+#include "storage-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "user-util.h"
+#include "varlink-util.h"
+#include "verbs.h"
+
+static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
+static PagerFlags arg_pager_flags = 0;
+static bool arg_legend = true;
+static bool arg_ask_password = true;
+static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
+
+static int help(void) {
+        int r;
+
+        help_cmdline("[OPTIONS...] COMMAND");
+        help_abstract("Enumerate storage volumes and providers.");
+
+        _cleanup_(table_unrefp) Table *verbs = NULL;
+        r = verbs_get_help_table(&verbs);
+        if (r < 0)
+                return r;
+
+        _cleanup_(table_unrefp) Table *options = NULL;
+        r = option_parser_get_help_table(&options);
+        if (r < 0)
+                return r;
+
+        (void) table_sync_column_widths(0, verbs, options);
+
+        help_section("Commands:");
+
+        r = table_print_or_warn(verbs);
+        if (r < 0)
+                return r;
+
+        help_section("Options:");
+
+        r = table_print_or_warn(options);
+        if (r < 0)
+                return r;
+
+        help_man_page_reference("storagectl", "1");
+        return 0;
+}
+
+VERB_COMMON_HELP_HIDDEN(help);
+
+static const char *ro_color(int ro) {
+        if (ro > 0)
+                return ansi_highlight_red();
+        if (ro == 0)
+                return ansi_highlight_green();
+
+        return NULL;
+}
+
+static int on_list_reply(
+                sd_varlink *link,
+                sd_json_variant *parameters,
+                const char *error_id,
+                sd_varlink_reply_flags_t flags,
+                void* userdata) {
+
+        Table *t = ASSERT_PTR(userdata);
+        int r;
+
+        assert(link);
+
+        const char *d = ASSERT_PTR(sd_varlink_get_description(link));
+
+        if (error_id) {
+                log_debug("%s: Received error '%s', ignoring.", d, error_id);
+                return 0;
+        }
+
+        _cleanup_free_ char *provider = NULL;
+        r = path_extract_filename(d, &provider);
+        if (r < 0)
+                return log_error_errno(r, "Failed to extract provider name from socket path: %m");
+
+        struct {
+                const char *name;
+                const char *type;
+                int read_only;
+                uint64_t size_bytes;
+                uint64_t used_bytes;
+        } p = {
+                .read_only = -1,
+                .size_bytes = UINT64_MAX,
+                .used_bytes = UINT64_MAX,
+        };
+
+        static const sd_json_dispatch_field dispatch_table[] = {
+                { "name",      SD_JSON_VARIANT_STRING,        sd_json_dispatch_const_string, voffsetof(p, name),       0 },
+                { "type",      SD_JSON_VARIANT_STRING,        sd_json_dispatch_const_string, voffsetof(p, type),       0 },
+                { "readOnly",  SD_JSON_VARIANT_BOOLEAN,       sd_json_dispatch_tristate,     voffsetof(p, read_only),  0 },
+                { "sizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64,       voffsetof(p, size_bytes), 0 },
+                { "usedBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64,       voffsetof(p, used_bytes), 0 },
+                {}
+        };
+
+        r = sd_json_dispatch(parameters, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
+        if (r < 0)
+                return log_error_errno(r, "Failed to decode List() reply: %m");
+
+        r = table_add_many(
+                        t,
+                        TABLE_STRING, provider,
+                        TABLE_STRING, p.name,
+                        TABLE_STRING, p.type,
+                        TABLE_TRISTATE, p.read_only,
+                        TABLE_SET_COLOR, ro_color(p.read_only));
+        if (r < 0)
+                return table_log_add_error(r);
+
+        if (p.size_bytes == UINT64_MAX)
+                r = table_add_many(t, TABLE_EMPTY, TABLE_SET_ALIGN_PERCENT, 100);
+        else
+                r = table_add_many(t, TABLE_SIZE, p.size_bytes, TABLE_SET_ALIGN_PERCENT, 100);
+        if (r < 0)
+                return table_log_add_error(r);
+
+        if (p.used_bytes == UINT64_MAX)
+                r = table_add_many(t, TABLE_EMPTY, TABLE_SET_ALIGN_PERCENT, 100);
+        else
+                r = table_add_many(t, TABLE_SIZE, p.used_bytes, TABLE_SET_ALIGN_PERCENT, 100);
+        if (r < 0)
+                return table_log_add_error(r);
+
+        return 0;
+}
+
+VERB(verb_list_volumes, "volumes", "GLOB", /* min_args= */ VERB_ANY, /* max_args= */ 2, VERB_DEFAULT, "List storage volumes");
+static int verb_list_volumes(int argc, char *argv[], uintptr_t data, void *userdata) {
+        int r;
+
+        assert(argc <= 2);
+
+        _cleanup_free_ char *socket_path = NULL;
+        r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine socket directory: %m");
+
+        _cleanup_(table_unrefp) Table *t = table_new("provider", "name", "type", "ro", "size", "used");
+        if (!t)
+                return log_oom();
+
+        (void) table_set_sort(t, (size_t) 0, (size_t) 1);
+        table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+        if (argc >= 2) {
+                r = sd_json_buildo(
+                                &v,
+                                SD_JSON_BUILD_PAIR_STRING("matchName", argv[1]));
+                if (r < 0)
+                        return log_oom();
+        }
+
+        ssize_t n = varlink_execute_directory(
+                        socket_path,
+                        "io.systemd.StorageProvider.ListVolumes",
+                        v,
+                        /* more= */ true,
+                        /* timeout_usec= */ 0, /* 0 means default */
+                        on_list_reply,
+                        t);
+        if (n < 0 && n != -ENOENT)
+                return log_error_errno(n, "Failed to enumerate storage volumes: %m");
+
+        if (!table_isempty(t)) {
+                r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+                if (r < 0)
+                        return r;
+        }
+
+        if (arg_legend && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
+                if (table_isempty(t))
+                        printf("No storage volumes.\n");
+                else
+                        printf("\n%zu storage volumes listed.\n", table_get_rows(t) - 1);
+        }
+
+        return 0;
+}
+
+static int on_list_templates_reply(
+                sd_varlink *link,
+                sd_json_variant *parameters,
+                const char *error_id,
+                sd_varlink_reply_flags_t flags,
+                void* userdata) {
+
+        Table *t = ASSERT_PTR(userdata);
+        int r;
+
+        assert(link);
+
+        const char *d = ASSERT_PTR(sd_varlink_get_description(link));
+
+        if (error_id) {
+                log_debug("%s: Received error '%s', ignoring.", d, error_id);
+                return 0;
+        }
+
+        _cleanup_free_ char *provider = NULL;
+        r = path_extract_filename(d, &provider);
+        if (r < 0)
+                return log_error_errno(r, "Failed to extract provider name from socket path: %m");
+
+        struct {
+                const char *name;
+                const char *type;
+        } p = {
+        };
+
+        static const sd_json_dispatch_field dispatch_table[] = {
+                { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), 0 },
+                { "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, type), 0 },
+                {}
+        };
+
+        r = sd_json_dispatch(parameters, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
+        if (r < 0)
+                return log_error_errno(r, "Failed to decode ListTemplates() reply: %m");
+
+        r = table_add_many(
+                        t,
+                        TABLE_STRING, provider,
+                        TABLE_STRING, p.name,
+                        TABLE_STRING, p.type);
+        if (r < 0)
+                return table_log_add_error(r);
+
+        return 0;
+}
+
+VERB(verb_templates, "templates", "GLOB", /* min_args= */ VERB_ANY, /* max_args= */ 2, /* flags= */ 0, "List storage volume templates");
+static int verb_templates(int argc, char *argv[], uintptr_t data, void *userdata) {
+        int r;
+
+        assert(argc <= 2);
+
+        _cleanup_free_ char *socket_path = NULL;
+        r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine socket directory: %m");
+
+        _cleanup_(table_unrefp) Table *t = table_new("provider", "name", "type");
+        if (!t)
+                return log_oom();
+
+        (void) table_set_sort(t, (size_t) 0, (size_t) 1);
+        table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+        if (argc >= 2) {
+                r = sd_json_buildo(
+                                &v,
+                                SD_JSON_BUILD_PAIR_STRING("matchName", argv[1]));
+                if (r < 0)
+                        return log_oom();
+        }
+
+        ssize_t n = varlink_execute_directory(
+                        socket_path,
+                        "io.systemd.StorageProvider.ListTemplates",
+                        v,
+                        /* more= */ true,
+                        /* timeout_usec= */ 0, /* 0 means default */
+                        on_list_templates_reply,
+                        t);
+        if (n < 0 && n != -ENOENT)
+                return log_error_errno(n, "Failed to enumerate storage volume templates: %m");
+
+        if (!table_isempty(t)) {
+                r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+                if (r < 0)
+                        return r;
+        }
+
+        if (arg_legend && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
+                if (table_isempty(t))
+                        printf("No templates.\n");
+                else
+                        printf("\n%zu templates listed.\n", table_get_rows(t) - 1);
+        }
+
+        return 0;
+}
+
+VERB_NOARG(verb_providers, "providers", "List storage providers");
+static int verb_providers(int argc, char *argv[], uintptr_t data, void *userdata) {
+        int r;
+
+        _cleanup_free_ char *socket_path = NULL;
+        r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine socket directory: %m");
+
+        _cleanup_(table_unrefp) Table *t = table_new("provider", "listening");
+        if (!t)
+                return log_oom();
+
+        (void) table_set_sort(t, (size_t) 0);
+        table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
+
+        _cleanup_close_ int fd = open(socket_path, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+        if (fd < 0) {
+                if (errno != ENOENT)
+                        return log_error_errno(errno, "Failed to open '%s': %m", socket_path);
+        } else {
+                _cleanup_free_ DirectoryEntries *dentries = NULL;
+                r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &dentries);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to enumerate '%s': %m", socket_path);
+
+                FOREACH_ARRAY(dp, dentries->entries, dentries->n_entries) {
+                        struct dirent *de = *dp;
+
+                        if (de->d_type != DT_SOCK)
+                                continue;
+
+                        if (!storage_provider_name_is_valid(de->d_name))
+                                continue;
+
+                        _cleanup_close_ int socket_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+                        if (socket_fd < 0)
+                                return log_error_errno(errno, "Failed to allocate AF_UNIX/SOCK_STREAM socket: %m");
+
+                        _cleanup_free_ char *no = NULL;
+                        r = connect_unix_path(socket_fd, fd, de->d_name);
+                        if (r < 0) {
+                                no = strjoin("no (", ERRNO_NAME(r), ")");
+                                if (!no)
+                                        return log_oom();
+                        }
+
+                        r = table_add_many(t,
+                                           TABLE_STRING, de->d_name,
+                                           TABLE_STRING, no ?: "yes",
+                                           TABLE_SET_COLOR, ansi_highlight_green_red(!no));
+                        if (r < 0)
+                                return table_log_add_error(r);
+                }
+        }
+
+        if (!table_isempty(t)) {
+                r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+                if (r < 0)
+                        return r;
+        }
+
+        if (arg_legend && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
+                if (table_isempty(t))
+                        printf("No providers.\n");
+                else
+                        printf("\n%zu providers listed.\n", table_get_rows(t) - 1);
+        }
+
+        return 0;
+}
+
+static int parse_argv(int argc, char *argv[], char ***args) {
+        int r;
+
+        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();
+
+                OPTION_COMMON_NO_PAGER:
+                        arg_pager_flags |= PAGER_DISABLE;
+                        break;
+
+                OPTION_COMMON_NO_LEGEND:
+                        arg_legend = false;
+                        break;
+
+                OPTION_COMMON_JSON:
+                        r = parse_json_argument(opts.arg, &arg_json_format_flags);
+                        if (r <= 0)
+                                return r;
+                        break;
+
+                OPTION_COMMON_NO_ASK_PASSWORD:
+                        arg_ask_password = false;
+                        break;
+
+                OPTION_LONG("system", NULL, "Operate in system mode"):
+                        arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
+                        break;
+
+                OPTION_LONG("user", NULL, "Operate in user mode"):
+                        arg_runtime_scope = RUNTIME_SCOPE_USER;
+                        break;
+                }
+
+        *args = option_parser_get_args(&opts);
+        return 1;
+}
+
+static int run_as_mount_helper(int argc, char *argv[]) {
+        int c, r;
+
+        /* Implements util-linux "external helper" command line interface, as per mount(8) man page.
+         *
+         * Usage:
+         *
+         *  mount -t storage fs:mydirvolume /some/place          # Directory volumes
+         *  mount -t storage.ext4 fs:myblkvolume /some/place     # Block volumes
+         */
+
+        const char *fstype = NULL, *options = NULL;
+        bool fake = false;
+
+        while ((c = getopt(argc, argv, "sfnvN:o:t:")) >= 0) {
+                switch (c) {
+
+                case 'f':
+                        fake = true;
+                        break;
+
+                case 'o':
+                        options = optarg;
+                        break;
+
+                case 't':
+                        fstype = startswith(optarg, "storage.");
+                        if (fstype) {
+                                /* Paranoia: don't allow "storage.storage.storage.…" chains... */
+                                if (startswith(fstype, "storage.") || streq(fstype, "storage"))
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing nested storage volumes.");
+                        } else if (!streq(optarg, "storage"))
+                                log_warning("Unexpected file system type '%s', ignoring.", optarg);
+
+                        break;
+
+                case 's': /* sloppy mount options */
+                case 'n': /* aka --no-mtab */
+                case 'v': /* aka --verbose */
+                        log_debug("Ignoring option -%c, not implemented.", c);
+                        break;
+
+                case 'N': /* aka --namespace= */
+                        return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Option -%c is not implemented, refusing.", c);
+
+                case '?':
+                        return -EINVAL;
+                }
+        }
+
+        if (optind + 2 != argc)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "Expected a storage volume specification and target directory as only arguments.");
+
+        const char *colon = strchr(argv[optind], ':');
+        if (!colon)
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid storage volume specification, refusing: %s", argv[optind]);
+
+        _cleanup_free_ char *provider = strndup(argv[optind], colon - argv[optind]);
+        if (!provider)
+                return log_oom();
+        if (!storage_provider_name_is_valid(provider))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid storage provider name: %s", provider);
+
+        _cleanup_free_ char *name = strdup(colon + 1);
+        if (!name)
+                return log_oom();
+        if (!storage_volume_name_is_valid(name))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid storage volume name: %s", name);
+
+        _cleanup_free_ char *path = NULL;
+        r = parse_path_argument(argv[optind+1], /* suppress_root= */ false, &path);
+        if (r < 0)
+                return r;
+
+        _cleanup_free_ char *filtered = NULL, *template = NULL;
+        CreateMode create_mode = _CREATE_MODE_INVALID;
+        uint64_t create_size = UINT64_MAX;
+        int read_only = -1;
+        for (const char *p = options;;) {
+                _cleanup_free_ char *word = NULL;
+
+                r = extract_first_word(&p, &word, ",", EXTRACT_KEEP_QUOTE|EXTRACT_UNESCAPE_SEPARATORS);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to extract mount option: %m");
+                if (r == 0)
+                        break;
+
+                const char *t = startswith(word, "storage.");
+                if (t) {
+                        const char *v;
+                        if ((v = startswith(t, "create="))) {
+                                create_mode = create_mode_from_string(v);
+                                if (create_mode < 0)
+                                        return log_error_errno(create_mode, "Failed to parse storage.create= parameter: %s", v);
+                        } else if ((v = startswith(t, "create-size="))) {
+                                r = parse_size(v, /* base= */ 1024, &create_size);
+                                if (r < 0)
+                                        return log_error_errno(r, "Failed to parse storage.create-size= parameter: %s", v);
+                        } else if ((v = startswith(t, "template="))) {
+                                if (!storage_template_name_is_valid(v))
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid template name, refusing: %s", v);
+
+                                r = free_and_strdup(&template, v);
+                                if (r < 0)
+                                        return log_oom();
+                        } else
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown mount option '%s', refusing.", word);
+                } else if (streq(word, "ro"))
+                        read_only = true;
+                else if (streq(word, "rw"))
+                        read_only = false;
+                else if (!strextend_with_separator(&filtered, ",", word))
+                        return log_oom();
+        }
+
+        if (fake)
+                return 0;
+
+        _cleanup_free_ char *socket_path = NULL;
+        r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine socket directory: %m");
+
+        if (!path_extend(&socket_path, provider))
+                return log_oom();
+
+        _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL;
+        r = sd_varlink_connect_address(&link, socket_path);
+        if (r < 0)
+                return log_error_errno(r, "Failed to connect to '%s': %m", socket_path);
+
+        r = sd_varlink_set_allow_fd_passing_input(link, true);
+        if (r < 0)
+                return log_error_errno(r, "Failed to enable file descriptor passing: %m");
+
+        (void) polkit_agent_open_if_enabled(BUS_TRANSPORT_LOCAL, arg_ask_password);
+
+        sd_json_variant *mreply = NULL;
+        const char *merror_id = NULL, *vtype = fstype ? "reg" : "dir";
+        r = sd_varlink_callbo(
+                        link,
+                        "io.systemd.StorageProvider.Acquire",
+                        &mreply,
+                        &merror_id,
+                        SD_JSON_BUILD_PAIR_STRING("name", name),
+                        SD_JSON_BUILD_PAIR_CONDITION(create_mode >= 0, "createMode", SD_JSON_BUILD_STRING(create_mode_to_string(create_mode))),
+                        JSON_BUILD_PAIR_STRING_NON_EMPTY("template", template),
+                        SD_JSON_BUILD_PAIR_CONDITION(read_only >= 0, "readOnly", SD_JSON_BUILD_BOOLEAN(read_only)),
+                        SD_JSON_BUILD_PAIR_STRING("requestAs", vtype),
+                        SD_JSON_BUILD_PAIR_CONDITION(create_size != UINT64_MAX, "createSizeBytes", SD_JSON_BUILD_UNSIGNED(create_size)),
+                        SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", arg_ask_password));
+        if (r < 0)
+                return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *reply = sd_json_variant_ref(mreply);
+        if (merror_id) {
+                /* Copy out the error ID, as the follow-up call will invalidate it */
+                _cleanup_free_ char *error_id = strdup(merror_id);
+                if (!error_id)
+                        return log_oom();
+
+                /* Hmm, the type might not have been right for the backend or the volume? then try
+                 * again, and switch from "reg" to "blk", maybe it works then. (We keep the original
+                 * reply referenced, since we prefer generating an error for the first error.) */
+                if (streq(vtype, "reg") && STR_IN_SET(error_id,
+                                         "io.systemd.StorageProvider.TypeNotSupported",
+                                         "io.systemd.StorageProvider.WrongType")) {
+
+                        sd_json_variant *freply = NULL;
+                        const char *ferror_id = NULL;
+                        r = sd_varlink_callbo(
+                                        link,
+                                        "io.systemd.StorageProvider.Acquire",
+                                        &freply,
+                                        &ferror_id,
+                                        SD_JSON_BUILD_PAIR_STRING("name", name),
+                                        SD_JSON_BUILD_PAIR_CONDITION(create_mode >= 0, "createMode", SD_JSON_BUILD_STRING(create_mode_to_string(create_mode))),
+                                        JSON_BUILD_PAIR_STRING_NON_EMPTY("template", template),
+                                        SD_JSON_BUILD_PAIR_CONDITION(read_only >= 0, "readOnly", SD_JSON_BUILD_BOOLEAN(read_only)),
+                                        SD_JSON_BUILD_PAIR_STRING("requestAs", "blk"),
+                                        SD_JSON_BUILD_PAIR_CONDITION(create_size != UINT64_MAX, "createSizeBytes", SD_JSON_BUILD_UNSIGNED(create_size)),
+                                        SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", arg_ask_password));
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
+                        if (!ferror_id) {
+                                /* The 2nd call worked? then let's forget about the first failure */
+                                sd_json_variant_unref(reply);
+                                reply = sd_json_variant_ref(freply);
+                                error_id = mfree(error_id);
+                        }
+
+                        /* NB: if both fail we show the Varlink error of the first call here, i.e. of the preferred type */
+                }
+
+                if (error_id) {
+                        if (streq(error_id, "io.systemd.StorageProvider.NoSuchVolume"))
+                                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Volume '%s' not known.", name);
+                        if (streq(error_id, "io.systemd.StorageProvider.NoSuchTemplate"))
+                                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Template '%s' not known.", template);
+                        if (streq(error_id, "io.systemd.StorageProvider.VolumeExists"))
+                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Volume '%s' exists already.", name);
+                        if (streq(error_id, "io.systemd.StorageProvider.TypeNotSupported"))
+                                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Storage provider does not support the specified volume type '%s'.", vtype);
+                        if (streq(error_id, "io.systemd.StorageProvider.WrongType"))
+                                return log_error_errno(SYNTHETIC_ERRNO(EADDRNOTAVAIL), "Volume '%s' is not of type '%s'.", name, vtype);
+                        if (streq(error_id, "io.systemd.StorageProvider.CreateNotSupported"))
+                                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Storage provider does not support creating volumes.");
+                        if (streq(error_id, "io.systemd.StorageProvider.CreateSizeRequired"))
+                                return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Storage provider requires a create size to be provided when creating volumes on-the-fly. Use 'storage.create-size=' mount option.");
+                        if (streq(error_id, "io.systemd.StorageProvider.ReadOnlyVolume"))
+                                return log_error_errno(SYNTHETIC_ERRNO(EROFS), "Volume '%s' is read-only.", name);
+                        if (streq(error_id, "io.systemd.StorageProvider.BadTemplate"))
+                                return log_error_errno(SYNTHETIC_ERRNO(EADDRNOTAVAIL), "Template does not apply to this volume type.");
+
+                        r = sd_varlink_error_to_errno(error_id, reply); /* If this is a system errno style error, output it with %m */
+                        if (r != -EBADR)
+                                return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
+
+                        return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %s", error_id);
+                }
+        }
+
+        struct {
+                unsigned fd_idx;
+                int read_only;
+                const char *type;
+                uid_t base_uid;
+                gid_t base_gid;
+        } p = {
+                .fd_idx = UINT_MAX,
+                .read_only = -1,
+                .base_uid = UID_INVALID,
+                .base_gid = GID_INVALID,
+        };
+
+        static const sd_json_dispatch_field dispatch_table[] = {
+                { "fileDescriptorIndex", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint,         voffsetof(p, fd_idx),     SD_JSON_MANDATORY },
+                { "readOnly",            SD_JSON_VARIANT_BOOLEAN,       sd_json_dispatch_tristate,     voffsetof(p, read_only),  0                 },
+                { "type",                SD_JSON_VARIANT_STRING,        sd_json_dispatch_const_string, voffsetof(p, type),       SD_JSON_MANDATORY },
+                { "baseUID",             _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid,      voffsetof(p, base_uid),   0                 },
+                { "baseGID",             _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid,      voffsetof(p, base_gid),   0                 },
+                {}
+        };
+
+        r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
+        if (r < 0)
+                return log_error_errno(r, "Failed to decode Acquire() reply: %m");
+
+        _cleanup_close_ int fd = sd_varlink_take_fd(link, p.fd_idx);
+        if (fd < 0)
+                return log_error_errno(fd, "Failed to acquire fd from Varlink connection: %m");
+
+        struct stat st;
+        if (fstat(fd, &st) < 0)
+                return log_error_errno(errno, "Failed to stat returned file descriptor: %m");
+
+        _cleanup_strv_free_ char **cmdline = strv_new("mount", "-c");
+        if (!cmdline)
+                return log_oom();
+
+        if (fstype) {
+                if (!STR_IN_SET(p.type, "reg", "blk"))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mounting as file system type '%s' requested, but volume is not a block device or regular file.", fstype);
+
+                r = stat_verify_regular_or_block(&st);
+                if (r < 0)
+                        return log_error_errno(r, "File descriptor for block/regular volume is not a block or regular inode: %m");
+
+                if (strv_extend_strv(&cmdline, STRV_MAKE("-t", fstype), /* filter_duplicates= */ false) < 0)
+                        return log_oom();
+        } else {
+                if (!streq(p.type, "dir"))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mount as directory requested, but volume is not a directory.");
+
+                if (!uid_is_valid(p.base_uid) || !gid_is_valid(p.base_gid))
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provider did not report base UID/GID, cannot mount.");
+
+                if (p.base_uid > UINT32_MAX - 0x10000U ||
+                    p.base_gid > UINT32_MAX - 0x10000U)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Returned base UID/GID out of range.");
+
+                r = stat_verify_directory(&st);
+                if (r < 0)
+                        return log_error_errno(r, "File descriptor for directory volume is not a directory inode: %m");
+
+                if (st.st_uid < p.base_uid || st.st_uid >= p.base_uid + 0x10000 ||
+                    st.st_gid < p.base_gid || st.st_gid >= p.base_gid + 0x10000)
+                        return log_error_errno(SYNTHETIC_ERRNO(EPERM), "File descriptor for directory volume is not owned by base UID/GID range, refusing.");
+
+                /* Now move the mount into our own UID/GID range */
+                _cleanup_free_ char *uid_line = asprintf_safe(
+                                UID_FMT " " UID_FMT " " UID_FMT "\n",
+                                p.base_uid, (uid_t) 0, (uid_t) 0x10000);
+                _cleanup_free_ char *gid_line = asprintf_safe(
+                                GID_FMT " " GID_FMT " " GID_FMT "\n",
+                                p.base_gid, (gid_t) 0, (gid_t) 0x10000);
+                if (!uid_line || !gid_line)
+                        return log_oom();
+
+                _cleanup_close_ int userns_fd = userns_acquire(uid_line, gid_line, /* setgroups_deny= */ true);
+                if (userns_fd < 0)
+                        return log_error_errno(userns_fd, "Failed to acquire new user namespace: %m");
+
+                _cleanup_close_ int remapped_fd = open_tree_attr_with_fallback(
+                                fd,
+                                /* path= */ NULL,
+                                OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC,
+                                &(struct mount_attr) {
+                                          .attr_set = MOUNT_ATTR_IDMAP,
+                                          .userns_fd = userns_fd,
+                                });
+                if (remapped_fd < 0)
+                        return log_error_errno(remapped_fd, "Failed to set ID mapping on returned mount: %m");
+
+                close_and_replace(fd, remapped_fd);
+
+                if (strv_extend(&cmdline, "--bind") < 0)
+                        return log_oom();
+        }
+
+        if (p.read_only > 0)
+                read_only = true;
+
+        if (!strextend_with_separator(&filtered, ",", read_only > 0 ? "ro" : "rw"))
+                return log_oom();
+
+        if (strv_extend_strv(&cmdline, STRV_MAKE("-o", filtered), /* filter_duplicates= */ false) < 0)
+                return log_oom();
+
+        if (strv_extend_strv(&cmdline, STRV_MAKE(FORMAT_PROC_FD_PATH(fd), path), /* filter_duplicates= */ false) < 0)
+                return log_oom();
+
+        r = fd_cloexec(fd, false);
+        if (r < 0)
+                return log_error_errno(r, "Failed to disable O_CLOEXEC for mount fd: %m");
+
+        if (DEBUG_LOGGING) {
+                _cleanup_free_ char *q = quote_command_line(cmdline, SHELL_ESCAPE_EMPTY);
+                log_debug("Chain-loading: %s", strna(q));
+        }
+
+        /* NB: we do not honour $PATH here, since as plugin to /bin/mount we might be called in a setuid()
+         * context, and hence don't want to chain to programs potentially under user control. */
+        execv("/bin/mount", cmdline);
+        return log_error_errno(errno, "Failed to execute mount tool: %m");
+}
+
+static int run(int argc, char *argv[]) {
+        int r;
+
+        log_setup();
+
+        if (invoked_as(argv, "mount.storage"))
+                return run_as_mount_helper(argc, argv);
+
+        char **args = NULL;
+        r = parse_argv(argc, argv, &args);
+        if (r <= 0)
+                return r;
+
+        return dispatch_verb_with_args(args, /* userdata= */ NULL);
+}
+
+DEFINE_MAIN_FUNCTION(run);