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 <noreply@anthropic.com>
This commit is contained in:
Daan De Meyer
2026-04-02 15:12:52 +02:00
committed by Daan De Meyer
parent b68898530e
commit 1b7a42178a
+42 -51
View File
@@ -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);