diff --git a/examples/consoles.c b/examples/consoles.c index 30a17a492..7e738e28e 100644 --- a/examples/consoles.c +++ b/examples/consoles.c @@ -5,11 +5,15 @@ #include #include #include +#include +#include #include #include #include +#define NUM_RESERVED_PORTS 64 + static int cmd_output(char *output, size_t output_size, const char *prog, ...) { va_list args; @@ -63,12 +67,11 @@ static int create_tmux_tty(const char *session_name) { char tty_path[256]; char wait_cmd[128]; - + snprintf(wait_cmd, sizeof(wait_cmd), "waitpid %d", (int)getpid()); if (cmd("tmux", "new-session", "-d", "-s", session_name, "sh", "-c", wait_cmd, NULL) != 0) return -1; - // Hook up tmux to send us SIGWINCH signal on resize char hook_cmd[128]; snprintf(hook_cmd, sizeof(hook_cmd), "run-shell 'kill -WINCH %d'", (int)getpid()); cmd("tmux", "set-hook", "-g", "client-resized", hook_cmd, NULL); @@ -90,23 +93,120 @@ static int mkfifo_if_needed(const char *path) return 0; } - static int create_fifo_inout(const char *fifo_in, const char *fifo_out, int *input_fd, int *output_fd) { if (mkfifo_if_needed(fifo_in) < 0) return -1; if (mkfifo_if_needed(fifo_out) < 0) return -1; - int in_fd = open(fifo_in, O_RDONLY | O_NONBLOCK); - if (in_fd < 0) return -1; + *input_fd = open(fifo_in, O_RDWR | O_NONBLOCK); + if (*input_fd < 0) return -1; - int out_fd = open(fifo_out, O_RDWR | O_NONBLOCK); - if (out_fd < 0) { close(in_fd); return -1; } + *output_fd = open(fifo_out, O_RDWR | O_NONBLOCK); + if (*output_fd < 0) { + close(*input_fd); + return -1; + } - *input_fd = in_fd; - *output_fd = out_fd; return 0; } +struct console_state { + uint32_t ctx_id; + uint32_t console_id; + int ready_fd; +}; + +static void *dynamic_console_thread(void *arg) +{ + struct console_state *state = arg; + int ready_fd = state->ready_fd; + + struct pollfd pfd = { .fd = ready_fd, .events = POLLIN }; + fprintf(stderr, "Waiting for console device...\n"); + if (poll(&pfd, 1, -1) < 0) { + perror("poll"); + return NULL; + } + + uint64_t val; + if (read(ready_fd, &val, sizeof(val)) != sizeof(val)) { + perror("read eventfd"); + return NULL; + } + + fprintf(stderr, "\n"); + fprintf(stderr, "=== VM Started ===\n"); + fprintf(stderr, "\n"); + fprintf(stderr, "*** To interact with the VM (hvc0), run in another terminal: ***\n"); + fprintf(stderr, " tmux attach -t krun-console-1\n"); + fprintf(stderr, "\n"); + fprintf(stderr, "Commands: 'c' = add console\n"); + fprintf(stderr, " 'p' = add pipe\n"); + fprintf(stderr, "\n"); + + int console_count = 1; /* console-1 already exists (hvc0) */ + int pipe_count = 0; + char line[16]; + while (1) { + fprintf(stderr, "> "); + if (fgets(line, sizeof(line), stdin) == NULL) break; + + if (line[0] == 'c' || line[0] == 'C') { + console_count++; + char sess[64], port[64]; + snprintf(sess, sizeof(sess), "krun-console-%d", console_count); + snprintf(port, sizeof(port), "console-%d", console_count); + + int fd = create_tmux_tty(sess); + if (fd < 0) { fprintf(stderr, "tmux: failed to create session '%s'\n", sess); continue; } + + int err = krun_add_console_port_tty(state->ctx_id, state->console_id, port, fd); + if (err) { fprintf(stderr, "add port: %s\n", strerror(-err)); close(fd); continue; } + + fprintf(stderr, "Created console '%s' (port %d, /dev/hvc%d):\n", port, console_count + pipe_count - 1, console_count - 1); + fprintf(stderr, " On host: tmux attach -t %s\n", sess); + fprintf(stderr, " In guest: setsid /sbin/agetty -a $(whoami) -L hvc%d xterm-256color\n", console_count - 1); + if (console_count + pipe_count > NUM_RESERVED_PORTS) { + fprintf(stderr, "Reached max reserved ports (%d)\n", NUM_RESERVED_PORTS); + break; + } + } + + if (line[0] == 'p' || line[0] == 'P') { + pipe_count++; + char port[64], fifo_in[128], fifo_out[128]; + snprintf(port, sizeof(port), "pipe-%d", pipe_count); + snprintf(fifo_in, sizeof(fifo_in), "/tmp/krun_pipe%d_in", pipe_count); + snprintf(fifo_out, sizeof(fifo_out), "/tmp/krun_pipe%d_out", pipe_count); + + int in_fd, out_fd; + if (create_fifo_inout(fifo_in, fifo_out, &in_fd, &out_fd) < 0) { + perror("create_fifo_inout"); continue; + } + + int err = krun_add_console_port_inout(state->ctx_id, state->console_id, port, in_fd, out_fd); + if (err) { + fprintf(stderr, "add port: %s\n", strerror(-err)); + close(in_fd); + close(out_fd); + continue; + } + + fprintf(stderr, "Created pipe '%s' (port %d):\n", port, console_count + pipe_count - 1); + fprintf(stderr, " In guest: DEV=/dev/$(grep -l %s /sys/class/virtio-ports/*/name | cut -d/ -f5)\n", port); + fprintf(stderr, " cat $DEV OR echo data > $DEV\n"); + fprintf(stderr, " On host: echo 'data' > %s # send to guest\n", fifo_in); + fprintf(stderr, " cat %s # receive from guest\n", fifo_out); + if (console_count + pipe_count > NUM_RESERVED_PORTS) { + fprintf(stderr, "Reached max reserved ports (%d)\n", NUM_RESERVED_PORTS); + break; + } + } + } + + return NULL; +} + int main(int argc, char *const argv[]) { if (argc < 3) { @@ -119,100 +219,66 @@ int main(int argc, char *const argv[]) const char *const *command_args = (argc > 3) ? (const char *const *)&argv[3] : NULL; const char *const envp[] = { 0 }; - krun_set_log_level(KRUN_LOG_LEVEL_WARN); + krun_set_log_level(KRUN_LOG_LEVEL_DEBUG); int err; int ctx_id = krun_create_ctx(); if (ctx_id < 0) { errno = -ctx_id; perror("krun_create_ctx"); return 1; } if ((err = krun_disable_implicit_console(ctx_id))) { - errno = -err; - perror("krun_disable_implicit_console"); - return 1; + errno = -err; perror("krun_disable_implicit_console"); return 1; } int console_id = krun_add_virtio_console_multiport(ctx_id); if (console_id < 0) { - errno = -console_id; - perror("krun_add_virtio_console_multiport"); - return 1; + errno = -console_id; perror("krun_add_virtio_console_multiport"); return 1; } - /* Configure console ports - edit this section to add/remove ports */ + /* Create 1 initial console BEFORE VM starts - this will run the command */ { - - // You could also use the controlling terminal of this process in the guest: - /* - if ((err = krun_add_console_port_tty(ctx_id, console_id, "host_tty", open("/dev/tty", O_RDWR)))) { - errno = -err; - perror("port host_tty"); - return 1; - } - */ - - int num_consoles = 3; - for (int i = 0; i < num_consoles; i++) { - char session_name[64]; - char port_name[64]; - snprintf(session_name, sizeof(session_name), "krun-console-%d", i + 1); - snprintf(port_name, sizeof(port_name), "console-%d", i + 1); - - int tmux_fd = create_tmux_tty(session_name); - if (tmux_fd < 0) { - perror("create_tmux_tty"); - return 1; - } - if ((err = krun_add_console_port_tty(ctx_id, console_id, port_name, tmux_fd))) { - errno = -err; - perror("krun_add_console_port_tty"); - return 1; - } - } - - int in_fd, out_fd; - if (create_fifo_inout("/tmp/consoles_example_in", "/tmp/consoles_example_out", &in_fd, &out_fd) < 0) { - perror("create_fifo_inout"); - return 1; - } - if ((err = krun_add_console_port_inout(ctx_id, console_id, "fifo_inout", in_fd, out_fd))) { - errno = -err; - perror("krun_add_console_port_inout"); - return 1; + int fd = create_tmux_tty("krun-console-1"); + if (fd < 0) { fprintf(stderr, "create_tmux_tty failed (session already exists?)\n"); return 1; } + if ((err = krun_add_console_port_tty(ctx_id, console_id, "console-1", fd))) { + errno = -err; perror("krun_add_console_port_tty"); return 1; } + } - fprintf(stderr, "\n=== Console ports configured ===\n"); - for (int i = 0; i < num_consoles; i++) { - fprintf(stderr, " console-%d: tmux attach -t krun-console-%d\n", i + 1, i + 1); - } - fprintf(stderr, " fifo_inout: /tmp/consoles_example_in (host->guest)\n"); - fprintf(stderr, " fifo_inout: /tmp/consoles_example_out (guest->host)\n"); - fprintf(stderr, "================================\n\n"); + /* Reserve ports for dynamic addition */ + if ((err = krun_console_reserve_ports(ctx_id, console_id, NUM_RESERVED_PORTS))) { + errno = -err; perror("krun_console_reserve_ports"); return 1; } if ((err = krun_set_vm_config(ctx_id, 4, 4096))) { - errno = -err; - perror("krun_set_vm_config"); - return 1; + errno = -err; perror("krun_set_vm_config"); return 1; } - if ((err = krun_set_root(ctx_id, root_dir))) { - errno = -err; - perror("krun_set_root"); - return 1; + errno = -err; perror("krun_set_root"); return 1; } - if ((err = krun_set_exec(ctx_id, command, command_args, envp))) { - errno = -err; - perror("krun_set_exec"); - return 1; + errno = -err; perror("krun_set_exec"); return 1; + } + + fprintf(stderr, "\nStarting VM...\n"); + + int ready_fd = krun_get_console_ready_fd(ctx_id, console_id); + if (ready_fd < 0) { + errno = -ready_fd; perror("krun_get_console_ready_fd"); return 1; } + struct console_state state = { + .ctx_id = ctx_id, + .console_id = console_id, + .ready_fd = ready_fd, + }; + + pthread_t dyn_thread; + pthread_create(&dyn_thread, NULL, dynamic_console_thread, &state); + pthread_detach(dyn_thread); + + /* Run VM in main thread - this blocks until VM exits, then calls _exit() */ if ((err = krun_start_enter(ctx_id))) { - errno = -err; - perror("krun_start_enter"); - return 1; + errno = -err; perror("krun_start_enter"); return 1; } + return 0; } - - diff --git a/include/libkrun.h b/include/libkrun.h index b0d89ee6e..23a30be53 100644 --- a/include/libkrun.h +++ b/include/libkrun.h @@ -1109,6 +1109,42 @@ int32_t krun_add_serial_console_default(uint32_t ctx_id, */ int32_t krun_add_virtio_console_multiport(uint32_t ctx_id); +/* + * Reserves additional port slots on a multi-port virtio-console device for dynamic addition + * after the VM has started. + * + * This function must be called before krun_start_enter(). The reserved ports can be populated + * later by calling krun_add_console_port_tty() or krun_add_console_port_inout() on a running VM. + * + * Arguments: + * "ctx_id" - the configuration context ID. + * "console_id" - the console ID returned by krun_add_virtio_console_multiport(). + * "num_ports" - the number of additional port slots to reserve. + * + * Returns: + * Zero on success or a negative error number on failure. + */ +int32_t krun_console_reserve_ports(uint32_t ctx_id, uint32_t console_id, uint32_t num_ports); + +/* + * Returns an eventfd that becomes readable when the virtio-console device is ready + * to accept dynamically added ports. + * + * This function must be called after krun_start_enter() has been invoked (typically from + * another thread). The returned fd can be polled; when readable, read an 8-byte value + * from it to consume the event, then call krun_add_console_port_tty() or + * krun_add_console_port_inout() to add ports dynamically. + * + * Arguments: + * "ctx_id" - the configuration context ID. + * "console_id" - the console ID returned by krun_add_virtio_console_multiport(). + * + * Returns: + * The eventfd file descriptor (>= 0) on success, or a negative error number on failure. + * Returns -EAGAIN if the VM has not been started yet. + */ +int32_t krun_get_console_ready_fd(uint32_t ctx_id, uint32_t console_id); + /* * Adds a TTY port to a multi-port virtio-console device. * diff --git a/src/devices/Cargo.toml b/src/devices/Cargo.toml index 9ec04c141..8597c669d 100644 --- a/src/devices/Cargo.toml +++ b/src/devices/Cargo.toml @@ -11,8 +11,8 @@ tdx = ["blk", "tee"] net = [] blk = [] efi = ["blk", "net"] -gpu = ["rutabaga_gfx", "thiserror", "zerocopy", "krun_display"] -snd = ["pw", "thiserror"] +gpu = ["rutabaga_gfx", "zerocopy", "krun_display"] +snd = ["pw"] input = ["zerocopy", "krun_input"] virgl_resource_map2 = [] nitro = [] @@ -27,7 +27,7 @@ log = "0.4.0" nix = { version = "0.30.1", features = ["ioctl", "net", "poll", "socket", "fs"] } pw = { package = "pipewire", version = "0.8.0", optional = true } rand = "0.9.2" -thiserror = { version = "2.0", optional = true } +thiserror = { version = "2.0" } virtio-bindings = "0.2.0" vm-memory = { version = ">=0.13", features = ["backend-mmap"] } zerocopy = { version = "0.8.26", optional = true, features = ["derive"] } diff --git a/src/devices/src/virtio/console/device.rs b/src/devices/src/virtio/console/device.rs index 2e4896e9c..33eb31523 100644 --- a/src/devices/src/virtio/console/device.rs +++ b/src/devices/src/virtio/console/device.rs @@ -1,13 +1,3 @@ -use std::cmp; -use std::io::Write; -use std::iter::zip; -use std::mem::{size_of, size_of_val}; -use std::os::unix::io::{AsRawFd, RawFd}; -use std::sync::Arc; - -use utils::eventfd::EventFd; -use vm_memory::{ByteValued, Bytes, GuestMemoryMmap}; - use super::super::{ ActivateError, ActivateResult, ConsoleError, DeviceState, Queue as VirtQueue, VirtioDevice, }; @@ -21,6 +11,16 @@ use crate::virtio::console::port_queue_mapping::{ num_queues, port_id_to_queue_idx, QueueDirection, }; use crate::virtio::{InterruptTransport, PortDescription, VmmExitObserver}; +use std::cmp; +use std::io::Write; +use std::iter::zip; +use std::mem::{size_of, size_of_val}; +use std::os::fd::BorrowedFd; +use std::os::unix::io::{AsRawFd, RawFd}; +use std::sync::{Arc, Mutex}; +use thiserror::Error; +use utils::eventfd::EventFd; +use vm_memory::{ByteValued, Bytes, GuestMemoryMmap}; pub(crate) const CONTROL_RXQ_INDEX: usize = 2; pub(crate) const CONTROL_TXQ_INDEX: usize = 3; @@ -67,13 +67,75 @@ pub struct Console { pub(crate) sigwinch_evt: EventFd, config: VirtioConsoleConfig, + ready: bool, + console_ready_evt: Arc, +} + +#[derive(Error, Debug)] +#[repr(i32)] +pub enum ConsoleControllerError { + #[error("Console device is not ready to ")] + NotReady, + + #[error("Backend implementation error")] + OutOfPorts, +} + +#[derive(Clone)] +pub struct ConsoleController { + console: Arc>, +} + +impl ConsoleController { + pub fn console_ready_fd(&self) -> BorrowedFd<'_> { + unsafe { + BorrowedFd::borrow_raw(self.console.lock().unwrap().console_ready_evt.as_raw_fd()) + } + } + + pub fn is_ready(&self) -> bool { + self.console.lock().unwrap().ready + } + + pub fn add_port( + &self, + port_description: PortDescription, + ) -> Result<(), ConsoleControllerError> { + let mut console = self.console.lock().unwrap(); + if !console.ready { + return Err(ConsoleControllerError::NotReady); + } + + if console.ports.len() as u32 >= console.config.max_nr_ports { + return Err(ConsoleControllerError::OutOfPorts); + } + + let port_id = console.ports.len() as u32; + console.ports.push(Port::new(port_id, port_description)); + + // Notify the guest we have added a port + // From this point forward, we will automatically set up the port as open and start worker + // threads once we receive the VIRTIO_CONSOLE_PORT_READY + console.control.port_add(port_id); + Ok(()) + } } impl Console { - pub fn new(ports: Vec) -> super::Result { - assert!(!ports.is_empty(), "Expected at least 1 port"); + pub fn new( + ports: Vec, + reserved_port_count: u32, + console_ready_evt: Arc, + ) -> super::Result { + assert!( + !ports.is_empty() || reserved_port_count > 0, + "Expected at least 1 port or reserved port" + ); - let num_queues = num_queues(ports.len()); + let max_ports = ports.len() as u32 + reserved_port_count; + + // Pre-allocate queues for all potential ports + let num_queues = num_queues(max_ports as usize); let queues = vec![VirtQueue::new(QUEUE_SIZE); num_queues]; let mut queue_events = Vec::new(); @@ -86,11 +148,13 @@ impl Console { .map(|(port_id, description)| Port::new(port_id, description)) .collect(); - let (cols, rows) = ports[0] - .terminal() + let (cols, rows) = ports + .first() + .and_then(|p| p.terminal()) .map(|t| t.get_win_size()) .unwrap_or((0, 0)); - let config = VirtioConsoleConfig::new(cols, rows, ports.len() as u32); + + let config = VirtioConsoleConfig::new(cols, rows, max_ports); Ok(Console { control: ConsoleControl::new(), @@ -105,9 +169,24 @@ impl Console { .map_err(ConsoleError::EventFd)?, device_state: DeviceState::Inactive, config, + ready: false, + console_ready_evt, }) } + pub fn with_controller( + ports: Vec, + reserved_port_count: u32, + console_ready_evt: Arc, + ) -> super::Result<(Arc>, ConsoleController)> { + let console = Arc::new(Mutex::new(Self::new( + ports, + reserved_port_count, + console_ready_evt, + )?)); + Ok((console.clone(), ConsoleController { console })) + } + pub fn id(&self) -> &str { defs::CONSOLE_DEV_ID } @@ -195,6 +274,11 @@ impl Console { for port_id in 0..self.ports.len() { self.control.port_add(port_id as u32); } + if let Err(e) = self.console_ready_evt.write(1) { + warn!("Failed to trigger console_ready_evt: {e:?}"); + } + self.console_ready_evt.write(1).unwrap(); + self.ready = true; } control_event::VIRTIO_CONSOLE_PORT_READY => { if cmd.value != 1 { @@ -234,12 +318,19 @@ impl Console { } }; - if !opened { + if opened { + ports_to_start.push(cmd.id); + } else if let Some(port) = self.ports.get_mut(cmd.id as usize) { log::debug!("Guest closed port {}", cmd.id); - continue; + port.shutdown(); + // TODO: close the underlying file descriptors for the port + // (note that it requires an API for being able to reopen the ports!) + } else { + warn!( + "Guest tried to close port {} but we don't have such port!", + cmd.id + ); } - - ports_to_start.push(cmd.id as usize); } _ => log::warn!("Unknown console control event {:x}", cmd.event), } @@ -247,13 +338,15 @@ impl Console { for port_id in ports_to_start { log::trace!("Starting port io for port {port_id}"); - self.ports[port_id].start( - mem.clone(), - self.queues[port_id_to_queue_idx(QueueDirection::Rx, port_id)].clone(), - self.queues[port_id_to_queue_idx(QueueDirection::Tx, port_id)].clone(), - interrupt.clone(), - self.control.clone(), - ); + if let Some(port) = self.ports.get_mut(port_id as usize) { + port.start( + mem.clone(), + self.queues[port_id_to_queue_idx(QueueDirection::Rx, port_id as usize)].clone(), + self.queues[port_id_to_queue_idx(QueueDirection::Tx, port_id as usize)].clone(), + interrupt.clone(), + self.control.clone(), + ); + } } raise_irq @@ -335,9 +428,10 @@ impl VirtioDevice for Console { // the device, but we don't support any scenario in which // neither GuestMemory nor the queue events would change, // so let's avoid doing any unnecessary work. - for port in &mut self.ports { + for port in self.ports.iter_mut() { port.shutdown(); } + self.ready = false; true } } diff --git a/src/devices/src/virtio/console/mod.rs b/src/devices/src/virtio/console/mod.rs index 33e47994c..4c785a277 100644 --- a/src/devices/src/virtio/console/mod.rs +++ b/src/devices/src/virtio/console/mod.rs @@ -8,7 +8,7 @@ mod process_rx; mod process_tx; pub use self::defs::uapi::VIRTIO_ID_CONSOLE as TYPE_CONSOLE; -pub use self::device::Console; +pub use self::device::{Console, ConsoleController, ConsoleControllerError}; pub use self::port::PortDescription; mod defs { diff --git a/src/libkrun/src/lib.rs b/src/libkrun/src/lib.rs index 13f0fb5f7..57253f7fd 100644 --- a/src/libkrun/src/lib.rs +++ b/src/libkrun/src/lib.rs @@ -28,18 +28,17 @@ use std::ffi::CString; use std::ffi::{c_void, CStr}; use std::fs::File; use std::io::IsTerminal; -#[cfg(target_os = "linux")] use std::os::fd::AsRawFd; use std::os::fd::{BorrowedFd, FromRawFd, RawFd}; use std::path::PathBuf; use std::slice; use std::sync::atomic::{AtomicI32, Ordering}; -use std::sync::LazyLock; use std::sync::Mutex; +use std::sync::{Arc, LazyLock}; use utils::eventfd::EventFd; use vmm::resources::{ - DefaultVirtioConsoleConfig, PortConfig, SerialConsoleConfig, TsiFlags, VirtioConsoleConfigMode, - VmResources, VsockConfig, + DefaultVirtioConsoleConfig, PortConfig, SerialConsoleConfig, TsiFlags, VirtioConsoleConfig, + VirtioConsoleConfigMode, VmResources, VsockConfig, }; #[cfg(feature = "blk")] use vmm::vmm_config::block::{BlockDeviceConfig, BlockRootConfig}; @@ -57,12 +56,14 @@ use vmm::vmm_config::machine_config::VmConfig; #[cfg(feature = "net")] use vmm::vmm_config::net::NetworkInterfaceConfig; use vmm::vmm_config::vsock::VsockDeviceConfig; +use vmm::Vmm; #[cfg(feature = "nitro")] use nitro::enclaves::NitroEnclave; #[cfg(feature = "gpu")] use devices::virtio::display::{DisplayInfoEdid, PhysicalSize, MAX_DISPLAYS}; +use devices::virtio::{port_io, ConsoleControllerError, PortDescription}; #[cfg(feature = "input")] use krun_input::{InputConfigBackend, InputEventProviderBackend}; @@ -428,7 +429,13 @@ fn with_cfg(ctx_id: u32, f: impl FnOnce(&mut ContextConfig) -> i32) -> i32 { } } +// Vmm configuration(s) to be started static CTX_MAP: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); + +// Running vmm instances +static RUNNING_CTX_MAP: Lazy>>>> = + Lazy::new(|| Mutex::new(HashMap::new())); + static CTX_IDS: AtomicI32 = AtomicI32::new(0); fn log_level_to_filter_str(level: u32) -> &'static str { @@ -2326,15 +2333,18 @@ pub unsafe extern "C" fn krun_add_virtio_console_default( Entry::Occupied(mut ctx_cfg) => { let cfg = ctx_cfg.get_mut(); - cfg.vmr - .virtio_consoles - .push(VirtioConsoleConfigMode::Autoconfigure( - DefaultVirtioConsoleConfig { - input_fd, - output_fd, - err_fd, - }, - )); + let console_ready_evt = Arc::new( + EventFd::new(utils::eventfd::EFD_NONBLOCK) + .expect("Failed to create console_ready_evt"), + ); + cfg.vmr.virtio_consoles.push(VirtioConsoleConfig { + mode: VirtioConsoleConfigMode::Autoconfigure(DefaultVirtioConsoleConfig { + input_fd, + output_fd, + err_fd, + }), + console_ready_evt, + }); } Entry::Vacant(_) => return -libc::ENOENT, } @@ -2350,9 +2360,17 @@ pub unsafe extern "C" fn krun_add_virtio_console_multiport(ctx_id: u32) -> i32 { let cfg = ctx_cfg.get_mut(); let console_id = cfg.vmr.virtio_consoles.len() as i32; - cfg.vmr - .virtio_consoles - .push(VirtioConsoleConfigMode::Explicit(Vec::new())); + let console_ready_evt = Arc::new( + EventFd::new(utils::eventfd::EFD_NONBLOCK) + .expect("Failed to create console_ready_evt"), + ); + cfg.vmr.virtio_consoles.push(VirtioConsoleConfig { + mode: VirtioConsoleConfigMode::Explicit { + ports: Vec::new(), + reserved_count: 0, + }, + console_ready_evt, + }); console_id } @@ -2360,6 +2378,61 @@ pub unsafe extern "C" fn krun_add_virtio_console_multiport(ctx_id: u32) -> i32 { } } +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub unsafe extern "C" fn krun_console_reserve_ports( + ctx_id: u32, + console_id: u32, + num_ports: u32, +) -> i32 { + match CTX_MAP.lock().unwrap().entry(ctx_id) { + Entry::Occupied(mut ctx_cfg) => { + match ctx_cfg + .get_mut() + .vmr + .virtio_consoles + .get_mut(console_id as usize) + { + Some(VirtioConsoleConfig { + mode: VirtioConsoleConfigMode::Explicit { reserved_count, .. }, + .. + }) => { + *reserved_count += num_ports; + KRUN_SUCCESS + } + _ => -libc::EINVAL, + } + } + Entry::Vacant(_) => -libc::ENOENT, + } +} + +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub unsafe extern "C" fn krun_get_console_ready_fd(ctx_id: u32, console_id: u32) -> i32 { + // Check if VM is running + if let Some(vmm) = RUNNING_CTX_MAP.lock().unwrap().get(&ctx_id).cloned() { + let vmm = vmm.lock().unwrap(); + let Some(console) = vmm.console_controller(console_id) else { + error!("krun_get_console_ready_fd: Invalid console id={console_id}"); + return -libc::ENOENT; + }; + return console.console_ready_fd().as_raw_fd(); + } + + // VM not running yet, get the pre-created eventfd from CTX_MAP + match CTX_MAP.lock().unwrap().get(&ctx_id) { + Some(ctx_cfg) => match ctx_cfg.vmr.virtio_consoles.get(console_id as usize) { + Some(console_cfg) => console_cfg.console_ready_evt.as_raw_fd(), + None => { + error!("krun_get_console_ready_fd: Invalid console id={console_id}"); + -libc::ENOENT + } + }, + None => -libc::ENOENT, + } +} + #[allow(clippy::missing_safety_doc)] #[no_mangle] pub unsafe extern "C" fn krun_add_console_port_tty( @@ -2385,12 +2458,36 @@ pub unsafe extern "C" fn krun_add_console_port_tty( return -libc::ENOTTY; } + if let Some(vmm) = RUNNING_CTX_MAP.lock().unwrap().get(&ctx_id) { + let mut vmm = vmm.lock().unwrap(); + vmm.setup_terminal_raw_mode(unsafe { BorrowedFd::borrow_raw(tty_fd) }, false); + + let Some(console) = vmm.console_controller(console_id) else { + error!("krun_add_console_port_tty: Invalid console id={console_id}"); + return -libc::ENOENT; + }; + + return match console.add_port(PortDescription { + name: name_str.into(), + input: Some(port_io::input_to_raw_fd_dup(tty_fd).unwrap()), + output: Some(port_io::output_to_raw_fd_dup(tty_fd).unwrap()), + terminal: Some(port_io::term_fd(tty_fd).unwrap()), + }) { + Ok(()) => KRUN_SUCCESS, + Err(ConsoleControllerError::NotReady) => -libc::EAGAIN, + Err(ConsoleControllerError::OutOfPorts) => -libc::ENOMEM, + }; + } + match CTX_MAP.lock().unwrap().entry(ctx_id) { Entry::Occupied(mut ctx_cfg) => { let cfg = ctx_cfg.get_mut(); match cfg.vmr.virtio_consoles.get_mut(console_id as usize) { - Some(VirtioConsoleConfigMode::Explicit(ports)) => { + Some(VirtioConsoleConfig { + mode: VirtioConsoleConfigMode::Explicit { ports, .. }, + .. + }) => { ports.push(PortConfig::Tty { name: name_str, tty_fd, @@ -2422,12 +2519,34 @@ pub unsafe extern "C" fn krun_add_console_port_inout( } }; + if let Some(vmm) = RUNNING_CTX_MAP.lock().unwrap().get(&ctx_id) { + let vmm = vmm.lock().unwrap(); + let Some(console) = vmm.console_controller(console_id) else { + error!("krun_add_console_port_tty: Invalid console id={console_id}"); + return -libc::ENOENT; + }; + + return match console.add_port(PortDescription { + name: name_str.into(), + input: Some(port_io::input_to_raw_fd_dup(input_fd).unwrap()), + output: Some(port_io::output_to_raw_fd_dup(output_fd).unwrap()), + terminal: None, + }) { + Ok(()) => KRUN_SUCCESS, + Err(ConsoleControllerError::NotReady) => -libc::EAGAIN, + Err(ConsoleControllerError::OutOfPorts) => -libc::ENOMEM, + }; + } + match CTX_MAP.lock().unwrap().entry(ctx_id) { Entry::Occupied(mut ctx_cfg) => { let cfg = ctx_cfg.get_mut(); match cfg.vmr.virtio_consoles.get_mut(console_id as usize) { - Some(VirtioConsoleConfigMode::Explicit(ports)) => { + Some(VirtioConsoleConfig { + mode: VirtioConsoleConfigMode::Explicit { ports, .. }, + .. + }) => { ports.push(PortConfig::InOut { name: name_str, input_fd, @@ -2654,7 +2773,7 @@ pub extern "C" fn krun_start_enter(ctx_id: u32) -> i32 { let (sender, _receiver) = unbounded(); - let _vmm = match vmm::builder::build_microvm( + let vmm = match vmm::builder::build_microvm( &ctx_cfg.vmr, &mut event_manager, ctx_cfg.shutdown_efd, @@ -2667,18 +2786,20 @@ pub extern "C" fn krun_start_enter(ctx_id: u32) -> i32 { } }; + RUNNING_CTX_MAP.lock().unwrap().insert(ctx_id, vmm.clone()); + #[cfg(target_os = "macos")] if ctx_cfg.gpu_virgl_flags.is_some() { - vmm::worker::start_worker_thread(_vmm.clone(), _receiver).unwrap(); + vmm::worker::start_worker_thread(vmm.clone(), _receiver).unwrap(); } #[cfg(target_arch = "x86_64")] if ctx_cfg.vmr.split_irqchip { - vmm::worker::start_worker_thread(_vmm.clone(), _receiver.clone()).unwrap(); + vmm::worker::start_worker_thread(vmm.clone(), _receiver.clone()).unwrap(); } #[cfg(any(feature = "amd-sev", feature = "tdx"))] - vmm::worker::start_worker_thread(_vmm.clone(), _receiver.clone()).unwrap(); + vmm::worker::start_worker_thread(vmm.clone(), _receiver.clone()).unwrap(); loop { match event_manager.run() { diff --git a/src/vmm/src/builder.rs b/src/vmm/src/builder.rs index 92ac87079..3c0fd35ec 100644 --- a/src/vmm/src/builder.rs +++ b/src/vmm/src/builder.rs @@ -24,7 +24,8 @@ use super::{Error, Vmm}; use crate::device_manager::legacy::PortIODeviceManager; use crate::device_manager::mmio::MMIODeviceManager; use crate::resources::{ - DefaultVirtioConsoleConfig, PortConfig, TsiFlags, VirtioConsoleConfigMode, VmResources, + DefaultVirtioConsoleConfig, PortConfig, TsiFlags, VirtioConsoleConfig, VirtioConsoleConfigMode, + VmResources, }; use crate::vmm_config::external_kernel::{ExternalKernel, KernelFormat}; #[cfg(feature = "net")] @@ -45,7 +46,9 @@ use devices::legacy::{IoApic, IrqChipT}; use devices::legacy::{IrqChip, IrqChipDevice}; #[cfg(all(target_os = "linux", target_arch = "aarch64"))] use devices::legacy::{KvmGicV2, KvmGicV3}; -use devices::virtio::{port_io, MmioTransport, PortDescription, VirtioDevice, Vsock}; +use devices::virtio::{ + port_io, Console, ConsoleController, MmioTransport, PortDescription, VirtioDevice, Vsock, +}; #[cfg(feature = "tee")] use kbs_types::Tee; @@ -55,7 +58,6 @@ use crate::device_manager; use crate::signal_handler::register_sigint_handler; #[cfg(target_os = "linux")] use crate::signal_handler::register_sigwinch_handler; -use crate::terminal::{term_restore_mode, term_set_raw_mode}; #[cfg(feature = "blk")] use crate::vmm_config::block::BlockBuilder; #[cfg(not(any(feature = "tee", feature = "nitro")))] @@ -196,6 +198,8 @@ pub enum StartMicrovmError { RegisterFsDevice(device_manager::mmio::Error), // Cannot initialize a MMIO Fs Device or add ad device to the MMIO Bus. RegisterConsoleDevice(device_manager::mmio::Error), + /// Cannot create console channel for dynamic port addition. + CreateConsoleChannel(io::Error), /// Cannot register SIGWINCH event file descriptor. #[cfg(target_os = "linux")] RegisterFsSigwinch(kvm_ioctls::Error), @@ -410,6 +414,12 @@ impl Display for StartMicrovmError { "Cannot initialize a MMIO Console Device or add a device to the MMIO Bus. {err_msg}" ) } + CreateConsoleChannel(ref err) => { + write!( + f, + "Cannot create console channel for dynamic port addition. {err}" + ) + } #[cfg(target_os = "linux")] RegisterFsSigwinch(ref err) => { let mut err_msg = format!("{err}"); @@ -962,20 +972,26 @@ pub fn build_microvm( mmio_device_manager, #[cfg(target_arch = "x86_64")] pio_device_manager, + console_controllers: Vec::new(), }; // Set raw mode for FDs that are connected to legacy serial devices. for serial_tty in serial_ttys { - setup_terminal_raw_mode(&mut vmm, Some(serial_tty), false); + vmm.setup_terminal_raw_mode(serial_tty, false); } #[cfg(not(feature = "tee"))] attach_balloon_device(&mut vmm, event_manager, intc.clone())?; #[cfg(not(feature = "tee"))] attach_rng_device(&mut vmm, event_manager, intc.clone())?; + + let mut console_controllers = Vec::with_capacity( + vm_resources.virtio_consoles.len() + !vm_resources.disable_implicit_console as usize, + ); + let mut console_id = 0; if !vm_resources.disable_implicit_console { - attach_console_devices( + let controller = attach_console_devices( &mut vmm, event_manager, intc.clone(), @@ -983,11 +999,12 @@ pub fn build_microvm( None, console_id, )?; + console_controllers.push(controller); console_id += 1; } for console_cfg in vm_resources.virtio_consoles.iter() { - attach_console_devices( + let controller = attach_console_devices( &mut vmm, event_manager, intc.clone(), @@ -995,9 +1012,12 @@ pub fn build_microvm( Some(console_cfg), console_id, )?; + console_controllers.push(controller); console_id += 1; } + vmm.console_controllers = console_controllers; + #[cfg(not(any(feature = "tee", feature = "nitro")))] let export_table: Option = if cfg!(feature = "gpu") { Some(Default::default()) @@ -2005,7 +2025,9 @@ fn autoconfigure_console_ports( .map(|fd| port_io::term_fd(fd.as_raw_fd()).unwrap()) .unwrap_or_else(|| port_io::term_fixed_size(0, 0)); - setup_terminal_raw_mode(vmm, term_fd, forwarding_sigint); + if let Some(fd) = term_fd { + vmm.setup_terminal_raw_mode(fd, forwarding_sigint); + } let mut ports = vec![PortDescription::console( console_input, @@ -2038,30 +2060,6 @@ fn autoconfigure_console_ports( } } -fn setup_terminal_raw_mode( - vmm: &mut Vmm, - term_fd: Option>, - handle_signals_by_terminal: bool, -) { - if let Some(term_fd) = term_fd { - match term_set_raw_mode(term_fd, handle_signals_by_terminal) { - Ok(old_mode) => { - let raw_fd = term_fd.as_raw_fd(); - vmm.exit_observers.push(Arc::new(Mutex::new(move || { - if let Err(e) = - term_restore_mode(unsafe { BorrowedFd::borrow_raw(raw_fd) }, &old_mode) - { - log::error!("Failed to restore terminal mode: {e}") - } - }))); - } - Err(e) => { - log::error!("Failed to set terminal to raw mode: {e}") - } - }; - } -} - fn create_explicit_ports( vmm: &mut Vmm, port_configs: &[PortConfig], @@ -2073,7 +2071,7 @@ fn create_explicit_ports( PortConfig::Tty { name, tty_fd } => { assert!(*tty_fd > 0, "PortConfig::Tty must have a valid tty_fd"); let term_fd = unsafe { BorrowedFd::borrow_raw(*tty_fd) }; - setup_terminal_raw_mode(vmm, Some(term_fd), false); + vmm.setup_terminal_raw_mode(term_fd, false); PortDescription { name: name.clone().into(), @@ -2113,25 +2111,46 @@ fn attach_console_devices( event_manager: &mut EventManager, intc: IrqChip, vm_resources: &VmResources, - cfg: Option<&VirtioConsoleConfigMode>, + cfg: Option<&VirtioConsoleConfig>, id_number: u32, -) -> std::result::Result<(), StartMicrovmError> { +) -> std::result::Result { use self::StartMicrovmError::*; let creating_implicit_console = cfg.is_none(); + let (reserved_count, console_ready_evt) = match cfg { + Some(VirtioConsoleConfig { + mode: VirtioConsoleConfigMode::Explicit { reserved_count, .. }, + console_ready_evt, + }) => (*reserved_count, console_ready_evt.clone()), + Some(VirtioConsoleConfig { + console_ready_evt, .. + }) => (0, console_ready_evt.clone()), + None => ( + 0, + Arc::new(EventFd::new(utils::eventfd::EFD_NONBLOCK).unwrap()), + ), + }; + let ports = match cfg { None => autoconfigure_console_ports(vmm, vm_resources, None, creating_implicit_console)?, - Some(VirtioConsoleConfigMode::Autoconfigure(autocfg)) => autoconfigure_console_ports( + Some(VirtioConsoleConfig { + mode: VirtioConsoleConfigMode::Autoconfigure(autocfg), + .. + }) => autoconfigure_console_ports( vmm, vm_resources, Some(autocfg), creating_implicit_console, )?, - Some(VirtioConsoleConfigMode::Explicit(ports)) => create_explicit_ports(vmm, ports)?, + Some(VirtioConsoleConfig { + mode: VirtioConsoleConfigMode::Explicit { ports, .. }, + .. + }) => create_explicit_ports(vmm, ports)?, }; - let console = Arc::new(Mutex::new(devices::virtio::Console::new(ports).unwrap())); + let (console, console_controller) = + Console::with_controller(ports, reserved_count, console_ready_evt).unwrap(); vmm.exit_observers.push(console.clone()); @@ -2147,7 +2166,7 @@ fn attach_console_devices( attach_mmio_device(vmm, format!("hvc{id_number}"), intc, console) .map_err(RegisterConsoleDevice)?; - Ok(()) + Ok(console_controller) } #[cfg(feature = "net")] diff --git a/src/vmm/src/lib.rs b/src/vmm/src/lib.rs index d029ce718..c074f2588 100644 --- a/src/vmm/src/lib.rs +++ b/src/vmm/src/lib.rs @@ -30,7 +30,7 @@ mod linux; use crate::linux::vstate; #[cfg(target_os = "macos")] mod macos; -mod terminal; +pub mod terminal; pub mod worker; #[cfg(target_os = "macos")] @@ -38,6 +38,7 @@ use macos::vstate; use std::fmt::{Display, Formatter}; use std::io; +use std::os::fd::BorrowedFd; use std::os::unix::io::AsRawFd; use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::{Arc, Mutex}; @@ -57,7 +58,7 @@ use crossbeam_channel::Sender; #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] use devices::fdt; use devices::legacy::IrqChip; -use devices::virtio::VmmExitObserver; +use devices::virtio::{ConsoleController, VmmExitObserver}; use devices::{BusDevice, DeviceType}; use kernel::cmdline::Cmdline as KernelCmdline; use polly::event_manager::{self, EventManager, Subscriber}; @@ -207,6 +208,7 @@ pub struct Vmm { mmio_device_manager: MMIODeviceManager, #[cfg(target_arch = "x86_64")] pio_device_manager: PortIODeviceManager, + console_controllers: Vec, } impl Vmm { @@ -392,6 +394,34 @@ impl Vmm { pub fn remove_mapping(&self, reply_sender: Sender, guest_addr: u64, len: u64) { self.vm.remove_mapping(reply_sender, guest_addr, len); } + + pub fn console_controller(&self, console_id: u32) -> Option<&ConsoleController> { + self.console_controllers.get(console_id as usize) + } + + /// Set up raw mode for a terminal fd and register cleanup on exit. + pub fn setup_terminal_raw_mode( + &mut self, + term_fd: BorrowedFd<'_>, + handle_signals_by_terminal: bool, + ) { + match terminal::term_set_raw_mode(term_fd, handle_signals_by_terminal) { + Ok(old_mode) => { + let raw_fd = term_fd.as_raw_fd(); + self.exit_observers.push(Arc::new(Mutex::new(move || { + if let Err(e) = terminal::term_restore_mode( + unsafe { BorrowedFd::borrow_raw(raw_fd) }, + &old_mode, + ) { + log::error!("Failed to restore terminal mode: {e}") + } + }))); + } + Err(e) => { + log::error!("Failed to set terminal to raw mode: {e}") + } + } + } } impl Subscriber for Vmm { diff --git a/src/vmm/src/resources.rs b/src/vmm/src/resources.rs index d8d0fff24..22bbb1622 100644 --- a/src/vmm/src/resources.rs +++ b/src/vmm/src/resources.rs @@ -9,6 +9,9 @@ use std::fs::File; use std::io::BufReader; use std::os::fd::RawFd; use std::path::PathBuf; +use std::sync::Arc; + +use utils::eventfd::EventFd; #[cfg(feature = "tee")] use serde::{Deserialize, Serialize}; @@ -97,7 +100,17 @@ pub struct DefaultVirtioConsoleConfig { pub enum VirtioConsoleConfigMode { Autoconfigure(DefaultVirtioConsoleConfig), - Explicit(Vec), + Explicit { + ports: Vec, + /// Number of additional reserved port slots for dynamic addition after VM start + reserved_count: u32, + }, +} + +pub struct VirtioConsoleConfig { + pub mode: VirtioConsoleConfigMode, + /// EventFd that will be signaled when the console device is ready + pub console_ready_evt: Arc, } pub enum PortConfig { @@ -188,7 +201,7 @@ pub struct VmResources { /// Serial consoles to attach to the guest pub serial_consoles: Vec, /// Virtio consoles to attach to the guest - pub virtio_consoles: Vec, + pub virtio_consoles: Vec, } impl VmResources {