From 1b7a42178a7c16d187e105a4f9e524d6ffec369d Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Fri, 27 Mar 2026 13:38:47 +0000 Subject: [PATCH] vmspawn: use PTY for native console to avoid QEMU O_NONBLOCK issue QEMU's stdio chardev sets O_NONBLOCK on both stdin and stdout (see chardev/char-stdio.c [1] and chardev/char-fd.c [2]). Since forked processes share file descriptions, and on a terminal all three stdio fds typically reference the same file description, this affects our own stdio too. Avoid this by using a PTY with chardev serial instead of chardev stdio for native console mode, matching the approach already used for interactive and read-only modes. The PTY forwarder shovels bytes transparently between our stdio and QEMU's PTY using the new PTY_FORWARD_DUMB_TERMINAL and PTY_FORWARD_TRANSPARENT flags, which disable terminal decoration (background tinting, window title, OSC context) and escape sequence handling (Ctrl-] exit, hotkeys) respectively. The chardev is configured with mux=on so the QEMU monitor remains accessible via Ctrl-a c. Also dedup CONSOLE_NATIVE, CONSOLE_READ_ONLY, and CONSOLE_INTERACTIVE handling by using fallthrough, with the only differences being the ptyfwd flags, mux setting, and monitor section. [1] https://gitlab.com/qemu-project/qemu/-/blob/master/chardev/char-stdio.c [2] https://gitlab.com/qemu-project/qemu/-/blob/master/chardev/char-fd.c Co-developed-by: Claude Opus 4.6 --- src/vmspawn/vmspawn.c | 93 +++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 51 deletions(-) diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c index 0de314ac18..8ad3fb6164 100644 --- a/src/vmspawn/vmspawn.c +++ b/src/vmspawn/vmspawn.c @@ -2557,12 +2557,22 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { PTYForwardFlags ptyfwd_flags = 0; switch (arg_console_mode) { - case CONSOLE_READ_ONLY: - ptyfwd_flags |= PTY_FORWARD_READ_ONLY; + case CONSOLE_NATIVE: + /* Use a PTY instead of chardev stdio to prevent QEMU from setting O_NONBLOCK on + * our stdio file descriptions (see qemu's chardev/char-stdio.c and char-fd.c). + * Use PTY_FORWARD_DUMB_TERMINAL|PTY_FORWARD_TRANSPARENT so the forwarder just + * shovels bytes without any terminal manipulation or escape sequence handling. */ + ptyfwd_flags |= PTY_FORWARD_DUMB_TERMINAL|PTY_FORWARD_TRANSPARENT; _fallthrough_; - case CONSOLE_INTERACTIVE: { + case CONSOLE_READ_ONLY: + if (arg_console_mode == CONSOLE_READ_ONLY) + ptyfwd_flags |= PTY_FORWARD_READ_ONLY; + + _fallthrough_; + + case CONSOLE_INTERACTIVE: { _cleanup_free_ char *pty_path = NULL; master = openpt_allocate(O_RDWR|O_NONBLOCK, &pty_path); @@ -2578,9 +2588,11 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { 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", - "path", pty_path); + "path", pty_path, + "mux", on_off(arg_console_mode == CONSOLE_NATIVE)); if (r < 0) return r; @@ -2590,6 +2602,13 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { if (r < 0) return r; + if (arg_console_mode == CONSOLE_NATIVE) { + r = qemu_config_section(config_file, "mon", "mon0", + "chardev", "console"); + if (r < 0) + return r; + } + break; } @@ -2620,36 +2639,6 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { break; - case CONSOLE_NATIVE: - r = strv_extend_many(&cmdline, "-nographic", "-nodefaults"); - if (r < 0) - return log_oom(); - - r = qemu_config_section(config_file, "chardev", "console", - "backend", "stdio", - "mux", "on", - "signal", "off"); - if (r < 0) - return r; - - 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, "mon", "mon0", - "chardev", "console"); - if (r < 0) - return r; - - break; - case CONSOLE_HEADLESS: r = strv_extend_many(&cmdline, "-nographic", "-nodefaults"); if (r < 0) @@ -3533,29 +3522,31 @@ static int run_virtual_machine(int kvm_device_fd, int vhost_device_fd) { _cleanup_(osc_context_closep) sd_id128_t osc_context_id = SD_ID128_NULL; _cleanup_(pty_forward_freep) PTYForward *forward = NULL; if (master >= 0) { - if (!terminal_is_dumb()) { - r = osc_context_open_vm(arg_machine, /* ret_seq= */ NULL, &osc_context_id); - if (r < 0) - return r; - } - r = pty_forward_new(event, master, ptyfwd_flags, &forward); if (r < 0) return log_error_errno(r, "Failed to create PTY forwarder: %m"); - if (!arg_background) { - _cleanup_free_ char *bg = NULL; + if (!FLAGS_SET(ptyfwd_flags, PTY_FORWARD_DUMB_TERMINAL)) { + if (!terminal_is_dumb()) { + r = osc_context_open_vm(arg_machine, /* ret_seq= */ NULL, &osc_context_id); + if (r < 0) + return r; + } - r = terminal_tint_color(130 /* green */, &bg); - if (r < 0) - log_debug_errno(r, "Failed to determine terminal background color, not tinting."); - else - (void) pty_forward_set_background_color(forward, bg); - } else if (!isempty(arg_background)) - (void) pty_forward_set_background_color(forward, arg_background); + if (!arg_background) { + _cleanup_free_ char *bg = NULL; - (void) pty_forward_set_window_title(forward, GLYPH_GREEN_CIRCLE, /* hostname= */ NULL, - STRV_MAKE("Virtual Machine", arg_machine)); + r = terminal_tint_color(130 /* green */, &bg); + if (r < 0) + log_debug_errno(r, "Failed to determine terminal background color, not tinting."); + else + (void) pty_forward_set_background_color(forward, bg); + } else if (!isempty(arg_background)) + (void) pty_forward_set_background_color(forward, arg_background); + + (void) pty_forward_set_window_title(forward, GLYPH_GREEN_CIRCLE, /* hostname= */ NULL, + STRV_MAKE("Virtual Machine", arg_machine)); + } } r = sd_event_loop(event);