From: Sam Leonard Date: Tue, 7 Nov 2023 14:04:11 +0000 (+0000) Subject: vmspawn: add swtpm feature X-Git-Tag: v256-rc1~913^2~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=cf3beb273b2b81e0928a177bc0e18cf01e24c392;p=thirdparty%2Fsystemd.git vmspawn: add swtpm feature --- diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index d5ece4acea0..d5eac5f50e3 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -132,6 +132,21 @@ + + BOOL + + + Configure whether to use qemu with a virtual TPM or not. + If the option is not specified vmspawn will detect the presence of + swtpm8 and use it if available. + If yes is specified swtpm8 + is always used, and vice versa if no is set swtpm + 8 is never used. + Note: the virtual TPM used may change in future. + + + + diff --git a/src/vmspawn/meson.build b/src/vmspawn/meson.build index 800d7c362c5..a67c66ebe1d 100644 --- a/src/vmspawn/meson.build +++ b/src/vmspawn/meson.build @@ -3,6 +3,7 @@ libvmspawn_core_sources = files( 'vmspawn-settings.c', 'vmspawn-util.c', + 'vmspawn-scope.c', ) libvmspawn_core = static_library( 'vmspawn-core', diff --git a/src/vmspawn/vmspawn-scope.c b/src/vmspawn/vmspawn-scope.c new file mode 100644 index 00000000000..676dee05927 --- /dev/null +++ b/src/vmspawn/vmspawn-scope.c @@ -0,0 +1,302 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include + +#include "sd-bus.h" + +#include "bus-error.h" +#include "bus-locator.h" +#include "bus-unit-util.h" +#include "bus-util.h" +#include "bus-wait-for-jobs.h" +#include "escape.h" +#include "macro.h" +#include "process-util.h" +#include "random-util.h" +#include "socket-util.h" +#include "strv.h" +#include "unit-def.h" +#include "unit-name.h" +#include "vmspawn-scope.h" + +int start_transient_scope(sd_bus *bus, const char *machine_name, bool allow_pidfd, char **ret_scope) { + _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL, *m = NULL; + _cleanup_free_ char *scope = NULL, *description = NULL; + const char *object; + int r; + + assert(bus); + assert(machine_name); + + /* Creates a transient scope unit which tracks the lifetime of the current process */ + + r = bus_wait_for_jobs_new(bus, &w); + if (r < 0) + return log_error_errno(r, "Could not watch job: %m"); + + if (asprintf(&scope, "machine-%"PRIu64"-%s.scope", random_u64(), machine_name) < 0) + return log_oom(); + + description = strjoin("Virtual Machine ", machine_name); + if (!description) + return log_oom(); + + r = bus_message_new_method_call(bus, &m, bus_systemd_mgr, "StartTransientUnit"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "ss", /* name */ scope, /* mode */ "fail"); + if (r < 0) + return bus_log_create_error(r); + + /* Properties */ + r = sd_bus_message_open_container(m, 'a', "(sv)"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "(sa)(sa)(sa)", + "Description", "s", description, + "AddRef", "b", 1, + "CollectMode", "s", "inactive-or-failed"); + + if (allow_pidfd) { + _cleanup_(pidref_done) PidRef pidref = PIDREF_NULL; + r = pidref_set_pid(&pidref, getpid_cached()); + if (r < 0) + return log_error_errno(r, "Failed to allocate PID reference: %m"); + + r = bus_append_scope_pidref(m, &pidref); + } else + r = sd_bus_message_append(m, "(sv)", "PIDs", "au", 1, getpid_cached()); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + /* No auxiliary units */ + r = sd_bus_message_append( + m, + "a(sa(sv))", + 0); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, 0, &error, &reply); + if (r < 0) { + /* If this failed with a property we couldn't write, this is quite likely because the server + * doesn't support PIDFDs yet, let's try without. */ + if (allow_pidfd && + sd_bus_error_has_names(&error, SD_BUS_ERROR_UNKNOWN_PROPERTY, SD_BUS_ERROR_PROPERTY_READ_ONLY)) + return start_transient_scope(bus, machine_name, false, ret_scope); + + return log_error_errno(r, "Failed to start transient scope unit: %s", bus_error_message(&error, r)); + } + + r = sd_bus_message_read(reply, "o", &object); + if (r < 0) + return bus_log_parse_error(r); + + r = bus_wait_for_jobs_one(w, object, /* quiet */ false, NULL); + if (r < 0) + return r; + + if (ret_scope) + *ret_scope = TAKE_PTR(scope); + + return 0; +} + +static int message_add_commands(sd_bus_message *m, const char *exec_type, char ***commands, size_t n_commands) { + int r; + + assert(m); + assert(exec_type); + assert(commands || n_commands == 0); + + /* A small helper for adding an ExecStart / ExecStopPost / etc.. property to an sd_bus_message */ + + r = sd_bus_message_open_container(m, 'r', "sv"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", exec_type); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'v', "a(sasb)"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'a', "(sasb)"); + if (r < 0) + return bus_log_create_error(r); + + FOREACH_ARRAY(cmd, commands, n_commands) { + char **cmdline = *cmd; + + r = sd_bus_message_open_container(m, 'r', "sasb"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", cmdline[0]); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_strv(m, cmdline); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "b", 0); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + } + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + return 0; +} + +void socket_service_pair_done(SocketServicePair *p) { + p->exec_start = strv_free(p->exec_start); + p->exec_stop_post = strv_free(p->exec_stop_post); + p->unit_name_prefix = mfree(p->unit_name_prefix); + p->runtime_directory = mfree(p->runtime_directory); + p->listen_address = mfree(p->listen_address); + p->socket_type = 0; +} + +int start_socket_service_pair(sd_bus *bus, const char *scope, SocketServicePair *p) { + _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL; + _cleanup_free_ char *service_desc = NULL, *service_name = NULL, *socket_name = NULL; + const char *object, *socket_type_str; + int r; + + /* Starts a socket/service unit pair bound to the given scope. */ + + assert(bus); + assert(scope); + assert(p); + assert(p->unit_name_prefix); + assert(p->exec_start); + assert(p->listen_address); + + r = bus_wait_for_jobs_new(bus, &w); + if (r < 0) + return log_error_errno(r, "Could not watch job: %m"); + + socket_name = strjoin(p->unit_name_prefix, ".socket"); + if (!socket_name) + return log_oom(); + + service_name = strjoin(p->unit_name_prefix, ".service"); + if (!service_name) + return log_oom(); + + service_desc = quote_command_line(p->exec_start, SHELL_ESCAPE_EMPTY); + if (!service_desc) + return log_oom(); + + socket_type_str = socket_address_type_to_string(p->socket_type); + if (!socket_type_str) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Invalid socket type: %d", p->socket_type); + + r = bus_message_new_method_call(bus, &m, bus_systemd_mgr, "StartTransientUnit"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "ssa(sv)", + /* ss - name, mode */ + socket_name, "fail", + /* a(sv) - Properties */ + 5, + "Description", "s", p->listen_address, + "AddRef", "b", 1, + "BindsTo", "as", 1, scope, + "Listen", "a(ss)", 1, socket_type_str, p->listen_address, + "CollectMode", "s", "inactive-or-failed"); + if (r < 0) + return bus_log_create_error(r); + + /* aux */ + r = sd_bus_message_open_container(m, 'a', "(sa(sv))"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'r', "sa(sv)"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "s", service_name); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(m, 'a', "(sv)"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(m, "(sv)(sv)(sv)(sv)", + "Description", "s", service_desc, + "AddRef", "b", 1, + "BindsTo", "as", 1, scope, + "CollectMode", "s", "inactive-or-failed"); + if (r < 0) + return bus_log_create_error(r); + + if (p->runtime_directory) { + r = sd_bus_message_append(m, "(sv)", "RuntimeDirectory", "as", 1, p->runtime_directory); + if (r < 0) + return bus_log_create_error(r); + } + + r = message_add_commands(m, "ExecStart", &p->exec_start, 1); + if (r < 0) + return r; + + if (p->exec_stop_post) { + r = message_add_commands(m, "ExecStopPost", &p->exec_stop_post, 1); + if (r < 0) + return r; + } + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_close_container(m); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, m, 0, &error, &reply); + if (r < 0) + return log_error_errno(r, "Failed to start %s as transient unit: %s", p->exec_start[0], bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "o", &object); + if (r < 0) + return bus_log_parse_error(r); + + return bus_wait_for_jobs_one(w, object, /* quiet */ false, NULL); +} diff --git a/src/vmspawn/vmspawn-scope.h b/src/vmspawn/vmspawn-scope.h new file mode 100644 index 00000000000..b807c3b2030 --- /dev/null +++ b/src/vmspawn/vmspawn-scope.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include + +#include "sd-bus.h" + +#include "macro.h" + +typedef struct SocketServicePair { + char **exec_start; + char **exec_stop_post; + char *unit_name_prefix; + char *runtime_directory; + char *listen_address; + int socket_type; +} SocketServicePair; + +void socket_service_pair_done(SocketServicePair *p); + +int start_transient_scope(sd_bus *bus, const char *machine_name, bool allow_pidfd, char **ret_scope); +int start_socket_service_pair(sd_bus *bus, const char *scope, SocketServicePair *p); diff --git a/src/vmspawn/vmspawn-util.h b/src/vmspawn/vmspawn-util.h index f2d1b621e21..c3dbdf205b1 100644 --- a/src/vmspawn/vmspawn-util.h +++ b/src/vmspawn/vmspawn-util.h @@ -10,6 +10,12 @@ #define ARCHITECTURE_SUPPORTS_SMBIOS 0 #endif +#if defined(__x86_64__) || defined(__arm__) || defined(__aarch64__) +#define ARCHITECTURE_SUPPORTS_TPM 1 +#else +#define ARCHITECTURE_SUPPORTS_TPM 0 +#endif + #if defined(__arm__) || defined(__aarch64__) #define DEFAULT_SERIAL_TTY "ttyAMA0" #elif defined(__s390__) || defined(__s390x__) diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index cc21efbf442..8138d142e87 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -3,7 +3,6 @@ #include #include #include -#include #include #include "alloc-util.h" @@ -22,17 +21,22 @@ #include "log.h" #include "machine-credential.h" #include "main-func.h" +#include "mkdir.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" +#include "path-lookup.h" #include "path-util.h" #include "pretty-print.h" #include "process-util.h" +#include "rm-rf.h" #include "sd-event.h" #include "signal-util.h" #include "socket-util.h" #include "strv.h" #include "tmpfile-util.h" +#include "unit-name.h" +#include "vmspawn-scope.h" #include "vmspawn-settings.h" #include "vmspawn-util.h" @@ -45,16 +49,21 @@ static uint64_t arg_qemu_mem = UINT64_C(2) * U64_GB; static int arg_qemu_kvm = -1; static int arg_qemu_vsock = -1; static unsigned arg_vsock_cid = VMADDR_CID_ANY; +static int arg_tpm = -1; static bool arg_qemu_gui = false; static int arg_secure_boot = -1; static MachineCredentialContext arg_credentials = {}; static SettingsMask arg_settings_mask = 0; static char **arg_parameters = NULL; static char *arg_firmware = NULL; +static char *arg_runtime_directory = NULL; +static bool arg_runtime_directory_created = false; +static bool arg_privileged = false; STATIC_DESTRUCTOR_REGISTER(arg_image, freep); STATIC_DESTRUCTOR_REGISTER(arg_machine, freep); STATIC_DESTRUCTOR_REGISTER(arg_qemu_smp, freep); +STATIC_DESTRUCTOR_REGISTER(arg_runtime_directory, freep); STATIC_DESTRUCTOR_REGISTER(arg_parameters, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_credentials, machine_credential_context_done); STATIC_DESTRUCTOR_REGISTER(arg_firmware, freep); @@ -84,6 +93,7 @@ static int help(void) { " --qemu-kvm=BOOL Configure whether to use KVM or not\n" " --qemu-vsock=BOOL Configure whether to use qemu with a vsock or not\n" " --vsock-cid= Specify the CID to use for the qemu guest's vsock\n" + " --tpm=BOOL Configure whether to use a virtual TPM or not\n" " --qemu-gui Start QEMU in graphical mode\n" " --secure-boot=BOOL Configure whether to search for firmware which\n" " supports Secure Boot\n" @@ -117,6 +127,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_QEMU_KVM, ARG_QEMU_VSOCK, ARG_VSOCK_CID, + ARG_TPM, ARG_QEMU_GUI, ARG_SECURE_BOOT, ARG_SET_CREDENTIAL, @@ -136,6 +147,7 @@ static int parse_argv(int argc, char *argv[]) { { "qemu-kvm", required_argument, NULL, ARG_QEMU_KVM }, { "qemu-vsock", required_argument, NULL, ARG_QEMU_VSOCK }, { "vsock-cid", required_argument, NULL, ARG_VSOCK_CID }, + { "tpm", required_argument, NULL, ARG_TPM }, { "qemu-gui", no_argument, NULL, ARG_QEMU_GUI }, { "secure-boot", required_argument, NULL, ARG_SECURE_BOOT }, { "set-credential", required_argument, NULL, ARG_SET_CREDENTIAL }, @@ -228,6 +240,12 @@ static int parse_argv(int argc, char *argv[]) { } break; + case ARG_TPM: + r = parse_tristate(optarg, &arg_tpm); + if (r < 0) + return log_error_errno(r, "Failed to parse --tpm=%s: %m", optarg); + break; + case ARG_QEMU_GUI: arg_qemu_gui = true; break; @@ -482,12 +500,80 @@ static int cmdline_add_vsock(char ***cmdline, int vsock_fd) { return 0; } +static int start_tpm(sd_bus *bus, const char *scope, const char *tpm, const char **ret_state_tempdir) { + _cleanup_(rm_rf_physical_and_freep) char *state_dir = NULL; + _cleanup_free_ char *scope_prefix = NULL; + _cleanup_(socket_service_pair_done) SocketServicePair ssp = { + .socket_type = SOCK_STREAM, + }; + int r; + + assert(bus); + assert(scope); + assert(tpm); + assert(ret_state_tempdir); + + r = unit_name_to_prefix(scope, &scope_prefix); + if (r < 0) + return log_error_errno(r, "Failed to strip .scope suffix from scope: %m"); + + ssp.unit_name_prefix = strjoin(scope_prefix, "-tpm"); + if (!ssp.unit_name_prefix) + 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 = path_join("systemd/vmspawn", ssp.unit_name_prefix); + if (!ssp.runtime_directory) + return log_oom(); + } + + ssp.listen_address = path_join(state_dir, "sock"); + if (!ssp.listen_address) + return log_oom(); + + ssp.exec_start = strv_new(tpm, "socket", "--tpm2", "--tpmstate"); + if (!ssp.exec_start) + return log_oom(); + + r = strv_extendf(&ssp.exec_start, "dir=%s", state_dir); + if (r < 0) + return log_oom(); + + r = strv_extend_many(&ssp.exec_start, "--ctrl", "type=unixio,fd=3"); + if (r < 0) + return log_oom(); + + r = start_socket_service_pair(bus, scope, &ssp); + if (r < 0) + return r; + + *ret_state_tempdir = TAKE_PTR(state_dir); + + return 0; +} + static int run_virtual_machine(void) { _cleanup_(ovmf_config_freep) OvmfConfig *ovmf_config = NULL; + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + _cleanup_close_ int vsock_fd = -EBADF; + _cleanup_free_ char *machine = NULL, *qemu_binary = NULL, *mem = NULL, *trans_scope = NULL; _cleanup_strv_free_ char **cmdline = NULL; - _cleanup_free_ char *machine = NULL, *qemu_binary = NULL, *mem = NULL; int r; - _cleanup_close_ int vsock_fd = -EBADF; + + if (arg_privileged) + r = sd_bus_default_system(&bus); + else + r = sd_bus_default_user(&bus); + if (r < 0) + return log_error_errno(r, "Failed to connect to systemd bus: %m"); + + r = start_transient_scope(bus, arg_machine, /* allow_pidfd= */ true, &trans_scope); + if (r < 0) + return r; bool use_kvm = arg_qemu_kvm > 0; if (arg_qemu_kvm < 0) { @@ -538,6 +624,20 @@ static int run_virtual_machine(void) { if (!cmdline) return log_oom(); + /* if we are going to be starting any units with state then create our runtime dir */ + if (arg_tpm != 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"); + if (r) { + /* r > 0 means we need to create our own runtime dir */ + r = mkdir_p(arg_runtime_directory, 0755); + if (r < 0) + return log_error_errno(r, "Failed to create runtime directory: %m"); + arg_runtime_directory_created = true; + } + } + bool use_vsock = arg_qemu_vsock > 0 && ARCHITECTURE_SUPPORTS_SMBIOS; if (arg_qemu_vsock < 0) { r = qemu_check_vsock_support(); @@ -685,6 +785,58 @@ static int run_virtual_machine(void) { } else log_warning("Cannot append extra args to kernel cmdline, native architecture doesn't support SMBIOS"); + /* disable TPM autodetection if the user's hardware doesn't support it */ + if (!ARCHITECTURE_SUPPORTS_TPM) { + if (arg_tpm < 0) { + arg_tpm = 0; + log_debug("TPM not support on %s, disabling tpm autodetection and continuing", architecture_to_string(native_architecture())); + } else if (arg_tpm > 0) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM not supported on %s, refusing", architecture_to_string(native_architecture())); + } + + _cleanup_free_ char *swtpm = NULL; + if (arg_tpm != 0) { + r = find_executable("swtpm", &swtpm); + if (r < 0) { + /* log if the user asked for swtpm and we cannot find it */ + if (arg_tpm > 0) + return log_error_errno(r, "Failed to find swtpm binary: %m"); + /* also log if we got an error other than ENOENT from find_executable */ + if (r != -ENOENT && arg_tpm < 0) + return log_error_errno(r, "Error detecting swtpm: %m"); + } + } + + _cleanup_free_ const char *tpm_state_tempdir = NULL; + if (swtpm) { + r = start_tpm(bus, trans_scope, swtpm, &tpm_state_tempdir); + if (r < 0) { + /* only bail if the user asked for a tpm */ + if (arg_tpm > 0) + return log_error_errno(r, "Failed to start tpm: %m"); + log_debug_errno(r, "Failed to start tpm, ignoring: %m"); + } + + r = strv_extend(&cmdline, "-chardev"); + if (r < 0) + return log_oom(); + + r = strv_extendf(&cmdline, "socket,id=chrtpm,path=%s/sock", tpm_state_tempdir); + if (r < 0) + return log_oom(); + + r = strv_extend_many(&cmdline, "-tpmdev", "emulator,id=tpm0,chardev=chrtpm"); + if (r < 0) + return log_oom(); + + if (native_architecture() == ARCHITECTURE_X86_64) + r = strv_extend_many(&cmdline, "-device", "tpm-tis,tpmdev=tpm0"); + else if (IN_SET(native_architecture(), ARCHITECTURE_ARM64, ARCHITECTURE_ARM64_BE)) + r = strv_extend_many(&cmdline, "-device", "tpm-tis-device,tpmdev=tpm0"); + if (r < 0) + return log_oom(); + } + if (use_vsock) { vsock_fd = open_vsock(); if (vsock_fd < 0) @@ -803,6 +955,8 @@ static int run(int argc, char *argv[]) { log_setup(); + arg_privileged = getuid() == 0; + r = parse_argv(argc, argv); if (r <= 0) return r;