Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion crates/bcvk-qemu/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ data-encoding = "2.9"
libc = "0.2"
nix = { version = "0.29", features = ["socket"] }
rustix = { version = "1", features = ["pipe", "process"] }
tokio = { version = "1", features = ["fs", "process", "time", "macros"] }
serde_json = "1"
tokio = { version = "1", features = ["fs", "io-util", "net", "process", "rt", "time", "macros"] }
tracing = { workspace = true }
vsock = "=0.5.1"

[dev-dependencies]
tempfile = "3"

[lints]
workspace = true
5 changes: 4 additions & 1 deletion crates/bcvk-qemu/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@

mod credentials;
mod qemu;
/// Minimal QMP (QEMU Machine Protocol) client for runtime VM control.
pub mod qmp;
mod virtiofsd;

pub use credentials::{
Expand All @@ -54,7 +56,8 @@ pub use credentials::{

pub use qemu::{
BootMode, DiskFormat, DisplayMode, MachineType, NetworkMode, QemuConfig, ResourceLimits,
RunningQemu, VirtioBlkDevice, VirtioSerialOut, VirtiofsMount, VHOST_VSOCK,
RunningQemu, VirtioBlkDevice, VirtioSerialBidir, VirtioSerialOut, VirtiofsMount,
QMP_SOCKET_PATH, VHOST_VSOCK,
};

pub use virtiofsd::{spawn_virtiofsd_async, validate_virtiofsd_config, VirtiofsConfig};
114 changes: 110 additions & 4 deletions crates/bcvk-qemu/src/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ use crate::VirtiofsConfig;
/// The device path for vsock allocation.
pub const VHOST_VSOCK: &str = "/dev/vhost-vsock";

/// Default path for the QMP (QEMU Machine Protocol) Unix socket.
pub const QMP_SOCKET_PATH: &str = "/run/bcvk-qmp.sock";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The default QMP socket path is hardcoded to a single global location (/run/bcvk-qmp.sock). This will cause conflicts and permission issues if multiple VMs are started simultaneously on the same host. Consider using a unique path per VM instance (e.g., by generating a path in a temporary directory or requiring the caller to provide a unique path).


/// VirtIO-FS mount point configuration.
#[derive(Debug, Clone)]
pub struct VirtiofsMount {
Expand All @@ -48,6 +51,23 @@ pub struct VirtioSerialOut {
pub append: bool,
}

/// Bidirectional VirtIO-Serial device backed by a Unix socket.
///
/// Unlike [`VirtioSerialOut`] which uses a write-only `chardev file` backend,
/// this uses a `chardev socket` backend for full bidirectional communication.
/// The guest sees `/dev/virtio-ports/{name}` as a read-write character device.
/// The host connects to the Unix socket to exchange data in both directions.
///
/// Used by the shell relay feature to provide interactive shell access to
/// VMs without requiring SSH.
#[derive(Debug, Clone)]
pub struct VirtioSerialBidir {
/// Device name (becomes /dev/virtio-ports/{name}).
pub name: String,
/// Unix socket path on the host for bidirectional communication.
pub socket_path: String,
}

/// Disk image format for virtio-blk devices.
#[derive(Debug, Clone, Copy, Default)]
pub enum DiskFormat {
Expand Down Expand Up @@ -100,6 +120,9 @@ pub enum NetworkMode {
/// Port forwarding rules: "tcp::2222-:22" format.
hostfwd: Vec<String>,
},
/// No network device. Useful for fully isolated VMs where
/// all host-guest communication happens over virtio-serial.
None,
}

impl Default for NetworkMode {
Expand Down Expand Up @@ -202,8 +225,10 @@ pub struct QemuConfig {
fdset: Vec<Arc<OwnedFd>>,
/// Additional VirtIO-FS mounts.
pub additional_mounts: Vec<VirtiofsMount>,
/// Virtio-serial output devices.
/// Virtio-serial output devices (unidirectional, guest -> host).
pub virtio_serial_devices: Vec<VirtioSerialOut>,
/// Virtio-serial bidirectional devices (backed by Unix sockets).
pub virtio_serial_bidir_devices: Vec<VirtioSerialBidir>,
/// Virtio-blk block devices.
pub virtio_blk_devices: Vec<VirtioBlkDevice>,
/// Display/console mode.
Expand All @@ -229,6 +254,12 @@ pub struct QemuConfig {

/// fw_cfg entries for passing config files to the guest
fw_cfg_entries: Vec<(String, Utf8PathBuf)>,

/// Path for the QMP (QEMU Machine Protocol) Unix socket.
///
/// QMP is always enabled. If not explicitly set, defaults to
/// [`QMP_SOCKET_PATH`] at spawn time.
pub qmp_socket_path: Option<String>,
}

impl QemuConfig {
Expand Down Expand Up @@ -442,6 +473,24 @@ impl QemuConfig {
Ok(read_fd)
}

/// Add a bidirectional virtio-serial device backed by a Unix socket.
///
/// QEMU creates a listening Unix socket at `socket_path`. The guest sees
/// the device as `/dev/virtio-ports/{name}` and can read/write it.
/// The host connects to the socket for bidirectional byte-stream
/// communication with the guest.
///
/// Unlike [`add_virtio_serial_out`] (which uses a write-only `chardev file`),
/// this uses `chardev socket` with `server=on,wait=off` so QEMU doesn't
/// block waiting for a host-side connection at startup.
pub fn add_virtio_serial_bidir(&mut self, name: &str, socket_path: String) -> &mut Self {
self.virtio_serial_bidir_devices.push(VirtioSerialBidir {
name: name.to_owned(),
socket_path,
});
self
}

/// Add SMBIOS credential for systemd credential passing.
pub fn add_smbios_credential(&mut self, credential: String) -> &mut Self {
self.smbios_credentials.push(credential);
Expand Down Expand Up @@ -676,7 +725,24 @@ fn spawn(
]);
}

// Configure network (only User mode supported now)
// Add bidirectional virtio-serial devices (chardev socket backend)
for (idx, bidir_device) in config.virtio_serial_bidir_devices.iter().enumerate() {
let char_id = format!("bidir_char{}", idx);
cmd.args([
"-chardev",
&format!(
"socket,id={},path={},server=on,wait=off",
char_id, bidir_device.socket_path
),
Comment on lines +733 to +736
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

QEMU uses commas as delimiters for -chardev options. If bidir_device.socket_path contains a comma, it will break the command line parsing and potentially allow for argument injection. QEMU allows escaping commas by doubling them (,,).

Suggested change
&format!(
"socket,id={},path={},server=on,wait=off",
char_id, bidir_device.socket_path
),
&format!(
"socket,id={},path={},server=on,wait=off",
char_id, bidir_device.socket_path.replace(',', ",,")
),

"-device",
&format!(
"virtserialport,chardev={},name={}",
char_id, bidir_device.name
),
]);
}

// Configure network
match &config.network_mode {
NetworkMode::User { hostfwd } => {
let mut netdev_parts = vec!["user".to_string(), "id=net0".to_string()];
Expand All @@ -694,6 +760,10 @@ fn spawn(
"virtio-net-pci,netdev=net0",
]);
}
NetworkMode::None => {
// No network device at all. Host-guest communication
// happens exclusively over virtio-serial.
}
}

// No GUI; serial console either to a log file or disabled.
Expand All @@ -704,10 +774,19 @@ fn spawn(
}
cmd.args(["-nographic", "-display", "none"]);

// QMP socket for runtime control (hot-plug, etc.) -- always enabled.
let qmp_path = config.qmp_socket_path.as_deref().unwrap_or(QMP_SOCKET_PATH);
cmd.args([
"-chardev",
&format!("socket,id=qmp0,path={qmp_path},server=on,wait=off"),
"-mon",
"chardev=qmp0,mode=control",
]);

match &config.display_mode {
DisplayMode::None => {
// Disable monitor in non-console mode
cmd.args(["-monitor", "none"]);
// QMP monitor is already configured above; no need for
// an additional human monitor.
}
DisplayMode::Console => {
cmd.args(["-device", "virtconsole,chardev=console0"]);
Expand Down Expand Up @@ -801,6 +880,8 @@ pub struct RunningQemu {
pub virtiofsd_processes: Vec<Pin<Box<dyn Future<Output = std::io::Result<Output>>>>>,
#[allow(dead_code)]
sd_notification: Option<VsockCopier>,
/// Path to the QMP socket (always available).
pub qmp_socket_path: String,
}

impl std::fmt::Debug for RunningQemu {
Expand Down Expand Up @@ -968,13 +1049,19 @@ impl RunningQemu {
})
.unwrap_or_default();

let qmp_socket_path = config
.qmp_socket_path
.clone()
.unwrap_or_else(|| QMP_SOCKET_PATH.to_string());

// Spawn QEMU process with additional VSOCK credential if needed
let qemu_process = spawn(&config, &creds, vsockdata)?;

Ok(Self {
qemu_process,
virtiofsd_processes,
sd_notification,
qmp_socket_path,
})
}

Expand Down Expand Up @@ -1029,6 +1116,25 @@ mod tests {
assert_eq!(DiskFormat::Qcow2.as_str(), "qcow2");
}

#[test]
fn test_virtio_serial_bidir_device_creation() {
let mut config = QemuConfig::new_direct_boot(
1024,
1,
"/test/kernel".to_string(),
"/test/initramfs".to_string(),
"/test/socket".into(),
);
config.add_virtio_serial_bidir("org.bcvk.ssh.0", "/run/bcvk-ssh-0.sock".to_string());

assert_eq!(config.virtio_serial_bidir_devices.len(), 1);
assert_eq!(config.virtio_serial_bidir_devices[0].name, "org.bcvk.ssh.0");
assert_eq!(
config.virtio_serial_bidir_devices[0].socket_path,
"/run/bcvk-ssh-0.sock"
);
}

#[test]
fn test_fw_cfg_entry() {
let mut config = QemuConfig::new_direct_boot(
Expand Down
Loading
Loading