mod args;
mod embed;
#[expect(dead_code)]
#[path = "../init/init.rs"]
mod ignore_only_used_for_linting;
use std::cell::LazyCell;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::env;
use std::env::home_dir;
use std::env::temp_dir;
use std::ffi::CStr;
use std::ffi::CString;
use std::ffi::OsStr;
use std::ffi::OsString;
use std::ffi::c_char;
use std::fs;
use std::fs::File;
use std::fs::remove_file;
use std::io;
use std::io::Seek as _;
use std::io::Write as _;
use std::mem::MaybeUninit;
use std::ops::Deref;
use std::os::unix::ffi::OsStrExt as _;
use std::os::unix::ffi::OsStringExt as _;
use std::os::unix::fs::OpenOptionsExt as _;
use std::os::unix::io::AsRawFd as _;
use std::os::unix::io::FromRawFd as _;
use std::os::unix::io::OwnedFd;
use std::path::Path;
use std::path::PathBuf;
use std::process;
use std::ptr;
use anyhow::Context as _;
use anyhow::Result;
use anyhow::ensure;
use clap::Parser;
use vmsh::detect_kernel_format;
use vmsh::hostname;
use crate::args::Args;
use crate::args::Command;
use crate::args::RunArgs;
const INIT_BINARY: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/vmsh-init"));
const KRUN_FS_ROOT_TAG: &CStr = c"/dev/root";
const KRUN_FS_ROOT_SHM_SIZE: u64 = 1 << 29;
fn compute_shares(cwd: &Path, mut share_rw: Vec<PathBuf>) -> (Vec<PathBuf>, bool) {
let root = Path::new("/");
let mut root_ro = true;
let () = share_rw.retain(|p| {
let retain = p != root;
if !retain {
root_ro = false;
}
retain
});
let defaults = [cwd.to_path_buf(), temp_dir()];
let () = share_rw.extend(defaults);
(share_rw, root_ro)
}
fn set_shares(ctx: u32, shares: &[PathBuf]) -> Result<()> {
for (idx, path) in shares.iter().enumerate() {
let tag = format!("vmsh-{idx}\0");
let c_tag = CString::from_vec_with_nul(tag.into_bytes()).unwrap();
let c_path = CString::new(path.as_os_str().as_bytes())
.with_context(|| format!("path `{}` contains NUL bytes", path.display()))?;
let read_only = false;
let rc =
unsafe { krun::krun_add_virtiofs3(ctx, c_tag.as_ptr(), c_path.as_ptr(), 0, read_only) };
ensure!(
rc >= 0,
"failed to add virtiofs device for `{}`",
path.display()
);
}
Ok(())
}
fn format_shares_env(shares: &[PathBuf]) -> Vec<u8> {
let mut out = Vec::new();
for (idx, path) in shares.iter().enumerate() {
if !out.is_empty() {
let () = out.push(b';');
}
let () = out.extend_from_slice(format!("vmsh-{idx}").as_bytes());
let () = out.push(b':');
let () = out.extend_from_slice(path.as_os_str().as_bytes());
}
out
}
struct CleanupGuard(Option<PathBuf>);
impl Deref for CleanupGuard {
type Target = Path;
fn deref(&self) -> &Self::Target {
self.0.as_deref().unwrap()
}
}
impl Drop for CleanupGuard {
fn drop(&mut self) {
if let Some(path) = self.0.take() {
let _result = remove_file(path);
}
}
}
fn deploy_init_binary(path: &Path) -> Result<CleanupGuard> {
let mut file = fs::OpenOptions::new()
.create_new(true)
.mode(0o755)
.write(true)
.open(path)
.with_context(|| format!("failed to create `{}`", path.display()))?;
let guard = CleanupGuard(Some(path.to_path_buf()));
let () = file
.write_all(INIT_BINARY)
.with_context(|| format!("failed to write init binary to {}", path.display()))?;
Ok(guard)
}
fn enter_user_namespace() -> Result<()> {
let uid = unsafe { libc::getuid() };
let gid = unsafe { libc::getgid() };
let rc = unsafe { libc::unshare(libc::CLONE_NEWUSER) };
if rc != 0 {
return Err(io::Error::last_os_error()).context("failed to enter new user namespace")
}
let write_proc = |path: &str, content: &str| -> Result<()> {
let mut file = fs::OpenOptions::new()
.write(true)
.open(path)
.with_context(|| format!("failed to open `{path}`"))?;
let () = file
.write_all(content.as_bytes())
.with_context(|| format!("failed to write `{path}`"))?;
Ok(())
};
let () = write_proc("/proc/self/setgroups", "deny\n")?;
let () = write_proc("/proc/self/uid_map", &format!("0 {uid} 1\n"))?;
let () = write_proc("/proc/self/gid_map", &format!("0 {gid} 1\n"))?;
Ok(())
}
fn build_env_content(
env_args: &[String],
all_envs: bool,
host_env: impl IntoIterator<Item = (OsString, OsString)>,
) -> Vec<u8> {
let host = LazyCell::new(|| host_env.into_iter().collect::<HashMap<_, _>>());
let mut out = BTreeMap::new();
if all_envs {
for (key, value) in &*host {
if let Some(k) = key.to_str() {
if k.starts_with("VMSH_") || k.starts_with("KRUN_") {
continue;
}
}
let _prev = out.insert(key.as_os_str(), value.as_os_str());
}
}
for arg in env_args {
if let Some(pos) = arg.find('=') {
let key = OsStr::new(&arg[..pos]);
let value = OsStr::new(&arg[pos + 1..]);
let _prev = out.insert(key, value);
} else {
let key = OsStr::new(arg);
if let Some(value) = host.get(key) {
let _prev = out.insert(key, value);
}
}
}
let mut buf = Vec::new();
for (key, value) in &out {
let () = buf.extend_from_slice(key.as_bytes());
let () = buf.push(b'=');
let () = buf.extend_from_slice(value.as_bytes());
let () = buf.push(b'\n');
}
buf
}
fn create_env_memfd(content: &[u8]) -> Result<OwnedFd> {
let fd = unsafe { libc::memfd_create(c"vmsh-env".as_ptr(), 0) };
ensure!(fd >= 0, "failed to create memfd for env vars");
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
let mut file = File::from(fd);
let () = file
.write_all(content)
.context("failed to write env vars to memfd")?;
let () = file.rewind().context("failed to seek memfd to start")?;
Ok(file.into())
}
fn set_kernel(ctx: u32, kernel: PathBuf, init_guest_path: &Path, verbosity: u8) -> Result<()> {
let kernel_format = detect_kernel_format(&kernel)?;
let quiet = if verbosity < 2 { "quiet" } else { "" };
let cmdline = format!(
"earlycon=uart,io,0x3f8 reboot=k panic=5 console=hvc0 rootfstype=virtiofs rw init={} {quiet}",
init_guest_path.display()
);
let c_kernel_path = CString::new(kernel.into_os_string().into_vec())?;
let c_cmdline = CString::new(cmdline).unwrap();
let initramfs = ptr::null();
let rc = unsafe {
krun::krun_set_kernel(
ctx,
c_kernel_path.as_ptr(),
kernel_format as u32,
initramfs,
c_cmdline.as_ptr(),
)
};
ensure!(rc >= 0, "failed to set kernel (code {rc})");
Ok(())
}
fn set_exec(
ctx: u32,
command: Vec<String>,
has_env_port: bool,
unlink_paths: &[&Path],
shares_env: Option<&[u8]>,
) -> Result<()> {
let hostname = hostname().context("failed to retrieve host name")?;
let hostname = CString::new(format!("HOSTNAME={hostname}")).unwrap();
let home_owned;
let home = if let Some(home_dir) = home_dir() {
home_owned = CString::new(format!("HOME={}", home_dir.display())).unwrap();
&home_owned
} else {
c"/root"
};
let cwd = env::current_dir()
.and_then(|p| p.canonicalize())
.context("failed to determine current directory")?;
let cwd = CString::new(format!("WORKDIR={}", cwd.display())).unwrap();
let stdin_redir = unsafe { libc::isatty(libc::STDIN_FILENO) == 0 };
let stdout_redir = unsafe { libc::isatty(libc::STDOUT_FILENO) == 0 };
let stderr_redir = unsafe { libc::isatty(libc::STDERR_FILENO) == 0 };
let mut env_ptrs = vec![hostname.as_ptr(), home.as_ptr(), cwd.as_ptr()];
let unlink_env = if !unlink_paths.is_empty() {
let value = unlink_paths
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(":");
Some(CString::new(format!("VMSH_UNLINK={value}"))?)
} else {
None
};
if let Some(ref env) = unlink_env {
let () = env_ptrs.push(env.as_ptr());
}
let env_port_env = c"VMSH_ENV_PORT=1";
if has_env_port {
let () = env_ptrs.push(env_port_env.as_ptr());
}
if stdin_redir {
let () = env_ptrs.push(c"VMSH_STDIN=1".as_ptr());
}
if stdout_redir {
let () = env_ptrs.push(c"VMSH_STDOUT=1".as_ptr());
}
if stderr_redir {
let () = env_ptrs.push(c"VMSH_STDERR=1".as_ptr());
}
let shares_env_cstr;
if let Some(val) = shares_env {
let mut buf = b"VMSH_SHARES=".to_vec();
let () = buf.extend_from_slice(val);
shares_env_cstr = CString::new(buf)?;
let () = env_ptrs.push(shares_env_cstr.as_ptr());
}
let () = env_ptrs.push(ptr::null());
if !command.is_empty() {
let cmd = CString::new(command[0].as_str())?;
let args = command[1..]
.iter()
.map(|a| CString::new(a.as_str()))
.collect::<Result<Vec<_>, _>>()?;
let mut argv = args
.iter()
.map(|a| a.as_ptr())
.collect::<Vec<*const c_char>>();
let () = argv.push(ptr::null());
let rc = unsafe { krun::krun_set_exec(ctx, cmd.as_ptr(), argv.as_ptr(), env_ptrs.as_ptr()) };
ensure!(rc >= 0, "failed to set exec command");
} else {
let rc = unsafe { krun::krun_set_env(ctx, env_ptrs.as_ptr()) };
ensure!(rc >= 0, "failed to set environment");
}
Ok(())
}
fn exec_vm(args: RunArgs, init_guest_path: &Path, unlink_paths: &[&Path]) -> Result<()> {
let RunArgs {
kernel,
cpus,
memory,
net,
uds,
command,
env_vars,
all_envs,
no_uid_map: _,
verbosity,
share_rw,
} = args;
let kernel = kernel.unwrap();
if verbosity > 0 {
let rc = unsafe {
krun::krun_init_log(
libc::STDERR_FILENO,
u32::from(verbosity),
2, 0, )
};
ensure!(rc >= 0, "failed to set log level");
}
let ctx = krun::krun_create_ctx() as u32;
let rc = krun::krun_set_vm_config(ctx, cpus, memory);
ensure!(rc >= 0, "failed to set VM config");
let rc = unsafe {
krun::krun_add_serial_console_default(
ctx,
-1,
if verbosity > 0 {
libc::STDERR_FILENO
} else {
-1
},
)
};
ensure!(rc >= 0, "failed to add serial console");
const KRUN_TSI_HIJACK_INET: u32 = 1 << 0;
const KRUN_TSI_HIJACK_UNIX: u32 = 1 << 1;
let rc = krun::krun_disable_implicit_vsock(ctx);
ensure!(rc >= 0, "failed to disable implicit vsock");
let mut tsi_features = 0;
if net {
tsi_features |= KRUN_TSI_HIJACK_INET;
}
if net || uds {
tsi_features |= KRUN_TSI_HIJACK_UNIX;
}
let rc = krun::krun_add_vsock(ctx, tsi_features);
ensure!(rc >= 0, "failed to add vsock device");
let () = set_kernel(ctx, kernel, init_guest_path, verbosity)?;
let cwd = env::current_dir()
.and_then(|p| p.canonicalize())
.context("failed to determine current directory")?;
let (shares, root_ro) = compute_shares(&cwd, share_rw);
let () = set_shares(ctx, &shares)?;
let c_rootfs = c"/";
let rc = unsafe {
krun::krun_add_virtiofs3(
ctx,
KRUN_FS_ROOT_TAG.as_ptr(),
c_rootfs.as_ptr(),
KRUN_FS_ROOT_SHM_SIZE,
root_ro,
)
};
ensure!(rc >= 0, "failed to set root filesystem");
let env_content = build_env_content(&env_vars, all_envs, env::vars_os());
let has_env_port = !env_content.is_empty();
let env_fd;
if has_env_port {
env_fd = create_env_memfd(&env_content)?;
let console_id = unsafe { krun::krun_add_virtio_console_multiport(ctx) };
ensure!(console_id >= 0, "failed to add virtio console for env port");
let rc = unsafe {
krun::krun_add_console_port_inout(
ctx,
console_id as u32,
c"krun-env".as_ptr(),
env_fd.as_raw_fd(),
-1,
)
};
ensure!(rc >= 0, "failed to add env console port");
}
let shares_env_val = format_shares_env(&shares);
let shares_env = if shares_env_val.is_empty() {
None
} else {
Some(shares_env_val.as_slice())
};
let () = set_exec(ctx, command, has_env_port, unlink_paths, shares_env)?;
let rc = krun::krun_start_enter(ctx);
ensure!(rc >= 0, "failed to start VM (code {rc})");
Ok(())
}
fn set_rlimits() -> Result<()> {
let mut limit = MaybeUninit::<libc::rlimit>::uninit();
let rc = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, limit.as_mut_ptr()) };
ensure!(rc >= 0, "failed to get `RLIMIT_NOFILE`");
let mut limit = unsafe { limit.assume_init() };
limit.rlim_cur = limit.rlim_max;
let _rc = unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &limit) };
Ok(())
}
fn main() -> Result<()> {
let args = Args::parse();
let mut args = match args.command {
Some(Command::Embed(embed_args)) => {
return embed::embed_kernel(&embed_args.kernel, embed_args.output.as_deref());
},
Some(Command::Run(run_args)) => run_args,
None => args.args,
};
let kernel_guard;
let extracted_kernel = match &mut args.kernel {
Some(path) => {
ensure!(
path.exists(),
"failed to find kernel at `{}`",
path.display()
);
None
},
None => {
kernel_guard = embed::extract_embedded_kernel()
.context("no kernel specified and no embedded kernel found")?;
args.kernel = Some(kernel_guard.to_path_buf());
Some(&*kernel_guard)
},
};
let () = set_rlimits()?;
let init_filename = format!("vmsh-init-{}", process::id());
let init_path = temp_dir().join(&init_filename);
let _guard = deploy_init_binary(&init_path)?;
let mut unlink_paths = vec![init_path.as_path()];
if let Some(kernel_path) = extracted_kernel {
let () = unlink_paths.push(kernel_path);
}
if !args.no_uid_map {
let () = enter_user_namespace().map_err(|err| {
if err
.root_cause()
.downcast_ref::<io::Error>()
.map(|err| err.kind() == io::ErrorKind::PermissionDenied)
.unwrap_or(false)
{
Result::<(), _>::Err(err)
.context(
"user namespace setup failed; check \
`kernel.unprivileged_userns_clone` or \
`kernel.apparmor_restrict_unprivileged_userns`, or rerun \
with `--no-uid-map` to skip user-namespace setup",
)
.unwrap_err()
} else {
err
}
})?;
}
let () = exec_vm(args, &init_path, &unlink_paths)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn host_env() -> Vec<(OsString, OsString)> {
vec![
(OsString::from("PATH"), OsString::from("/usr/bin")),
(OsString::from("USER"), OsString::from("alice")),
(OsString::from("HOSTNAME"), OsString::from("myhost")),
(OsString::from("HOME"), OsString::from("/home/alice")),
(OsString::from("VMSH_REDIRECT"), OsString::from("3")),
(
OsString::from("VMSH_KERNEL"),
OsString::from("/boot/vmlinuz"),
),
(OsString::from("KRUN_LOG_LEVEL"), OsString::from("debug")),
(OsString::from("EDITOR"), OsString::from("vim")),
]
}
#[test]
fn no_flags_empty_output() {
let all_envs = false;
let content = build_env_content(&[], all_envs, Vec::<(OsString, OsString)>::new());
assert!(content.is_empty());
}
#[test]
fn all_envs_exports_host() {
let all_envs = true;
let content = build_env_content(&[], all_envs, host_env());
let text = String::from_utf8(content).unwrap();
assert!(text.contains("PATH=/usr/bin\n"));
assert!(text.contains("USER=alice\n"));
assert!(text.contains("EDITOR=vim\n"));
}
#[test]
fn all_envs_skips_blocked() {
let all_envs = true;
let content = build_env_content(&[], all_envs, host_env());
let text = String::from_utf8(content).unwrap();
assert!(text.contains("HOSTNAME=myhost\n"));
assert!(text.contains("HOME=/home/alice\n"));
assert!(!text.contains("VMSH_REDIRECT="));
assert!(!text.contains("VMSH_KERNEL="));
assert!(!text.contains("KRUN_LOG_LEVEL="));
}
#[test]
fn env_key_resolves_from_host() {
let all_envs = false;
let content = build_env_content(&["PATH".to_string()], all_envs, host_env());
let text = String::from_utf8(content).unwrap();
assert_eq!(text, "PATH=/usr/bin\n");
}
#[test]
fn env_key_value_explicit() {
let all_envs = false;
let content = build_env_content(
&["FOO=bar".to_string()],
all_envs,
Vec::<(OsString, OsString)>::new(),
);
let text = String::from_utf8(content).unwrap();
assert_eq!(text, "FOO=bar\n");
}
#[test]
fn env_key_missing_skipped() {
let all_envs = false;
let content = build_env_content(&["NONEXISTENT".to_string()], all_envs, host_env());
assert!(content.is_empty());
}
#[test]
fn env_overrides_all_envs() {
let all_envs = true;
let content = build_env_content(&["PATH=custom".to_string()], all_envs, host_env());
let text = String::from_utf8(content).unwrap();
assert!(text.contains("PATH=custom\n"));
assert!(!text.contains("PATH=/usr/bin\n"));
}
#[test]
fn no_duplicates() {
let all_envs = true;
let content = build_env_content(&["PATH".to_string()], all_envs, host_env());
let text = String::from_utf8(content).unwrap();
let count = text.matches("PATH=").count();
assert_eq!(count, 1);
}
#[test]
fn share_metadata_format() {
let shares = [
PathBuf::from("/home/user/project"),
PathBuf::from("/tmp"),
PathBuf::from("/opt"),
];
let env = format_shares_env(&shares);
assert_eq!(env, b"vmsh-0:/home/user/project;vmsh-1:/tmp;vmsh-2:/opt");
}
#[test]
fn non_utf8_paths() {
let rw_path = PathBuf::from(OsString::from_vec(b"/rw-\x80".to_vec()));
let env = format_shares_env(&[rw_path]);
assert_eq!(env, b"vmsh-0:/rw-\x80");
}
}