From a586648800bf6c3f83c8b64f123d8e9df4857bc4 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Thu, 2 Apr 2026 09:57:46 +0000 Subject: [PATCH] vmspawn: Add --console-transport= option to select serial vs virtio-serial Add a --console-transport= option that selects between virtio-serial (the default, appearing as /dev/hvc0) and a regular serial port (appearing as /dev/ttyS0 or /dev/ttyAMA0 depending on architecture). This is primarily useful for testing purposes, for example to test sd-stub's automatic console= kernel command line parameter handling. It allows verifying that the guest OS correctly handles serial console configurations without virtio. When serial transport is selected, -serial chardev:console is used on the QEMU command line to connect the chardev to the platform's default serial device. This cannot be done via the QEMU config file as on some platforms (e.g. ARM) the serial device is a sysbus device that can only be connected via serial_hd() which is populated by -serial. --- man/systemd-vmspawn.xml | 14 ++++++++++ src/vmspawn/vmspawn-settings.c | 7 +++++ src/vmspawn/vmspawn-settings.h | 8 ++++++ src/vmspawn/vmspawn-util.h | 6 ++++ src/vmspawn/vmspawn.c | 50 ++++++++++++++++++++++++++-------- 5 files changed, 73 insertions(+), 12 deletions(-) diff --git a/man/systemd-vmspawn.xml b/man/systemd-vmspawn.xml index 129f5ba141..9feb7407ca 100644 --- a/man/systemd-vmspawn.xml +++ b/man/systemd-vmspawn.xml @@ -712,6 +712,20 @@ + + + + Configures the transport to use for the VM console. Takes one of + virtio or serial. Defaults to virtio. + virtio uses a virtio-serial device, which appears as + /dev/hvc0 in the VM. serial uses a regular serial port, + which appears as /dev/ttyS0 (or /dev/ttyAMA0 on ARM) in the VM. This option only has an effect in + , , and + modes. + + + + diff --git a/src/vmspawn/vmspawn-settings.c b/src/vmspawn/vmspawn-settings.c index 776b590252..56a07b3f6f 100644 --- a/src/vmspawn/vmspawn-settings.c +++ b/src/vmspawn/vmspawn-settings.c @@ -37,3 +37,10 @@ static const char *const console_mode_table[_CONSOLE_MODE_MAX] = { }; DEFINE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode); + +static const char *const console_transport_table[_CONSOLE_TRANSPORT_MAX] = { + [CONSOLE_TRANSPORT_VIRTIO] = "virtio", + [CONSOLE_TRANSPORT_SERIAL] = "serial", +}; + +DEFINE_STRING_TABLE_LOOKUP(console_transport, ConsoleTransport); diff --git a/src/vmspawn/vmspawn-settings.h b/src/vmspawn/vmspawn-settings.h index b897f148e1..83d2872535 100644 --- a/src/vmspawn/vmspawn-settings.h +++ b/src/vmspawn/vmspawn-settings.h @@ -42,6 +42,13 @@ typedef enum ConsoleMode { _CONSOLE_MODE_INVALID = -EINVAL, } ConsoleMode; +typedef enum ConsoleTransport { + CONSOLE_TRANSPORT_VIRTIO, /* virtio-serial (hvc0) */ + CONSOLE_TRANSPORT_SERIAL, /* regular serial port (ttyS0/ttyAMA0) */ + _CONSOLE_TRANSPORT_MAX, + _CONSOLE_TRANSPORT_INVALID = -EINVAL, +} ConsoleTransport; + typedef enum SettingsMask { SETTING_START_MODE = UINT64_C(1) << 0, SETTING_MACHINE_ID = UINT64_C(1) << 6, @@ -53,5 +60,6 @@ typedef enum SettingsMask { } SettingsMask; DECLARE_STRING_TABLE_LOOKUP(console_mode, ConsoleMode); +DECLARE_STRING_TABLE_LOOKUP(console_transport, ConsoleTransport); DECLARE_STRING_TABLE_LOOKUP(disk_type, DiskType); DECLARE_STRING_TABLE_LOOKUP(image_format, ImageFormat); diff --git a/src/vmspawn/vmspawn-util.h b/src/vmspawn/vmspawn-util.h index 90efd93661..9fec6641aa 100644 --- a/src/vmspawn/vmspawn-util.h +++ b/src/vmspawn/vmspawn-util.h @@ -49,6 +49,12 @@ # define QEMU_MACHINE_TYPE "none" #endif +#if defined(__arm__) || defined(__aarch64__) +# define QEMU_SERIAL_CONSOLE_NAME "ttyAMA0" +#else +# define QEMU_SERIAL_CONSOLE_NAME "ttyS0" +#endif + typedef struct OvmfConfig { char *path; char *format; diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 1891ac8bad..85165e1af7 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -137,6 +137,7 @@ static int arg_tpm = -1; static char *arg_linux = NULL; static char **arg_initrds = NULL; static ConsoleMode arg_console_mode = CONSOLE_INTERACTIVE; +static ConsoleTransport arg_console_transport = CONSOLE_TRANSPORT_VIRTIO; static NetworkStack arg_network_stack = NETWORK_STACK_NONE; static MachineCredentialContext arg_credentials = {}; static uid_t arg_uid_shift = UID_INVALID, arg_uid_range = 0x10000U; @@ -291,6 +292,8 @@ static int help(void) { "\n%3$sInput/Output:%4$s\n" " --console=MODE Console mode (interactive, native, gui, read-only\n" " or headless)\n" + " --console-transport=TRANSPORT\n" + " Console transport (virtio or serial)\n" " --background=COLOR Set ANSI color for background\n" "\n%3$sCredentials:%4$s\n" " --set-credential=ID:VALUE\n" @@ -375,6 +378,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_USER, ARG_IMAGE_FORMAT, ARG_IMAGE_DISK_TYPE, + ARG_CONSOLE_TRANSPORT, }; static const struct option options[] = { @@ -402,6 +406,7 @@ static int parse_argv(int argc, char *argv[]) { { "linux", required_argument, NULL, ARG_LINUX }, { "initrd", required_argument, NULL, ARG_INITRD }, { "console", required_argument, NULL, ARG_CONSOLE }, + { "console-transport", required_argument, NULL, ARG_CONSOLE_TRANSPORT }, { "qemu-gui", no_argument, NULL, ARG_QEMU_GUI }, /* compat option */ { "network-tap", no_argument, NULL, 'n' }, { "network-user-mode", no_argument, NULL, ARG_NETWORK_USER_MODE }, @@ -578,6 +583,13 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_CONSOLE_TRANSPORT: + arg_console_transport = console_transport_from_string(optarg); + if (arg_console_transport < 0) + return log_error_errno(arg_console_transport, "Failed to parse specified console transport: %s", optarg); + + break; + case ARG_QEMU_GUI: arg_console_mode = CONSOLE_GUI; break; @@ -2588,11 +2600,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (r < 0) return log_oom(); - r = qemu_config_section(config_file, "device", "vmspawn-virtio-serial-pci", - "driver", "virtio-serial-pci"); - if (r < 0) - return r; - /* Enable mux for native console so the QEMU monitor is accessible via Ctrl-a c */ r = qemu_config_section(config_file, "chardev", "console", "backend", "serial", @@ -2601,12 +2608,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (r < 0) return r; - r = qemu_config_section(config_file, "device", "virtconsole0", - "driver", "virtconsole", - "chardev", "console"); - if (r < 0) - return r; - if (arg_console_mode == CONSOLE_NATIVE) { r = qemu_config_section(config_file, "mon", "mon0", "chardev", "console"); @@ -2655,6 +2656,29 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { assert_not_reached(); } + if (!IN_SET(arg_console_mode, CONSOLE_GUI, CONSOLE_HEADLESS)) { + if (arg_console_transport == CONSOLE_TRANSPORT_SERIAL) { + /* Use -serial to connect the chardev to the platform's default serial + * device (e.g. isa-serial on x86, PL011 on ARM). On some platforms the + * serial device is a sysbus device that can only be connected via + * serial_hd() which is populated by -serial, not via the config file. */ + r = strv_extend_many(&cmdline, "-serial", "chardev:console"); + if (r < 0) + return log_oom(); + } else { + r = qemu_config_section(config_file, "device", "vmspawn-virtio-serial-pci", + "driver", "virtio-serial-pci"); + if (r < 0) + return r; + + r = qemu_config_section(config_file, "device", "virtconsole0", + "driver", "virtconsole", + "chardev", "console"); + if (r < 0) + return r; + } + } + r = qemu_config_section(config_file, "drive", "ovmf-code", "if", "pflash", "format", ovmf_config_format(ovmf_config), @@ -3028,7 +3052,9 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { } if (!IN_SET(arg_console_mode, CONSOLE_GUI, CONSOLE_HEADLESS)) { - r = strv_prepend(&arg_kernel_cmdline_extra, "console=hvc0"); + r = strv_prepend(&arg_kernel_cmdline_extra, + arg_console_transport == CONSOLE_TRANSPORT_SERIAL ? + "console=" QEMU_SERIAL_CONSOLE_NAME : "console=hvc0"); if (r < 0) return log_oom();