diff --git a/TODO.md b/TODO.md
index 61114235f3..f3d1070c6c 100644
--- a/TODO.md
+++ b/TODO.md
@@ -138,7 +138,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- StorageProvider interface + storagectl
- hook-up in systemd-nspawn
- - hook-up in systemd-vmspawn
- hook-up in service manager (BindVolume=)
- introduce a locking concept: right now all access to volumes is fully
shared. Let's add a basic locking concept: supporting backends can take an
diff --git a/man/machinectl.xml b/man/machinectl.xml
index b4fb15b4f9..10ab225074 100644
--- a/man/machinectl.xml
+++ b/man/machinectl.xml
@@ -357,6 +357,38 @@
+
+ bind-volume NAME SPEC
+
+ Acquire a storage volume from a
+ storagectl1
+ provider and attach it to the running machine. SPEC is a string of the form
+ PROVIDER:VOLUME[:CONFIG][:K=V,…],
+ identical in grammar to the argument of
+ systemd-vmspawn1.
+
+ The attached volume is identified by the name PROVIDER:VOLUME
+ and may be detached at runtime via unbind-volume. Currently only supported for
+ systemd-vmspawn machines that expose an
+ io.systemd.MachineInstance control socket.
+
+
+
+
+
+ unbind-volume NAME STORAGE-NAME
+
+ Detach a storage volume from the running machine. STORAGE-NAME
+ is the PROVIDER:VOLUME
+ identifier that was specified at bind-volume time. Volumes that were attached at machine
+ startup (e.g. via on
+ systemd-vmspawn1)
+ cannot be detached and will fail with
+ io.systemd.MachineInstance.StorageImmutable.
+
+
+
+
copy-to NAME PATH [PATH]
diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml
index 5c5ec4ccbc..b23c665142 100644
--- a/man/systemd-vmspawn.xml
+++ b/man/systemd-vmspawn.xml
@@ -566,6 +566,39 @@
+
+
+
+ Acquire a storage volume from a
+ storagectl1
+ provider and attach it to the virtual machine. PROVIDER is the
+ provider name (typically block or fs). VOLUME
+ is the volume name passed to the provider's Acquire() method.
+ CONFIG selects the guest device type and takes one of
+ virtio-blk, virtio-scsi, nvme, or
+ scsi-cd. If empty or omitted, defaults to virtio-blk.
+
+ The trailing comma-separated K=V list passes parameters to
+ io.systemd.StorageProvider.Acquire(): template=,
+ create= (one of any, new, open),
+ read-only= (or ro=; takes a boolean or auto),
+ size= / create-size= (size for created volumes),
+ request-as= (one of blk, reg,
+ dir; dir is rejected by vmspawn).
+
+ Each attached volume is identified by the name PROVIDER:VOLUME.
+ Volumes attached at startup via this option cannot be detached at runtime via
+ machinectl unbind-volume; only volumes added at runtime via
+ machinectl bind-volume are removable.
+
+ The provider is looked up under
+ /run/systemd/io.systemd.StorageProvider/ for system mode (or
+ $XDG_RUNTIME_DIR/systemd/io.systemd.StorageProvider/ for user mode), matching
+ the runtime scope chosen via / .
+
+
+
+
diff --git a/shell-completion/bash/machinectl b/shell-completion/bash/machinectl
index 50b46fb279..d6627e6ba7 100644
--- a/shell-completion/bash/machinectl
+++ b/shell-completion/bash/machinectl
@@ -48,7 +48,7 @@ _machinectl() {
[MACHINES]='status show start stop login shell enable disable poweroff reboot pause resume terminate kill
image-status show-image remove export-tar export-raw'
[MACHINES_OR_FILES]='edit cat'
- [MACHINE_ONLY]='clone rename set-limit'
+ [MACHINE_ONLY]='clone rename set-limit bind-volume unbind-volume'
[READONLY]='read-only'
[FILE]='import-tar import-raw'
[MACHINES_AND_FILES]='copy-to copy-from bind'
diff --git a/shell-completion/bash/systemd-vmspawn b/shell-completion/bash/systemd-vmspawn
index efa0dae58d..62fa5ab520 100644
--- a/shell-completion/bash/systemd-vmspawn
+++ b/shell-completion/bash/systemd-vmspawn
@@ -38,7 +38,7 @@ _systemd_vmspawn() {
[BIND]='--bind --bind-ro'
[SSH_KEY]='--ssh-key'
[CONSOLE]='--console'
- [ARG]='--cpus --ram --vsock-cid -M --machine --uuid --private-users --background --set-credential --load-credential --forward-journal-max-use --forward-journal-keep-free --forward-journal-max-file-size --forward-journal-max-files'
+ [ARG]='--cpus --ram --vsock-cid -M --machine --uuid --private-users --background --set-credential --load-credential --forward-journal-max-use --forward-journal-keep-free --forward-journal-max-file-size --forward-journal-max-files --bind-volume'
[IMAGE_FORMAT]='--image-format'
[IMAGE_DISK_TYPE]='--image-disk-type'
)
diff --git a/shell-completion/zsh/_machinectl b/shell-completion/zsh/_machinectl
index 31ddf4fca5..d61a62ee27 100644
--- a/shell-completion/zsh/_machinectl
+++ b/shell-completion/zsh/_machinectl
@@ -45,6 +45,8 @@
"copy-to:Copy files from the host to a container"
"copy-from:Copy files from a container to the host"
"bind:Bind mount a path from the host into a container"
+ "bind-volume:Attach a storage volume to a running machine"
+ "unbind-volume:Detach a storage volume from a running machine"
"list-images:Show available container and VM images"
"image-status:Show image details"
@@ -115,6 +117,12 @@
else stop=1
fi ;;
+ bind-volume|unbind-volume)
+ if (( CURRENT == 2 )); then _sd_machines
+ elif (( CURRENT == 3 )); then _message "volume spec"
+ else stop=1
+ fi ;;
+
read-only)
if (( CURRENT == 2 )); then _machinectl_images
elif (( CURRENT == 3 )); then _values 'read-only flag' 'true' 'false'
diff --git a/src/basic/user-util.h b/src/basic/user-util.h
index 003420dbe3..a8902aca61 100644
--- a/src/basic/user-util.h
+++ b/src/basic/user-util.h
@@ -90,6 +90,10 @@ int take_etc_passwd_lock(const char *root);
#define UID_NOBODY ((uid_t) 65534U)
#define GID_NOBODY ((gid_t) 65534U)
+/* Conventional size of a user-namespace UID/GID delegation block (64K).
+ * Untyped so it can be used in both UID and GID contexts without casts. */
+#define USERNS_RANGE_SIZE 0x10000U
+
/* If REMOUNT_IDMAPPING_HOST_ROOT is set for remount_idmap() we'll include a mapping here that maps the host
* root user accessing the idmapped mount to the this user ID on the backing fs. This is the last valid UID in
* the *signed* 32-bit range. You might wonder why precisely use this specific UID for this purpose? Well, we
diff --git a/src/libsystemd/sd-json/json-util.h b/src/libsystemd/sd-json/json-util.h
index cea2d368b4..0db1e445e6 100644
--- a/src/libsystemd/sd-json/json-util.h
+++ b/src/libsystemd/sd-json/json-util.h
@@ -268,6 +268,8 @@ enum {
SD_JSON_BUILD_PAIR_CONDITION(condition, name, SD_JSON_BUILD_UNSIGNED(value))
#define JSON_BUILD_PAIR_CONDITION_BOOLEAN(condition, name, value) \
SD_JSON_BUILD_PAIR_CONDITION(condition, name, SD_JSON_BUILD_BOOLEAN(value))
+#define JSON_BUILD_PAIR_CONDITION_STRING(condition, name, value) \
+ SD_JSON_BUILD_PAIR_CONDITION(condition, name, SD_JSON_BUILD_STRING(value))
#define JSON_BUILD_PAIR_CONDITION_STRV(condition, name, value) \
SD_JSON_BUILD_PAIR_CONDITION(condition, name, SD_JSON_BUILD_STRV(value))
diff --git a/src/machine/machinectl.c b/src/machine/machinectl.c
index ae5473aa99..3d7f782a8c 100644
--- a/src/machine/machinectl.c
+++ b/src/machine/machinectl.c
@@ -29,16 +29,19 @@
#include "cgroup-util.h"
#include "edit-util.h"
#include "env-util.h"
+#include "fd-util.h"
#include "format-ifname.h"
#include "format-table.h"
#include "format-util.h"
#include "hostname-util.h"
#include "import-util.h"
#include "in-addr-util.h"
+#include "json-util.h"
#include "label-util.h"
#include "log.h"
#include "logs-show.h"
#include "machine-dbus.h"
+#include "machine-util.h"
#include "main-func.h"
#include "nulstr-util.h"
#include "osc-context.h"
@@ -55,6 +58,7 @@
#include "ptyfwd.h"
#include "runtime-scope.h"
#include "stdio-util.h"
+#include "storage-util.h"
#include "string-table.h"
#include "string-util.h"
#include "strv.h"
@@ -1338,6 +1342,120 @@ static int verb_copy_files(int argc, char *argv[], uintptr_t _data, void *userda
return 0;
}
+static int verb_bind_volume(int argc, char *argv[], uintptr_t _data, void *userdata) {
+ int r;
+
+ if (arg_transport != BUS_TRANSPORT_LOCAL)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "bind-volume is only supported on the local transport.");
+
+ _cleanup_(bind_volume_freep) BindVolume *bv = NULL;
+ r = bind_volume_parse(argv[2], &bv);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse bind-volume argument '%s': %m", argv[2]);
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ /* Locate and connect to the target machine before acquiring storage, so a missing
+ * machine doesn't trigger 'create=new' side effects on the StorageProvider. */
+ _cleanup_free_ char *address = NULL;
+ r = machine_get_control_address(argv[1], &address);
+ if (r == -EOPNOTSUPP)
+ return log_error_errno(r, "Machine '%s' does not expose a varlink control socket.", argv[1]);
+ if (r < 0)
+ return r;
+
+ _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
+ r = sd_varlink_connect_address(&vl, address);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to machine control socket %s: %m", address);
+
+ r = sd_varlink_set_allow_fd_passing_output(vl, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enable fd passing on varlink connection: %m");
+
+ _cleanup_(storage_acquire_reply_done) StorageAcquireReply reply = STORAGE_ACQUIRE_REPLY_INIT;
+ _cleanup_free_ char *acquire_error_id = NULL;
+ r = storage_acquire_volume(arg_runtime_scope, bv, arg_ask_password, &acquire_error_id, &reply);
+ if (r < 0) {
+ if (acquire_error_id)
+ return log_error_errno(r, "Failed to acquire storage volume '%s:%s' from provider: %s",
+ bv->provider, bv->volume, acquire_error_id);
+ return log_error_errno(r, "Failed to acquire storage volume '%s:%s' from provider: %m",
+ bv->provider, bv->volume);
+ }
+
+ int fd_index = sd_varlink_push_fd(vl, reply.fd);
+ if (fd_index < 0)
+ return log_error_errno(fd_index, "Failed to push storage fd onto varlink connection: %m");
+ TAKE_FD(reply.fd);
+
+ _cleanup_free_ char *name = strjoin(bv->provider, ":", bv->volume);
+ if (!name)
+ return log_oom();
+
+ sd_json_variant *vl_reply = NULL;
+ const char *error_id = NULL;
+ r = sd_varlink_callbo(
+ vl,
+ "io.systemd.MachineInstance.AddStorage",
+ &vl_reply, &error_id,
+ SD_JSON_BUILD_PAIR_INTEGER("fileDescriptorIndex", fd_index),
+ SD_JSON_BUILD_PAIR_STRING("name", name),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("config", bv->config));
+ if (r < 0)
+ return log_error_errno(r, "Failed to call io.systemd.MachineInstance.AddStorage: %m");
+ if (error_id)
+ return log_error_errno(sd_varlink_error_to_errno(error_id, vl_reply),
+ "AddStorage failed for '%s': %s", name, error_id);
+
+ return 0;
+}
+
+static int verb_unbind_volume(int argc, char *argv[], uintptr_t _data, void *userdata) {
+ int r;
+
+ if (arg_transport != BUS_TRANSPORT_LOCAL)
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "unbind-volume is only supported on the local transport.");
+
+ r = machine_storage_name_split(argv[2], /* ret_provider= */ NULL, /* ret_volume= */ NULL);
+ if (r == -ENOMEM)
+ return log_oom();
+ if (r < 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Invalid unbind-volume name '%s', expected ':'.", argv[2]);
+
+ (void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
+
+ _cleanup_free_ char *address = NULL;
+ r = machine_get_control_address(argv[1], &address);
+ if (r == -EOPNOTSUPP)
+ return log_error_errno(r, "Machine '%s' does not expose a varlink control socket.", argv[1]);
+ if (r < 0)
+ return r;
+
+ _cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
+ r = sd_varlink_connect_address(&vl, address);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to machine control socket %s: %m", address);
+
+ sd_json_variant *reply = NULL;
+ const char *error_id = NULL;
+ r = sd_varlink_callbo(
+ vl,
+ "io.systemd.MachineInstance.RemoveStorage",
+ &reply, &error_id,
+ SD_JSON_BUILD_PAIR_STRING("name", argv[2]));
+ if (r < 0)
+ return log_error_errno(r, "Failed to call io.systemd.MachineInstance.RemoveStorage: %m");
+ if (error_id)
+ return log_error_errno(sd_varlink_error_to_errno(error_id, reply),
+ "RemoveStorage failed for '%s': %s", argv[2], error_id);
+
+ return 0;
+}
+
static int verb_bind_mount(int argc, char *argv[], uintptr_t _data, void *userdata) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
sd_bus *bus = ASSERT_PTR(userdata);
@@ -2606,6 +2724,8 @@ static int machinectl_main(int argc, char *argv[], sd_bus *bus) {
{ "login", VERB_ANY, 2, 0, verb_login_machine },
{ "shell", VERB_ANY, VERB_ANY, 0, verb_shell_machine },
{ "bind", 3, 4, 0, verb_bind_mount },
+ { "bind-volume", 3, 3, 0, verb_bind_volume },
+ { "unbind-volume", 3, 3, 0, verb_unbind_volume },
{ "edit", 2, VERB_ANY, 0, verb_edit_settings },
{ "cat", 2, VERB_ANY, 0, verb_cat_settings },
{ "copy-to", 3, 4, 0, verb_copy_files },
diff --git a/src/shared/machine-util.c b/src/shared/machine-util.c
index fa5e46ace1..43a4fdfdd8 100644
--- a/src/shared/machine-util.c
+++ b/src/shared/machine-util.c
@@ -4,7 +4,11 @@
#include "extract-word.h"
#include "machine-util.h"
#include "parse-argument.h"
+#include "parse-util.h"
+#include "storage-util.h"
#include "string-table.h"
+#include "string-util.h"
+#include "strv.h"
static const char *const image_format_table[_IMAGE_FORMAT_MAX] = {
[IMAGE_FORMAT_RAW] = "raw",
@@ -13,6 +17,14 @@ static const char *const image_format_table[_IMAGE_FORMAT_MAX] = {
DEFINE_STRING_TABLE_LOOKUP(image_format, ImageFormat);
+static const char *const read_only_mode_table[_READ_ONLY_MAX] = {
+ [READ_ONLY_NO] = "no",
+ [READ_ONLY_YES] = "yes",
+ [READ_ONLY_AUTO] = "auto",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(read_only_mode, ReadOnlyMode);
+
static const char *const disk_type_table[_DISK_TYPE_MAX] = {
[DISK_TYPE_VIRTIO_BLK] = "virtio-blk",
[DISK_TYPE_VIRTIO_SCSI] = "virtio-scsi",
@@ -100,3 +112,168 @@ int parse_disk_spec(
*ret_path = TAKE_PTR(path);
return 0;
}
+
+BindVolume* bind_volume_free(BindVolume *v) {
+ if (!v)
+ return NULL;
+
+ free(v->provider);
+ free(v->volume);
+ free(v->config);
+ free(v->template);
+
+ return mfree(v);
+}
+
+static int bind_volume_apply_extra(BindVolume *v, const char *key, const char *value) {
+ int r;
+
+ assert(v);
+ assert(key);
+ assert(value);
+
+ if (streq(key, "template")) {
+ if (v->template)
+ return -EINVAL;
+ if (!storage_template_name_is_valid(value))
+ return -EINVAL;
+ r = free_and_strdup(&v->template, value);
+ if (r < 0)
+ return r;
+ return 0;
+ }
+
+ if (streq(key, "create")) {
+ if (v->create_mode >= 0)
+ return -EINVAL;
+ CreateMode m = create_mode_from_string(value);
+ if (m < 0)
+ return m;
+ v->create_mode = m;
+ return 0;
+ }
+
+ if (STR_IN_SET(key, "read-only", "ro")) {
+ if (v->read_only >= 0)
+ return -EINVAL;
+ ReadOnlyMode m = read_only_mode_from_string(value);
+ if (m < 0) {
+ r = parse_boolean(value);
+ if (r < 0)
+ return r;
+ m = r ? READ_ONLY_YES : READ_ONLY_NO;
+ }
+ v->read_only = m;
+ return 0;
+ }
+
+ if (STR_IN_SET(key, "size", "create-size")) {
+ if (v->create_size_bytes != UINT64_MAX)
+ return -EINVAL;
+ uint64_t sz;
+ r = parse_size(value, 1024, &sz);
+ if (r < 0)
+ return r;
+ if (sz == 0)
+ return -EINVAL;
+ v->create_size_bytes = sz;
+ return 0;
+ }
+
+ if (streq(key, "request-as")) {
+ if (v->request_as >= 0)
+ return -EINVAL;
+ VolumeType t = volume_type_from_string(value);
+ if (t < 0)
+ return t;
+ v->request_as = t;
+ return 0;
+ }
+
+ return -EINVAL;
+}
+
+int bind_volume_parse(const char *arg, BindVolume **ret) {
+ _cleanup_(bind_volume_freep) BindVolume *v = NULL;
+ int r;
+
+ assert(arg);
+ assert(ret);
+
+ v = new(BindVolume, 1);
+ if (!v)
+ return -ENOMEM;
+
+ *v = BIND_VOLUME_INIT;
+
+ const char *p = arg;
+ _cleanup_free_ char *provider = NULL, *volume = NULL, *config = NULL;
+
+ r = extract_first_word(&p, &provider, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return r;
+ if (r == 0 || isempty(provider) || !storage_provider_name_is_valid(provider))
+ return -EINVAL;
+
+ r = extract_first_word(&p, &volume, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return r;
+ if (r == 0 || isempty(volume) || !storage_volume_name_is_valid(volume))
+ return -EINVAL;
+
+ r = extract_first_word(&p, &config, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ if (r < 0)
+ return r;
+
+ v->provider = TAKE_PTR(provider);
+ v->volume = TAKE_PTR(volume);
+ if (!isempty(config)) {
+ if (!string_is_safe(config, /* flags= */ 0))
+ return -EINVAL;
+ v->config = TAKE_PTR(config);
+ }
+
+ for (;;) {
+ _cleanup_free_ char *kv = NULL, *key = NULL, *value = NULL;
+
+ r = extract_first_word(&p, &kv, ",", 0);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ break;
+
+ r = split_pair(kv, "=", &key, &value);
+ if (r < 0)
+ return r;
+ if (isempty(key))
+ return -EINVAL;
+
+ r = bind_volume_apply_extra(v, key, value);
+ if (r < 0)
+ return r;
+ }
+
+ *ret = TAKE_PTR(v);
+ return 0;
+}
+
+int machine_storage_name_split(const char *s, char **ret_provider, char **ret_volume) {
+ _cleanup_free_ char *p = NULL, *v = NULL;
+ int r;
+
+ if (isempty(s))
+ return -EINVAL;
+
+ r = split_pair(s, ":", &p, &v);
+ if (r < 0)
+ return r;
+
+ if (!storage_provider_name_is_valid(p) || !storage_volume_name_is_valid(v))
+ return -EINVAL;
+
+ if (ret_provider)
+ *ret_provider = TAKE_PTR(p);
+ if (ret_volume)
+ *ret_volume = TAKE_PTR(v);
+ return 0;
+}
diff --git a/src/shared/machine-util.h b/src/shared/machine-util.h
index 3937ce1703..a992e24448 100644
--- a/src/shared/machine-util.h
+++ b/src/shared/machine-util.h
@@ -2,6 +2,7 @@
#pragma once
#include "shared-forward.h"
+#include "storage-util.h"
typedef enum ImageFormat {
IMAGE_FORMAT_RAW,
@@ -30,3 +31,56 @@ int parse_disk_spec(
ImageFormat *format,
DiskType *disk_type,
char **ret_path);
+
+typedef enum ReadOnlyMode {
+ READ_ONLY_NO,
+ READ_ONLY_YES,
+ READ_ONLY_AUTO,
+ _READ_ONLY_MAX,
+ _READ_ONLY_INVALID = -EINVAL,
+} ReadOnlyMode;
+
+DECLARE_STRING_TABLE_LOOKUP(read_only_mode, ReadOnlyMode);
+
+/* Map ReadOnlyMode onto the Acquire() wire tristate (-1 unset/auto, 0 no, 1 yes). */
+static inline int read_only_mode_to_tristate(ReadOnlyMode m) {
+ switch (m) {
+ case READ_ONLY_NO: return 0;
+ case READ_ONLY_YES: return 1;
+ default: return -1;
+ }
+}
+
+/* Parsed "PROVIDER:VOLUME[:CONFIG][:K=V,K=V,...]" used by --bind-volume,
+ * machinectl bind-volume, and (future) the BindVolume= unit setting. The 'config'
+ * field is opaque here and interpreted per-backend (vmspawn: a DiskType name;
+ * nspawn: a mount path). */
+typedef struct BindVolume {
+ char *provider;
+ char *volume;
+ char *config;
+
+ /* Acquire() parameters parsed from the trailing key=value list. */
+ char *template;
+ CreateMode create_mode;
+ ReadOnlyMode read_only;
+ uint64_t create_size_bytes;
+ VolumeType request_as;
+} BindVolume;
+
+#define BIND_VOLUME_INIT \
+ (BindVolume) { \
+ .create_mode = _CREATE_MODE_INVALID, \
+ .read_only = _READ_ONLY_INVALID, \
+ .create_size_bytes = UINT64_MAX, \
+ .request_as = _VOLUME_TYPE_INVALID, \
+ }
+
+BindVolume* bind_volume_free(BindVolume *v);
+DEFINE_TRIVIAL_CLEANUP_FUNC(BindVolume*, bind_volume_free);
+
+int bind_volume_parse(const char *arg, BindVolume **ret);
+
+/* Validate a ":" binding name as used by AddStorage/RemoveStorage.
+ * ret_provider/ret_volume may each be NULL when the caller only wants validation. */
+int machine_storage_name_split(const char *s, char **ret_provider, char **ret_volume);
diff --git a/src/shared/meson.build b/src/shared/meson.build
index 0529d35d26..d4cd5ca431 100644
--- a/src/shared/meson.build
+++ b/src/shared/meson.build
@@ -199,6 +199,7 @@ shared_sources = files(
'socket-netlink.c',
'specifier.c',
'ssl-util.c',
+ 'storage-util.c',
'switch-root.c',
'swtpm-util.c',
'tar-util.c',
diff --git a/src/shared/nsresource.h b/src/shared/nsresource.h
index 5633fd9bf3..c26dd4f8a5 100644
--- a/src/shared/nsresource.h
+++ b/src/shared/nsresource.h
@@ -2,9 +2,10 @@
#pragma once
#include "shared-forward.h"
+#include "user-util.h"
/* Helpful constants for the only numbers of UIDs that can currently be allocated */
-#define NSRESOURCE_UIDS_64K 0x10000U
+#define NSRESOURCE_UIDS_64K USERNS_RANGE_SIZE
#define NSRESOURCE_UIDS_1 1U
int nsresource_connect(sd_varlink **ret);
diff --git a/src/shared/storage-util.c b/src/shared/storage-util.c
new file mode 100644
index 0000000000..3a3c1e6c57
--- /dev/null
+++ b/src/shared/storage-util.c
@@ -0,0 +1,130 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-varlink.h"
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "json-util.h"
+#include "machine-util.h"
+#include "path-lookup.h"
+#include "path-util.h"
+#include "runtime-scope.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);
+
+void storage_acquire_reply_done(StorageAcquireReply *reply) {
+ if (!reply)
+ return;
+
+ reply->fd = safe_close(reply->fd);
+}
+
+int storage_acquire_volume(
+ RuntimeScope scope,
+ const BindVolume *bv,
+ bool allow_interactive_auth,
+ char **reterr_error_id,
+ StorageAcquireReply *ret) {
+
+ int r;
+
+ assert(bv);
+ assert(bv->provider);
+ assert(bv->volume);
+ assert(ret);
+
+ /* Defense-in-depth: this is a libshared helper that may grow new callers; reject
+ * provider names that could escape the StorageProvider runtime directory. */
+ if (!storage_provider_name_is_valid(bv->provider))
+ return -EINVAL;
+
+ _cleanup_free_ char *socket_path = NULL;
+ r = runtime_directory_generic(scope, "systemd/io.systemd.StorageProvider", &socket_path);
+ if (r < 0)
+ return r;
+
+ if (!path_extend(&socket_path, bv->provider))
+ return -ENOMEM;
+
+ _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL;
+ r = sd_varlink_connect_address(&link, socket_path);
+ if (r < 0)
+ return r;
+
+ r = sd_varlink_set_allow_fd_passing_input(link, true);
+ if (r < 0)
+ return r;
+
+ sd_json_variant *mreply = NULL;
+ const char *merror_id = NULL;
+ r = sd_varlink_callbo(
+ link,
+ "io.systemd.StorageProvider.Acquire",
+ &mreply,
+ &merror_id,
+ SD_JSON_BUILD_PAIR_STRING("name", bv->volume),
+ JSON_BUILD_PAIR_CONDITION_STRING(bv->create_mode >= 0, "createMode", create_mode_to_string(bv->create_mode)),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("template", bv->template),
+ JSON_BUILD_PAIR_TRISTATE_NON_NULL("readOnly", read_only_mode_to_tristate(bv->read_only)),
+ JSON_BUILD_PAIR_CONDITION_STRING(bv->request_as >= 0, "requestAs", volume_type_to_string(bv->request_as)),
+ JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("createSizeBytes", bv->create_size_bytes, UINT64_MAX),
+ SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", allow_interactive_auth));
+ if (r < 0)
+ return r;
+
+ if (merror_id) {
+ if (reterr_error_id) {
+ char *copy = strdup(merror_id);
+ if (!copy)
+ return -ENOMEM;
+ *reterr_error_id = copy;
+ }
+
+ r = sd_varlink_error_to_errno(merror_id, mreply);
+ return r == -EBADR ? -EPROTO : r;
+ }
+
+ /* tmp.fd holds the JSON fd index until sd_varlink_take_fd() swaps it for the real fd. */
+ StorageAcquireReply tmp = STORAGE_ACQUIRE_REPLY_INIT;
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "fileDescriptorIndex", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_int, voffsetof(StorageAcquireReply, fd), SD_JSON_MANDATORY },
+ { "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(StorageAcquireReply, read_only), 0 },
+ { "type", SD_JSON_VARIANT_STRING, json_dispatch_volume_type, voffsetof(StorageAcquireReply, type), SD_JSON_MANDATORY },
+ { "baseUID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, voffsetof(StorageAcquireReply, base_uid), 0 },
+ { "baseGID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, voffsetof(StorageAcquireReply, base_gid), 0 },
+ {}
+ };
+
+ r = sd_json_dispatch(mreply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &tmp);
+ if (r < 0)
+ return r;
+ if (tmp.fd < 0)
+ return -EBADMSG;
+
+ _cleanup_close_ int fd = sd_varlink_take_fd(link, tmp.fd);
+ if (fd < 0)
+ return fd;
+
+ tmp.fd = TAKE_FD(fd);
+ *ret = tmp;
+ return 0;
+}
diff --git a/src/storage/storage-util.h b/src/shared/storage-util.h
similarity index 51%
rename from src/storage/storage-util.h
rename to src/shared/storage-util.h
index f7a62aeec0..b37515bedb 100644
--- a/src/storage/storage-util.h
+++ b/src/shared/storage-util.h
@@ -41,3 +41,32 @@ static inline bool storage_template_name_is_valid(const char *n) {
static inline bool storage_provider_name_is_valid(const char *n) {
return string_is_safe(n, STRING_FILENAME);
}
+
+typedef struct StorageAcquireReply {
+ int fd;
+ VolumeType type;
+ int read_only;
+ uid_t base_uid;
+ gid_t base_gid;
+} StorageAcquireReply;
+
+#define STORAGE_ACQUIRE_REPLY_INIT \
+ (StorageAcquireReply) { \
+ .fd = -EBADF, \
+ .type = _VOLUME_TYPE_INVALID, \
+ .read_only = -1, \
+ .base_uid = UID_INVALID, \
+ .base_gid = GID_INVALID, \
+ }
+
+void storage_acquire_reply_done(StorageAcquireReply *reply);
+
+/* On varlink failure, reterr_error_id (if non-NULL) is set to the io.systemd.StorageProvider.*
+ * error name. The reply is untouched on any error. */
+typedef struct BindVolume BindVolume;
+int storage_acquire_volume(
+ RuntimeScope scope,
+ const BindVolume *bv,
+ bool allow_interactive_auth,
+ char **reterr_error_id,
+ StorageAcquireReply *ret);
diff --git a/src/shared/varlink-io.systemd.MachineInstance.c b/src/shared/varlink-io.systemd.MachineInstance.c
index 365b6f5f9e..6630402709 100644
--- a/src/shared/varlink-io.systemd.MachineInstance.c
+++ b/src/shared/varlink-io.systemd.MachineInstance.c
@@ -25,8 +25,27 @@ static SD_VARLINK_DEFINE_METHOD_FULL(
SD_VARLINK_FIELD_COMMENT("Event-specific payload"),
SD_VARLINK_DEFINE_OUTPUT(data, SD_VARLINK_OBJECT, SD_VARLINK_NULLABLE));
+static SD_VARLINK_DEFINE_METHOD(
+ AddStorage,
+ SD_VARLINK_FIELD_COMMENT("Index of the attached file descriptor for the storage volume"),
+ SD_VARLINK_DEFINE_INPUT(fileDescriptorIndex, SD_VARLINK_INT, 0),
+ SD_VARLINK_FIELD_COMMENT("Unique storage name of the form ':' identifying this binding for later removal"),
+ SD_VARLINK_DEFINE_INPUT(name, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("Backend-specific configuration"),
+ SD_VARLINK_DEFINE_INPUT(config, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
+
+static SD_VARLINK_DEFINE_METHOD(
+ RemoveStorage,
+ SD_VARLINK_FIELD_COMMENT("Unique storage name ':' to detach"),
+ SD_VARLINK_DEFINE_INPUT(name, SD_VARLINK_STRING, 0));
+
static SD_VARLINK_DEFINE_ERROR(NotConnected);
static SD_VARLINK_DEFINE_ERROR(NotSupported);
+static SD_VARLINK_DEFINE_ERROR(NoSuchStorage);
+static SD_VARLINK_DEFINE_ERROR(StorageExists);
+static SD_VARLINK_DEFINE_ERROR(StorageImmutable);
+static SD_VARLINK_DEFINE_ERROR(BadConfig);
+static SD_VARLINK_DEFINE_ERROR(ConfigNotSupported);
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_MachineInstance,
@@ -45,7 +64,21 @@ SD_VARLINK_DEFINE_INTERFACE(
&vl_method_Describe,
SD_VARLINK_SYMBOL_COMMENT("Subscribe to machine events. Returns a stream of events as they occur."),
&vl_method_SubscribeEvents,
+ SD_VARLINK_SYMBOL_COMMENT("Attach a storage volume (passed via file descriptor) to the running machine"),
+ &vl_method_AddStorage,
+ SD_VARLINK_SYMBOL_COMMENT("Detach a previously-attached storage volume from the running machine"),
+ &vl_method_RemoveStorage,
SD_VARLINK_SYMBOL_COMMENT("The connection to the machine backend is not available"),
&vl_error_NotConnected,
SD_VARLINK_SYMBOL_COMMENT("The requested operation is not supported"),
- &vl_error_NotSupported);
+ &vl_error_NotSupported,
+ SD_VARLINK_SYMBOL_COMMENT("The named storage binding does not exist"),
+ &vl_error_NoSuchStorage,
+ SD_VARLINK_SYMBOL_COMMENT("A storage binding with this name already exists"),
+ &vl_error_StorageExists,
+ SD_VARLINK_SYMBOL_COMMENT("The storage binding cannot be detached at runtime (e.g. attached at boot)"),
+ &vl_error_StorageImmutable,
+ SD_VARLINK_SYMBOL_COMMENT("The supplied 'config' value is not valid for this backend"),
+ &vl_error_BadConfig,
+ SD_VARLINK_SYMBOL_COMMENT("The supplied 'config' value is recognized but not supported by this backend"),
+ &vl_error_ConfigNotSupported);
diff --git a/src/storage/meson.build b/src/storage/meson.build
index 21456141de..bcd68e612d 100644
--- a/src/storage/meson.build
+++ b/src/storage/meson.build
@@ -4,18 +4,15 @@ executables += [
libexec_template + {
'name' : 'systemd-storage-block',
'sources' : files('storage-block.c'),
- 'extract' : files('storage-util.c')
},
libexec_template + {
'name' : 'systemd-storage-fs',
'sources' : files('storage-fs.c'),
- 'objects' : ['systemd-storage-block'],
},
executable_template + {
'name' : 'storagectl',
'public' : true,
'sources' : files('storagectl.c'),
- 'objects' : ['systemd-storage-block'],
},
]
diff --git a/src/storage/storage-util.c b/src/storage/storage-util.c
deleted file mode 100644
index 793946c03a..0000000000
--- a/src/storage/storage-util.c
+++ /dev/null
@@ -1,23 +0,0 @@
-/* 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/storagectl.c b/src/storage/storagectl.c
index f88dff29bc..bbe09e01b1 100644
--- a/src/storage/storagectl.c
+++ b/src/storage/storagectl.c
@@ -19,7 +19,7 @@
#include "format-table.h"
#include "format-util.h"
#include "help-util.h"
-#include "json-util.h"
+#include "machine-util.h"
#include "main-func.h"
#include "mount-util.h"
#include "namespace-util.h"
@@ -524,7 +524,7 @@ static int run_as_mount_helper(int argc, char *argv[]) {
_cleanup_free_ char *filtered = NULL, *template = NULL;
CreateMode create_mode = _CREATE_MODE_INVALID;
uint64_t create_size = UINT64_MAX;
- int read_only = -1;
+ ReadOnlyMode read_only = _READ_ONLY_INVALID;
for (const char *p = options;;) {
_cleanup_free_ char *word = NULL;
@@ -555,9 +555,9 @@ static int run_as_mount_helper(int argc, char *argv[]) {
} else
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown mount option '%s', refusing.", word);
} else if (streq(word, "ro"))
- read_only = true;
+ read_only = READ_ONLY_YES;
else if (streq(word, "rw"))
- read_only = false;
+ read_only = READ_ONLY_NO;
else if (!strextend_with_separator(&filtered, ",", word))
return log_oom();
}
@@ -565,141 +565,69 @@ static int run_as_mount_helper(int argc, char *argv[]) {
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();
+ VolumeType requested_type = fstype ? VOLUME_REG : VOLUME_DIR;
- /* 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")) {
+ BindVolume bv = BIND_VOLUME_INIT;
+ bv.provider = provider;
+ bv.volume = name;
+ bv.create_mode = create_mode;
+ bv.template = template;
+ bv.read_only = read_only;
+ bv.request_as = requested_type;
+ bv.create_size_bytes = create_size;
- 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);
+ _cleanup_(storage_acquire_reply_done) StorageAcquireReply reply = STORAGE_ACQUIRE_REPLY_INIT;
+ _cleanup_free_ char *acquire_error_id = NULL;
+ r = storage_acquire_volume(arg_runtime_scope, &bv, arg_ask_password, &acquire_error_id, &reply);
+ if (r < 0 && fstype &&
+ STR_IN_SET(strna(acquire_error_id),
+ "io.systemd.StorageProvider.TypeNotSupported",
+ "io.systemd.StorageProvider.WrongType")) {
+ _cleanup_(storage_acquire_reply_done) StorageAcquireReply retry = STORAGE_ACQUIRE_REPLY_INIT;
+ assert(bv.request_as == VOLUME_REG);
+ bv.request_as = VOLUME_BLK;
+ int k = storage_acquire_volume(arg_runtime_scope, &bv, arg_ask_password, /* reterr_error_id= */ NULL, &retry);
+ if (k >= 0) {
+ storage_acquire_reply_done(&reply);
+ reply = retry;
+ retry = STORAGE_ACQUIRE_REPLY_INIT;
+ acquire_error_id = mfree(acquire_error_id);
+ requested_type = VOLUME_BLK;
+ r = 0;
}
}
- 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,
- };
+ if (r < 0) {
+ const char *eid = acquire_error_id;
- 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 },
- {}
- };
+ if (streq_ptr(eid, "io.systemd.StorageProvider.NoSuchVolume"))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Volume '%s' not known.", name);
+ if (streq_ptr(eid, "io.systemd.StorageProvider.NoSuchTemplate"))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Template '%s' not known.", template);
+ if (streq_ptr(eid, "io.systemd.StorageProvider.VolumeExists"))
+ return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Volume '%s' exists already.", name);
+ if (streq_ptr(eid, "io.systemd.StorageProvider.TypeNotSupported"))
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Storage provider does not support the specified volume type '%s'.", volume_type_to_string(requested_type));
+ if (streq_ptr(eid, "io.systemd.StorageProvider.WrongType"))
+ return log_error_errno(SYNTHETIC_ERRNO(EADDRNOTAVAIL), "Volume '%s' is not of type '%s'.", name, volume_type_to_string(requested_type));
+ if (streq_ptr(eid, "io.systemd.StorageProvider.CreateNotSupported"))
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Storage provider does not support creating volumes.");
+ if (streq_ptr(eid, "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_ptr(eid, "io.systemd.StorageProvider.ReadOnlyVolume"))
+ return log_error_errno(SYNTHETIC_ERRNO(EROFS), "Volume '%s' is read-only.", name);
+ if (streq_ptr(eid, "io.systemd.StorageProvider.BadTemplate"))
+ return log_error_errno(SYNTHETIC_ERRNO(EADDRNOTAVAIL), "Template does not apply to this volume type.");
- 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");
+ if (eid)
+ return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call (%s): %m", eid);
+ return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
+ }
struct stat st;
- if (fstat(fd, &st) < 0)
+ if (fstat(reply.fd, &st) < 0)
return log_error_errno(errno, "Failed to stat returned file descriptor: %m");
_cleanup_strv_free_ char **cmdline = strv_new("mount", "-c");
@@ -707,7 +635,7 @@ static int run_as_mount_helper(int argc, char *argv[]) {
return log_oom();
if (fstype) {
- if (!STR_IN_SET(p.type, "reg", "blk"))
+ if (!IN_SET(reply.type, VOLUME_REG, VOLUME_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);
@@ -717,31 +645,31 @@ static int run_as_mount_helper(int argc, char *argv[]) {
if (strv_extend_strv(&cmdline, STRV_MAKE("-t", fstype), /* filter_duplicates= */ false) < 0)
return log_oom();
} else {
- if (!streq(p.type, "dir"))
+ if (reply.type != VOLUME_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))
+ if (!uid_is_valid(reply.base_uid) || !gid_is_valid(reply.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)
+ if (reply.base_uid > UINT32_MAX - USERNS_RANGE_SIZE ||
+ reply.base_gid > UINT32_MAX - USERNS_RANGE_SIZE)
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)
+ if (st.st_uid < reply.base_uid || st.st_uid >= reply.base_uid + USERNS_RANGE_SIZE ||
+ st.st_gid < reply.base_gid || st.st_gid >= reply.base_gid + USERNS_RANGE_SIZE)
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);
+ reply.base_uid, (uid_t) 0, USERNS_RANGE_SIZE);
_cleanup_free_ char *gid_line = asprintf_safe(
GID_FMT " " GID_FMT " " GID_FMT "\n",
- p.base_gid, (gid_t) 0, (gid_t) 0x10000);
+ reply.base_gid, (gid_t) 0, USERNS_RANGE_SIZE);
if (!uid_line || !gid_line)
return log_oom();
@@ -750,7 +678,7 @@ static int run_as_mount_helper(int argc, char *argv[]) {
return log_error_errno(userns_fd, "Failed to acquire new user namespace: %m");
_cleanup_close_ int remapped_fd = open_tree_attr_with_fallback(
- fd,
+ reply.fd,
/* path= */ NULL,
OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC,
&(struct mount_attr) {
@@ -760,25 +688,25 @@ static int run_as_mount_helper(int argc, char *argv[]) {
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);
+ close_and_replace(reply.fd, remapped_fd);
if (strv_extend(&cmdline, "--bind") < 0)
return log_oom();
}
- if (p.read_only > 0)
- read_only = true;
+ if (reply.read_only > 0)
+ read_only = READ_ONLY_YES;
- if (!strextend_with_separator(&filtered, ",", read_only > 0 ? "ro" : "rw"))
+ if (!strextend_with_separator(&filtered, ",", read_only == READ_ONLY_YES ? "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)
+ if (strv_extend_strv(&cmdline, STRV_MAKE(FORMAT_PROC_FD_PATH(reply.fd), path), /* filter_duplicates= */ false) < 0)
return log_oom();
- r = fd_cloexec(fd, false);
+ r = fd_cloexec(reply.fd, false);
if (r < 0)
return log_error_errno(r, "Failed to disable O_CLOEXEC for mount fd: %m");
diff --git a/src/test/meson.build b/src/test/meson.build
index 09c367d307..f4288119f9 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -141,6 +141,7 @@ simple_tests += files(
'test-log.c',
'test-logarithm.c',
'test-login-util.c',
+ 'test-machine-util.c',
'test-macro.c',
'test-memfd-util.c',
'test-memory-util.c',
diff --git a/src/test/test-machine-util.c b/src/test/test-machine-util.c
new file mode 100644
index 0000000000..8774be2189
--- /dev/null
+++ b/src/test/test-machine-util.c
@@ -0,0 +1,144 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "machine-util.h"
+#include "tests.h"
+
+TEST(bind_volume_parse_minimal) {
+ _cleanup_(bind_volume_freep) BindVolume *v = NULL;
+
+ ASSERT_OK(bind_volume_parse("block:/dev/sda", &v));
+ ASSERT_STREQ(v->provider, "block");
+ ASSERT_STREQ(v->volume, "/dev/sda");
+ ASSERT_NULL(v->config);
+ ASSERT_NULL(v->template);
+ ASSERT_EQ(v->create_mode, _CREATE_MODE_INVALID);
+ ASSERT_EQ(v->request_as, _VOLUME_TYPE_INVALID);
+ ASSERT_EQ(v->read_only, _READ_ONLY_INVALID);
+ ASSERT_EQ(v->create_size_bytes, UINT64_MAX);
+}
+
+TEST(bind_volume_parse_with_config) {
+ _cleanup_(bind_volume_freep) BindVolume *v = NULL;
+
+ ASSERT_OK(bind_volume_parse("block:/dev/sda:virtio-scsi", &v));
+ ASSERT_STREQ(v->provider, "block");
+ ASSERT_STREQ(v->volume, "/dev/sda");
+ ASSERT_STREQ(v->config, "virtio-scsi");
+}
+
+TEST(bind_volume_parse_empty_config) {
+ _cleanup_(bind_volume_freep) BindVolume *v = NULL;
+
+ ASSERT_OK(bind_volume_parse("fs:vol-1::create=new,size=64M,template=sparse-file", &v));
+ ASSERT_STREQ(v->provider, "fs");
+ ASSERT_STREQ(v->volume, "vol-1");
+ ASSERT_NULL(v->config);
+ ASSERT_EQ(v->create_mode, CREATE_NEW);
+ ASSERT_STREQ(v->template, "sparse-file");
+ ASSERT_EQ(v->create_size_bytes, UINT64_C(64) * 1024 * 1024);
+}
+
+TEST(bind_volume_parse_full) {
+ _cleanup_(bind_volume_freep) BindVolume *v = NULL;
+
+ ASSERT_OK(bind_volume_parse(
+ "fs:vol-2:nvme:create=any,template=allocated-file,size=128M,ro=auto,request-as=blk",
+ &v));
+ ASSERT_STREQ(v->provider, "fs");
+ ASSERT_STREQ(v->volume, "vol-2");
+ ASSERT_STREQ(v->config, "nvme");
+ ASSERT_EQ(v->create_mode, CREATE_ANY);
+ ASSERT_STREQ(v->template, "allocated-file");
+ ASSERT_EQ(v->request_as, VOLUME_BLK);
+ ASSERT_EQ(v->create_size_bytes, UINT64_C(128) * 1024 * 1024);
+ ASSERT_EQ(v->read_only, READ_ONLY_AUTO);
+}
+
+TEST(bind_volume_parse_read_only) {
+ _cleanup_(bind_volume_freep) BindVolume *v = NULL;
+
+ ASSERT_OK(bind_volume_parse("block:/dev/sdb:scsi-cd:read-only=yes", &v));
+ ASSERT_EQ(v->read_only, READ_ONLY_YES);
+
+ v = bind_volume_free(v);
+ ASSERT_OK(bind_volume_parse("block:/dev/sdb:scsi-cd:ro=no", &v));
+ ASSERT_EQ(v->read_only, READ_ONLY_NO);
+}
+
+TEST(bind_volume_parse_invalid) {
+ BindVolume *v = NULL;
+
+ /* Missing provider */
+ ASSERT_ERROR(bind_volume_parse(":vol", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Missing volume */
+ ASSERT_ERROR(bind_volume_parse("block:", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Provider with control char */
+ ASSERT_ERROR(bind_volume_parse("bl\x01ock:vol", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Config with control char */
+ ASSERT_ERROR(bind_volume_parse("block:vol:nv\x01me", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Unknown extras key */
+ ASSERT_ERROR(bind_volume_parse("block:vol::bogus=foo", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Bogus create mode */
+ ASSERT_ERROR(bind_volume_parse("block:vol::create=bogus", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Bogus request-as */
+ ASSERT_ERROR(bind_volume_parse("block:vol::request-as=bogus", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Extras entry without '=' */
+ ASSERT_ERROR(bind_volume_parse("block:vol::nokey", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Empty key (=value with no key) */
+ ASSERT_ERROR(bind_volume_parse("block:vol::=value", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Duplicate key */
+ ASSERT_ERROR(bind_volume_parse("block:vol::create=new,create=any", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Aliased duplicate (size / create-size) */
+ ASSERT_ERROR(bind_volume_parse("block:vol::size=64M,create-size=128M", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Zero-byte size */
+ ASSERT_ERROR(bind_volume_parse("block:vol::size=0", &v), EINVAL);
+ ASSERT_NULL(v);
+
+ /* Duplicate read-only with explicit yes/no values */
+ ASSERT_ERROR(bind_volume_parse("block:vol::read-only=yes,read-only=no", &v), EINVAL);
+ ASSERT_NULL(v);
+ ASSERT_ERROR(bind_volume_parse("block:vol::read-only=yes,ro=auto", &v), EINVAL);
+ ASSERT_NULL(v);
+}
+
+TEST(machine_storage_name_split) {
+ _cleanup_free_ char *p = NULL, *v = NULL;
+
+ ASSERT_OK(machine_storage_name_split("block:/dev/sda", &p, &v));
+ ASSERT_STREQ(p, "block");
+ ASSERT_STREQ(v, "/dev/sda");
+
+ /* NULL outputs — validate-only mode */
+ ASSERT_OK(machine_storage_name_split("fs:vol-1", NULL, NULL));
+
+ ASSERT_ERROR(machine_storage_name_split(NULL, NULL, NULL), EINVAL);
+ ASSERT_ERROR(machine_storage_name_split("", NULL, NULL), EINVAL);
+ ASSERT_ERROR(machine_storage_name_split("no-colon", NULL, NULL), EINVAL);
+ ASSERT_ERROR(machine_storage_name_split(":vol", NULL, NULL), EINVAL);
+ ASSERT_ERROR(machine_storage_name_split("block:", NULL, NULL), EINVAL);
+ ASSERT_ERROR(machine_storage_name_split("bl\x01ock:vol", NULL, NULL), EINVAL);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/vmspawn/meson.build b/src/vmspawn/meson.build
index 6d08755fed..6bc31c77c6 100644
--- a/src/vmspawn/meson.build
+++ b/src/vmspawn/meson.build
@@ -6,6 +6,7 @@ endif
vmspawn_sources = files(
'vmspawn.c',
+ 'vmspawn-bind-volume.c',
'vmspawn-qemu-config.c',
'vmspawn-qmp.c',
'vmspawn-varlink.c',
diff --git a/src/vmspawn/vmspawn-bind-volume.c b/src/vmspawn/vmspawn-bind-volume.c
new file mode 100644
index 0000000000..d67fc61de0
--- /dev/null
+++ b/src/vmspawn/vmspawn-bind-volume.c
@@ -0,0 +1,202 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+#include
+
+#include "sd-varlink.h"
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "log.h"
+#include "runtime-scope.h"
+#include "stat-util.h"
+#include "storage-util.h"
+#include "string-util.h"
+#include "vmspawn-bind-volume.h"
+#include "vmspawn-qmp.h"
+
+DiskType disk_type_from_bind_volume_config(const char *config) {
+ if (isempty(config))
+ return DISK_TYPE_VIRTIO_BLK;
+ return disk_type_from_string(config);
+}
+
+int vmspawn_bind_volume_acquire(
+ RuntimeScope scope,
+ const BindVolume *v,
+ bool removable,
+ sd_varlink *link,
+ DriveInfo **ret,
+ char **reterr_error_id) {
+
+ _cleanup_(storage_acquire_reply_done) StorageAcquireReply reply = STORAGE_ACQUIRE_REPLY_INIT;
+ _cleanup_(drive_info_unrefp) DriveInfo *d = NULL;
+ _cleanup_free_ char *err = NULL;
+ int r;
+
+ assert(v);
+ assert(ret);
+
+ DiskType dt = disk_type_from_bind_volume_config(v->config);
+ if (dt < 0) {
+ r = dt;
+ goto fail;
+ }
+
+ r = storage_acquire_volume(scope, v, /* allow_interactive_auth= */ false, &err, &reply);
+ if (r < 0)
+ goto fail;
+
+ if (reply.type == VOLUME_DIR) {
+ r = log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
+ "Directory volumes are not supported for vmspawn block devices.");
+ goto fail;
+ }
+
+ struct stat st;
+ if (fstat(reply.fd, &st) < 0) {
+ r = -errno;
+ goto fail;
+ }
+ r = stat_verify_regular_or_block(&st);
+ if (r < 0)
+ goto fail;
+
+ d = drive_info_new();
+ if (!d) {
+ r = -ENOMEM;
+ goto fail;
+ }
+
+ d->id = strjoin(v->provider, ":", v->volume);
+ d->disk_driver = strdup(ASSERT_PTR(qemu_device_driver_to_string(dt)));
+ d->format = strdup("raw");
+ d->path = strdup(v->volume);
+ if (!d->id || !d->disk_driver || !d->format || !d->path) {
+ r = -ENOMEM;
+ goto fail;
+ }
+
+ d->disk_type = dt;
+ d->fd = TAKE_FD(reply.fd);
+
+ if (reply.type == VOLUME_BLK || S_ISBLK(st.st_mode))
+ d->flags |= QMP_DRIVE_BLOCK_DEVICE;
+ if (reply.read_only > 0 || dt == DISK_TYPE_VIRTIO_SCSI_CDROM)
+ d->flags |= QMP_DRIVE_READ_ONLY;
+ if (removable)
+ d->flags |= QMP_DRIVE_REMOVABLE;
+ d->link = sd_varlink_ref(link);
+
+ *ret = TAKE_PTR(d);
+ return 0;
+
+fail:
+ if (reterr_error_id)
+ *reterr_error_id = TAKE_PTR(err);
+ return r;
+}
+
+/* Takes ownership of fd unconditionally — it is closed on every error path too. */
+int vmspawn_bind_volume_attach_fd(
+ VmspawnQmpBridge *bridge,
+ sd_varlink *link,
+ int fd,
+ const char *name,
+ const char *config) {
+
+ _cleanup_close_ int owned_fd = fd;
+ int r;
+
+ assert(bridge);
+ assert(link);
+ assert(fd >= 0);
+ assert(name);
+
+ DiskType dt = disk_type_from_bind_volume_config(config);
+ if (dt < 0)
+ return dt;
+
+ struct stat st;
+ if (fstat(owned_fd, &st) < 0)
+ return -errno;
+ r = stat_verify_regular_or_block(&st);
+ if (r < 0)
+ return r;
+
+ _cleanup_(drive_info_unrefp) DriveInfo *d = drive_info_new();
+ if (!d)
+ return -ENOMEM;
+
+ d->id = strdup(name);
+ d->disk_driver = strdup(ASSERT_PTR(qemu_device_driver_to_string(dt)));
+ d->format = strdup("raw");
+ d->path = strdup(name);
+ if (!d->id || !d->disk_driver || !d->format || !d->path)
+ return -ENOMEM;
+
+ int oflags = fcntl(owned_fd, F_GETFL);
+ if (oflags < 0)
+ return -errno;
+
+ d->disk_type = dt;
+ d->fd = TAKE_FD(owned_fd);
+ if (S_ISBLK(st.st_mode))
+ d->flags |= QMP_DRIVE_BLOCK_DEVICE;
+ if (dt == DISK_TYPE_VIRTIO_SCSI_CDROM || (oflags & O_ACCMODE_STRICT) == O_RDONLY)
+ d->flags |= QMP_DRIVE_READ_ONLY;
+ d->flags |= QMP_DRIVE_REMOVABLE;
+ d->link = sd_varlink_ref(link);
+
+ return vmspawn_qmp_add_block_device(bridge, TAKE_PTR(d));
+}
+
+void bind_volumes_done(BindVolumes *bv) {
+ assert(bv);
+ FOREACH_ARRAY(v, bv->items, bv->n_items)
+ bind_volume_free(*v);
+ bv->items = mfree(bv->items);
+ bv->n_items = 0;
+}
+
+int vmspawn_bind_volume_prepare_boot(
+ RuntimeScope scope,
+ const BindVolumes *bv,
+ DriveInfos *drives) {
+
+ int r;
+
+ assert(bv);
+ assert(drives);
+
+ if (bv->n_items == 0)
+ return 0;
+
+ if (!GREEDY_REALLOC(drives->drives, drives->n_drives + bv->n_items))
+ return log_oom();
+
+ FOREACH_ARRAY(it, bv->items, bv->n_items) {
+ BindVolume *v = *it;
+ _cleanup_(drive_info_unrefp) DriveInfo *d = NULL;
+ _cleanup_free_ char *error_id = NULL;
+
+ r = vmspawn_bind_volume_acquire(
+ scope, v,
+ /* removable= */ false,
+ /* link= */ NULL,
+ &d, &error_id);
+ if (r < 0) {
+ if (error_id)
+ return log_error_errno(r,
+ "Failed to acquire storage volume '%s:%s' (%s): %m",
+ v->provider, v->volume, error_id);
+ return log_error_errno(r,
+ "Failed to acquire storage volume '%s:%s': %m",
+ v->provider, v->volume);
+ }
+
+ drives->drives[drives->n_drives++] = TAKE_PTR(d);
+ }
+
+ return 0;
+}
diff --git a/src/vmspawn/vmspawn-bind-volume.h b/src/vmspawn/vmspawn-bind-volume.h
new file mode 100644
index 0000000000..23b3ff52f3
--- /dev/null
+++ b/src/vmspawn/vmspawn-bind-volume.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "machine-util.h"
+#include "shared-forward.h"
+#include "vmspawn-qmp.h"
+
+/* Empty/NULL defaults to virtio-blk; otherwise delegates to disk_type_from_string(). */
+DiskType disk_type_from_bind_volume_config(const char *config);
+
+/* Acquires the volume and builds a DriveInfo with id=":" (the
+ * bridge-visible name; QMP-side names are still allocated by add_block_device). */
+int vmspawn_bind_volume_acquire(
+ RuntimeScope scope,
+ const BindVolume *v,
+ bool removable,
+ sd_varlink *link,
+ DriveInfo **ret,
+ char **reterr_error_id);
+
+typedef struct BindVolumes {
+ BindVolume **items;
+ size_t n_items;
+} BindVolumes;
+
+void bind_volumes_done(BindVolumes *bv);
+
+int vmspawn_bind_volume_prepare_boot(
+ RuntimeScope scope,
+ const BindVolumes *bv,
+ DriveInfos *drives);
+
+/* Takes ownership of fd unconditionally. */
+int vmspawn_bind_volume_attach_fd(
+ VmspawnQmpBridge *bridge,
+ sd_varlink *link,
+ int fd,
+ const char *name,
+ const char *config);
diff --git a/src/vmspawn/vmspawn-qmp.c b/src/vmspawn/vmspawn-qmp.c
index d09576213a..52d9e11b9f 100644
--- a/src/vmspawn/vmspawn-qmp.c
+++ b/src/vmspawn/vmspawn-qmp.c
@@ -826,7 +826,7 @@ static int on_add_device_add_complete(
return 0;
if (d->link) {
- (void) sd_varlink_replybo(d->link, SD_JSON_BUILD_PAIR_STRING("id", d->id));
+ (void) sd_varlink_reply(d->link, NULL);
d->link = sd_varlink_unref(d->link);
}
@@ -882,7 +882,7 @@ static int qmp_setup_scsi_controller(VmspawnQmpBridge *bridge, const char *pcie_
return 0;
}
-static int vmspawn_qmp_add_block_device(VmspawnQmpBridge *bridge, DriveInfo *drive) {
+int vmspawn_qmp_add_block_device(VmspawnQmpBridge *bridge, DriveInfo *drive) {
int r;
assert(bridge);
@@ -989,7 +989,6 @@ static int qmp_setup_regular_drive(VmspawnQmpBridge *bridge, DriveInfo *drive) {
assert(bridge);
assert(drive);
assert(drive->fd >= 0);
- assert(!drive->id);
return vmspawn_qmp_add_block_device(bridge, drive);
}
@@ -1028,7 +1027,9 @@ int vmspawn_qmp_remove_block_device(VmspawnQmpBridge *bridge, sd_varlink *link,
DriveInfo *drive = hashmap_get(bridge->block_devices, id);
if (!drive)
- return reply_qmp_error(link, "Unknown block device id", -ENOENT);
+ return sd_varlink_error(link, "io.systemd.MachineInstance.NoSuchStorage", NULL);
+ if (!FLAGS_SET(drive->flags, QMP_DRIVE_REMOVABLE))
+ return sd_varlink_error(link, "io.systemd.MachineInstance.StorageImmutable", NULL);
if (!FLAGS_SET(drive->state, BLOCK_DEVICE_STATE_BLOCKDEV_ADDED))
return reply_qmp_error(link, "Block device add pending", -EBUSY);
if (FLAGS_SET(drive->state, BLOCK_DEVICE_STATE_REMOVE_PENDING))
diff --git a/src/vmspawn/vmspawn-qmp.h b/src/vmspawn/vmspawn-qmp.h
index d8403520c9..a2f9297320 100644
--- a/src/vmspawn/vmspawn-qmp.h
+++ b/src/vmspawn/vmspawn-qmp.h
@@ -72,6 +72,7 @@ typedef enum QmpDriveFlags {
QMP_DRIVE_BOOT = 1u << 4,
QMP_DRIVE_IO_URING = 1u << 5,
QMP_DRIVE_DISCARD_NO_UNREF = 1u << 6, /* qcow2 only */
+ QMP_DRIVE_REMOVABLE = 1u << 7, /* may be detached at runtime via RemoveStorage */
} QmpDriveFlags;
typedef enum BlockDeviceStateFlags {
@@ -177,5 +178,6 @@ int vmspawn_qmp_setup_drives(VmspawnQmpBridge *bridge, DriveInfos *drives);
int vmspawn_qmp_setup_network(VmspawnQmpBridge *bridge, NetworkInfo *network);
int vmspawn_qmp_setup_virtiofs(VmspawnQmpBridge *bridge, const VirtiofsInfos *virtiofs);
int vmspawn_qmp_setup_vsock(VmspawnQmpBridge *bridge, VsockInfo *vsock);
+int vmspawn_qmp_add_block_device(VmspawnQmpBridge *bridge, DriveInfo *drive);
int vmspawn_qmp_remove_block_device(VmspawnQmpBridge *bridge, sd_varlink *link, const char *id);
int vmspawn_qmp_dispatch_device_deleted(VmspawnQmpBridge *bridge, sd_json_variant *data);
diff --git a/src/vmspawn/vmspawn-varlink.c b/src/vmspawn/vmspawn-varlink.c
index 9aa6afeae0..a782a5c9c1 100644
--- a/src/vmspawn/vmspawn-varlink.c
+++ b/src/vmspawn/vmspawn-varlink.c
@@ -2,6 +2,7 @@
#include "alloc-util.h"
#include "errno-util.h"
+#include "fd-util.h"
#include "hashmap.h"
#include "log.h"
#include "path-util.h"
@@ -11,6 +12,7 @@
#include "varlink-io.systemd.MachineInstance.h"
#include "varlink-io.systemd.VirtualMachineInstance.h"
#include "varlink-util.h"
+#include "vmspawn-bind-volume.h"
#include "vmspawn-qmp.h"
#include "vmspawn-varlink.h"
@@ -168,6 +170,77 @@ static int vl_method_describe(sd_varlink *link, sd_json_variant *parameters, sd_
return qmp_execute_varlink_async(ctx, link, "query-status", /* arguments= */ NULL, on_qmp_describe_complete);
}
+static int vl_method_add_storage(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata);
+ int r;
+
+ struct {
+ int fd_index;
+ const char *name;
+ const char *config;
+ } p = {
+ .fd_index = -1,
+ };
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "fileDescriptorIndex", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_int, voffsetof(p, fd_index), SD_JSON_MANDATORY },
+ { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), SD_JSON_MANDATORY },
+ { "config", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, config), 0 },
+ {}
+ };
+
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
+ if (r != 0)
+ return r;
+
+ if (machine_storage_name_split(p.name, /* ret_provider= */ NULL, /* ret_volume= */ NULL) < 0)
+ return sd_varlink_error_invalid_parameter_name(link, "name");
+
+ if (disk_type_from_bind_volume_config(p.config) < 0)
+ return sd_varlink_error(link, "io.systemd.MachineInstance.BadConfig", NULL);
+
+ if (p.fd_index < 0)
+ return sd_varlink_error_invalid_parameter_name(link, "fileDescriptorIndex");
+
+ _cleanup_close_ int fd = sd_varlink_take_fd(link, p.fd_index);
+ if (fd < 0)
+ return sd_varlink_error_errno(link, fd);
+
+ r = vmspawn_bind_volume_attach_fd(ctx->bridge, link, TAKE_FD(fd), p.name, p.config);
+ if (r == -EEXIST)
+ return sd_varlink_error(link, "io.systemd.MachineInstance.StorageExists", NULL);
+ if (r == -EOPNOTSUPP)
+ return sd_varlink_error(link, "io.systemd.MachineInstance.ConfigNotSupported", NULL);
+ if (r < 0)
+ return sd_varlink_error_errno(link, r);
+
+ /* Async reply via on_add_device_add_complete. */
+ return 0;
+}
+
+static int vl_method_remove_storage(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata);
+ int r;
+
+ struct {
+ const char *name;
+ } p = {};
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), SD_JSON_MANDATORY },
+ {}
+ };
+
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
+ if (r != 0)
+ return r;
+
+ if (machine_storage_name_split(p.name, /* ret_provider= */ NULL, /* ret_volume= */ NULL) < 0)
+ return sd_varlink_error_invalid_parameter_name(link, "name");
+
+ return vmspawn_qmp_remove_block_device(ctx->bridge, link, p.name);
+}
+
static int vl_method_subscribe_events(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
VmspawnVarlinkContext *ctx = ASSERT_PTR(userdata);
_cleanup_strv_free_ char **filter = NULL;
@@ -380,9 +453,10 @@ int vmspawn_varlink_setup(
if (!ctx)
return log_oom();
- /* Create varlink server for VM control */
+ /* AddStorage receives an fd from the caller. */
r = varlink_server_new(&ctx->varlink_server,
- SD_VARLINK_SERVER_INHERIT_USERDATA,
+ SD_VARLINK_SERVER_INHERIT_USERDATA |
+ SD_VARLINK_SERVER_ALLOW_FD_PASSING_INPUT,
ctx);
if (r < 0)
return log_error_errno(r, "Failed to create varlink server: %m");
@@ -402,7 +476,9 @@ int vmspawn_varlink_setup(
"io.systemd.MachineInstance.Resume", vl_method_resume,
"io.systemd.MachineInstance.Reboot", vl_method_reboot,
"io.systemd.MachineInstance.Describe", vl_method_describe,
- "io.systemd.MachineInstance.SubscribeEvents", vl_method_subscribe_events);
+ "io.systemd.MachineInstance.SubscribeEvents", vl_method_subscribe_events,
+ "io.systemd.MachineInstance.AddStorage", vl_method_add_storage,
+ "io.systemd.MachineInstance.RemoveStorage", vl_method_remove_storage);
if (r < 0)
return log_error_errno(r, "Failed to bind varlink methods: %m");
diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c
index 81c035c250..ee8ae518f0 100644
--- a/src/vmspawn/vmspawn.c
+++ b/src/vmspawn/vmspawn.c
@@ -88,6 +88,7 @@
#include "user-record.h"
#include "user-util.h"
#include "utf8.h"
+#include "vmspawn-bind-volume.h"
#include "vmspawn-mount.h"
#include "vmspawn-qemu-config.h"
#include "vmspawn-qmp.h"
@@ -163,6 +164,7 @@ static bool arg_keep_unit = false;
static sd_id128_t arg_uuid = {};
static char **arg_kernel_cmdline_extra = NULL;
static ExtraDriveContext arg_extra_drives = {};
+static BindVolumes arg_bind_volumes = {};
static char *arg_background = NULL;
static bool arg_pass_ssh_key = true;
static char *arg_ssh_key_type = NULL;
@@ -200,6 +202,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_runtime_mounts, runtime_mount_context_done);
STATIC_DESTRUCTOR_REGISTER(arg_forward_journal, freep);
STATIC_DESTRUCTOR_REGISTER(arg_kernel_cmdline_extra, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_extra_drives, extra_drive_context_done);
+STATIC_DESTRUCTOR_REGISTER(arg_bind_volumes, bind_volumes_done);
STATIC_DESTRUCTOR_REGISTER(arg_background, freep);
STATIC_DESTRUCTOR_REGISTER(arg_ssh_key_type, freep);
STATIC_DESTRUCTOR_REGISTER(arg_smbios11, strv_freep);
@@ -766,6 +769,30 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
+ OPTION_LONG("bind-volume", "PROVIDER:VOLUME[:CONFIG][:KEY=VALUE,...]",
+ "Acquire a storage volume from a StorageProvider and attach it to the VM"): {
+ _cleanup_(bind_volume_freep) BindVolume *bv = NULL;
+
+ r = bind_volume_parse(opts.arg, &bv);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --bind-volume= argument '%s': %m", opts.arg);
+
+ if (disk_type_from_bind_volume_config(bv->config) < 0) {
+ _cleanup_free_ char *valid = NULL;
+ for (DiskType t = 0; t < _DISK_TYPE_MAX; t++)
+ if (!strextend_with_separator(&valid, ", ", disk_type_to_string(t)))
+ return log_oom();
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Unknown device type '%s' for --bind-volume=. Valid values: %s.",
+ bv->config, valid);
+ }
+
+ if (!GREEDY_REALLOC(arg_bind_volumes.items, arg_bind_volumes.n_items + 1))
+ return log_oom();
+ arg_bind_volumes.items[arg_bind_volumes.n_items++] = TAKE_PTR(bv);
+ break;
+ }
+
OPTION_LONG("bind-user", "NAME", "Bind user from host to virtual machine"):
if (!valid_user_group_name(opts.arg, /* flags= */ 0))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid user name to bind: %s", opts.arg);
@@ -2475,7 +2502,7 @@ static int prepare_device_info(const char *runtime_dir, MachineConfig *c) {
/* Build drive info for QMP-based setup. vmspawn opens all image files and
* passes fds to QEMU via add-fd — QEMU never needs filesystem access. */
- drives->drives = new0(DriveInfo*, 1 + arg_extra_drives.n_drives);
+ drives->drives = new0(DriveInfo*, 1 + arg_extra_drives.n_drives + arg_bind_volumes.n_items);
if (!drives->drives)
return log_oom();
@@ -2487,6 +2514,10 @@ static int prepare_device_info(const char *runtime_dir, MachineConfig *c) {
if (r < 0)
return r;
+ r = vmspawn_bind_volume_prepare_boot(arg_runtime_scope, &arg_bind_volumes, drives);
+ if (r < 0)
+ return r;
+
return assign_pcie_ports(c);
}
diff --git a/test/units/TEST-87-AUX-UTILS-VM.bind-volume.sh b/test/units/TEST-87-AUX-UTILS-VM.bind-volume.sh
new file mode 100755
index 0000000000..6339e39093
--- /dev/null
+++ b/test/units/TEST-87-AUX-UTILS-VM.bind-volume.sh
@@ -0,0 +1,166 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+# Test --bind-volume / machinectl bind-volume / unbind-volume integration with the
+# StorageProvider Varlink interface.
+#
+# Exercises:
+# - --bind-volume parser + runtime_directory_generic + Acquire round-trip
+# - boot-time attach via DriveInfo (non-removable)
+# - runtime hotplug via io.systemd.MachineInstance.AddStorage (removable)
+# - runtime hot-remove via io.systemd.MachineInstance.RemoveStorage
+# - StorageImmutable rejection for boot-time attached volumes
+set -eux
+set -o pipefail
+
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+if [[ -v ASAN_OPTIONS ]]; then
+ echo "vmspawn launches QEMU which doesn't work under ASan, skipping"
+ exit 0
+fi
+
+if ! command -v systemd-vmspawn >/dev/null 2>&1; then
+ echo "systemd-vmspawn not found, skipping"
+ exit 0
+fi
+
+if ! command -v storagectl >/dev/null 2>&1; then
+ echo "storagectl not found, skipping"
+ exit 0
+fi
+
+if ! find_qemu_binary; then
+ echo "QEMU not found, skipping"
+ exit 0
+fi
+
+if ! command -v mke2fs >/dev/null 2>&1; then
+ echo "mke2fs not found, skipping"
+ exit 0
+fi
+
+# Storage providers are socket-activated; skip if the fs provider socket isn't present.
+if ! test -S /run/systemd/io.systemd.StorageProvider/fs; then
+ echo "StorageProvider fs socket not found, skipping"
+ exit 0
+fi
+
+# Find a kernel for direct boot
+KERNEL=""
+for k in /usr/lib/modules/"$(uname -r)"/vmlinuz /boot/vmlinuz-"$(uname -r)" /boot/vmlinuz; do
+ if [[ -f "$k" ]]; then
+ KERNEL="$k"
+ break
+ fi
+done
+
+if [[ -z "$KERNEL" ]]; then
+ echo "No kernel found for direct VM boot, skipping"
+ exit 0
+fi
+
+WORKDIR="$(mktemp -d /tmp/test-bind-volume.XXXXXXXXXX)"
+
+at_exit() {
+ set +e
+ if [[ -n "${MACHINE:-}" ]]; then
+ if machinectl status "$MACHINE" &>/dev/null; then
+ machinectl terminate "$MACHINE" 2>/dev/null
+ timeout 10 bash -c "while machinectl status '$MACHINE' &>/dev/null; do sleep .5; done" 2>/dev/null
+ fi
+ fi
+ [[ -n "${VMSPAWN_PID:-}" ]] && { kill "$VMSPAWN_PID" 2>/dev/null; wait "$VMSPAWN_PID" 2>/dev/null; }
+ rm -rf "$WORKDIR"
+ rm -f /var/lib/storage/test-bind-volume-*.volume
+}
+trap at_exit EXIT
+
+# Build a minimal root for direct boot — guest just sleeps.
+mkdir -p "$WORKDIR/rootfs/sbin"
+cat >"$WORKDIR/rootfs/sbin/init" <<'INITEOF'
+#!/bin/sh
+exec sleep infinity
+INITEOF
+chmod +x "$WORKDIR/rootfs/sbin/init"
+
+truncate -s 256M "$WORKDIR/root.raw"
+mke2fs -t ext4 -q -d "$WORKDIR/rootfs" "$WORKDIR/root.raw"
+
+BOOT_VOL="test-bind-volume-boot-$$"
+RUNTIME_VOL="test-bind-volume-runtime-$$"
+
+wait_for_machine() {
+ local machine="$1" pid="$2" log="$3"
+ timeout 30 bash -c "
+ while ! machinectl list --no-legend 2>/dev/null | grep >/dev/null '$machine'; do
+ if ! kill -0 $pid 2>/dev/null; then
+ echo 'vmspawn exited before machine registration'
+ cat '$log'
+ exit 77
+ fi
+ sleep .5
+ done
+ " || {
+ local rc=$?
+ if [[ $rc -eq 77 ]]; then exit 0; fi
+ exit "$rc"
+ }
+}
+
+# --- Boot the VM with one boot-time bind-volume ---
+MACHINE="test-bind-volume-$$"
+systemd-vmspawn \
+ --machine="$MACHINE" \
+ --ram=256M \
+ --image="$WORKDIR/root.raw" \
+ --bind-volume="fs:${BOOT_VOL}::create=new,size=64M,template=sparse-file" \
+ --linux="$KERNEL" \
+ --tpm=no \
+ --console=headless \
+ root=/dev/vda rw \
+ &>"$WORKDIR/vmspawn.log" &
+VMSPAWN_PID=$!
+
+wait_for_machine "$MACHINE" "$VMSPAWN_PID" "$WORKDIR/vmspawn.log"
+echo "Machine '$MACHINE' registered"
+
+VARLINK_ADDR=$(varlinkctl call /run/systemd/machine/io.systemd.Machine \
+ io.systemd.Machine.List "{\"name\":\"$MACHINE\"}" | jq -r '.controlAddress')
+assert_neq "$VARLINK_ADDR" "null"
+
+varlinkctl call "$VARLINK_ADDR" io.systemd.MachineInstance.Describe '{}' \
+ | jq -e '.running == true' >/dev/null
+echo "VM running with boot-time bind-volume attached"
+
+# --- Hot-add a second volume via machinectl bind-volume (must succeed) ---
+machinectl bind-volume "$MACHINE" \
+ "fs:${RUNTIME_VOL}:virtio-scsi:create=new,size=32M,template=sparse-file"
+echo "Hot-added runtime bind-volume succeeded"
+
+# --- Hot-remove the runtime-added volume (must succeed) ---
+machinectl unbind-volume "$MACHINE" "fs:${RUNTIME_VOL}"
+echo "Hot-removed runtime bind-volume succeeded"
+
+# --- Removing the boot-time volume must fail with StorageImmutable ---
+if machinectl unbind-volume "$MACHINE" "fs:${BOOT_VOL}" 2>"$WORKDIR/unbind.err"; then
+ echo "ERROR: unbind-volume of boot-time volume should have failed"
+ cat "$WORKDIR/unbind.err"
+ exit 1
+fi
+grep StorageImmutable "$WORKDIR/unbind.err" >/dev/null
+echo "Boot-time bind-volume correctly rejected with StorageImmutable"
+
+# --- Removing a non-existent name must fail with NoSuchStorage ---
+if machinectl unbind-volume "$MACHINE" "fs:no-such-volume-$$" 2>"$WORKDIR/unbind-noexist.err"; then
+ echo "ERROR: unbind-volume of non-existent name should have failed"
+ cat "$WORKDIR/unbind-noexist.err"
+ exit 1
+fi
+grep NoSuchStorage "$WORKDIR/unbind-noexist.err" >/dev/null
+echo "Non-existent unbind-volume correctly rejected with NoSuchStorage"
+
+machinectl terminate "$MACHINE"
+timeout 10 bash -c "while machinectl status '$MACHINE' &>/dev/null; do sleep .5; done"
+timeout 10 bash -c "while kill -0 '$VMSPAWN_PID' 2>/dev/null; do sleep .5; done"
+echo "All bind-volume tests passed"