Add initial virtiofs support
CI / build (push) Failing after 14s

This commit is contained in:
hulthe committed 2026-09-26 13:23:21 +02:00
1 parent bfda3e626a
commit 8ad6754328
6 files changed
+301 -47

No files matched your search

+53 -25
View File
@@ -3,7 +3,11 @@
//! at $XDG_DATA_HOME/boco/registry/vmlinuz. A universal /boco/init script (injected
//! at build time) handles VM bootstrap; /boco/exec runs the container's
//! CMD/ENTRYPOINT. Runtime overrides are delivered as `boco.cmd=<base64>`
//! on the kernel cmdline.
//! on the kernel cmdline. Host directories are shared into the guest via
//! virtio-fs: one virtiofsd daemon (under `podman unshare` for rootless
//! UID mapping) per `--mount`, plus a `-chardev socket` + `-device
//! vhost-user-fs-pci` pair on the QEMU side. The guest mounts each share
//! with `mount -t virtiofs <tag> <dest>` (see `boco.mount=` cmdline).
use anyhow::{Context, Result, bail};
use base64::Engine;
@@ -13,6 +17,7 @@ use std::{fmt::Write as _, process::Command};
use crate::{
forward::PortForward,
registry::{registry_base_dir, registry_dir},
virtiofs::{self, VirtiofsShare},
};
#[derive(Args, Debug)]
@@ -29,11 +34,11 @@ pub struct BootCmd {
forward: Vec<PortForward>,
/// Override the command to exec in the VM (base64-encoded on the kernel
/// cmdline as boco.cmd=<b64>). Overrides the CMD inferred at build time.
/// cmdline as `boco.cmd=<b64>`). Overrides the CMD inferred at build time.
#[clap(long)]
cmd: Option<String>,
/// Share a host directory into the VM via 9p. Format:
/// Share a host directory into the VM via virtio-fs. Format:
/// `<host-path>` or `<host-path>:<guest-path>`. When the guest path is
/// omitted, the host path is used. Non-absolute guest paths are
/// relative to the image's WORKINGDIR. Can be repeated.
@@ -91,35 +96,55 @@ pub(crate) fn boot(
cmdline.push(format!("boco.cmd={encoded}"));
}
// Build 9p shares for each --mount. Tags are short (boco0, boco1, …)
// because 9p mount_tag has a ~31-byte limit. The guest destination path
// is passed on the kernel cmdline as boco.mount=<tag>:<base64(guest_path)>.
// If no guest path is specified, the host path is used as the guest path.
let mut virtfs_args: Vec<String> = Vec::new();
// Build virtio-fs shares for each `--mount`. Each share gets its own
// virtiofsd daemon under `podman unshare` (rootless UID mapping) and a
// matching `-chardev socket` + `-device vhost-user-fs-pci` pair on the
// QEMU side. The guest destination path is passed on the kernel cmdline
// as `boco.mount=<tag>:<base64(guest_path)>` and resolved by /boco/init.
let mut shares: Vec<VirtiofsShare> = Vec::with_capacity(mount.len());
let mut fsdev_args: Vec<String> = Vec::new();
for (i, spec) in mount.iter().enumerate() {
let (host_path, guest_path) = match spec.split_once(':') {
Some((h, g)) => (h, g),
None => (spec.as_str(), spec.as_str()),
Some((h, g)) => (h, g.to_string()),
None => (spec.as_str(), spec.to_string()),
};
let canonical = std::path::Path::new(host_path)
.canonicalize()
.with_context(|| format!("Cannot resolve mount path '{host_path}'"))?;
let host_str = canonical
let share = virtiofs::spawn_share(i, host_path, guest_path)?;
let tag = share.tag.clone();
let chardev = format!("char{i}");
let socket = share
.socket_path()
.to_str()
.context("Mount path is not valid UTF-8")?;
let tag = format!("boco{i}");
let dest_b64 = base64::engine::general_purpose::STANDARD.encode(guest_path.as_bytes());
.context("socket path is not UTF-8")?
.to_string();
let dest_b64 =
base64::engine::general_purpose::STANDARD.encode(share.guest_path.as_bytes());
// QEMU's QemuOpts splits on commas — escape literal commas in the
// path as ",," per QEMU convention.
let host_escaped = host_str.replace(',', ",,");
virtfs_args.push("-virtfs".into());
virtfs_args.push(format!(
"local,path={host_escaped},mount_tag={tag},security_model=mapped-xattr"
fsdev_args.push("-chardev".into());
fsdev_args.push(format!("socket,id={chardev},path={socket}"));
fsdev_args.push("-device".into());
fsdev_args.push(format!(
"vhost-user-fs-pci,queue-size=1024,chardev={chardev},tag={tag}"
));
cmdline.push(format!("boco.mount={tag}:{dest_b64}"));
shares.push(share);
}
// vhost-user devices require shared guest RAM so virtiofsd (a separate
// process) can DMA into it.
let memory_args: Vec<String> = if shares.is_empty() {
vec!["-m".into(), memory.clone()]
} else {
vec![
"-m".into(),
memory.clone(),
"-object".into(),
format!("memory-backend-memfd,id=mem,size={memory},share=on"),
"-numa".into(),
"node,memdev=mem".into(),
]
};
println!("Booting {name} from registry: {}", reg_dir.display());
let cmdline = cmdline.join(" ");
@@ -136,11 +161,11 @@ pub(crate) fn boot(
command
.args(["-accel", "kvm"])
.args(["-cpu", "host", "-smp", "cpus=8"])
.args(["-m", &memory])
.args(&memory_args)
.arg("-kernel")
.arg(vmlinuz_path.as_os_str())
.args(&fs_args)
.args(&virtfs_args)
.args(&fsdev_args)
.args(["-snapshot"])
.args(["-no-reboot"])
.args(["-append", &cmdline])
@@ -153,6 +178,9 @@ pub(crate) fn boot(
.status()
.context("Failed to launch qemu-system-x86_64")?;
// Ensure virtiofsd daemons are torn down even on error returns below.
drop(shares);
match status.code() {
Some(0) => println!("QEMU exited cleanly."),
Some(126) | Some(127) => {