From 5c57a8650630304d8434a348283b6b643fbc676b Mon Sep 17 00:00:00 2001 From: Sam Leonard Date: Wed, 24 Jan 2024 15:02:06 +0000 Subject: [PATCH 1/6] vmspawn: add support for -D/--directory --- src/vmspawn/vmspawn-util.h | 18 +++ src/vmspawn/vmspawn.c | 251 ++++++++++++++++++++++++++++++------- 2 files changed, 225 insertions(+), 44 deletions(-) diff --git a/src/vmspawn/vmspawn-util.h b/src/vmspawn/vmspawn-util.h index e1ce780671..90cb97ec00 100644 --- a/src/vmspawn/vmspawn-util.h +++ b/src/vmspawn/vmspawn-util.h @@ -16,6 +16,12 @@ #define ARCHITECTURE_SUPPORTS_TPM 0 #endif +#if defined(__x86_64__) || defined(__i386__) +#define ARCHITECTURE_SUPPORTS_SMM 1 +#else +#define ARCHITECTURE_SUPPORTS_SMM 0 +#endif + #if defined(__arm__) || defined(__aarch64__) #define DEFAULT_SERIAL_TTY "ttyAMA0" #elif defined(__s390__) || defined(__s390x__) @@ -26,6 +32,18 @@ #define DEFAULT_SERIAL_TTY "ttyS0" #endif +#if defined(__x86_64__) || defined(__i386__) +#define QEMU_MACHINE_TYPE "q35" +#elif defined(__arm__) || defined(__aarch64__) +#define QEMU_MACHINE_TYPE "virt" +#elif defined(__s390__) || defined(__s390x__) +#define QEMU_MACHINE_TYPE "s390-ccw-virtio" +#elif defined(__powerpc__) || defined(__powerpc64__) +#define QEMU_MACHINE_TYPE "pseries" +#else +#error "No qemu machine defined for this architecture" +#endif + typedef struct OvmfConfig { char *path; char *format; diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index d5ab2a1a27..89dd196427 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -38,6 +38,7 @@ #include "path-util.h" #include "pretty-print.h" #include "process-util.h" +#include "random-util.h" #include "rm-rf.h" #include "signal-util.h" #include "socket-util.h" @@ -51,6 +52,7 @@ static bool arg_quiet = false; static PagerFlags arg_pager_flags = 0; +static char *arg_directory = NULL; static char *arg_image = NULL; static char *arg_machine = NULL; static char *arg_qemu_smp = NULL; @@ -72,6 +74,7 @@ static bool arg_runtime_directory_created = false; static bool arg_privileged = false; static char **arg_kernel_cmdline_extra = NULL; +STATIC_DESTRUCTOR_REGISTER(arg_directory, freep); STATIC_DESTRUCTOR_REGISTER(arg_image, freep); STATIC_DESTRUCTOR_REGISTER(arg_machine, freep); STATIC_DESTRUCTOR_REGISTER(arg_qemu_smp, freep); @@ -99,6 +102,7 @@ static int help(void) { " -q --quiet Do not show status information\n" " --no-pager Do not pipe output into a pager\n" "\n%3$sImage:%4$s\n" + " -D --directory=PATH Root directory for the container\n" " -i --image=PATH Root file system disk image (or device node) for\n" " the virtual machine\n" "\n%3$sHost Configuration:%4$s\n" @@ -162,6 +166,7 @@ static int parse_argv(int argc, char *argv[]) { { "quiet", no_argument, NULL, 'q' }, { "no-pager", no_argument, NULL, ARG_NO_PAGER }, { "image", required_argument, NULL, 'i' }, + { "directory", required_argument, NULL, 'D' }, { "machine", required_argument, NULL, 'M' }, { "qemu-smp", required_argument, NULL, ARG_QEMU_SMP }, { "qemu-mem", required_argument, NULL, ARG_QEMU_MEM }, @@ -187,7 +192,7 @@ static int parse_argv(int argc, char *argv[]) { assert(argv); optind = 0; - while ((c = getopt_long(argc, argv, "+hi:M:nq", options, NULL)) >= 0) + while ((c = getopt_long(argc, argv, "+hD:i:M:nq", options, NULL)) >= 0) switch (c) { case 'h': return help(); @@ -199,6 +204,14 @@ static int parse_argv(int argc, char *argv[]) { arg_quiet = true; break; + case 'D': + r = parse_path_argument(optarg, false, &arg_directory); + if (r < 0) + return r; + + arg_settings_mask |= SETTING_DIRECTORY; + break; + case 'i': r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image); if (r < 0) @@ -629,6 +642,94 @@ static int discover_root(char **ret) { return log_oom(); *ret = TAKE_PTR(root); + return 0; +} + +static int find_virtiofsd(char **ret) { + int r; + _cleanup_free_ char *virtiofsd = NULL; + + assert(ret); + + r = find_executable("virtiofsd", &virtiofsd); + if (r < 0 && r != -ENOENT) + return log_error_errno(r, "Error while searching for virtiofsd: %m"); + + if (!virtiofsd) { + FOREACH_STRING(file, "/usr/libexec/virtiofsd", "/usr/lib/virtiofsd") { + if (access(file, X_OK) >= 0) { + virtiofsd = strdup(file); + if (!virtiofsd) + return log_oom(); + break; + } + + if (!IN_SET(errno, ENOENT, EACCES)) + return log_error_errno(errno, "Error while searching for virtiofsd: %m"); + } + } + + if (!virtiofsd) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Failed to find virtiofsd binary."); + + *ret = TAKE_PTR(virtiofsd); + return 0; +} + +static int start_virtiofsd(sd_bus *bus, const char *scope, const char *directory, char **ret_state_tempdir, char **ret_sock_name) { + _cleanup_(rm_rf_physical_and_freep) char *state_dir = NULL; + _cleanup_free_ char *virtiofsd = NULL, *sock_name = NULL, *scope_prefix = NULL; + _cleanup_(socket_service_pair_done) SocketServicePair ssp = { + .socket_type = SOCK_STREAM, + }; + static unsigned virtiofsd_instance = 0; + int r; + + assert(bus); + assert(scope); + assert(directory); + assert(ret_state_tempdir); + assert(ret_sock_name); + + r = find_virtiofsd(&virtiofsd); + if (r < 0) + return r; + + r = unit_name_to_prefix(scope, &scope_prefix); + if (r < 0) + return log_error_errno(r, "Failed to strip .scope suffix from scope: %m"); + + if (asprintf(&ssp.unit_name_prefix, "%s-virtiofsd-%u", scope_prefix, virtiofsd_instance++) < 0) + return log_oom(); + + state_dir = path_join(arg_runtime_directory, ssp.unit_name_prefix); + if (!state_dir) + return log_oom(); + + if (arg_runtime_directory_created) { + ssp.runtime_directory = strjoin("systemd/vmspawn/", ssp.unit_name_prefix); + if (!ssp.runtime_directory) + return log_oom(); + } + + if (asprintf(&sock_name, "sock-%"PRIx64, random_u64()) < 0) + return log_oom(); + + ssp.listen_address = path_join(state_dir, sock_name); + if (!ssp.listen_address) + return log_oom(); + + /* QEMU doesn't support submounts so don't announce them */ + ssp.exec_start = strv_new(virtiofsd, "--shared-dir", directory, "--xattr", "--fd", "3", "--no-announce-submounts"); + if (!ssp.exec_start) + return log_oom(); + + r = start_socket_service_pair(bus, scope, &ssp); + if (r < 0) + return r; + + *ret_state_tempdir = TAKE_PTR(state_dir); + *ret_sock_name = TAKE_PTR(sock_name); return 0; } @@ -658,12 +759,12 @@ static int kernel_cmdline_maybe_append_root(void) { static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { _cleanup_(ovmf_config_freep) OvmfConfig *ovmf_config = NULL; _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; - _cleanup_free_ char *qemu_binary = NULL, *mem = NULL, *trans_scope = NULL; + _cleanup_free_ char *machine = NULL, *qemu_binary = NULL, *mem = NULL, *trans_scope = NULL, *kernel = NULL; _cleanup_close_ int notify_sock_fd = -EBADF; _cleanup_strv_free_ char **cmdline = NULL; _cleanup_free_ int *pass_fds = NULL; size_t n_pass_fds = 0; - const char *machine, *accel; + const char *accel, *shm; int r; if (arg_privileged) @@ -697,18 +798,28 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { log_warning("Couldn't find OVMF firmware blob with Secure Boot support, " "falling back to OVMF firmware blobs without Secure Boot support."); + shm = arg_directory ? ",memory-backend=mem" : ""; + if (ARCHITECTURE_SUPPORTS_SMM) + machine = strjoin("type=" QEMU_MACHINE_TYPE ",smm=", on_off(ovmf_config->supports_sb), shm); + else + machine = strjoin("type=" QEMU_MACHINE_TYPE, shm); + if (!machine) + return log_oom(); + + if (arg_linux) { + kernel = strdup(arg_linux); + if (!kernel) + return log_oom(); + } else if (arg_directory) + return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Please specify a kernel (--linux=) when using -D/--directory=, refusing."); + r = find_qemu_binary(&qemu_binary); if (r == -EOPNOTSUPP) return log_error_errno(r, "Native architecture is not supported by qemu."); if (r < 0) return log_error_errno(r, "Failed to find QEMU binary: %m"); - if (IN_SET(native_architecture(), ARCHITECTURE_ARM64, ARCHITECTURE_ARM64_BE)) - machine = "type=virt"; - else - machine = ovmf_config->supports_sb ? "type=q35,smm=on" : "type=q35,smm=off"; - - if (asprintf(&mem, "%" PRIu64, DIV_ROUND_UP(arg_qemu_mem, U64_MB)) < 0) + if (asprintf(&mem, "%" PRIu64 "M", DIV_ROUND_UP(arg_qemu_mem, U64_MB)) < 0) return log_oom(); cmdline = strv_new( @@ -723,7 +834,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { return log_oom(); /* if we are going to be starting any units with state then create our runtime dir */ - if (arg_tpm != 0) { + if (arg_tpm != 0 || arg_directory) { r = runtime_directory(&arg_runtime_directory, arg_privileged ? RUNTIME_SCOPE_SYSTEM : RUNTIME_SCOPE_USER, "systemd/vmspawn"); if (r < 0) return log_error_errno(r, "Failed to lookup runtime directory: %m"); @@ -745,6 +856,26 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (r < 0) return log_oom(); + /* A shared memory backend might increase ram usage so only add one if actually necessary for virtiofsd. */ + if (arg_directory) { + r = strv_extend(&cmdline, "-object"); + if (r < 0) + return log_oom(); + + r = strv_extendf(&cmdline, "memory-backend-memfd,id=mem,size=%s,share=on", mem); + if (r < 0) + return log_oom(); + } + + bool use_vsock = arg_qemu_vsock > 0 && ARCHITECTURE_SUPPORTS_SMBIOS; + if (arg_qemu_vsock < 0) { + r = qemu_check_vsock_support(); + if (r < 0) + return log_error_errno(r, "Failed to check for VSock support: %m"); + + use_vsock = r; + } + if (!use_kvm && kvm_device_fd >= 0) { log_warning("KVM is disabled but fd for /dev/kvm was passed, closing fd and ignoring"); kvm_device_fd = safe_close(kvm_device_fd); @@ -776,15 +907,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (r < 0) return log_oom(); - bool use_vsock = arg_qemu_vsock > 0 && ARCHITECTURE_SUPPORTS_SMBIOS; - if (arg_qemu_vsock < 0) { - r = qemu_check_vsock_support(); - if (r < 0) - return log_error_errno(r, "Failed to check for VSock support: %m"); - - use_vsock = r; - } - _cleanup_close_ int child_vsock_fd = -EBADF; if (use_vsock) { int device_fd = vhost_device_fd; @@ -901,8 +1023,8 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { return log_oom(); } - if (arg_linux) { - r = strv_extend_many(&cmdline, "-kernel", arg_linux); + if (kernel) { + r = strv_extend_many(&cmdline, "-kernel", kernel); if (r < 0) return log_oom(); @@ -915,20 +1037,50 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { } } - r = strv_extend(&cmdline, "-drive"); - if (r < 0) - return log_oom(); + if (arg_image) { + assert(!arg_directory); - r = strv_extendf(&cmdline, "if=none,id=mkosi,file=%s,format=raw", arg_image); - if (r < 0) - return log_oom(); + r = strv_extend(&cmdline, "-drive"); + if (r < 0) + return log_oom(); - r = strv_extend_many( - &cmdline, + r = strv_extendf(&cmdline, "if=none,id=mkosi,file=%s,format=raw", arg_image); + if (r < 0) + return log_oom(); + + r = strv_extend_many(&cmdline, "-device", "virtio-scsi-pci,id=scsi", "-device", "scsi-hd,drive=mkosi,bootindex=1"); - if (r < 0) - return log_oom(); + if (r < 0) + return log_oom(); + } + + if (arg_directory) { + _cleanup_free_ char *sock_path = NULL, *sock_name = NULL; + r = start_virtiofsd(bus, trans_scope, arg_directory, &sock_path, &sock_name); + if (r < 0) + return r; + + r = strv_extend(&cmdline, "-chardev"); + if (r < 0) + return log_oom(); + + r = strv_extendf(&cmdline, "socket,id=%1$s,path=%2$s/%1$s", sock_name, sock_path); + if (r < 0) + return log_oom(); + + r = strv_extend(&cmdline, "-device"); + if (r < 0) + return log_oom(); + + r = strv_extendf(&cmdline, "vhost-user-fs-pci,queue-size=1024,chardev=%s,tag=root", sock_name); + if (r < 0) + return log_oom(); + + r = strv_extend(&arg_kernel_cmdline_extra, "root=root rootfstype=virtiofs rw"); + if (r < 0) + return log_oom(); + } r = strv_prepend(&arg_kernel_cmdline_extra, "console=" DEFAULT_SERIAL_TTY); if (r < 0) @@ -939,7 +1091,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (!kcl) return log_oom(); - if (arg_linux) { + if (kernel) { r = strv_extend_many(&cmdline, "-append", kcl); if (r < 0) return log_oom(); @@ -1106,20 +1258,30 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { static int determine_names(void) { int r; - if (!arg_image) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Missing required argument -i/--image=, quitting"); + if (!arg_directory && !arg_image) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to determine path, please use -D or -i."); if (!arg_machine) { - char *e; + if (arg_directory && path_equal(arg_directory, "/")) { + arg_machine = gethostname_malloc(); + if (!arg_machine) + return log_oom(); + } else if (arg_image) { + char *e; - r = path_extract_filename(arg_image, &arg_machine); - if (r < 0) - return log_error_errno(r, "Failed to extract file name from '%s': %m", arg_image); + r = path_extract_filename(arg_image, &arg_machine); + if (r < 0) + return log_error_errno(r, "Failed to extract file name from '%s': %m", arg_image); - /* Truncate suffix if there is one */ - e = endswith(arg_machine, ".raw"); - if (e) - *e = 0; + /* Truncate suffix if there is one */ + e = endswith(arg_machine, ".raw"); + if (e) + *e = 0; + } else { + r = path_extract_filename(arg_directory, &arg_machine); + if (r < 0) + return log_error_errno(r, "Failed to extract file name from '%s': %m", arg_directory); + } hostname_cleanup(arg_machine); if (!hostname_is_valid(arg_machine, 0)) @@ -1158,11 +1320,12 @@ static int run(int argc, char *argv[]) { if (!arg_quiet) { _cleanup_free_ char *u = NULL; - (void) terminal_urlify_path(arg_image, arg_image, &u); + const char *vm_path = arg_image ?: arg_directory; + (void) terminal_urlify_path(vm_path, vm_path, &u); log_info("%s %sSpawning VM %s on %s.%s\n" "%s %sPress %sCtrl-a x%s to kill VM.%s", - special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), arg_machine, u ?: arg_image, ansi_normal(), + special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), arg_machine, u ?: vm_path, ansi_normal(), special_glyph(SPECIAL_GLYPH_LIGHT_SHADE), ansi_grey(), ansi_highlight(), ansi_grey(), ansi_normal()); } From acdef55ef75ea82d7fea857995c23f6f3c5c02fc Mon Sep 17 00:00:00 2001 From: Sam Leonard Date: Tue, 30 Jan 2024 13:24:22 +0000 Subject: [PATCH 2/6] basic/namespace-util: add parse_userns_uid_range --- src/basic/namespace-util.c | 39 ++++++++++++++++++++++++++++++++++++++ src/basic/namespace-util.h | 2 ++ 2 files changed, 41 insertions(+) diff --git a/src/basic/namespace-util.c b/src/basic/namespace-util.c index d517263b36..c537c0f9b7 100644 --- a/src/basic/namespace-util.c +++ b/src/basic/namespace-util.c @@ -11,6 +11,7 @@ #include "missing_magic.h" #include "missing_sched.h" #include "namespace-util.h" +#include "parse-util.h" #include "process-util.h" #include "stat-util.h" #include "stdio-util.h" @@ -305,3 +306,41 @@ int in_same_namespace(pid_t pid1, pid_t pid2, NamespaceType type) { return stat_inode_same(&ns_st1, &ns_st2); } + +int parse_userns_uid_range(const char *s, uid_t *ret_uid_shift, uid_t *ret_uid_range) { + _cleanup_free_ char *buffer = NULL; + const char *range, *shift; + int r; + uid_t uid_shift, uid_range = 65536; + + assert(s); + + range = strchr(s, ':'); + if (range) { + buffer = strndup(s, range - s); + if (!buffer) + return log_oom(); + shift = buffer; + + range++; + r = safe_atou32(range, &uid_range); + if (r < 0) + return log_error_errno(r, "Failed to parse UID range \"%s\": %m", range); + } else + shift = s; + + r = parse_uid(shift, &uid_shift); + if (r < 0) + return log_error_errno(r, "Failed to parse UID \"%s\": %m", s); + + if (!userns_shift_range_valid(uid_shift, uid_range)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID range cannot be empty or go beyond " UID_FMT ".", UID_INVALID); + + if (ret_uid_shift) + *ret_uid_shift = uid_shift; + + if (ret_uid_range) + *ret_uid_range = uid_range; + + return 0; +} diff --git a/src/basic/namespace-util.h b/src/basic/namespace-util.h index d1d015612f..34cbec3f6e 100644 --- a/src/basic/namespace-util.h +++ b/src/basic/namespace-util.h @@ -53,3 +53,5 @@ static inline bool userns_shift_range_valid(uid_t shift, uid_t range) { int userns_acquire(const char *uid_map, const char *gid_map); int netns_acquire(void); int in_same_namespace(pid_t pid1, pid_t pid2, NamespaceType type); + +int parse_userns_uid_range(const char *s, uid_t *ret_uid_shift, uid_t *ret_uid_range); From d4a58877e458e0e81f8810a73ce2d7836cd89db1 Mon Sep 17 00:00:00 2001 From: Sam Leonard Date: Tue, 30 Jan 2024 13:24:49 +0000 Subject: [PATCH 3/6] nspawn: use parse_userns_uid_range --- src/nspawn/nspawn.c | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index ae5172853b..e7dd2a370c 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -1255,33 +1255,11 @@ static int parse_argv(int argc, char *argv[]) { arg_uid_shift = 0; arg_uid_range = UINT32_C(0x10000); } else { - _cleanup_free_ char *buffer = NULL; - const char *range, *shift; - /* anything else: User namespacing on, UID range is explicitly configured */ - - range = strchr(optarg, ':'); - if (range) { - buffer = strndup(optarg, range - optarg); - if (!buffer) - return log_oom(); - shift = buffer; - - range++; - r = safe_atou32(range, &arg_uid_range); - if (r < 0) - return log_error_errno(r, "Failed to parse UID range \"%s\": %m", range); - } else - shift = optarg; - - r = parse_uid(shift, &arg_uid_shift); + r = parse_userns_uid_range(optarg, &arg_uid_shift, &arg_uid_range); if (r < 0) - return log_error_errno(r, "Failed to parse UID \"%s\": %m", optarg); - + return r; arg_userns_mode = USER_NAMESPACE_FIXED; - - if (!userns_shift_range_valid(arg_uid_shift, arg_uid_range)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "UID range cannot be empty or go beyond " UID_FMT ".", UID_INVALID); } arg_settings_mask |= SETTING_USERNS; From c05ca33af3e81905b91bcb49c50339737a0f1d08 Mon Sep 17 00:00:00 2001 From: Sam Leonard Date: Mon, 4 Dec 2023 18:54:40 +0000 Subject: [PATCH 4/6] vmspawn: add support for --private-users --- src/vmspawn/vmspawn.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 89dd196427..def8eef73c 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -67,6 +67,7 @@ static bool arg_qemu_gui = false; static QemuNetworkStack arg_network_stack = QEMU_NET_NONE; static int arg_secure_boot = -1; static MachineCredentialContext arg_credentials = {}; +static uid_t arg_uid_shift = UID_INVALID, arg_uid_range = 0x10000U; static SettingsMask arg_settings_mask = 0; static char *arg_firmware = NULL; static char *arg_runtime_directory = NULL; @@ -122,6 +123,10 @@ static int help(void) { " --firmware=PATH|list Select firmware definition file (or list available)\n" "\n%3$sSystem Identity:%4$s\n" " -M --machine=NAME Set the machine name for the virtual machine\n" + "\n%3$sUser Namespacing:%4$s\n" + " --private-users=UIDBASE[:NUIDS]\n" + " Configure the UID/GID range to map into the\n" + " virtiofsd namespace\n" "\n%3$sCredentials:%4$s\n" " --set-credential=ID:VALUE\n" " Pass a credential with literal value to the\n" @@ -155,6 +160,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_QEMU_GUI, ARG_NETWORK_USER_MODE, ARG_SECURE_BOOT, + ARG_PRIVATE_USERS, ARG_SET_CREDENTIAL, ARG_LOAD_CREDENTIAL, ARG_FIRMWARE, @@ -180,6 +186,7 @@ static int parse_argv(int argc, char *argv[]) { { "network-tap", no_argument, NULL, 'n' }, { "network-user-mode", no_argument, NULL, ARG_NETWORK_USER_MODE }, { "secure-boot", required_argument, NULL, ARG_SECURE_BOOT }, + { "private-users", required_argument, NULL, ARG_PRIVATE_USERS }, { "set-credential", required_argument, NULL, ARG_SET_CREDENTIAL }, { "load-credential", required_argument, NULL, ARG_LOAD_CREDENTIAL }, { "firmware", required_argument, NULL, ARG_FIRMWARE }, @@ -315,6 +322,12 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(r, "Failed to parse --secure-boot=%s: %m", optarg); break; + case ARG_PRIVATE_USERS: + r = parse_userns_uid_range(optarg, &arg_uid_shift, &arg_uid_range); + if (r < 0) + return r; + break; + case ARG_SET_CREDENTIAL: { r = machine_credential_set(&arg_credentials, optarg); if (r < 0) @@ -724,6 +737,24 @@ static int start_virtiofsd(sd_bus *bus, const char *scope, const char *directory if (!ssp.exec_start) return log_oom(); + if (arg_uid_shift != UID_INVALID) { + r = strv_extend(&ssp.exec_start, "--uid-map"); + if (r < 0) + return log_oom(); + + r = strv_extendf(&ssp.exec_start, ":0:" UID_FMT ":" UID_FMT ":", arg_uid_shift, arg_uid_range); + if (r < 0) + return log_oom(); + + r = strv_extend(&ssp.exec_start, "--gid-map"); + if (r < 0) + return log_oom(); + + r = strv_extendf(&ssp.exec_start, ":0:" GID_FMT ":" GID_FMT ":", arg_uid_shift, arg_uid_range); + if (r < 0) + return log_oom(); + } + r = start_socket_service_pair(bus, scope, &ssp); if (r < 0) return r; From 7fe9c9909db0b49d4b915cffdda25c10addf504d Mon Sep 17 00:00:00 2001 From: Sam Leonard Date: Mon, 4 Dec 2023 17:33:12 +0000 Subject: [PATCH 5/6] vmspawn: document --directory and --private-users --- man/systemd-vmspawn.xml | 43 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index b84cc9c062..184ba6e590 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -69,6 +69,20 @@ Image Options + + + + + Directory to use as file system root for the virtual machine. + + One of either or must be specified. + Note: If mounting a non-root owned directory you may require + to map into the user's subuid namespace. + + + + + @@ -249,7 +263,34 @@ - + + + + User Namespacing Options + + + + UID_SHIFT[:UID_RANGE] + + Controls user namespacing under . + If enabled, virtiofsd1 + is instructed to map user and group ids (UIDs and GIDs). This involves mapping the private UIDs/GIDs used in the virtual machine + (starting with the virtual machine's root user 0 and up) to a range of UIDs/GIDs on the host that are not used for other + purposes (usually in the range beyond the host's UID/GID 65536). + + If one or two colon-separated numbers are specified, user namespacing is turned on. UID_SHIFT + specifies the first host UID/GID to map, UID_RANGE is optional and specifies number of host + UIDs/GIDs to assign to the virtual machine. If UID_RANGE is omitted, 65536 UIDs/GIDs are assigned. + + When user namespaces are used, the GID range assigned to each virtual machine is always chosen identical to the + UID range. + + + + + + + Credentials From a8f940c4b5df5b9f74315f26ecf86713b5600484 Mon Sep 17 00:00:00 2001 From: Sam Leonard Date: Wed, 13 Dec 2023 16:54:34 +0000 Subject: [PATCH 6/6] vmspawn: add support for --bind(-ro)= --- man/systemd-vmspawn.xml | 23 ++++++++++- src/vmspawn/meson.build | 1 + src/vmspawn/vmspawn-mount.c | 67 ++++++++++++++++++++++++++++++++ src/vmspawn/vmspawn-mount.h | 19 +++++++++ src/vmspawn/vmspawn-settings.h | 1 + src/vmspawn/vmspawn.c | 71 ++++++++++++++++++++++++++++++---- 6 files changed, 173 insertions(+), 9 deletions(-) create mode 100644 src/vmspawn/vmspawn-mount.c create mode 100644 src/vmspawn/vmspawn-mount.h diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index 184ba6e590..0446d27630 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -262,7 +262,6 @@ - @@ -290,6 +289,28 @@ + + Mount Options + + + + + PATH + PATH + + Mount a directory from the host into the virtual machine. Takes one of: a path + argument — in which case the specified path will be mounted from the host to the same path in the virtual machine, or + a colon-separated pair of paths — in which case the first specified path is the source in the host, and the + second path is the destination in the virtual machine. If the source path is not absolute, it is resolved + relative to the current working directory. The option creates read-only bind mounts. + Backslash escapes are interpreted, so \: may be used to embed colons in either path. + This option may be specified multiple times for creating multiple independent bind mount points. + + + + + + Credentials diff --git a/src/vmspawn/meson.build b/src/vmspawn/meson.build index 5f54364fac..a7b34bdbab 100644 --- a/src/vmspawn/meson.build +++ b/src/vmspawn/meson.build @@ -4,6 +4,7 @@ libvmspawn_core_sources = files( 'vmspawn-settings.c', 'vmspawn-util.c', 'vmspawn-scope.c', + 'vmspawn-mount.c', ) libvmspawn_core = static_library( 'vmspawn-core', diff --git a/src/vmspawn/vmspawn-mount.c b/src/vmspawn/vmspawn-mount.c new file mode 100644 index 0000000000..ee63bda96c --- /dev/null +++ b/src/vmspawn/vmspawn-mount.c @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "extract-word.h" +#include "macro.h" +#include "parse-argument.h" +#include "path-util.h" +#include "string-util.h" +#include "vmspawn-mount.h" + +static void runtime_mount_done(RuntimeMount *mount) { + assert(mount); + + mount->source = mfree(mount->source); + mount->target = mfree(mount->target); +} + +void runtime_mount_context_done(RuntimeMountContext *ctx) { + assert(ctx); + + FOREACH_ARRAY(mount, ctx->mounts, ctx->n_mounts) + runtime_mount_done(mount); + + free(ctx->mounts); +} + +int runtime_mount_parse(RuntimeMountContext *ctx, const char *s, bool read_only) { + _cleanup_(runtime_mount_done) RuntimeMount mount = { .read_only = read_only }; + _cleanup_free_ char *source_rel = NULL; + int r; + + assert(ctx); + + r = extract_first_word(&s, &source_rel, ":", EXTRACT_DONT_COALESCE_SEPARATORS); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + + if (isempty(source_rel)) + return -EINVAL; + + r = path_make_absolute_cwd(source_rel, &mount.source); + if (r < 0) + return r; + + /* virtiofsd only supports directories */ + r = is_dir(mount.source, /* follow= */ true); + if (r < 0) + return r; + if (!r) + return -ENOTDIR; + + mount.target = s ? strdup(s) : TAKE_PTR(source_rel); + if (!mount.target) + return -ENOMEM; + + if (!path_is_absolute(mount.target)) + return -EINVAL; + + if (!GREEDY_REALLOC(ctx->mounts, ctx->n_mounts + 1)) + return log_oom(); + + ctx->mounts[ctx->n_mounts++] = TAKE_STRUCT(mount); + + return 0; +} diff --git a/src/vmspawn/vmspawn-mount.h b/src/vmspawn/vmspawn-mount.h new file mode 100644 index 0000000000..2ea24fd035 --- /dev/null +++ b/src/vmspawn/vmspawn-mount.h @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include +#include + +typedef struct RuntimeMount { + bool read_only; + char *source; + char *target; +} RuntimeMount; + +typedef struct RuntimeMountContext { + RuntimeMount *mounts; + size_t n_mounts; +} RuntimeMountContext; + +void runtime_mount_context_done(RuntimeMountContext *ctx); +int runtime_mount_parse(RuntimeMountContext *ctx, const char *s, bool read_only); diff --git a/src/vmspawn/vmspawn-settings.h b/src/vmspawn/vmspawn-settings.h index 268a874664..60ea10e6de 100644 --- a/src/vmspawn/vmspawn-settings.h +++ b/src/vmspawn/vmspawn-settings.h @@ -5,6 +5,7 @@ typedef enum SettingsMask { SETTING_START_MODE = UINT64_C(1) << 0, + SETTING_BIND_MOUNTS = UINT64_C(1) << 11, SETTING_DIRECTORY = UINT64_C(1) << 26, SETTING_CREDENTIALS = UINT64_C(1) << 30, _SETTING_FORCE_ENUM_WIDTH = UINT64_MAX diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index def8eef73c..d5de11bfe0 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -19,6 +19,7 @@ #include "dissect-image.h" #include "escape.h" #include "event-util.h" +#include "extract-word.h" #include "fileio.h" #include "format-util.h" #include "fs-util.h" @@ -46,6 +47,7 @@ #include "strv.h" #include "tmpfile-util.h" #include "unit-name.h" +#include "vmspawn-mount.h" #include "vmspawn-scope.h" #include "vmspawn-settings.h" #include "vmspawn-util.h" @@ -68,6 +70,7 @@ static QemuNetworkStack arg_network_stack = QEMU_NET_NONE; static int arg_secure_boot = -1; static MachineCredentialContext arg_credentials = {}; static uid_t arg_uid_shift = UID_INVALID, arg_uid_range = 0x10000U; +static RuntimeMountContext arg_runtime_mounts = {}; static SettingsMask arg_settings_mask = 0; static char *arg_firmware = NULL; static char *arg_runtime_directory = NULL; @@ -84,6 +87,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_credentials, machine_credential_context_done); STATIC_DESTRUCTOR_REGISTER(arg_firmware, freep); STATIC_DESTRUCTOR_REGISTER(arg_linux, freep); STATIC_DESTRUCTOR_REGISTER(arg_initrd, freep); +STATIC_DESTRUCTOR_REGISTER(arg_runtime_mounts, runtime_mount_context_done); STATIC_DESTRUCTOR_REGISTER(arg_kernel_cmdline_extra, strv_freep); static int help(void) { @@ -127,6 +131,12 @@ static int help(void) { " --private-users=UIDBASE[:NUIDS]\n" " Configure the UID/GID range to map into the\n" " virtiofsd namespace\n" + "\n%3$sMounts:%4$s\n" + " --bind=SOURCE[:TARGET]\n" + " Mount a file or directory from the host into\n" + " the VM.\n" + " --bind-ro=SOURCE[:TARGET]\n" + " Similar, but creates a read-only mount\n" "\n%3$sCredentials:%4$s\n" " --set-credential=ID:VALUE\n" " Pass a credential with literal value to the\n" @@ -159,6 +169,8 @@ static int parse_argv(int argc, char *argv[]) { ARG_INITRD, ARG_QEMU_GUI, ARG_NETWORK_USER_MODE, + ARG_BIND, + ARG_BIND_RO, ARG_SECURE_BOOT, ARG_PRIVATE_USERS, ARG_SET_CREDENTIAL, @@ -185,6 +197,8 @@ static int parse_argv(int argc, char *argv[]) { { "qemu-gui", no_argument, NULL, ARG_QEMU_GUI }, { "network-tap", no_argument, NULL, 'n' }, { "network-user-mode", no_argument, NULL, ARG_NETWORK_USER_MODE }, + { "bind", required_argument, NULL, ARG_BIND }, + { "bind-ro", required_argument, NULL, ARG_BIND_RO }, { "secure-boot", required_argument, NULL, ARG_SECURE_BOOT }, { "private-users", required_argument, NULL, ARG_PRIVATE_USERS }, { "set-credential", required_argument, NULL, ARG_SET_CREDENTIAL }, @@ -212,7 +226,7 @@ static int parse_argv(int argc, char *argv[]) { break; case 'D': - r = parse_path_argument(optarg, false, &arg_directory); + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_directory); if (r < 0) return r; @@ -316,6 +330,15 @@ static int parse_argv(int argc, char *argv[]) { arg_network_stack = QEMU_NET_USER; break; + case ARG_BIND: + case ARG_BIND_RO: + r = runtime_mount_parse(&arg_runtime_mounts, optarg, c == ARG_BIND_RO); + if (r < 0) + return log_error_errno(r, "Failed to parse --bind(-ro)= argument %s: %m", optarg); + + arg_settings_mask |= SETTING_BIND_MOUNTS; + break; + case ARG_SECURE_BOOT: r = parse_tristate(optarg, &arg_secure_boot); if (r < 0) @@ -689,7 +712,7 @@ static int find_virtiofsd(char **ret) { return 0; } -static int start_virtiofsd(sd_bus *bus, const char *scope, const char *directory, char **ret_state_tempdir, char **ret_sock_name) { +static int start_virtiofsd(sd_bus *bus, const char *scope, const char *directory, bool uidmap, char **ret_state_tempdir, char **ret_sock_name) { _cleanup_(rm_rf_physical_and_freep) char *state_dir = NULL; _cleanup_free_ char *virtiofsd = NULL, *sock_name = NULL, *scope_prefix = NULL; _cleanup_(socket_service_pair_done) SocketServicePair ssp = { @@ -737,7 +760,7 @@ static int start_virtiofsd(sd_bus *bus, const char *scope, const char *directory if (!ssp.exec_start) return log_oom(); - if (arg_uid_shift != UID_INVALID) { + if (uidmap && arg_uid_shift != UID_INVALID) { r = strv_extend(&ssp.exec_start, "--uid-map"); if (r < 0) return log_oom(); @@ -865,7 +888,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { return log_oom(); /* if we are going to be starting any units with state then create our runtime dir */ - if (arg_tpm != 0 || arg_directory) { + if (arg_tpm != 0 || arg_directory || arg_runtime_mounts.n_mounts != 0) { r = runtime_directory(&arg_runtime_directory, arg_privileged ? RUNTIME_SCOPE_SYSTEM : RUNTIME_SCOPE_USER, "systemd/vmspawn"); if (r < 0) return log_error_errno(r, "Failed to lookup runtime directory: %m"); @@ -888,7 +911,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { return log_oom(); /* A shared memory backend might increase ram usage so only add one if actually necessary for virtiofsd. */ - if (arg_directory) { + if (arg_directory || arg_runtime_mounts.n_mounts != 0) { r = strv_extend(&cmdline, "-object"); if (r < 0) return log_oom(); @@ -1088,7 +1111,7 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (arg_directory) { _cleanup_free_ char *sock_path = NULL, *sock_name = NULL; - r = start_virtiofsd(bus, trans_scope, arg_directory, &sock_path, &sock_name); + r = start_virtiofsd(bus, trans_scope, arg_directory, /* uidmap= */ true, &sock_path, &sock_name); if (r < 0) return r; @@ -1117,6 +1140,38 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (r < 0) return log_oom(); + FOREACH_ARRAY(mount, arg_runtime_mounts.mounts, arg_runtime_mounts.n_mounts) { + _cleanup_free_ char *sock_path = NULL, *sock_name = NULL, *clean_target = NULL; + r = start_virtiofsd(bus, trans_scope, mount->source, /* uidmap= */ false, &sock_path, &sock_name); + if (r < 0) + return r; + + r = strv_extend(&cmdline, "-chardev"); + if (r < 0) + return log_oom(); + + r = strv_extendf(&cmdline, "socket,id=%1$s,path=%2$s/%1$s", sock_name, sock_path); + if (r < 0) + return log_oom(); + + r = strv_extend(&cmdline, "-device"); + if (r < 0) + return log_oom(); + + r = strv_extendf(&cmdline, "vhost-user-fs-pci,queue-size=1024,chardev=%1$s,tag=%1$s", sock_name); + if (r < 0) + return log_oom(); + + clean_target = xescape(mount->target, "\":"); + if (!clean_target) + return log_oom(); + + r = strv_extendf(&arg_kernel_cmdline_extra, "systemd.mount-extra=\"%s:%s:virtiofs:%s\"", + sock_name, clean_target, mount->read_only ? "ro" : "rw"); + if (r < 0) + return log_oom(); + } + if (ARCHITECTURE_SUPPORTS_SMBIOS) { _cleanup_free_ char *kcl = strv_join(arg_kernel_cmdline_extra, " "); if (!kcl) @@ -1219,6 +1274,8 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { log_debug("Executing: %s", joined); } + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, -1) >= 0); + _cleanup_(sd_event_source_unrefp) sd_event_source *notify_event_source = NULL; _cleanup_(sd_event_unrefp) sd_event *event = NULL; r = sd_event_new(&event); @@ -1376,8 +1433,6 @@ static int run(int argc, char *argv[]) { } } - assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, -1) >= 0); - return run_virtual_machine(kvm_device_fd, vhost_device_fd); }